Remove duplicate rows
How to remove duplicate rows from a CSV file
rows.page removes duplicate rows from a CSV of up to 1.5 GB with one SQL query and exports up to 1,000,000 clean rows. It's free with no signup, and the file stays in your browser unless you click Share.
No file handy? Open CO2 emissions by country and year (50k rows, 14 MB).
Decide what counts as a duplicate
A whole-row duplicate matches the row above it in every column. A key duplicate repeats one column, like email, while other columns differ. If the same customer appears twice with two timestamps, a whole-row check keeps both, so for key duplicates you also pick which row survives: the latest update or the first one in the file.
Find duplicates before you delete them
Drop your CSV on https://rows.page and type SQL in the search box; the table is called data. This lists every email that appears more than once:
SELECT email, count(*) AS cnt
FROM data
GROUP BY email
HAVING count(*) > 1
ORDER BY cnt DESCTo find whole-row duplicates, group by every column. Leave out __rid, the hidden row number rows.page adds to each row, or every row counts as unique:
SELECT * EXCLUDE (__rid), count(*) AS cnt
FROM data
GROUP BY ALL
HAVING count(*) > 1Remove duplicates in rows.page
Run one of the queries below, then open the Export menu and choose CSV. The export has the header row, commas and UTF-8 without a BOM, and __rid is left out.
Drop exact duplicate rows
SELECT DISTINCT * EXCLUDE (__rid) FROM dataDISTINCT doesn't keep the original row order. To keep each row where it first appeared, use this instead:
SELECT * EXCLUDE (first_rid)
FROM (SELECT * EXCLUDE (__rid), min(__rid) AS first_rid FROM data GROUP BY ALL)
ORDER BY first_ridKeep the latest row per key
SELECT * EXCLUDE (__rid)
FROM data
QUALIFY row_number() OVER (PARTITION BY email ORDER BY updated_at DESC) = 1
ORDER BY __ridKeep the first row per key
SELECT * EXCLUDE (__rid)
FROM data
QUALIFY row_number() OVER (PARTITION BY email ORDER BY __rid) = 1
ORDER BY __ridThe final ORDER BY __rid puts the surviving rows back in file order. For a key made of several columns, list them all after PARTITION BY. To match keys regardless of case and stray spaces, such as Ann@x.com and ann@x.com, partition by lower(trim(email)) instead of email.
Remove duplicates from files over 1.5 GB with DuckDB
A CSV over 1.5 GB loads only partly in rows.page, with the note "Loaded the first N rows: the file is X, more than the browser can hold." SQL results stop at 1,000,000 rows. Past either limit, run the DuckDB command-line tool on your computer, where there is no __rid column:
COPY (SELECT DISTINCT * FROM 'in.csv') TO 'out.csv' (HEADER);Like DISTINCT in rows.page, this doesn't keep the original row order. To keep the latest row per key instead, use the same QUALIFY clause on the file:
COPY (
SELECT * FROM 'in.csv'
QUALIFY row_number() OVER (PARTITION BY email ORDER BY updated_at DESC) = 1
) TO 'out.csv' (HEADER);Remove duplicates in Excel or on the command line
| Method | Keeps the header on top | Keeps row order | Key duplicates |
|---|---|---|---|
rows.page SELECT DISTINCT | Yes | No | No |
rows.page QUALIFY | Yes | Yes, with ORDER BY __rid | Yes |
| DuckDB command line | Yes | No | No |
| Excel Remove Duplicates | Yes | Yes | Yes, keeps the first |
awk '!seen[$0]++' | Yes | Yes | No, whole lines only |
sort -u | No, the header gets sorted in | No | No |
Excel
Select the table and click Data > Remove Duplicates. Tick every column to drop exact duplicates, or only your key columns to drop key duplicates. Excel keeps the first occurrence, so sort newest first beforehand if you want the latest row to survive.
rows.page doesn't open .xlsx or .xls files. Copy the cells in Excel or Google Sheets and click Paste data instead; they arrive tab-separated and open as a table.
awk
This prints each line the first time it appears, so the header stays on top and the order is kept:
awk '!seen[$0]++' in.csv > out.csvsort -u
sort -u in.csv also drops duplicate lines, but it sorts the header into the data. Keep the header out of the sort:
(head -n 1 in.csv && tail -n +2 in.csv | sort -u) > out.csvBoth awk and sort compare raw text, so a stray space or different quoting makes two rows look different, and they can't keep the latest row per key.
rows.page limits and prices
| What | Limit or price |
|---|---|
| Open a file in your browser | Files 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
Why does SELECT DISTINCT * FROM data keep duplicate rows in rows.page?
Every row in rows.page has a hidden __rid row number, which makes each row unique. Write SELECT DISTINCT * EXCLUDE (__rid) FROM data so only your own columns are compared.
Is my CSV uploaded when I remove duplicates in rows.page?
No. The file is read and queried inside your browser with DuckDB-WASM. Nothing is uploaded unless you click Share or push it with curl -T data.csv https://rows.page.
How big can the CSV be?
Up to 1.5 GB per browser tab, with no row limit in the grid. A SQL result, and so a deduplicated export, stops at 1,000,000 rows; for more, use the DuckDB command line.
Does rows.page open semicolon-separated or non-UTF-8 CSV files?
It detects the delimiter, semicolons included. A file that isn't valid UTF-8 is read as Latin-1, but Windows characters such as the euro sign or curly quotes make it fail with "Couldn't open this data"; re-save it as CSV UTF-8. Exports are always comma-separated UTF-8 without a BOM.
Can I export the deduplicated rows as Parquet or JSON?
Yes. The Export menu saves the SQL result, up to 1,000,000 rows, as CSV, Parquet (zstd), JSON, JSONL or a Markdown table of the first 1,000 rows.