Guide

How to compare two CSV files and find what changed

Two exports of the same report, taken a week apart, and the question is always the same: what actually changed? Eyeballing them fails past a few dozen rows, and a spreadsheet diff tells you which cells differ without telling you which records were added or dropped.

Decide what identifies a row

Every meaningful comparison starts with a key — the column, or set of columns, that identifies the same real-world record in both files. Usually an order ID, a customer ID, or a date plus a product code.

Without a key you can only compare files as unordered bags of rows, which detects that something changed but never what. Establishing the key first is the whole job.

Find rows that exist in only one file

A FULL OUTER JOIN on the key puts both versions side by side and makes additions and removals fall out of which side is null.

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 exist in both but changed

For rows present on both sides, compare the value columns. Use IS DISTINCT FROM rather than <> — plain inequality returns null when either side is null, so genuinely changed rows silently vanish from the results.

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;

Summarize before reading the detail

A count per change type tells you whether you are looking at a routine update or something went wrong upstream. Three added rows is a normal week; four thousand removed rows is a broken export.

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 false differences

Most surprising diffs are formatting, not data. Trailing whitespace, a changed date format, and numbers exported as text in one file but not the other all register as changes.

Normalizing inside the comparison keeps both source files untouched while removing the noise.

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));

Doing it without writing the SQL

NoEgress has a dataset comparison view that takes two opened tables and a key column and produces the added, removed, changed, and unchanged breakdown directly. It runs the same kind of query described above, locally, so comparing two confidential exports never involves uploading either one.

Frequently asked

How do I find rows added or deleted between two CSV files?

FULL OUTER JOIN the two files on a key column. Rows where the left side is null were added; rows where the right side is null were removed.

Why does my comparison miss rows where a value became null?

Because `a <> b` evaluates to null when either side is null, so the row is filtered out. Use `IS DISTINCT FROM`, which treats null as a comparable value.

Can I compare two files without uploading them anywhere?

Yes. Both files are read into a local DuckDB session in the browser, and the comparison runs on your machine.

Try it on your own file

More guides

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

Data quality checks to run before trusting a dataset

How to query a JSON file with SQL