How to compare two CSV files and find what changed
You have two exports of the same report, a week apart, and one question: what changed? Eyeballing works for a few dozen rows. A cell-by-cell diff shows which cells differ, but not which records were added or dropped. SQL answers both.
Decide what identifies a row
Every useful comparison starts with a key: the column, or columns, that identify the same record in both files. Usually that’s an order ID or a customer ID. Sometimes it’s a date plus a product code.
Without a key you can tell that something changed, but not what. Pick the key first. Most of the work is right there.
Find rows that exist in only one file
A FULL OUTER JOIN on the key lines up both versions side by side. Whichever side comes back null tells you whether the row was added or removed.
SELECT
COALESCE(a.order_id, b.order_id) AS order_id,
CASE
WHEN a.order_id IS NULL THEN 'added'
WHEN b.order_id IS NULL THEN 'removed'
ELSE 'in both'
END AS status
FROM week_1 a
FULL OUTER JOIN week_2 b
ON a.order_id = b.order_id
WHERE a.order_id IS NULL OR b.order_id IS NULL;Find rows that changed
For rows in both files, compare the value columns. Use IS DISTINCT FROM, not <>. Plain inequality returns null when either side is null, so real changes drop out of your results without a trace.
SELECT
a.order_id,
a.status AS old_status,
b.status AS new_status,
a.revenue AS old_revenue,
b.revenue AS new_revenue
FROM week_1 a
JOIN week_2 b
ON a.order_id = b.order_id
WHERE a.status IS DISTINCT FROM b.status
OR a.revenue IS DISTINCT FROM b.revenue;Get the summary first
Count rows per change type before you read any detail. Three new rows is a normal week. Four thousand removed rows means the export broke.
SELECT
CASE
WHEN a.order_id IS NULL THEN 'added'
WHEN b.order_id IS NULL THEN 'removed'
WHEN a.revenue IS DISTINCT FROM b.revenue THEN 'changed'
ELSE 'unchanged'
END AS change_type,
COUNT(*) AS rows
FROM week_1 a
FULL OUTER JOIN week_2 b
ON a.order_id = b.order_id
GROUP BY change_type
ORDER BY rows DESC;Watch for fake differences
Most surprising diffs turn out to be formatting. Trailing spaces, a new date format, or numbers exported as text in one file will all show up as changes.
Normalize inside the comparison. The noise goes away and both source files stay untouched.
SELECT a.order_id
FROM week_1 a
JOIN week_2 b
ON a.order_id = b.order_id
WHERE TRIM(LOWER(a.customer_name)) IS DISTINCT FROM TRIM(LOWER(b.customer_name));Skip the SQL entirely
NoEgress has a compare view. Pick two opened tables and a key column, and it shows you the added, removed, changed, and unchanged rows. It runs the same kind of query as above, locally, so neither export gets uploaded.
Frequently asked
How do I find rows added or deleted between two CSV files?
FULL OUTER JOIN the files on a key column. A null on the left means the row was added. A null on the right means it was removed.
Why does my comparison miss rows where a value became null?
Because `a <> b` returns null when either side is null, and the row gets filtered out. Use `IS DISTINCT FROM`, which compares nulls like any other value.
Can I compare two files without uploading them anywhere?
Yes. Both files load into DuckDB in your browser, and the comparison runs on your machine.
More guides
NoEgress vs Excel, Power Query, ChatGPT and DuckDB
How to use ChatGPT-style AI on sensitive spreadsheets without uploading them
An Excel alternative for files with millions of rows
How to query a CSV file with SQL
How to open large CSV files Excel can't handle
How to run SQL on an Excel spreadsheet
How to analyze confidential data without uploading it
How to check if a website uploads your file
Data quality checks to run before trusting a dataset
How to query a JSON file with SQL
DuckDB online: run DuckDB in your browser with nothing to install
How to ask a local AI model about your CSV or Excel data
How to remove duplicate rows from a CSV or Excel file
How to split a CSV or Excel file into multiple files by column