CSV Diff
Two slots: the original file on one side, the updated file on the other. Rows are paired up on the column that identifies them, never on what line they happen to sit on, so re-sorting a file between exports produces exactly zero false differences. Both files stay in this tab.
Comparing more than two files, or joining them instead? Open the app
The question behind every CSV comparison
It is always some version of "what moved since last time, and who moved it". The files change, the question does not:
- Yesterday's export against today's. Plenty of systems keep no usable audit log, so a nightly CSV dump is the only history anyone has.
- A supplier's new price list. Nine hundred lines arrive, thirty of them matter, and the covering email says "minor updates". You need the thirty.
- Migration checks. Export from the old system, export from the new one, and prove nothing was dropped, truncated or silently retyped on the way across.
- A spreadsheet that went out and came back. Someone edited it, saved it under a new name, and cannot remember which cells they touched.
- Two environments that should agree. Reference tables, feature flags, tax rates, plan definitions. Staging and production drift apart quietly and get compared only after something breaks.
- Before and after a bulk update. You ran an update on 4,000 rows, and now you have to show it touched those 4,000 and nothing else.
Most comparison tools answer a different question. They treat a CSV as a stack of lines and report which lines differ, which is a fine answer for source code and a useless one for data. Change the sort order in the export screen and every line has moved. The usual workaround is to sort both files first, which only holds if the sort key and the collation match on both sides, and across two systems they often do not.
Worked example: three real changes hidden in a re-sorted file
Original file, accounts-q1.csv:
id,name,plan,seats
E-100,Ada,Team,5
E-101,Grace,Free,1
E-102,Alan,Team,3
E-103,Katherine,Pro,12
Updated file, accounts-q2.csv. Grace upgraded, Radia is new, Alan is gone, and the export came out in a completely different order:
id,name,plan,seats
E-103,Katherine,Pro,12
E-104,Radia,Team,4
E-101,Grace,Pro,1
E-100,Ada,Team,5
Line up those two files position by position and not one of the four data lines matches its opposite number, so a line-based comparer flags the entire file and leaves you exactly where you started. Drop them into the two slots above, leave the key on id, and the download is:
_status,_changed_columns,id,name,plan,seats,plan__was
added,,E-104,Radia,Team,4,
changed,plan,E-101,Grace,Pro,1,Free
removed,,E-102,Alan,Team,3,
Three rows for three changes. Above the table the widget reads: 1 added, 1 removed, 1 changed, 2 unchanged, matched on id. Ada and Katherine are the two unchanged rows, and they are absent from the download because the default is to list differences only. Grace's row carries her new plan in the plan column and her old one in plan__was, and _changed_columns names the single field that moved. Notice there is no seats__was or name__was: those columns were compared everywhere and never differed, so no column was created for them.
Switch the download to Include unchanged and the same comparison gives you a full reconciliation instead, every account accounted for:
_status,_changed_columns,id,name,plan,seats,plan__was
unchanged,,E-103,Katherine,Pro,12,
added,,E-104,Radia,Team,4,
changed,plan,E-101,Grace,Pro,1,Free
unchanged,,E-100,Ada,Team,5,
removed,,E-102,Alan,Team,3,
Rows come out in the updated file's own order, with anything that disappeared listed at the end. Sort either file differently and feed them back in: the same three changes, every time.
How the rows are paired up
The mechanism is short enough to describe in full, which is a good property for something you are about to trust with an audit:
- Both files are parsed in the tab, each with its delimiter sniffed from its own contents, so a comma file and a semicolon file compare happily.
- Every column name the two files have in common becomes a candidate key, offered as checkboxes under the result. Until you touch them the first shared column is the key, which is why a file whose first column is
idneeds no setup at all. - Each file is indexed on the key values. Every row of the updated file is then looked up in the original: no match means added, a match with every comparable column identical means unchanged, and a match with at least one difference means changed.
- Keys that exist in the original and never appear in the updated file are the removed rows.
- Padding is forgiven by default, nothing else is. Bank and ERP exports pad cells to a fixed width, so
100against100is not a difference unless you switch Whitespace to Exact. Case, on the other hand, is respected unless you ask for Ignore case, and there is no date parsing at all. - Numbers can be given slack. Set a tolerance of
0.01and1.0against1, or two floats that drifted in the last decimal, stop counting as changes. Leave it empty and numbers are compared as text, which is what someone auditing an export usually wants. - Columns can be left out of the comparison. An
updated_atstamp or a row hash changes on every export and buries the real differences; tick it under Ignore columns and it stays in the output without ever making a row "changed". Key columns cannot be ignored, since they are what matching means. - Tick a different key column, tolerance or ignore setting and the comparison reruns at once on the files you already provided, since nothing was uploaded to re-upload.
Reading the diff file
_statusis one of added, removed, changed or unchanged. Filter or pivot on it and you have your summary._changed_columnslists the field names that moved on that row, separated by spaces, and is empty for every other status. Sorting by it groups rows that changed in the same way, which is how one bad import that touched forty records identically gives itself away.- The data columns follow the original file's order, with anything the updated file introduced appended at the end. Values come from the updated file where the row exists there, and from the original for removed rows.
- The
__wascolumns hold the previous value, and only exist for fields that changed somewhere in the file. They are blank on added, removed and unchanged rows. - The filename comes from the original file.
accounts-q1.csvdownloads asaccounts-q1-diff.csv, so a folder of these stays readable months later. - A clean comparison says so. When nothing differs the widget leads with "No differences" and the download is the header row alone, which is a fine artefact to attach to a sign-off.
Gotchas worth knowing
- Duplicate keys are compared once and announced. The first row holding a key is the one used, the rest are skipped, and a warning says which file repeated how many keys. Ignoring that message is how a diff quietly under-reports.
- One key column is often not enough. Stock rows keyed on
skualone collapse as soon as the same product appears for two regions. Tickregionas well and the summary switches to matched on region + sku, with the duplicate warning gone. - Ticking every shared column as the key leaves nothing to compare, so rows can only come out added or removed. The widget warns when you have done it, because that result looks alarming otherwise.
- Columns that exist on one side only ride along in the output and get named in a warning, but they are never compared. A column added between two exports has no old value to have changed from.
- No shared column names means no comparison. The widget stops and says so rather than guessing, and the usual cause is a file missing its header row.
- 100 MB per file, both read locally. Two files that size sit in the tab at once, so a machine short on memory will feel it.
Frequently Asked Questions
Do the two files have to be sorted the same way?
No, and that is the whole point of matching on a key. Line numbers are never consulted. Reverse one of the files, sort it by a different column, or let a database hand the rows back in whatever order it likes, and the added, removed and changed counts come out identical.
What happens if the key column has duplicates?
The first row carrying a given key is the one compared, later rows with that key are skipped, and a warning names the file and how many keys repeated. The fix is almost always a second key column: an order line is identified by order id plus line number, not by order id on its own.
Can I match on more than one column?
Yes. The key picker is a set of checkboxes over every column name the two files share, and ticking several makes rows match only when all of them agree. Comparing stock levels keyed on region plus sku, the summary line reads matched on region + sku, and the two rows that share a sku no longer collide.
What are the __was columns in the download?
The value a field held before it changed. A row shows the updated file's value in the normal column and the previous one in the matching __was column. Only fields that actually moved somewhere in the file get one, so a hundred-column file where two things changed produces two extra columns rather than a hundred empty ones.
Do both files need exactly the same columns?
No. Column names present in both are compared; a column that exists on only one side is carried into the output but never compared, and a warning names it. The one hard requirement is at least one shared column name, since without that there is no way to decide what counts as the same row.
Are my files uploaded anywhere?
No. Both files are read in your tab, matched there, and the diff CSV is assembled there. There is no upload endpoint behind this page, no account, and nothing kept after you close it. That matters more than usual when the files being compared are customer records or payroll.
Related
Compare two CSV files
Free, no account, nothing uploaded. Drop both files in, pick the column that identifies a row, and read the three numbers that matter.
Back to the comparison