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.

.csv.tsv
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).

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:

sql
SELECT email, count(*) AS cnt
FROM data
GROUP BY email
HAVING count(*) > 1
ORDER BY cnt DESC

To 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:

sql
SELECT * EXCLUDE (__rid), count(*) AS cnt
FROM data
GROUP BY ALL
HAVING count(*) > 1

Remove 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

sql
SELECT DISTINCT * EXCLUDE (__rid) FROM data

DISTINCT doesn't keep the original row order. To keep each row where it first appeared, use this instead:

sql
SELECT * EXCLUDE (first_rid)
FROM (SELECT * EXCLUDE (__rid), min(__rid) AS first_rid FROM data GROUP BY ALL)
ORDER BY first_rid

Keep the latest row per key

sql
SELECT * EXCLUDE (__rid)
FROM data
QUALIFY row_number() OVER (PARTITION BY email ORDER BY updated_at DESC) = 1
ORDER BY __rid

Keep the first row per key

sql
SELECT * EXCLUDE (__rid)
FROM data
QUALIFY row_number() OVER (PARTITION BY email ORDER BY __rid) = 1
ORDER BY __rid

The 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:

sql
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:

sql
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

Ways to remove duplicates from a CSV
MethodKeeps the header on topKeeps row orderKey duplicates
rows.page SELECT DISTINCTYesNoNo
rows.page QUALIFYYesYes, with ORDER BY __ridYes
DuckDB command lineYesNoNo
Excel Remove DuplicatesYesYesYes, keeps the first
awk '!seen[$0]++'YesYesNo, whole lines only
sort -uNo, the header gets sorted inNoNo

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:

bash
awk '!seen[$0]++' in.csv > out.csv

sort -u

sort -u in.csv also drops duplicate lines, but it sorts the header into the data. Keep the header out of the sort:

bash
(head -n 1 in.csv && tail -n +2 in.csv | sort -u) > out.csv

Both 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

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

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.