Hook: This is the classic 'who brought in the most money?' query — interviewers love it because one tiny SQL mistake can change the winner.
Question: Write a SQL query to find the top customer by total spend.
Answer: First, add up each customer's orders with SUM. Then use a CTE, which means a Common Table Expression — a named temporary result inside one SQL statement — so the totals are easy to read and reuse. Finally, pick the customer whose total matches the highest total, and keep ties if two customers spent the same amount.
Interview-Ready Answer: I’d write a CTE that groups orders by customer and calculates total spend with SUM. Then I’d use a subquery to compare each customer total against MAX(total_spend) from that CTE. That gives me a clear, readable solution, and it also handles ties correctly if multiple customers share the top total.
SUM(amount).A subquery is a query inside another query. A CTE is similar, but it has a name, so the SQL reads like a tiny step-by-step program. That is why CTEs are popular in interviews: they make the data flow obvious.
Without a CTE, the same aggregate can become hard to read, especially when you need to reuse it more than once. A CTE lets you say, 'here are the customer totals,' and then the outer query can focus only on selecting the top result.
| Approach | Best for | Watch out |
|---|---|---|
| CTE + subquery | Readable logic | Old PostgreSQL may materialize it |
| ORDER BY ... LIMIT 1 | Exactly one row | Hides ties unless you handle them |
| Window function | Top N or ties | More advanced for beginners |
customer_id (and maybe customer name if needed).The main cost is reading and grouping the orders. If there are N order rows, the work is usually close to O(N) with a hash aggregate, or O(N log N) if the engine sorts first. For a table with 10 million orders, the database still has to inspect all relevant rows unless you already have a pre-aggregated summary table.
An index on orders(customer_id) can help some engines group or join more efficiently, but it does not remove the need to count or sum the rows. The biggest win for large systems is often a summary table or materialized view updated on a schedule.
Version note: in PostgreSQL 12+, non-recursive CTEs are often inlined by the optimizer, which means they can behave more like a subquery. In older PostgreSQL versions, CTEs were more likely to be materialized, which could block some optimizations.
LEFT JOIN plus COALESCE includes them.SUM ignores NULL values, but if every row is null, COALESCE can turn the result into 0.Memory tip: think of the CTE as a labeled tray of receipts: first sort each customer's receipts into a tray, then ask which tray is heaviest.
Imagine a subscription billing service that sends VIP rewards to the top customer every month. The team needs a query that finds the customer with the highest lifetime spend or monthly spend, depending on the campaign. A CTE makes the query easy for analysts and engineers to review during incident debugging.
Here is the kind of bug that happens when the logic is misunderstood: a developer uses MAX(amount) instead of SUM(amount). One customer places a single big order, another places many smaller orders that add up to more. The system rewards the wrong user, and support starts seeing tickets like 'Why did I lose VIP status even though I spent more overall?'. Logs show the wrong customer ID, the dashboard disagrees with finance numbers, and the mistake is hard to spot until someone checks the aggregation rule.
Why this matters: in production, the difference between 'largest single order' and 'largest total spend' can change promotions, rankings, and revenue reports. This query is small, but the business impact is not.
-- Demo schema and sample data for a "top customer" query.
-- This example returns all customers tied for the highest total spend.
DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS customers;
CREATE TABLE customers (
customer_id INTEGER,
customer_name VARCHAR(50)
);
CREATE TABLE orders (
order_id INTEGER,
customer_id INTEGER,
amount DECIMAL(10,2)
);
INSERT INTO customers (customer_id, customer_name) VALUES
(1, 'Ada'),
(2, 'Ben'),
(3, 'Cara'),
(4, 'Dana');
INSERT INTO orders (order_id, customer_id, amount) VALUES
(101, 1, 120.00),
(102, 2, 500.00),
(103, 2, 700.00),
(104, 3, 1200.00);
WITH customer_spend AS (
SELECT
c.customer_id,
c.customer_name,
COALESCE(SUM(o.amount), 0) AS total_spend
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.customer_id
GROUP BY c.customer_id, c.customer_name
)
SELECT
customer_id,
customer_name,
total_spend
FROM customer_spend
WHERE total_spend = (SELECT MAX(total_spend) FROM customer_spend)
ORDER BY customer_id;
-- Result:
-- Ben and Cara both return because they are tied at 1200.00.
-- Ada has 120.00, and Dana has 0.00 because LEFT JOIN keeps customers with no orders.
--
-- If your interview wants exactly one row, add a deterministic tie-breaker,
-- for example: ORDER BY total_spend DESC, customer_id ASC, then pick one row.
--
-- Common mistake to avoid:
-- SELECT customer_id, MAX(amount) ...
-- That finds the biggest single order, not the customer with the biggest total spend.ORDER BY total_spend DESC and a limit of 3, or use a window function like DENSE_RANK() if you want to handle ties cleanly.customer_id ASC or customer_name ASC, so the result is stable and repeatable.LEFT JOIN from customers to orders and wrap the sum with COALESCE(..., 0).LIMIT 1 not always enough? Because it hides ties and can return an arbitrary row if the sort is not fully deterministic. A tie-aware query is safer for business reports.MAX(amount)? Because that answers a different question: it finds the largest single order, not the customer with the largest total spend across all orders.Common Mistakes:
MAX(amount) or COUNT(*) when the question really wants total spend. Fix: confirm whether 'top' means most money, most orders, or latest purchase.LIMIT 1 can hide another customer with the same total. Fix: return all rows matching the maximum, or define a tie-breaker.INNER JOIN removes customers with no orders. Fix: use LEFT JOIN if the report needs the full customer list.Memory Hook: 'First weigh each customer’s bag of receipts, then pick the heaviest bag.' That is exactly what the CTE plus max comparison is doing.
Cheat Sheet:
SUM(amount).LEFT JOIN + COALESCE for zero-order customers.Practice Tasks: