Why rewrite instead of delete
The instinct when deduplicating is to loop through the sheet and call deleteRow on each duplicate. That works, but it has a sharp edge: every call to deleteRow shifts all rows below it up by one. If you delete row 3, what was row 4 is now row 3, and your loop index is already pointing at the new row 4. The safe workaround people reach for is iterating bottom-to-top, which works but means holding the full duplicate set in memory anyway, making one API call per deleted row.
The approach here sidesteps the problem entirely. Read everything once with getDataRange().getValues() — a single API call that returns a 2D array. Build the deduplicated array in JavaScript using a Set to track which row fingerprints you have already seen. Then clear the sheet and write the result back with one setValues call. Two API calls total, regardless of how many duplicates you had. For a 10,000-row sheet, this is the difference between a script that finishes in two seconds and one that times out at the six-minute Apps Script execution limit.
The row key is built with data[i].join('|'). The pipe character works as a separator in practice; if your data actually contains pipe characters in every column, use a multi-character sentinel like '||~||' to eliminate false collisions. I have watched this bite people on CRM exports where one column was a phone number formatted as +1|555|0100 — the joined key collides across rows that are genuinely distinct.