Hook: This is the SQL version of picking the newest card from each stack: it looks simple, but ties and duplicates can quietly ruin the result.
Question: How do you get the latest record per group in SQL?
Answer: Use a window function such as ROW_NUMBER() to number rows inside each group after sorting them from newest to oldest. Then keep only the row with rn = 1 for each group. This is usually better than joining on MAX(date) because you can add a tie-breaker, like an id, so the result is deterministic.
Interview-Ready Answer: I would solve this with ROW_NUMBER() over a PARTITION BY on the group key and an ORDER BY on the timestamp descending, plus a second sort key for ties. Then I filter to rn = 1. That gives exactly one latest row per group and avoids the common bug where a MAX() join returns duplicate rows when two records share the same timestamp.
When interviewers say latest record per group, they mean: for each group key, return the single row that should be considered newest by some ordering column such as created_at, updated_at, or an auto-incrementing id. A window function is a function that looks across a set of related rows without collapsing them into one row, which is why it is perfect here.
ROW_NUMBER() works under the hoodPARTITION BY customer_id means each customer gets its own mini-set of rows.ORDER BY created_at DESC, order_id DESC puts the newest row first. The second key is the tie-breaker, which makes the result deterministic when two rows share the same timestamp.1, the next gets 2, and so on.rn = 1 returns the latest row from each group.MAX()?The classic mistake is to find the latest timestamp with MAX(created_at) and then join back to the table. That can work if timestamps are unique, but it breaks when two rows share the same max value. Suddenly you get two rows for one group, which is wrong if the business wants exactly one latest record.
| Method | Best for | Main risk | Portability |
|---|---|---|---|
ROW_NUMBER() | One row per group | Needs tie-breaker | Very high |
RANK() | All tied latest rows | Returns multiples | Very high |
MAX() + join | Simple unique timestamps | Duplicates on ties | Very high |
DISTINCT ON | PostgreSQL shortcut | Vendor-specific | Low |
Memory hook: think of each group as a race lane. Sort the runners by finish time, and ROW_NUMBER() hands the gold medal to lane 1.
Use this pattern when you need the latest order per customer, the newest status per ticket, the most recent login per user, or the freshest sensor reading per device. It is especially useful in dashboards, audit trails, and event streams where every entity accumulates many history rows.
In practice, the engine must inspect all candidate rows, so the work is usually O(n log n) because of sorting, with memory usage that can grow with the size of each partition. On large tables, the database may spill to disk if the sort does not fit in memory. A useful index is often (group_key, sort_key DESC, tie_breaker DESC), because it can reduce sorting work or allow a more efficient ordered scan. If the table has millions of rows and thousands of groups, that index can be the difference between a fast index-assisted plan and a slow temp-file sort.
RANK() or DENSE_RANK() instead of ROW_NUMBER().LEFT JOIN the ranked result.Mentally, the process is simple: sort inside the group, number the rows, keep number one. That is the whole pattern.
Imagine a checkout service for an e-commerce app. Every order has many status rows: paid, packed, shipped, delivered. The customer support screen needs the current status for each order, not the whole history. The backend query uses the latest-record-per-group pattern to show one row per order id.
What goes wrong when this is misunderstood? A team uses MAX(updated_at) and joins back to the status table. One day, two status updates land in the same second for a busy order, so the join returns two rows. The UI starts showing duplicate cards, metrics count the order twice, and support sees logs like expected 1 row, got 2. The bug looks random, but the root cause is that the query never guaranteed a single winner.
-- Latest record per group, plus a failure path that shows why a naive MAX() join can duplicate rows.
-- This runs in SQLite and in most modern SQL engines that support window functions.
WITH orders(customer_id, order_id, created_at, status) AS (
VALUES
(1, 101, '2025-07-01 10:00:00', 'paid'),
(1, 102, '2025-07-02 09:30:00', 'shipped'),
(1, 103, '2025-07-02 09:30:00', 'refunded'), -- same timestamp as order 102, so ties matter
(2, 201, '2025-07-03 08:00:00', 'paid'),
(2, 202, '2025-07-04 12:00:00', 'shipped'),
(3, 301, '2025-07-05 15:00:00', 'paid')
),
latest_ts AS (
SELECT customer_id, MAX(created_at) AS max_created_at
FROM orders
GROUP BY customer_id
),
naive AS (
SELECT o.customer_id, o.order_id, o.created_at, o.status
FROM orders AS o
JOIN latest_ts AS l
ON l.customer_id = o.customer_id
AND l.max_created_at = o.created_at
),
ranked AS (
SELECT
o.customer_id,
o.order_id,
o.created_at,
o.status,
ROW_NUMBER() OVER (
PARTITION BY o.customer_id
ORDER BY o.created_at DESC, o.order_id DESC
) AS rn
FROM orders AS o
)
SELECT 'naive_max_join' AS method, customer_id, order_id, created_at, status
FROM naive
UNION ALL
SELECT 'row_number_fix' AS method, customer_id, order_id, created_at, status
FROM ranked
WHERE rn = 1
ORDER BY method, customer_id, order_id;
Follow-up & Tricky Questions:
ROW_NUMBER(), or use DISTINCT ON (group_key) with a matching ORDER BY. DISTINCT ON is shorter, but it is PostgreSQL-specific.(customer_id, created_at DESC, order_id DESC). That gives the optimizer a better chance to read rows in the right order.COALESCE or a database-specific NULLS FIRST/LAST rule.ROW_NUMBER() and filter to rn <= 3. That is the same pattern, just a wider slice of each partition.RANK() = 1 mean the same thing as ROW_NUMBER() = 1? No. RANK() returns all tied rows at the top, while ROW_NUMBER() forces a single row when you give it a full ordering.MAX(created_at) alone give the latest row? No. It gives only the timestamp value, not the full row, and it can match more than one row when timestamps tie.Tricky / gotcha questions:
ORDER BY places first. Without a tie-breaker, the result is not guaranteed to be stable.GROUP BY return the whole latest row? Not by itself. GROUP BY collapses rows, so you lose the other columns unless you join back or use a window function.Common Mistakes:
MAX() and stopping there. Correction: MAX() gives only a value, not the full row; you still need a join or a window function.RANK() when you want one row. Correction: use ROW_NUMBER() for exactly one latest row per group.ORDER BY does not fully break ties, the engine is free to choose any tied row.Memory Hook: Partition, sort, number, keep one. Picture each group as a small queue where the newest person stands first and only the first person gets selected.
Cheat Sheet:
ROW_NUMBER() is the standard answer for one latest row per group.PARTITION BY defines the group.ORDER BY ... DESC puts the newest row first.rn = 1 keeps only the winner.RANK() or DENSE_RANK().Practice Tasks:
rn <= 3.