Ben used to be on Pro. He downgraded this morning. A report that finds his latest Pro record will still call him a Pro customer. A report that finds his latest record first will get the current state right.
We will solve both parts of the problem: choose one complete row per customer, and apply a current-state filter at the correct stage.
Explore the idea
Two records. One timestamp. Which wins?
Give Ada’s updates an explicit tie-break instead of trusting row order.
2
records tied at 10:00
The newest timestamp is not unique
Ada’s Team and Pro updates share a timestamp. Time alone cannot choose a deterministic winner.
Pro
Ada’s selected current plan
Our ingestion-sequence rule chooses ID 3
Order by updated_at descending, then the unique ingestion ID descending. ID 3 wins this declared tie-break.
The update history
These are complete customer snapshots, not partial patches. Every update contains the whole current plan. id is a unique ingestion sequence number; a larger ID wins when two updates have the same timestamp. That is our explicit tie-break policy, not a claim that every source system should use ingestion order.
updates6 rows
Six updates, three customers
IDs are unique ingestion sequence numbers. Ada has a timestamp tie. Ben downgraded. Cy has no recorded update time.
| id | customer_id | name | updated_at | plan |
|---|---|---|---|---|
| 1 | 101 | Ada | 2026-01-08 09:00:00 | free |
| 2 | 101 | Ada | 2026-01-08 10:00:00 | team |
| 3 | 101 | Ada | 2026-01-08 10:00:00 | pro |
| 4 | 102 | Ben | 2026-01-08 09:00:00 | pro |
| 5 | 102 | Ben | 2026-01-08 11:00:00 | free |
| 6 | 103 | Cy | NULL | free |
Ada has two records at 10:00. Ben has a later downgrade. Cy has only a record with an unknown timestamp. These cases make an apparently simple “latest” query worth examining.
Rank the complete records
Partition the rows by customer and order each partition from newest to oldest:
Give every update a rank
SELECT id, customer_id, plan,
row_number() OVER (PARTITION BY customer_id
ORDER BY updated_at DESC NULLS LAST, id DESC) AS recency_rank
FROM updates ORDER BY customer_id, recency_rank;
| id | customer_id | plan | recency_rank |
|---|---|---|---|
| 3 | 101 | pro | 1 |
| 2 | 101 | team | 2 |
| 1 | 101 | free | 3 |
| 5 | 102 | free | 1 |
| 4 | 102 | pro | 2 |
| 6 | 103 | free | 1 |
Scroll for more columns →
ROW_NUMBER assigns a unique rank within each customer. updated_at DESC puts newer events first. NULLS LAST makes the policy for unknown times visible. id DESC resolves Ada’s timestamp tie reproducibly.
Cy still receives rank 1 because there is no competing record. If your business rule excludes undated updates entirely, apply that as a separate documented rule. “Unknown timestamp” is not the same as “oldest known event.”
A query using only MAX(updated_at) gives you a timestamp, not necessarily the rest of the matching record. Joining that maximum back to the history can return both of Ada’s tied updates. Selecting unrelated maxima for the plan and name can even assemble a row that never existed.
Keep rank one with QUALIFY
DuckDB’s QUALIFY clause filters the results of window functions. It lets us keep the winning row without exposing the rank as a final column:
Choose one complete record per customer
SELECT id, customer_id, name, plan
FROM updates
QUALIFY row_number() OVER (
PARTITION BY customer_id
ORDER BY updated_at DESC NULLS LAST, id DESC
) = 1
ORDER BY customer_id;
| id | customer_id | name | plan |
|---|---|---|---|
| 3 | 101 | Ada | pro |
| 5 | 102 | Ben | free |
| 6 | 103 | Cy | free |
Scroll for more columns →
The result keeps Ada’s Pro record, Ben’s Free record, and Cy’s only record. We have preserved entire records under a declared ordering rule.
This ordering must be total for the result to be deterministic. If your final tie-break column also has duplicates, either add a stable unique key or return ties for review. Do not let a convenient row order become an accidental business decision.
Filter current state after selecting it
WHERE plan = 'pro' removes rows before the window function runs. That would hide Ben’s downgrade and allow his earlier Pro record to win. Put the latest-record query in a CTE, then filter its output.
Your turn · check your answer
Who is on Pro now?
Return customer_id and name for customers whose latest record has plan pro. Use the same timestamp and ID tie-break rule.
See the expected result
| customer_id | name |
|---|---|
| 101 | Ada |
Preparing the editor…
Your result
Need a hint?
Choose each customer’s latest record first. Filtering to pro before ranking can bring back an old subscription.
Show a solution
WITH latest AS (
SELECT customer_id, name, plan FROM updates
QUALIFY row_number() OVER (PARTITION BY customer_id
ORDER BY updated_at DESC NULLS LAST, id DESC) = 1
)
SELECT customer_id, name FROM latest
WHERE plan = 'pro' ORDER BY customer_id;The distinction generalizes: latest successful payment and latest payment if successful answer different questions. Choose the sequence that matches the wording of the report.
Know what “latest” means in your system
An event timestamp and an ingestion timestamp describe different things. A late-arriving correction can be newest by ingestion time but older by event time. Establish which one governs the report before choosing an ORDER BY.
This example also assumes full snapshots. For patch events that omit unchanged fields, choosing one row is not enough; you must reconstruct state or use a source that already provides complete versions.
Once the latest-record query is reliable, save it as a view and test the tie and downgrade cases whenever its rules change. You can then compare snapshots to audit which customers’ current states changed between runs.