Pair your devices with a code and playback position follows you: pause on this device, hit resume on the other. Position is saved to the site every minute and on pause.
Open this panel on your other device and enter the same code.
Starts this lesson and continues through 1 more to the end of the course.
Every data engineer writes these two constantly:
They're the same query. Both are solved with ROW_NUMBER(), and knowing that saves you from the
self-join mess most people write first.
-- Latest row per customer
SELECT *
FROM (
SELECT
*,
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY updated_at DESC, event_id DESC -- tie-breaker, see below
) AS rn
FROM customer_events
) t
WHERE rn = 1;
Read the window clause as a sentence: restart the numbering for each customer_id, order the rows
within that customer by updated_at descending, and number them. Row 1 is the latest. Filter to
rn = 1.
For top-N, change one character: WHERE rn <= 3.
⚠️ You cannot filter on a window function in WHERE directly. Window functions are evaluated
after WHERE — the query must be wrapped in a subquery or CTE. WHERE ROW_NUMBER() OVER (...) = 1 is
an error. This is a standard interview question and the reason is the evaluation order from lesson 3.
This is the part that gets skipped and then causes a non-deterministic pipeline.
-- BAD: if two rows share the same updated_at, which one is rn = 1?
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY updated_at DESC)
The answer is undefined. The engine may return either, and it may return a different one on each run — different plan, different parallelism, different result. You get a pipeline that produces slightly different output from identical input, which is one of the worst classes of bug to diagnose because it doesn't reproduce.
Always order by enough columns to be deterministic. Add a monotonic tie-breaker — a sequence, an event id, a file offset, an ingestion timestamp:
ORDER BY updated_at DESC, event_id DESC
If no such column exists, that is a real gap in your source data and worth raising, not papering over.
ROW_NUMBER vs RANK vs DENSE_RANKGiven scores 100, 90, 90, 80:
| Function | Result | Use it when |
|---|---|---|
ROW_NUMBER() |
1, 2, 3, 4 | You want exactly one row per group — dedup |
RANK() |
1, 2, 2, 4 | Ties share a rank and consume the next positions |
DENSE_RANK() |
1, 2, 2, 3 | Ties share a rank, no gaps |
Dedup always uses ROW_NUMBER(). If you use RANK() and there's a tie, you get two rows back for
rn = 1 and your "deduplicated" table still has duplicates — a delightful bug that only appears when
real data contains a tie.
"Top 3 including ties" is RANK() <= 3. "Exactly 3 rows" is ROW_NUMBER() <= 3. Interviewers ask
which you'd use and why; the answer is a question back — do you want exactly three, or everyone tied
for third?
DISTINCT is not deduplicationSELECT DISTINCT customer_id, name, email FROM customers;
This removes rows that are identical across every selected column. If the same customer appears
twice with a different updated_at, DISTINCT keeps both — and you still have duplicates by
customer_id.
⚠️ SELECT DISTINCT is the standard wrong fix for fan-out (lesson 2) and for dedup. It answers "are
these rows byte-identical?" which is almost never the question. The question is nearly always "which
row is the current one per key?" — and that requires ROW_NUMBER(), because you have to choose.
The tell: if you can't say which row DISTINCT kept, you weren't deduplicating, you were hoping.
QUALIFY — some engines support a clause that filters window results directly, removing the
subquery:
SELECT * FROM customer_events
QUALIFY ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY updated_at DESC) = 1;
⚠️ QUALIFY is not in the PostgreSQL documentation and is not universally available. I have not
verified which of Redshift, Athena/Trino, and Spark SQL support it — check your engine. The wrapped
subquery form works everywhere, so prefer it for portable code and use QUALIFY only where you've
confirmed support.
DISTINCT ON — a PostgreSQL extension that keeps the first row per group. Convenient, but it's a
PostgreSQL extension and Redshift's documentation explicitly warns against assuming shared semantics
(lesson 5). Don't reach for it in warehouse code.
Aggregate tricks — MAX(struct/row) or array_agg ... [1] patterns exist in some engines. They
can be faster than a sort-based window, but they're engine-specific and harder to read. Optimise to
them only with a measurement in hand.
Deduplication is exactly the kind of transformation that should be asserted, not trusted:
-- 1. The output must be unique by key. This must return zero rows.
SELECT customer_id, COUNT(*) FROM deduped GROUP BY customer_id HAVING COUNT(*) > 1;
-- 2. No keys were lost. These must be equal.
SELECT COUNT(DISTINCT customer_id) FROM customer_events;
SELECT COUNT(*) FROM deduped;
-- 3. Spot-check a key with known duplicates and confirm the right row won.
SELECT * FROM customer_events WHERE customer_id = '...' ORDER BY updated_at DESC;
Check 2 is the one people skip, and it catches the most damaging failure: a WHERE clause in the
inner query that silently drops keys entirely. Dedup should reduce rows, never keys.
NULLS clause in window functionsFrom Unsupported PostgreSQL features (verified 2026-08-12), the list of unsupported features includes:
"NULLS clause in Window functions"
So ORDER BY updated_at DESC NULLS LAST inside an OVER (...) is not available in Redshift. That
matters directly here: if updated_at can be NULL, you need to control where those rows sort
explicitly, for example by ordering on a coalesced expression or by adding a leading
ORDER BY (updated_at IS NULL) term — verify the exact form against your engine before shipping it.
⚠️ Don't let a NULL sort to the top of your dedup ordering and silently win. A NULL updated_at
becoming "the latest row" is a genuinely common production bug, and it produces a record with all the
newest fields missing.
WHERE ROW_NUMBER() OVER (...) = 1?RANK() and still have duplicates. Why?ORDER BY?SELECT DISTINCT the wrong tool for "one row per customer"?WHERE in the logical order of operations, so the alias
doesn't exist yet. Wrap it in a subquery or CTE and filter outside.RANK() gives tied rows the same rank, so a tie at the top produces two rows with rank 1. Dedup
needs ROW_NUMBER(), which is always unique within a partition.DISTINCT removes rows identical across all selected columns. Two rows for the same customer with
different timestamps are not identical, so both survive. It also can't express which row should
win.COUNT(DISTINCT key) in the source against COUNT(*) in the output. Dedup should reduce
rows but never lose keys.PARTITION BY versus GROUP BY.SELECT DISTINCT as a reflex, and dedup ordering on a nullable column. Both are
extremely common and both are silent.