ApexAutomation
Apex Automation

Apex Automation free tools

Merge Two CSV Files

Join them on a key, or stack them, and find out what did not match

Two files, one key column, and a number at the end that says how much of it worked. The joining is the easy part. The part that costs an afternoon is the fourteen rows that did not match, because one file wrote the id as 00721 and the other wrote it as 721. This page lists those rows, names the closest key on the other side, and tells you which single setting would match them.

  • Free to use
  • No account needed
  • Nothing uploaded
  • No row limit

The first file

Drop a .csv or .xlsx here, or

This is the list you want to keep. Every row of it survives a left join.

The second file

Drop a .csv or .xlsx here, or

This is the one you are looking things up in.

What these files can be

  • .csv, .tsv or .xlsxSemicolon, tab and pipe separated too. In a workbook the first sheet holding a table is read, so a cover page with a title on it is stepped over.
  • Key columns with different namesaccount_id on one side can meet customer on the other. Nothing has to be renamed first.
  • Keys made of more than one columnFirst name plus postcode, or branch plus date. The parts are kept apart, so one column cannot bleed into the next.
  • Any encodingUTF-8 and UTF-16, with or without a byte order mark. A CSV that Excel saved on Windows reads correctly.
  • Any number of rowsNo cap and no sign up. The work happens on your own machine, so the limit is your machine.
  • Columns you do not wantPick which columns come across from the second file. A column name that exists on both sides is renamed rather than quietly covering the other.
  • Keys that do not quite agreeLeading zeros, upper and lower case, punctuation, accents, scientific notation, and company suffixes such as Inc and Ltd, each a separate switch that touches only the key.
  • Keys that disagree more than thatFuzzy matching with a score on every match. Where two candidates are equally close, nothing is chosen and the row is named.

What will not work. A file with no header row is read with its first row as the header, which is the usual convention but worth knowing. A row whose key cell is empty never matches another empty key, because empty is not a value. And a join is not a merge of cell contents: where both files have a column of the same name, both are kept, side by side, under different names.

Two different jobs, one word

Search for "merge CSV" and you will be shown two kinds of tool without being told which is which. One stacks files: more rows, the same columns, which is what you want when an export was split across months. The other joins them: the same rows, more columns, which is what VLOOKUP does and what people usually mean when they have a customer list and an orders list.

Almost everything ranking for that phrase does the first and calls it merging. The page you are on does both, with a switch at the top, because the words are not going to stop being confused and guessing which one somebody meant is not a service.

The fourteen rows that did not match

Every join tool prints a match rate. Almost none of them answers the question that follows it, which is the only question anybody actually has: which rows, and why.

The reasons are boringly consistent. One file has a trailing space. One was exported in capitals. An id went through a spreadsheet and 00721 came out as 721. A long account number came back as 1.23457E+11. A company is Acme Inc on one side and Acme, Inc. on the other. None of these is a data problem, and all of them look like one.

So the page does the obvious thing that nobody does: it tries.

Each of those settings is switched on behind the scenes, the join is run again, and the difference is counted. What comes back is a count rather than a suggestion: "turning on leading zeros would match 14 more rows". The same is done for fuzzy matching at five different thresholds, so the slider has a real number under it rather than a feeling. And for the rows still stuck, the page names the closest key on the other side with a similarity score, so you can see whether the record is genuinely missing or merely spelled differently.

What is claimed, and what is not. No accuracy percentage, because a join is exactly as accurate as its keys. What is claimed is checkable on your own file: every row that went in is either matched or listed as unmatched, the number of rows coming out is the number the keys say it should be, and every fuzzy match is shown with its score rather than folded in quietly.

Why a join can hand back more rows than it was given

Three hundred customers joined to three hundred orders can produce three hundred and forty rows, and the first reaction is always that something is broken. Nothing is. If a key appears twice on the other side, every matching row comes out twice, which is what a join means in every database there has ever been.

It is still worth knowing before it happens rather than after, so repeated keys are counted on both sides and the row count is worked out before the join runs. The worst offending key is named, with how many times it appears, because that is usually enough to see what went wrong in the export.

What has actually been tested

Every figure here is a test run that has to pass before anything reaches this page. If one fails, nothing ships. A number on a page that nothing checks goes stale, so a test reads this page, pulls out every figure in it, and compares each one with the code behind it.

191automated checks, all passing
15suites, each runnable on its own
106,510joined rows checked against the keys
20deliberate faults, every one caught
400generated file pairs, every row accounted for
0bytes uploaded
Every row is counted in and counted out The rule is simple enough to check on any file: the rows that matched plus the rows that did not must be the number that went in, on both sides, and the number of rows written must be the number the keys say it should be. The same count is worked out twice, once before the join from the keys alone and once after from what was written, and the two must agree.
And the stacking has its own rule Not one row lost, not one invented, and the number of filled cells going in is the number coming out. A column one file does not have comes out empty rather than shifting the rest of the row along, which is the usual way a stack quietly ruins a file.
Five thousand pairs of deliberate rubbish Unclosed quotes, a file of nothing but commas, rows longer than their header, three columns all called id, control characters, right to left overrides, mixed delimiters, and strings of random bytes, in both files at once. Every pair must either join and add up, or be refused with a reason. That hunt found a real one: a file with two columns of the same name was quietly losing a column of values when stacked.
Every control is clicked, not just rendered A browser opens the page and presses every switch, every join type, the fuzzy slider at both ends, both key pickers, every download button and every formula block, and checks that each one changes something. A control that sits there looking right and does nothing is a bug that only clicking finds.
Every test is broken on purpose before it is trusted Deliberate faults are injected into the code one at a time, the suites are run, and the fault is reverted. A fault that survives means the tests have no opinion about it, and the missing test gets written.

What it will not do

Questions people ask first

What is the difference between merging and joining?

They are two different jobs that share one word. Stacking puts one file under the other: more rows, the same columns, which is what you want when an export was split by month or by region. Joining puts them side by side on a shared key: the same rows, more columns, which is what VLOOKUP does. Most tools that rank for "merge CSV" only stack, which is why so many people search for merge, get a stacker, and conclude the tool is broken. Both are here, with a switch at the top.

Why did my rows not match?

Almost always the key rather than the data. Trailing spaces, different capitalisation, an id that lost its leading zeros when a spreadsheet opened it, a long number turned into scientific notation, or a company written as Acme Inc on one side and Acme, Inc. on the other. Rather than listing those causes and leaving you to guess, this page switches each one on behind the scenes, runs the join again, and tells you how many more rows it would match. For the rows still stuck it names the closest key on the other side with a similarity score.

Why did the join give me more rows than I started with?

Because a key repeats on the other side. If an order id appears twice in the second file, every matching row from the first comes out twice. Every database behaves that way, and it still catches people out. Repeated keys are counted on both sides before the join runs, the worst one is named with how many times it appears, and the expected row count is worked out from the keys so the number you get is never a shock.

Can I join on more than one column?

Yes. Tick more than one column on each side and they are joined as a composite key, first name plus postcode for instance. The parts are kept apart internally, so a row with "ab" and "c" can never collide with a row with "a" and "bc", which is a real bug in tools that simply glue the values together. The number of key columns has to match on both sides.

What does fuzzy matching actually do here?

It only runs on rows that did not match exactly, and only when you turn it on. Each leftover key is compared with the keys on the other side and the closest one is taken if it is above the threshold you chose. Every match is listed with its score, so you can check the guesses instead of trusting them, and where two candidates are equally close nothing is chosen and the row is named as ambiguous. Before you turn it on, the page tells you how many rows each threshold would actually add.

Can I get the rows that did not match?

Yes, from both sides, as their own files. That is almost always the real next step: the unmatched rows are the finding, not the leftovers. They are also included as their own sheets in the Excel download, along with the join report and, if fuzzy matching was used, every match it made with its score.

Are my files uploaded?

No. Both files are read and joined by code already sitting in this browser tab, including the Excel reader, which is written out rather than pulled in from a library, so there is no third party script to audit. There is no upload endpoint and no server waiting to receive anything. Disconnect after the page has loaded and it carries on working, which is checked by a test on every run.

Can I have the formula instead?

Yes, and that is unusual, so it is worth saying what you get. The same join is written out as XLOOKUP, as VLOOKUP for older Excel and Google Sheets, as INDEX and MATCH for when the column you want sits to the left of the key, as SQL, and as pandas. Each one carries the normalisation you chose on this page, so the formula matches the same rows the preview shows, and each comes with a note about where that particular form goes wrong.

Tell us what would make this better

A pair of files that this page joins differently from the way you expected is the most useful thing anyone can send us. The shape is enough, you do not need to send the real data. toolssupport@apexautomationteam.com

  • A join that came out wrong, with the key columns you used
  • A way keys disagree that is not on the settings list yet
  • A layout your spreadsheet or BI tool wants that is not here
  • A version of this wired into your own platform or workflow
  • Something else altogether, built to your spec

Email our team See what we build

A row that joined to the wrong record jumps the queue. Everything else is a feature request and waits its turn.