cswork / libraryOpen practice workspace

Coding questions · Amazon

Find top-spend categories per customer with ranking

This question evaluates proficiency with SQL aggregation and JOINs, analytic/window functions (RANK()), tie and NULL/edge-case handling, and percent-of-total calculations for per-customer spend analysis.

Using the schema and sample data below, write a single ANSI SQL query (CTEs allowed; no temp tables) that returns, for each customer, their top 2 product categories by total spend. Output columns: customer_id, category, total_spend, earliest_order_date, rank_in_customer. Requirements:

  • total_spend = SUM(oi.qty * oi.unit_price) per (customer_id, category).
  • earliest_order_date = MIN(o.order_date) within that (customer_id, category).
  • Use RANK() OVER (PARTITION BY customer_id ORDER BY total_spend DESC, earliest_order_date ASC) as rank_in_customer.
  • Return rows where rank_in_customer <= 2, including all ties at rank 2.
  • Include customers with no orders as a single row: category = NULL, total_spend = 0, earliest_order_date = NULL, rank_in_customer = NULL (do not assign rank for zero-spend customers). Hint: careful LEFT JOINs and conditional ranking.
  • Avoid vendor-specific extensions beyond standard window functions. Follow-ups (closely related; keep as part of the same query via additional columns/CTEs): A) Add percent_of_total = total_spend / SUM(total_spend) OVER (PARTITION BY customer_id), and ensure it is 0 for customers with no orders. B) Return only customer_ids whose top category’s percent_of_total < 0.5 (i.e., their spend is not dominated by a single category).

Schema:

customers(id INT PK, name TEXT)
orders(id INT PK, customer_id INT FK -> customers.id, order_date DATE)
order_items(id INT PK, order_id INT FK -> orders.id, product_id INT FK -> products.id, qty INT, unit_price DECIMAL(10,2))
products(id INT PK, category TEXT, name TEXT)

Sample data:

customers
id | name
1 | Alice
2 | Bob
3 | Cara
4 | Dana
orders
id | customer_id | order_date
101 | 1 | 2025-07-02
102 | 1 | 2025-07-15
103 | 2 | 2025-07-20
104 | 2 | 2025-07-21
105 | 3 | 2025-07-22
order_items
id | order_id | product_id | qty | unit_price
1001 | 101 | 201 | 2 | 10.00
1002 | 101 | 202 | 1 | 20.00
1003 | 102 | 201 | 1 | 10.00
1004 | 102 | 203 | 5 | 5.00
1005 | 103 | 202 | 2 | 20.00
1006 | 104 | 203 | 10 | 5.00
1007 | 105 | 204 | 1 | 100.00
products
id | category | name
201 | CatA | A
202 | CatA | B
203 | CatB | C
204 | CatC | D

Top-2 spend categories per customer with ranking, percent-of-total, and dominance filter

Using the tables and sample data below, write a single ANSI SQL query (CTEs allowed; no temp tables) that returns, for each customer, their top 2 product categories by total spend.

Output columns:

  • customer_id
  • category
  • total_spend
  • earliest_order_date
  • rank_in_customer
  • percent_of_total

Rules:

  1. total_spend = SUM(order_items.qty * order_items.unit_price) per (customer_id, category).
  2. earliest_order_date = MIN(orders.order_date) within that (customer_id, category).
  3. rank_in_customer must be computed as: RANK() OVER (PARTITION BY customer_id ORDER BY total_spend DESC, earliest_order_date ASC)
  4. Return rows where rank_in_customer <= 2, including all ties at rank 2.
  5. Include customers with no orders as a single row with: category = NULL, total_spend = 0, earliest_order_date = NULL, rank_in_customer = NULL, percent_of_total = 0. (Do not assign a rank for zero-spend customers.)
  6. percent_of_total = total_spend / SUM(total_spend) OVER (PARTITION BY customer_id). Ensure it is 0 for customers with no orders.
  7. Final filter: return only customer_ids whose TOP category’s percent_of_total < 0.5 (i.e., their spend is not dominated by a single category). Customers with no orders should be treated as having top category percent_of_total = 0.

Use only standard SQL features (window functions are allowed).

Table customers

columntypeconstraints
idINTPRIMARY KEY, NOT NULL
nameVARCHAR(100)NOT NULL

Sample data:

idname
1Alice
2Bob
3Cara
4Dana
5Evan

Table orders

columntypeconstraints
idINTPRIMARY KEY, NOT NULL
customer_idINTNOT NULL
order_dateDATENOT NULL

Sample data:

customer_ididorder_date
11012025-07-02
11022025-07-15
21032025-07-20
21042025-07-21
31052025-07-22
51062025-07-10

Table order_items

columntypeconstraints
idINTPRIMARY KEY, NOT NULL
order_idINTNOT NULL
product_idINTNOT NULL
qtyINTNOT NULL
unit_priceDECIMAL(10,2)NOT NULL

Sample data:

idorder_idproduct_idqtyunit_price
1001101201210
1002101202120
1003102201110
100410220355
1005103202220
1006104203105
10071052041100
1008106201410
100910620375
1010106204135

Table products

columntypeconstraints
idINTPRIMARY KEY, NOT NULL
categoryVARCHAR(50)NOT NULL
nameVARCHAR(100)NOT NULL

Sample data:

categoryidname
CatA201A
CatA202B
CatB203C
CatC204D

Expected output

Each returned row is one of the (up to) top-2 ranked categories per qualifying customer (including ties at rank 2), with spend, earliest order date for that category, the category rank within the customer, and percent of that customer's total spend. Customers with no orders appear as one row with NULL category, 0 spend, NULL dates/rank, and percent_of_total = 0. Additionally, only customers whose top category percent_of_total is < 0.5 are included.

categorycustomer_idearliest_order_datepercent_of_totalrank_in_customertotal_spend
NULL4NULL0NULL0
CatA52025-07-100.3636140
CatB52025-07-100.3182235
CatC52025-07-100.3182235

Hints

  1. Aggregate spend and earliest order date per (customer_id, category) first, then apply window functions.
  2. Use RANK() and filter rank_in_customer <= 2 to keep ties at rank 2.
  3. Compute top-category percent via MAX(CASE WHEN rank=1 THEN percent END) OVER (PARTITION BY customer_id) and filter it to be < 0.5.
  4. Handle customers with no orders using a separate CTE and UNION ALL a single default row; keep their rank NULL and percent_of_total = 0.