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

subs
cust_idplanupdated_at
7pro10:05
7free10: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;
result
cust_idplan
7pro

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.

subs
sub_idplanupdated_at
1pro2024-05-01 10:00
2free2024-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;
result — flickers
cust_idplan
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;
result
cust_idplan
7free

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, GROUP BY, and QUALIFY dedup differentlyopen the unit →

DISTINCT and GROUP BY collapse rows; DISTINCT ON and QUALIFY keep a chosen row — each dedups differently and costs differently.

Deduping after a join can't undo the damageopen the unit →

If a join has already multiplied rows, deduping afterward can't restore the original counts — dedup at the source grain first.

Joining back on MAX re-introduces tiesopen the unit →

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.

Soft duplicates need a survivorship ruleopen the unit →

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

3 of 3 free questions left

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 →
The P13 pattern pageWhen a join multiplies your rowsRanking with window functions