This isn’t the same problem as the delete-duplicates recipe. That one removes exact repeat rows. This one keeps the single most recent row per key from a table where every row is legitimately different — a change log, an events table, a slowly-changing dimension — and it’s a SELECT, not a DELETE.
-- MySQL 8+, PostgreSQL, SQLite 3.25+ — no QUALIFY, wrap ROW_NUMBER in a subquery/CTE
WITH ranked AS (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY updated_at DESC) AS rn
FROM customer_events
)
SELECT * FROM ranked WHERE rn = 1;
-- BigQuery, Snowflake, DuckDB — QUALIFY filters the window function inline
SELECT *
FROM customer_events
QUALIFY ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY updated_at DESC) = 1;
To persist the result as its own “latest” table instead of re-running the window function every time, INSERT INTO ... SELECT works identically in all six dialects:
DELETE FROM customers_latest;
INSERT INTO customers_latest
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY updated_at DESC) AS rn
FROM customer_events
) t
WHERE rn = 1;
PARTITION BYpicks the key,ORDER BY ... DESCpicks the winner. SwapORDER BY updated_at DESCfor whatever “latest” means for your data —ORDER BY version DESCorORDER BY created_at DESC, id DESC. Always add a unique tiebreaker (likeid) after the main sort column: two rows sharing the exact sameupdated_atotherwise get an arbitrary winner, which can flip between runs. And notern = 1(notrn <= 1) — for the top N survivors per group instead of just one, see the top-N-per-group recipe, which is the same pattern with<= N.