P13 · SQL & Querying
SQL deduplication — the flickering latest-row bug
How to keep one row per entity in SQL — and the trap where two rows share a timestamp, so 'the latest' flickers between refreshes with no error.
Start with the basics
Deduplication is picking one row per entity when a table has several — the latest subscription per customer, one row per order after a messy load.
The usual tool is a window function: rank the rows per entity by some order, then keep the first. The order decides which row wins.
It reads as solved, and it is — as long as the ordering reaches a unique tiebreaker. When two rows tie on the ordering value, 'first' becomes a coin flip.
One that works
| cust_id | plan | updated_at |
|---|---|---|
| 7 | pro | 10:05 |
| 7 | free | 10:02 |
SELECT cust_id, plan FROM (
SELECT cust_id, plan,
ROW_NUMBER() OVER (PARTITION BY cust_id
ORDER BY updated_at DESC) rn
FROM subs
) t WHERE rn = 1;| cust_id | plan |
|---|---|
| 7 | pro |
Distinct timestamps make 'latest' well-defined — the 10:05 row wins.
To keep one row per entity you rank the rows and take the first — but the ranking must break every tie, or 'first' is arbitrary.
When to use it
- Reach for dedup when a table has more than one row per entity and you want exactly one.
- Rank per entity with a window function, then keep the first — and make the order end in a unique key.
- Prefer a window over joining back on a MAX timestamp; the window is cheaper and avoids re-introducing ties.
That rule — the order must reach a unique tiebreaker — is exactly what a shared timestamp breaks. When two rows tie on 'latest', the query picks one arbitrarily, and the answer flickers between runs. Here is the day that bites.
See it happen
The 'latest row' that flickers — a tie with no tiebreaker
A customer has two subscription rows updated at the exact same instant. You take the latest. Watch how the query picks one arbitrarily — a different one on a different run.
| sub_id | plan | updated_at |
|---|---|---|
| 1 | pro | 2024-05-01 10:00 |
| 2 | free | 2024-05-01 10:00 |
ORDER BY updated_at — ties are arbitrary
SELECT cust_id, plan FROM (
SELECT cust_id, plan, ROW_NUMBER() OVER (
PARTITION BY cust_id
ORDER BY updated_at DESC) rn
FROM subs
) t WHERE rn = 1;| cust_id | plan |
|---|---|
| 7 | 'pro' or 'free' |
add a unique tiebreaker — deterministic
SELECT cust_id, plan FROM (
SELECT cust_id, plan, ROW_NUMBER() OVER (
PARTITION BY cust_id
ORDER BY updated_at DESC, sub_id DESC) rn
FROM subs
) t WHERE rn = 1;| cust_id | plan |
|---|---|
| 7 | free |
The customer's plan flickers between refreshes of the same table — the bug that can't be reproduced, because both answers are consistent with the query.
Walk the full example in the unit →The rest of the family
More ways a dedup picks the wrong row
DISTINCT and GROUP BY collapse rows; DISTINCT ON and QUALIFY keep a chosen row — each dedups differently and costs differently.
If a join has already multiplied rows, deduping afterward can't restore the original counts — dedup at the source grain first.
Finding the latest timestamp and joining back on it re-creates the tie problem and adds a second scan; a window function is usually cheaper and safer.
When two rows describe one entity with conflicting values, you resolve them field by field with an explicit rule, not a blind pick.
Self-check
How to spot it in your own SQL
- A 'latest record' result changes between runs with no data change.
- You dedup by ranking on a timestamp that can repeat.
- You join back on a MAX timestamp to find the latest row.
- You deduped after a join and the counts still look inflated.
What people miss
Deduplication looks done the moment the row count drops. The bug hides in the ties: a 'latest' with no unique tiebreaker is a coin flip that lands the same in dev and differently in prod — impossible to reproduce, easy to ship.
Go deeper
Ask Colearn
Instant answers, grounded in the same verified material the diagnostic grades against.
Why does my 'latest row per customer' change between runs?
Two rows share the same timestamp, so ordering by that timestamp leaves a tie — and the query breaks the tie arbitrarily, differently on different runs.
Add a unique column (like the row's id) to the end of the ORDER BY so 'latest' is fully determined.
from the unit →Should I use DISTINCT, GROUP BY, or a window to dedup?
DISTINCT and GROUP BY collapse duplicate rows but can't keep a specific one. To keep a chosen row per entity, rank with a window function (or use DISTINCT ON / QUALIFY where available).
Pick based on whether you need to keep a particular row or just remove exact duplicates.
from the unit →Why is joining back on MAX(updated_at) a bad idea?
Finding the max timestamp and joining back on it re-introduces the same ties (two rows can share the max) and scans the table twice.
Rank the rows with a window function and keep rank 1 instead — one pass, and you control the tiebreak.
from the unit →I deduped after a join but the totals are still too high — why?
A join can multiply rows before you dedup; once the numbers are inflated, deduping the result can't recover the original per-entity counts.
Dedup (or pre-aggregate) at the source grain before the join, not after.
from the unit →That’s the free taste