Compare two CSV files

How to compare two CSV files and see what changed

To compare two CSV files in rows.page, push them as version 1 and version 2 of the same link, up to 100 MB each, and a banner counts new, gone and changed rows by key. It's free with no signup. rows.page compares versions of one hosted dataset, not two files on your computer.

.csv
Drop a file here
Stays in your browserUp to 1.5 GB

No file handy? Open CO2 emissions by country and year (50k rows, 14 MB).

Line diffs vs row diffs matched by a key

diff old.csv new.csv and git diff compare a CSV line by line, from top to bottom. If an export comes out in a different order, almost every line looks changed, and an edited row shows up as one line removed and one added.

Matching rows on a key column such as order_id or email fixes both problems. Each record is found wherever it sits in the file, and every difference lands in one of three groups: keys that are new, keys that are gone, and keys whose values changed.

CSV comparison methods
MethodMatches rows by keyHandles reordered rowsSetup
rows.page version diffYes, with X-Rows-Id-Column or a key picked in the bannerYescurl and a browser
DuckDB EXCEPTNo, compares whole rowsYesInstall the duckdb CLI
DuckDB FULL JOIN on a keyYesYesInstall the duckdb CLI
diff or git diffNo, compares linesNodiff comes with macOS and Linux

Compare two versions of a CSV in rows.page

rows.page compares versions of one hosted dataset: you push the old file, then push the new file to the same link. Name the key column on the first push, and use the rpt_ token it prints for the second:

bash
curl -T old.csv -H "X-Rows-Id-Column: order_id" https://rows.page
curl -T new.csv -H "Authorization: Bearer rpt_..." https://rows.page/<id>
  1. The first push creates v1 and prints the link (https://rows.page/<id>), an owner link, a summary line and the rpt_ token.
  2. The second push adds v2 to the same link. Its reply lists the change in rows and columns, with a link to the diff.
  3. Open https://rows.page/<id>. The banner reads "+N new, N gone, N changed vs v1", matched by order_id.

A row counts as changed when its key is in both versions and any shared column differs. Without a key, the banner says "No unique column to match rows, so changes show as new and gone." and suggests columns to pick. You can also set the key in the address with #key=<column>.

The banner shows counts, not a row-by-row list or highlighted cells. If the previous version is over 64 MB, the comparison waits until you click "See what changed". To compare against an older version, add #diff=<n> to the address.

How long rows.page keeps versions

  • No account: up to 20 versions per link, and the link expires after 7 days.
  • Free ($0): sign in and choose Keep on my account to keep up to 10 datasets; the last 3 versions stay visible and older ones are purged after 90 days.
  • Pro ($12 per month): 100 datasets, private datasets and 90 days of version history.
  • Max ($39 per month): 1,000 datasets, private datasets and 1 year of version history.

A single push takes up to 100 MB on every plan. Pro and Max take files up to 500 MB and 2 GB through multipart uploads.

Compare two local CSV files with DuckDB

rows.page can't compare two files on your computer. To keep both files local, run these queries in the DuckDB command-line tool. Both expect the same columns in both files.

Rows that differ: EXCEPT

sql
SELECT * FROM 'new.csv' EXCEPT SELECT * FROM 'old.csv';
SELECT * FROM 'old.csv' EXCEPT SELECT * FROM 'new.csv';

The first query lists rows that are new or edited, as they look now. The second lists rows that were deleted or edited, as they looked before. Row order doesn't matter, but an edited row appears in both results.

Changes by key: FULL JOIN

sql
SELECT order_id,
  CASE WHEN o.order_id IS NULL THEN 'new'
       WHEN n.order_id IS NULL THEN 'gone'
       ELSE 'changed' END AS change
FROM 'old.csv' AS o
FULL JOIN 'new.csv' AS n USING (order_id)
WHERE o IS DISTINCT FROM n
ORDER BY order_id;

This lists every key that differs with its change, the same three groups as the rows.page banner. Add o.*, n.* after AS change to see the old and new values side by side.

Compare two Excel files

rows.page doesn't read .xlsx or .xls files. If you push one, you get "This looks like an Excel or zip file. Export it as CSV and push again."

In Excel, save the sheet from each workbook with File > Save As as CSV UTF-8. Then push the old CSV as v1 and the new one as v2 with the curl commands above, or compare them locally with DuckDB.

rows.page limits and prices

WhatLimit or price
Open a file in your browserFiles up to 1.5 GB, $0, no account, nothing uploaded
Share a link without an account$0, files up to 100 MB, link deleted 7 days after creation
Free account$0, 10 datasets kept, last 3 versions
Pro$12/month, 100 datasets, files up to 500 MB, private links
Max$39/month, 1,000 datasets, files up to 2 GB, private links

Questions

Can I compare two local CSV files in rows.page without uploading them?

No. rows.page compares versions of one hosted dataset, so both files have to be pushed to the same link. To compare files that stay on your computer, run the DuckDB EXCEPT or FULL JOIN queries in this guide.

Does the version diff work for TSV, JSON or Parquet?

Yes. You can push CSV, TSV, JSON, JSONL or Parquet versions with the same curl commands, and the banner compares them the same way.

How do I compare CSV exports in a CI script?

Sign in on any plan, including Free, and create an rpk_ API key (up to 20 per account). Push with curl --data-binary @report.csv -H "Authorization: Bearer rpk_..." "https://rows.page/api/v1/push?key=nightly-report", and each run adds a version to the same dataset.

What if my CSV uses semicolons or a Windows encoding?

rows.page detects the delimiter. A file that isn't valid UTF-8 opens as Latin-1 with the note "Read as Latin-1 text.", but Windows characters such as the euro sign or curly quotes make it fail with "Couldn't open this data". Save the file as CSV UTF-8 before you push it.

Can I download an older version later?

Yes. https://rows.page/<id>/v<n>/raw.<ext> serves version n and https://rows.page/<id>/raw.<ext> the latest, as long as your plan keeps that version: 7 days without an account, the last 3 versions on Free, 90 days on Pro and 1 year on Max.