Hook: Interviewers love this because it checks whether you can turn the word latest into a precise SQL rule instead of guessing with MAX().
Question: How do you get the latest order for each customer?
Answer: You want one row per customer: the order with the newest date, and if two orders share the same date, you need a tie-breaker so the result is stable. The most common safe solution is to rank each customer’s orders with ROW_NUMBER(), sort newest first, then keep only row 1.
Interview-Ready Answer: I would partition the orders by customer_id, order each customer’s rows by order_date DESC and a second key like order_id DESC, then filter to ROW_NUMBER() = 1. That gives exactly one latest order per customer and avoids the classic bug where a simple MAX(order_date) join returns duplicate rows when two orders have the same timestamp.
“Latest” means the most recent order in time for each customer. In SQL, that sounds simple, but the tricky part is returning the whole row, not just the date. A date by itself tells you when the latest order happened; it does not tell you which order ID, amount, or status belongs to that date.
PARTITION BY customer_id. A partition is just a logical mini-table for each customer.ORDER BY order_date DESC, order_id DESC. Newest dates come first, and the extra sort key breaks ties in a deterministic way.ROW_NUMBER(). The first row in each customer group gets 1, the next gets 2, and so on.WHERE rn = 1. That leaves one latest order per customer.ROW_NUMBER() is usually the best answerROW_NUMBER() is a window function, which means it computes a value across related rows without collapsing them into one row the way GROUP BY does. That is exactly what we need here: preserve the full row, but still know which row is the newest.
| Approach | Good part | Risk |
|---|---|---|
ROW_NUMBER() | Deterministic, clear | Needs window support |
MAX() + join | Works in old SQL | Duplicates on ties |
| Correlated subquery | Easy to read | Can be slower on big data |
Most databases will scan the relevant rows once and sort within each customer partition. In practice, the cost is often close to O(n log n) because of sorting, though a helpful index can reduce the work a lot. A composite index like (customer_id, order_date DESC, order_id DESC) is the usual performance booster because it matches the partition and sort order.
If you only have a few thousand rows, any correct method is fine. On millions of rows, the window-function approach is still preferred because it is predictable and usually easier for the optimizer to reason about than a self-join on aggregates.
Use a MAX() join only when you know the latest timestamp is unique per customer, or when the table is tiny and the rule is safe. Use a correlated subquery if your team finds it easier to read, but be careful: some engines will execute it row by row unless the optimizer rewrites it well.
Memory trick: think of each customer as a stack of order cards. Sort each stack newest-to-oldest, then take the top card. That is exactly what ROW_NUMBER() = 1 does.
Real-World Story: Imagine a checkout service for an e-commerce app. The support team wants a dashboard that shows the latest order per customer so they can answer, “What did this user buy most recently?” The backend uses this query to prefill customer service screens and shipping lookups.
Now imagine a developer writes the classic MAX(order_date) plus join-back query, but two orders share the exact same timestamp because the system batches writes. The dashboard suddenly shows duplicate latest orders for some customers, support agents click the wrong row, and a shipment note is attached to the wrong order. In logs, you might see repeated customer IDs for the same “latest” result, and in the UI the customer card could flicker between two orders depending on load order.
The fix is not just “use a different query”; it is to make the business rule explicit. If two orders tie, decide whether the higher order_id, newer created_at, or some other column wins. Once that rule is written into the ORDER BY, the result becomes stable and safe for production.
-- Latest order per customer, with a deterministic tie-breaker.
-- PostgreSQL-compatible SQL.
DROP TABLE IF EXISTS orders;
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL,
order_date TIMESTAMP NOT NULL,
amount NUMERIC(10,2) NOT NULL,
status VARCHAR(20) NOT NULL
);
INSERT INTO orders (order_id, customer_id, order_date, amount, status) VALUES
(1, 101, TIMESTAMP '2026-07-01 09:00:00', 25.00, 'placed'),
(2, 101, TIMESTAMP '2026-07-03 10:15:00', 40.00, 'shipped'),
(3, 101, TIMESTAMP '2026-07-03 10:15:00', 42.00, 'shipped'), -- same timestamp as order 2
(4, 102, TIMESTAMP '2026-06-30 12:00:00', 15.00, 'placed'),
(5, 102, TIMESTAMP '2026-07-02 08:30:00', 30.00, 'cancelled'),
(6, 103, TIMESTAMP '2026-07-01 18:45:00', 55.00, 'delivered');
-- Naive idea (commented out):
-- SELECT customer_id, MAX(order_date)
-- FROM orders
-- GROUP BY customer_id;
-- This returns only the date, not the full order row.
-- If you join it back, customer 101 can return two rows because two orders share the same latest timestamp.
WITH ranked_orders AS (
SELECT
o.order_id,
o.customer_id,
o.order_date,
o.amount,
o.status,
ROW_NUMBER() OVER (
PARTITION BY o.customer_id
ORDER BY o.order_date DESC, o.order_id DESC
) AS rn
FROM orders o
WHERE o.order_date IS NOT NULL
)
SELECT
customer_id,
order_id,
order_date,
amount,
status
FROM ranked_orders
WHERE rn = 1
ORDER BY customer_id;
Follow-up & Tricky Questions:
MAX(order_date) per customer in a CTE, then join back to the orders table. It works, but you must add a tie-breaker or you may get duplicate rows when more than one order shares the same latest date.ROW_NUMBER() logic, but filter with rn <= 3 instead of rn = 1. That is a very common extension of this pattern.(customer_id, order_date DESC, order_id DESC) helps the database find and sort each customer’s newest rows faster.WHERE status <> 'cancelled' inside the ranked CTE so the ranking only considers eligible orders.order_id DESC or created_at DESC so the result is deterministic.MAX(order_date) alone solve the problem? No. It gives only the latest timestamp, not the full order row. You still need a second step to pick the matching record.RANK() instead of ROW_NUMBER()? Usually no for this question. RANK() gives the same rank to ties, so you can get multiple rows as “#1”; ROW_NUMBER() gives exactly one row per customer.Common Mistakes:
MAX(order_date) alone — correction: that only returns a date, not the order row.order_id DESC so the result is stable.Memory Hook: Picture each customer’s orders as a stack of cards. Sort the stack newest to oldest, then take the top card — that is ROW_NUMBER() = 1.
Cheat Sheet:
ROW_NUMBER() with PARTITION BY customer_id.order_date DESC, then add a tie-breaker.rn = 1 for one row per customer.MAX() alone does not return the full row.Practice Tasks:
ROW_NUMBER().