Hook: This is the SQL version of asking, 'Which package arrived last for each person?' Interviewers love it because it tests grouping, sorting, and whether you can keep the full row, not just the date.
Question: How do I return the latest order for each customer?
Answer: Use a subquery or, more commonly, a CTE with ROW_NUMBER(). Partition the rows by customer, sort each customer’s orders from newest to oldest, then keep only row number 1. That gives you one deterministic latest order per customer, even when you add a tie-breaker.
Interview-Ready Answer: I’d write a CTE that ranks orders per customer with ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date DESC, order_id DESC), then select the rows where rn = 1. I like this approach because it returns the full latest row, not just the max date, and the extra order_id tie-breaker makes the result deterministic when two orders share the same timestamp.
Detailed Explanation: A CTE (Common Table Expression) is a named temporary result set used inside one SQL statement. The core idea here is: do not collapse orders too early. If you only ask for MAX(order_date), you get a date, but you lose the rest of the row. Instead, rank all orders inside each customer group and keep the first row.
customer_id; this is called partitioning, which means dividing rows into buckets that share the same key.order_date DESC.ROW_NUMBER(); the newest row gets 1, the next gets 2, and so on.rn = 1 to keep only the latest order for each customer.That mental model is easy to replay in an interview: group, sort, rank, keep the first.
MAX(order_date) gives the latest date, but not the full order row. To get the full row, many people join back to the table, and that is where bugs appear. If two orders share the same max timestamp, the join returns both rows. If a customer only has NULL dates, a normal equality join can return nothing because NULL = NULL is not true in SQL.
| Approach | Good | Weakness |
|---|---|---|
| CTE + ROW_NUMBER | Clear, deterministic | Needs a sort |
| MAX + JOIN | Simple-looking | Ties and NULLs |
| Correlated subquery | Readable for small sets | Can be slower |
Use this pattern whenever you need the latest row per parent key: latest order, latest login, latest event, latest status change. It is especially useful when the row has more columns than just the timestamp, because the ranking step keeps the whole row intact.
Without an index, the database usually has to sort a lot of rows, so the work is roughly O(n log n). With a composite index like (customer_id, order_date DESC, order_id DESC), the engine may do far less work, especially on large tables. On a table with millions of orders, that index can be the difference between a quick index-assisted plan and a big sort that spills to disk if memory is tight. In PostgreSQL, CTE behavior also changed over time: in version 11 and earlier, a CTE was often an optimization fence; from 12 onward, the planner can inline it when safe, which often makes CTEs behave more like readable subqueries.
order_id DESC so the result is deterministic.Real-World Example: Imagine a checkout service for an e-commerce site. The customer profile page shows a 'most recent order' card, and the support team uses the same query to see the last purchase before a refund. If the team writes the query as MAX(order_date) JOIN orders, a Black Friday customer with two orders in the same second may see duplicate latest orders, or a customer with one imported legacy order with a null timestamp may disappear from the result entirely.
In production, that bug shows up as confusing UI cards, duplicate shipments in support dashboards, and logs like 'multiple rows returned for customer_id 1842'. Users notice stale or duplicated order summaries, support agents open the wrong order, and refund workflows can attach to the wrong row. The fix is not just 'make it work'; it is to choose a ranking strategy that matches the business rule and breaks ties clearly.
-- Demo data: one customer has two orders at the exact same time,
-- and one customer has only a NULL timestamp. This lets us see
-- why a MAX(order_date) join can fail, and why ranking is safer.
DROP TABLE IF EXISTS orders;
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL,
order_date TIMESTAMP NULL,
total_amount DECIMAL(10,2) NOT NULL
);
INSERT INTO orders (order_id, customer_id, order_date, total_amount) VALUES
(101, 1, TIMESTAMP '2024-06-01 10:00:00', 49.99),
(102, 1, TIMESTAMP '2024-06-02 09:30:00', 19.99),
(103, 1, TIMESTAMP '2024-06-02 09:30:00', 29.99), -- same latest timestamp as 102
(104, 2, TIMESTAMP '2024-05-20 14:15:00', 79.00),
(105, 2, TIMESTAMP '2024-06-03 08:00:00', 15.00),
(106, 3, NULL, 12.50); -- edge case: only NULL date for this customer
-- Failure path: MAX() + JOIN looks tempting, but it duplicates ties
-- and drops customers whose max date is NULL.
SELECT
o.customer_id,
o.order_id,
o.order_date,
o.total_amount
FROM orders o
JOIN (
SELECT customer_id, MAX(order_date) AS max_order_date
FROM orders
GROUP BY customer_id
) latest
ON latest.customer_id = o.customer_id
AND latest.max_order_date = o.order_date
ORDER BY o.customer_id, o.order_id;
-- Correct approach: rank rows inside each customer bucket,
-- then keep the newest row only. The order_id tie-breaker makes
-- the result deterministic when timestamps match.
WITH ranked_orders AS (
SELECT
o.customer_id,
o.order_id,
o.order_date,
o.total_amount,
ROW_NUMBER() OVER (
PARTITION BY o.customer_id
ORDER BY COALESCE(o.order_date, TIMESTAMP '1900-01-01 00:00:00') DESC,
o.order_id DESC
) AS rn
FROM orders o
)
SELECT
customer_id,
order_id,
order_date,
total_amount
FROM ranked_orders
WHERE rn = 1
ORDER BY customer_id;Follow-up & Tricky Questions:
ROW_NUMBER() pattern, but filter rn <= 3 instead of rn = 1. That keeps a small ranked history for each customer.order_id DESC or created_at DESC, order_id DESC. Without a tie-breaker, the database may pick either row, and the result is not stable.ROW_NUMBER() is usually clearer and more flexible.(customer_id, order_date DESC, order_id DESC) can reduce sort work and speed up finding the first row in each group. It does not remove the need to inspect the rows, but it can make the plan much cheaper.MAX(order_date) + join a gotcha? Because it can return multiple rows on ties and can miss customers whose only date is NULL. It answers 'what is the date?' but not reliably 'which row is the latest order?'.NULL order dates? Decide the business rule first. Most teams treat them as oldest or ignore them, then make that explicit in the ORDER BY so the query behaves predictably.Common Mistakes:
MAX(order_date) alone. Correction: that only gives the date, not the full order row.NULL timestamps. Correction: decide whether nulls should sort first, last, or be filtered out, and write it explicitly.Memory Hook: Think of each customer as a line at a coffee shop: sort the line so the newest customer is at the front, then take the first person. That is exactly what ROW_NUMBER() OVER (...) = 1 does.
Cheat Sheet:
customer_id.ORDER BY order_date DESC.order_id DESC.ROW_NUMBER() = 1.(customer_id, order_date DESC, order_id DESC) when this query is frequent.Practice Tasks: