How to Remove Duplicates in Google Sheets Automatically (and Keep the Right Row)
Duplicates in a Google Sheet are rarely just untidy. In a product list they mean the same item rings up at two prices. In a CRM sheet they mean two salespeople call the same lead. Removing them by hand works once; then they come back.
We built the approach below for a retailer with 3,259 product records spread over two 10,000-row sheets and 1,013 duplicate barcodes. Cleaning it took two people a full day. The script now does it in minutes and only asks a person to look at the genuine conflicts.
Why the built-in tool isn’t enough
Sheets has Data → Data cleanup → Remove duplicates, which deletes rows that match exactly. Real duplicates usually don’t match exactly: the same barcode has a different price, or the same email has a different phone number. You need to decide which row wins, and you want a record of what was different.
The rule we use
- Group rows by a key: barcode, SKU, or lowercase email.
- Keep the most recently updated row for each key.
- If the duplicates disagree on something that matters, such as price, log them to a Review tab for a human decision.
- Write the clean list to a separate Clean tab, so the original data is never destroyed.
The script
Open Extensions → Apps Script in your sheet and paste this. It assumes a Products tab with Barcode, Price and Updated columns; change the names to match yours.
function resolveDuplicates() {
const ss = SpreadsheetApp.getActive();
const rows = ss.getSheetByName('Products').getDataRange().getValues();
const head = rows.shift();
const iKey = head.indexOf('Barcode');
const iPrice = head.indexOf('Price');
const iDate = head.indexOf('Updated');
const latest = new Map(); // key -> newest row
const conflicts = [];
rows.forEach((r) => {
const key = String(r[iKey]).trim();
if (!key) return;
const seen = latest.get(key);
if (!seen) { latest.set(key, r); return; }
if (seen[iPrice] !== r[iPrice]) conflicts.push([key, seen[iPrice], r[iPrice]]);
if (new Date(r[iDate]) > new Date(seen[iDate])) latest.set(key, r);
});
writeTab_(ss, 'Clean', [head, ...latest.values()]);
writeTab_(ss, 'Review', [['Barcode', 'Price A', 'Price B'], ...conflicts]);
}
function writeTab_(ss, name, data) {
const sh = ss.getSheetByName(name) || ss.insertSheet(name);
sh.clearContents();
if (data.length) sh.getRange(1, 1, data.length, data[0].length).setValues(data);
}
Because it reads and writes the whole range in one go, this handles tens of thousands of rows comfortably inside Apps Script’s 6-minute limit.
Making it stick
- Run it on a schedule. Add a nightly time-driven trigger so duplicates never build up again.
- Normalise keys first. Trim spaces, lowercase emails and strip leading zeros from barcodes if your systems do that inconsistently. Most “missed” duplicates are formatting.
- Merge sheets the same way. To combine two sources, read both ranges into one array before the loop. The newest row still wins.
- Keep an audit trail. Add a timestamp column to the Review tab so you can see when each conflict appeared.
Beyond barcodes
The same pattern de-duplicates CRM contacts by email, merges supplier price lists, and cleans enriched lead lists before they reach a sales team. On one lead project we enriched 13,616 businesses; de-duplicating first saved paying for the same lookup twice.
Got a sheet that keeps filling with duplicates? Book a free Workflow Audit and we’ll show you what a permanent fix looks like.