advanced~6h

SQL Practice — Advanced (Problems 71-110)

40 problems on window functions — RANK, DENSE_RANK, ROW_NUMBER, NTILE, LAG, LEAD, running totals, and moving averages — every one solved, run against real data, and explained.

Learning objectives

  • Pick the right ranking function (RANK vs DENSE_RANK vs ROW_NUMBER) based on how ties should behave.
  • Use PARTITION BY to reset a window calculation per group, and know when to omit it for a grand total.
  • Build running totals, period-over-period comparisons, and moving averages with window frames.
  • Recognize when a window function inside a CTE is required because a WHERE can't filter it directly.

P71 — Rank Employees by Salary  ·  Advanced

Rank all employees from highest to lowest salary.

SQL

SELECT employee_id, first_name, salary, RANK() OVER (ORDER BY salary DESC) AS salary_rank FROM employees;

Output (computed against this section's live 9-table dataset)

employee_idfirst_namesalarysalary_rank
1Arvind285,0001
2Meera215,0002
6Anil205,0003
3Karan198,0004
4Priya175,0005
7Sunita168,0006
14Rohan167,0007
5Suresh160,0008
8Rohan152,0009
9Ananya148,00010

(showing 10 of 40 rows)

Explanation

RANK() OVER (ORDER BY salary DESC) assigns every employee a position in the salary order without collapsing rows the way GROUP BY would — every one of the 40 employees still appears, just now carrying a rank number. RANK() specifically leaves gaps after ties: if two employees tied for rank 3, the next distinct salary would jump straight to rank 5, not 4 (contrast this with DENSE_RANK in Problem 74, which doesn't leave gaps).

💻 Code example

SELECT employee_id, first_name, salary, RANK() OVER (ORDER BY salary DESC) AS salary_rank FROM employees;

P72 — Rank Employees Within Department  ·  Advanced

Rank employees by salary within each department.

SQL

SELECT employee_id, first_name, department_id, salary, RANK() OVER ( PARTITION BY department_id ORDER BY salary DESC ) AS department_rank FROM employees;

Output (computed against this section's live 9-table dataset)

employee_idfirst_namedepartment_idsalarydepartment_rank
5Suresh4160,0001
35Varun490,680.582
23Tanvi443,454.563
17Rakesh439,926.014
29Aditi438,194.725
1Arvind1285,0001
2Meera1215,0002
14Rohan1167,0003
8Rohan1152,0004
9Ananya1148,0005

(showing 10 of 40 rows)

Explanation

Adding PARTITION BY department_id restarts the ranking from 1 at the start of every department, instead of ranking across the whole company — so each department gets its own #1 earner. Interestingly, HR's top earner (Suresh Iyer, ₹160,000) doesn't come close to Engineering's top earner (Arvind, ₹285,000), which is exactly why 'rank #1 in your department' and 'rank #1 company-wide' (Problem 71) can point to very different people.

💻 Code example

SELECT employee_id, first_name, department_id, salary, RANK() OVER ( PARTITION BY department_id ORDER BY salary DESC ) AS department_rank FROM employees;

P73 — Top 3 Employees Per Department  ·  Advanced

Return the top three salary earners from every department.

SQL

WITH ranked AS ( SELECT employee_id, first_name, department_id, salary, ROW_NUMBER() OVER ( PARTITION BY department_id ORDER BY salary DESC ) AS rn FROM employees ) SELECT * FROM ranked WHERE rn <= 3;

Output (computed against this section's live 9-table dataset)

employee_idfirst_namedepartment_idsalaryrn
1Arvind1285,0001
2Meera1215,0002
3Karan2198,0001
4Priya3175,0001
5Suresh4160,0001
6Anil5205,0001
7Sunita6168,0001
10Vikram2131,0002
11Kavya3118,0002
12Deepak5142,0002

(showing 10 of 18 rows)

Explanation

Window functions like RANK() can't be filtered directly in the same SELECT they're computed in (a WHERE rn <= 3 right after the window function is a syntax error), so the ranking has to be computed inside a CTE first and then filtered in an outer query. ROW_NUMBER() is used here instead of RANK() specifically because the goal is 'give me exactly 3 rows per department' — RANK() could return more than 3 rows for a department with a 3-way tie at the top, since tied rows share a rank number.

💻 Code example

WITH ranked AS ( SELECT employee_id, first_name, department_id, salary, ROW_NUMBER() OVER ( PARTITION BY department_id ORDER BY salary DESC ) AS rn FROM employees ) SELECT * FROM ranked WHERE rn <= 3;

P74 — Dense Salary Ranking  ·  Advanced

Assign dense salary ranks across all employees.

SQL

SELECT employee_id, first_name, salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rank FROM employees;

Output (computed against this section's live 9-table dataset)

employee_idfirst_namesalarydense_rank
1Arvind285,0001
2Meera215,0002
6Anil205,0003
3Karan198,0004
4Priya175,0005
7Sunita168,0006
14Rohan167,0007
5Suresh160,0008
8Rohan152,0009
9Ananya148,00010

(showing 10 of 40 rows)

Explanation

DENSE_RANK() behaves like RANK() but never skips a number after a tie — if two people tied for rank 1, the very next distinct salary gets rank 2, not 3. In this dataset, since all 40 salaries happen to be distinct, DENSE_RANK and RANK (Problem 71) produce identical results here; the difference only becomes visible once real ties exist, as it does within some departments in Problem 72.

💻 Code example

SELECT employee_id, first_name, salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rank FROM employees;

P75 — Running Revenue Total  ·  Advanced

Calculate cumulative order revenue over time.

SQL

SELECT order_date, order_id, total_amount, SUM(total_amount) OVER ( ORDER BY order_date, order_id ) AS running_revenue FROM orders WHERE status <> 'Cancelled';

Output (computed against this section's live 9-table dataset)

order_dateorder_idtotal_amountrunning_revenue
2024-01-051266,5966,596
2024-01-1112711,18917,785
2024-01-169936,98954,774
2024-01-213514,79169,565
2024-02-101449,09578,660
2024-02-209614,39293,052
2024-02-251112,39895,450
2024-03-031154,49599,945
2024-03-04205,294105,239
2024-03-0414617,789123,028

(showing 10 of 124 rows)

Explanation

SUM(total_amount) OVER (ORDER BY order_date, order_id) (with no PARTITION BY) computes a cumulative total across all non-cancelled orders in chronological order — each row's running_revenue is the sum of every order up to and including that one. The order_id tiebreaker in the ORDER BY matters because several orders can share the same order_date; without a secondary sort key, ties would have an undefined order and the running total could differ between runs.

💻 Code example

SELECT order_date, order_id, total_amount, SUM(total_amount) OVER ( ORDER BY order_date, order_id ) AS running_revenue FROM orders WHERE status <> 'Cancelled';

P76 — Running Revenue Per Customer  ·  Advanced

Calculate each customer's cumulative spending over their order history.

SQL

SELECT customer_id, order_id, order_date, total_amount, SUM(total_amount) OVER ( PARTITION BY customer_id ORDER BY order_date, order_id ) AS running_customer_spend FROM orders WHERE status <> 'Cancelled';

Output (computed against this section's live 9-table dataset)

customer_idorder_idorder_datetotal_amountrunning_customer_spend
31462024-03-0417,78917,789
3232024-05-0638,58856,377
31322024-06-1017,48973,866
3952024-07-2417,78991,655
3412024-08-1123,996115,651
3552024-10-1529,787145,438
3152024-11-2552,588198,026
31282025-04-1760,191258,217
31302025-05-205,994264,211
3492025-07-147,797272,008

(showing 10 of 124 rows)

Explanation

Adding PARTITION BY customer_id to Problem 75's pattern resets the running total to zero at the start of every customer's order history, so each customer accumulates their own spending independently instead of sharing one company-wide total. Customer 3's first order (₹17,789) establishes their running total, and each subsequent order adds on top of it — this is the building block behind lifetime-value and lifetime-order-count tracking.

💻 Code example

SELECT customer_id, order_id, order_date, total_amount, SUM(total_amount) OVER ( PARTITION BY customer_id ORDER BY order_date, order_id ) AS running_customer_spend FROM orders WHERE status <> 'Cancelled';

P77 — Previous Order Amount  ·  Advanced

Show each customer's previous order amount.

SQL

SELECT customer_id, order_id, order_date, total_amount, LAG(total_amount) OVER ( PARTITION BY customer_id ORDER BY order_date, order_id ) AS previous_order_amount FROM orders;

Output (computed against this section's live 9-table dataset)

customer_idorder_idorder_datetotal_amountprevious_order_amount
31462024-03-0417,789NULL
3232024-05-0638,58817,789
31322024-06-1017,48938,588
3952024-07-2417,78917,489
3412024-08-1123,99617,789
3552024-10-1529,78723,996
3152024-11-2552,58829,787
31282025-04-1760,19152,588
31302025-05-205,99460,191
3492025-07-147,7975,994

(showing 10 of 149 rows)

Explanation

LAG(total_amount) OVER (PARTITION BY customer_id ORDER BY order_date, order_id) looks one row back within each customer's own chronologically ordered orders — the very first order for any customer has nothing before it, so LAG correctly returns NULL there (visible for customer 3's first order), and every subsequent order shows the amount that came immediately before it.

💻 Code example

SELECT customer_id, order_id, order_date, total_amount, LAG(total_amount) OVER ( PARTITION BY customer_id ORDER BY order_date, order_id ) AS previous_order_amount FROM orders;

P78 — Next Order Date  ·  Advanced

Show the next order date for every customer order.

SQL

SELECT customer_id, order_id, order_date, LEAD(order_date) OVER ( PARTITION BY customer_id ORDER BY order_date, order_id ) AS next_order_date FROM orders;

Output (computed against this section's live 9-table dataset)

customer_idorder_idorder_datenext_order_date
4962024-02-202024-03-11
4342024-03-112024-06-11
41332024-06-112024-07-19
41212024-07-192024-08-19
41422024-08-192024-09-23
4162024-09-232025-02-27
4462025-02-272025-04-12
4532025-04-122025-06-26
4592025-06-262025-10-16
4432025-10-16NULL

(showing 10 of 149 rows)

Explanation

LEAD is the mirror image of LAG: it looks one row forward instead of back. For customer 4's order on 2024-02-20, LEAD(order_date) returns 2024-03-11 — the date of their very next order — which is exactly the kind of 'time until next purchase' signal that feeds into repeat-purchase and churn analysis later on (P132, P146).

💻 Code example

SELECT customer_id, order_id, order_date, LEAD(order_date) OVER ( PARTITION BY customer_id ORDER BY order_date, order_id ) AS next_order_date FROM orders;

P79 — Difference From Previous Order  ·  Advanced

Calculate the difference between the current order amount and the previous order amount for each customer.

SQL

SELECT customer_id, order_id, total_amount, total_amount - LAG(total_amount) OVER ( PARTITION BY customer_id ORDER BY order_date, order_id ) AS amount_difference FROM orders;

Output (computed against this section's live 9-table dataset)

customer_idorder_idtotal_amountamount_difference
314617,789NULL
32338,58820,799
313217,489-21,099
39517,789300
34123,9966,207
35529,7875,791
31552,58822,801
312860,1917,603
31305,994-54,197
3497,7971,803

(showing 10 of 149 rows)

Explanation

Subtracting LAG(total_amount) from the current row's total_amount turns Problem 77's 'previous amount' into a period-over-period change — a positive amount_difference means the customer spent more than last time, negative means less. Just like LAG itself, the very first order for each customer has no previous value to subtract, so its difference is NULL rather than some fallback like 0 — a NULL here genuinely means 'not applicable', not 'zero change'.

💻 Code example

SELECT customer_id, order_id, total_amount, total_amount - LAG(total_amount) OVER ( PARTITION BY customer_id ORDER BY order_date, order_id ) AS amount_difference FROM orders;

P80 — Department Salary Percentage  ·  Advanced

Find what percentage of total department salary each employee earns.

SQL

SELECT employee_id, first_name, department_id, salary, ROUND( 100.0 * salary / SUM(salary) OVER (PARTITION BY department_id), 2 ) AS salary_percentage FROM employees;

Output (computed against this section's live 9-table dataset)

employee_idfirst_namedepartment_idsalarysalary_percentage
1Arvind1285,00022.85
2Meera1215,00017.24
3Karan2198,00030.51
4Priya3175,00025.22
5Suresh4160,00042.98
6Anil5205,00035.31
7Sunita6168,00031.11
8Rohan1152,00012.19
9Ananya1148,00011.87
10Vikram2131,00020.19

(showing 10 of 40 rows)

Explanation

SUM(salary) OVER (PARTITION BY department_id) computes each department's total payroll without collapsing the individual employee rows — unlike a GROUP BY, every employee still appears individually, just now also carrying their department's total as a repeated value on every row in that partition. Dividing each employee's own salary by that shared total (and multiplying by 100) shows how much of the department's payroll budget one person represents: Karan Malhotra alone accounts for 30.51% of Sales' total payroll, reflecting how few employees are in that department relative to how senior he is.

💻 Code example

SELECT employee_id, first_name, department_id, salary, ROUND( 100.0 * salary / SUM(salary) OVER (PARTITION BY department_id), 2 ) AS salary_percentage FROM employees;

P81 — Highest Paid Employee Per Department  ·  Advanced

Return the highest-paid employee from each department.

SQL

WITH ranked AS ( SELECT e.*, ROW_NUMBER() OVER ( PARTITION BY department_id ORDER BY salary DESC, employee_id ) AS rn FROM employees e ) SELECT * FROM ranked WHERE rn = 1;

Output (computed against this section's live 9-table dataset)

employee_idfirst_namelast_nameemaildepartment_idmanager_idsalaryhire_datecityrn
5SureshIyersuresh.iyer@company.com41160,0002019-11-05Delhi1
1ArvindKrishnanarvind.krishnan@company.com1NULL285,0002019-01-10Bengaluru1
4PriyaChoprapriya.chopra@company.com31175,0002020-02-20Mumbai1
7SunitaRaosunita.rao@company.com61168,0002020-03-10Hyderabad1
3KaranMalhotrakaran.malhotra@company.com21198,0002019-08-01Mumbai1
6AnilBhattanil.bhatt@company.com51205,0002020-01-15Pune1

(6 rows total)

Explanation

This is Problem 73's pattern applied without a category-size cap — ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC, employee_id) gives exactly one 'rank 1' row per department, and filtering WHERE rn = 1 returns exactly 6 rows, one highest-paid employee per department. The employee_id tiebreaker after salary DESC guarantees a single deterministic winner even if two people in the same department were ever tied on salary.

💻 Code example

WITH ranked AS ( SELECT e.*, ROW_NUMBER() OVER ( PARTITION BY department_id ORDER BY salary DESC, employee_id ) AS rn FROM employees e ) SELECT * FROM ranked WHERE rn = 1;

P82 — Second Highest Salary Per Department  ·  Advanced

Find the second highest distinct salary in each department.

SQL

WITH ranked AS ( SELECT department_id, salary, DENSE_RANK() OVER ( PARTITION BY department_id ORDER BY salary DESC ) AS rnk FROM employees ) SELECT department_id, salary FROM ranked WHERE rnk = 2;

Output (computed against this section's live 9-table dataset)

department_idsalary
1215,000
5142,000
3118,000
6109,000
490,680.58
2131,000

(6 rows total)

Explanation

Using DENSE_RANK instead of ROW_NUMBER here matters: if two people in the same department were tied for the highest salary, ROW_NUMBER would arbitrarily assign one of them rank 2 (making them the 'second highest' even though they're actually tied for first), whereas DENSE_RANK correctly treats the tie as both being rank 1 and moves the true second-highest distinct salary to rank 2. All 6 departments have a well-defined second-highest salary in this dataset.

💻 Code example

WITH ranked AS ( SELECT department_id, salary, DENSE_RANK() OVER ( PARTITION BY department_id ORDER BY salary DESC ) AS rnk FROM employees ) SELECT department_id, salary FROM ranked WHERE rnk = 2;

P83 — Employees Above Department Average  ·  Advanced

Use a window function to find employees earning above their department average.

SQL

SELECT * FROM ( SELECT employee_id, first_name, department_id, salary, AVG(salary) OVER ( PARTITION BY department_id ) AS department_avg FROM employees ) x WHERE salary > department_avg;

Output (computed against this section's live 9-table dataset)

employee_idfirst_namedepartment_idsalarydepartment_avg
1Arvind1285,000138,588.51
2Meera1215,000138,588.51
3Karan2198,00092,711.24
4Priya3175,00099,118.03
5Suresh4160,00074,451.17
6Anil5205,00096,749.44
7Sunita6168,00090,014.29
8Rohan1152,000138,588.51
9Ananya1148,000138,588.51
10Vikram2131,00092,711.24

(showing 10 of 15 rows)

Explanation

This uses a window function (AVG(salary) OVER (PARTITION BY department_id)) inside a subquery so the department average can be attached to every employee row, then filters in an outer query on salary > department_avg — functionally the same result as the correlated subquery in Problem 42, but computed differently: the window function calculates every department's average once and broadcasts it to all rows in that partition, rather than re-running a subquery once per employee. 15 employees clear their department's average, matching Problem 42 exactly.

💻 Code example

SELECT * FROM ( SELECT employee_id, first_name, department_id, salary, AVG(salary) OVER ( PARTITION BY department_id ) AS department_avg FROM employees ) x WHERE salary > department_avg;

P84 — Customer Order Number  ·  Advanced

Assign a sequential order number to every customer's orders.

SQL

SELECT customer_id, order_id, order_date, ROW_NUMBER() OVER ( PARTITION BY customer_id ORDER BY order_date, order_id ) AS customer_order_number FROM orders;

Output (computed against this section's live 9-table dataset)

customer_idorder_idorder_datecustomer_order_number
31462024-03-041
3232024-05-062
31322024-06-103
3952024-07-244
3412024-08-115
3552024-10-156
3152024-11-257
31282025-04-178
31302025-05-209
3492025-07-1410

(showing 10 of 149 rows)

Explanation

ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date, order_id) numbers each customer's own orders 1, 2, 3... in chronological order — customer 3's earliest order (2024-03-04) is their order #1, the next their #2, and so on. This numbering is exactly what Problem 85 and Problem 86 filter on to isolate a single order (first or latest) per customer.

💻 Code example

SELECT customer_id, order_id, order_date, ROW_NUMBER() OVER ( PARTITION BY customer_id ORDER BY order_date, order_id ) AS customer_order_number FROM orders;

P85 — First Order for Each Customer  ·  Advanced

Return the complete row for each customer's first order.

SQL

WITH ranked AS ( SELECT o.*, ROW_NUMBER() OVER ( PARTITION BY customer_id ORDER BY order_date, order_id ) AS rn FROM orders o ) SELECT * FROM ranked WHERE rn = 1;

Output (computed against this section's live 9-table dataset)

order_idcustomer_idemployee_idorder_datestatustotal_amountrn
6112332024-12-01Shipped24,3881
3020152024-07-12Cancelled24,6881
12611332024-01-05Delivered6,5961
3516152024-01-21Delivered14,7911
992632024-01-16Shipped36,9891
1445102024-02-10Delivered9,0951
409152025-04-13Cancelled6,3961
8514332024-04-05Delivered19,5951
4817272024-09-12Cancelled8,0931
9215272024-09-17Pending6,5981

(showing 10 of 28 rows)

Explanation

Filtering Problem 84's numbering down to rn = 1 returns the complete row (every column, via o.*) for each customer's very first order — not just the date, but the full order record including status and amount. 28 rows come back, one per customer who has placed at least one order.

💻 Code example

WITH ranked AS ( SELECT o.*, ROW_NUMBER() OVER ( PARTITION BY customer_id ORDER BY order_date, order_id ) AS rn FROM orders o ) SELECT * FROM ranked WHERE rn = 1;

P86 — Latest Order for Each Customer  ·  Advanced

Return the complete latest order row for each customer.

SQL

WITH ranked AS ( SELECT o.*, ROW_NUMBER() OVER ( PARTITION BY customer_id ORDER BY order_date DESC, order_id DESC ) AS rn FROM orders o ) SELECT * FROM ranked WHERE rn = 1;

Output (computed against this section's live 9-table dataset)

order_idcustomer_idemployee_idorder_datestatustotal_amountrn
92392025-11-18Cancelled13,8911
1499,99932025-08-05Pending2,4991
1343212025-11-03Cancelled12,4951
606152025-06-09Shipped11,2901
5111332024-04-19Delivered8,8921
7116152025-11-17Delivered11,7941
992632024-01-16Shipped36,9891
621212025-09-02Delivered17,5911
12915272025-12-06Delivered45,5901
9022272025-12-04Pending9,6911

(showing 10 of 28 rows)

Explanation

Reversing the ORDER BY to order_date DESC, order_id DESC before numbering flips which order gets rn = 1 — now it's each customer's most recent order instead of their earliest. Interestingly, this also picks up the orphan order (customer_id = 9999, which doesn't match any real row in customers) as its own 'customer's' latest order — a reminder that this query only ever looks at orders in isolation, so a customer_id that doesn't actually exist in the customers table can still show up here.

💻 Code example

WITH ranked AS ( SELECT o.*, ROW_NUMBER() OVER ( PARTITION BY customer_id ORDER BY order_date DESC, order_id DESC ) AS rn FROM orders o ) SELECT * FROM ranked WHERE rn = 1;

P87 — Top 10 Percent Employees  ·  Advanced

Find employees belonging to the top 10% by salary.

SQL

WITH ranked AS ( SELECT e.*, NTILE(10) OVER (ORDER BY salary DESC) AS salary_bucket FROM employees e ) SELECT * FROM ranked WHERE salary_bucket = 1;

Output (computed against this section's live 9-table dataset)

employee_idfirst_namelast_nameemaildepartment_idmanager_idsalaryhire_datecitysalary_bucket
1ArvindKrishnanarvind.krishnan@company.com1NULL285,0002019-01-10Bengaluru1
2MeeraNairmeera.nair@company.com11215,0002019-06-15Bengaluru1
6AnilBhattanil.bhatt@company.com51205,0002020-01-15Pune1
3KaranMalhotrakaran.malhotra@company.com21198,0002019-08-01Mumbai1

(4 rows total)

Explanation

NTILE(10) splits the entire ordered result into 10 roughly-equal-sized buckets, numbered 1 (highest salaries, since the ordering is DESC) through 10 (lowest) — with 40 employees split 10 ways, each bucket holds exactly 4. Filtering WHERE salary_bucket = 1 returns the top decile: the 4 highest-paid people in the company, led by Arvind, Meera, and Anil Bhatt.

💻 Code example

WITH ranked AS ( SELECT e.*, NTILE(10) OVER (ORDER BY salary DESC) AS salary_bucket FROM employees e ) SELECT * FROM ranked WHERE salary_bucket = 1;

P88 — Quartile of Product Prices  ·  Advanced

Divide products into four price groups.

SQL

SELECT product_id, product_name, price, NTILE(4) OVER (ORDER BY price) AS price_quartile FROM products;

Output (computed against this section's live 9-table dataset)

product_idproduct_namepriceprice_quartile
26Atomic Habits4991
28Sapiens5991
15Non-stick Frying Pan6991
1Wireless Mouse7991
10Yoga Mat8991
25The Pragmatic Programmer8991
24Running Track Pants9991
21Men's Casual Shirt1,0992
16Electric Kettle1,1992
4USB-C Hub1,2992

(showing 10 of 28 rows)

Explanation

NTILE(4) divides the 28 products into 4 roughly-equal price quartiles (7 products each) after sorting by price ascending — quartile 1 holds the cheapest products (starting with Atomic Habits at ₹499), while quartile 4 would hold the most expensive. This is a common way to bucket a continuous value like price into a small number of discrete tiers for reporting or filtering.

💻 Code example

SELECT product_id, product_name, price, NTILE(4) OVER (ORDER BY price) AS price_quartile FROM products;

P89 — Monthly Revenue With Previous Month  ·  Advanced

Show monthly revenue and the previous month's revenue.

SQL

WITH monthly AS ( SELECT DATE_TRUNC('month', order_date) AS month, SUM(total_amount) AS revenue FROM orders WHERE status <> 'Cancelled' GROUP BY DATE_TRUNC('month', order_date) ) SELECT month, revenue, LAG(revenue) OVER (ORDER BY month) AS previous_month_revenue FROM monthly ORDER BY month;

Output (computed against this section's live 9-table dataset)

monthrevenueprevious_month_revenue
2024-01-0169,565NULL
2024-02-0125,88569,565
2024-03-0173,65625,885
2024-04-0134,48373,656
2024-05-0169,06334,483
2024-06-0145,77069,063
2024-07-01166,14245,770
2024-08-01141,448166,142
2024-09-0194,961141,448
2024-10-01247,51194,961

(showing 10 of 24 rows)

Explanation

The CTE first collapses orders into one revenue figure per month, then LAG(revenue) OVER (ORDER BY month) — with no PARTITION BY, since there's only one revenue series here — looks back to the immediately preceding month's total. January 2024 (this dataset's first month) correctly has no previous_month_revenue at all (NULL), since there's no month before it in the data; February's previous_month_revenue is populated with January's ₹69,565.

💻 Code example

WITH monthly AS ( SELECT DATE_TRUNC('month', order_date) AS month, SUM(total_amount) AS revenue FROM orders WHERE status <> 'Cancelled' GROUP BY DATE_TRUNC('month', order_date) ) SELECT month, revenue, LAG(revenue) OVER (ORDER BY month) AS previous_month_revenue FROM monthly ORDER BY month;

P90 — Month-over-Month Growth  ·  Advanced

Calculate monthly revenue growth percentage.

SQL

WITH monthly AS ( SELECT DATE_TRUNC('month', order_date) AS month, SUM(total_amount) AS revenue FROM orders WHERE status <> 'Cancelled' GROUP BY DATE_TRUNC('month', order_date) ), comparison AS ( SELECT month, revenue, LAG(revenue) OVER (ORDER BY month) AS previous_revenue FROM monthly ) SELECT month, revenue, ROUND( 100.0 * (revenue - previous_revenue) / NULLIF(previous_revenue, 0), 2 ) AS growth_percentage FROM comparison ORDER BY month;

Output (computed against this section's live 9-table dataset)

monthrevenuegrowth_percentage
2024-01-0169,565NULL
2024-02-0125,885-62.79
2024-03-0173,656184.55
2024-04-0134,483-53.18
2024-05-0169,063100.28
2024-06-0145,770-33.73
2024-07-01166,142262.99
2024-08-01141,448-14.86
2024-09-0194,961-32.87
2024-10-01247,511160.64

(showing 10 of 24 rows)

Explanation

Building on Problem 89's LAG, this computes (revenue - previous_revenue) / previous_revenue * 100 to express the change as a percentage rather than a raw difference — NULLIF(previous_revenue, 0) guards against a divide-by-zero error in case any month had exactly ₹0 in prior revenue (none did here, but it's cheap insurance). Revenue actually fell 62.79% from January to February 2024 in this dataset before rebounding sharply (+184.55%) in March — real month-to-month swings like this are exactly why growth percentages, not raw revenue, are often what gets tracked on a dashboard.

💻 Code example

WITH monthly AS ( SELECT DATE_TRUNC('month', order_date) AS month, SUM(total_amount) AS revenue FROM orders WHERE status <> 'Cancelled' GROUP BY DATE_TRUNC('month', order_date) ), comparison AS ( SELECT month, revenue, LAG(revenue) OVER (ORDER BY month) AS previous_revenue FROM monthly ) SELECT month, revenue, ROUND( 100.0 * (revenue - previous_revenue) / NULLIF(previous_revenue, 0), 2 ) AS growth_percentage FROM comparison ORDER BY month;

P91 — Three-Month Moving Average  ·  Advanced

Calculate a three-month moving average of revenue.

SQL

WITH monthly AS ( SELECT DATE_TRUNC('month', order_date) AS month, SUM(total_amount) AS revenue FROM orders WHERE status <> 'Cancelled' GROUP BY DATE_TRUNC('month', order_date) ) SELECT month, revenue, AVG(revenue) OVER ( ORDER BY month ROWS BETWEEN 2 PRECEDING AND CURRENT ROW ) AS moving_average FROM monthly;

Output (computed against this section's live 9-table dataset)

monthrevenuemoving_average
2024-01-0169,56569,565
2024-02-0125,88547,725
2024-03-0173,65656,368.67
2024-04-0134,48344,674.67
2024-05-0169,06359,067.33
2024-06-0145,77049,772
2024-07-01166,14293,658.33
2024-08-01141,448117,786.67
2024-09-0194,961134,183.67
2024-10-01247,511161,306.67

(showing 10 of 24 rows)

Explanation

AVG(revenue) OVER (ORDER BY month ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) computes a genuine 3-month rolling window: for any given month, it averages that month's revenue together with the two months immediately before it. The very first month (January 2024) only has itself to average (no prior months exist yet), so its moving average equals its own revenue exactly; only from the third month onward does the window contain a full three data points, which smooths out month-to-month noise like the sharp February dip seen in Problem 90.

💻 Code example

WITH monthly AS ( SELECT DATE_TRUNC('month', order_date) AS month, SUM(total_amount) AS revenue FROM orders WHERE status <> 'Cancelled' GROUP BY DATE_TRUNC('month', order_date) ) SELECT month, revenue, AVG(revenue) OVER ( ORDER BY month ROWS BETWEEN 2 PRECEDING AND CURRENT ROW ) AS moving_average FROM monthly;

P92 — Customers With Consecutive Orders  ·  Advanced

Find customers who placed orders on consecutive calendar days.

SQL

WITH x AS ( SELECT customer_id, order_date, LAG(order_date) OVER ( PARTITION BY customer_id ORDER BY order_date ) AS previous_date FROM orders ) SELECT DISTINCT customer_id FROM x WHERE order_date = previous_date + INTERVAL '1 day';

Output (computed against this section's live 9-table dataset)

customer_id
1

(1 row total)

Explanation

LAG(order_date) OVER (PARTITION BY customer_id ORDER BY order_date) finds each customer's immediately preceding order date, and the outer query checks whether the current order landed exactly one calendar day later. Only customer 1 qualifies in this dataset — they placed one order on 2025-03-14 and another the very next day, 2025-03-15, a pattern deliberately built into the data to demonstrate this exact technique.

💻 Code example

WITH x AS ( SELECT customer_id, order_date, LAG(order_date) OVER ( PARTITION BY customer_id ORDER BY order_date ) AS previous_date FROM orders ) SELECT DISTINCT customer_id FROM x WHERE order_date = previous_date + INTERVAL '1 day';

P93 — Duplicate Orders by Customer and Date  ·  Advanced

Find customers who placed more than one order on the same date.

SQL

SELECT customer_id, order_date, COUNT(*) AS order_count FROM orders GROUP BY customer_id, order_date HAVING COUNT(*) > 1;

Output (computed against this section's live 9-table dataset)

customer_idorder_dateorder_count
22025-06-102

(1 row total)

Explanation

Unlike Problem 92 (which looks for orders exactly one day apart), this groups by both customer_id and the exact same order_date to catch same-day duplicates — a signal that could indicate a genuine repeat purchase, a data-entry duplicate, or a retried checkout. Customer 2 placed two separate orders on 2025-06-10, deliberately included in this dataset to exercise exactly this check.

💻 Code example

SELECT customer_id, order_date, COUNT(*) AS order_count FROM orders GROUP BY customer_id, order_date HAVING COUNT(*) > 1;

P94 — Products With Revenue Above Category Average  ·  Advanced

Find products whose revenue is above the average product revenue in their category.

SQL

WITH product_revenue AS ( SELECT p.product_id, p.product_name, p.category, COALESCE(SUM( CASE WHEN o.status <> 'Cancelled' THEN oi.quantity * oi.unit_price ELSE 0 END ), 0) AS revenue FROM products p LEFT JOIN order_items oi ON p.product_id = oi.product_id LEFT JOIN orders o ON oi.order_id = o.order_id GROUP BY p.product_id, p.product_name, p.category ) SELECT * FROM ( SELECT pr.*, AVG(revenue) OVER (PARTITION BY category) AS category_avg FROM product_revenue pr ) x WHERE revenue > category_avg;

Output (computed against this section's live 9-table dataset)

product_idproduct_namecategoryrevenuecategory_avg
327-inch 4K MonitorElectronics849,966219,770
25The Pragmatic ProgrammerBooks25,17220,953
27Designing Data-Intensive ApplicationsBooks40,47320,953
20Ceramic Dinner SetHome & Kitchen85,76179,504.50
19Mixer GrinderHome & Kitchen95,67179,504.50
17Air FryerHome & Kitchen215,96479,504.50
23Denim JacketClothing82,46738,701
11Dumbbell Set 10kgSports96,16337,779.17

(8 rows total)

Explanation

Computing per-product revenue with conditional aggregation first (so cancelled orders never contribute), the outer window function then attaches each category's average product revenue to every row in that category via AVG(revenue) OVER (PARTITION BY category), and the final WHERE keeps only products beating their own category's average. The 27-inch 4K Monitor dominates Electronics so heavily (₹849,966 vs. a category average of ₹219,770) that it single-handedly makes the category average hard for any other Electronics product to clear.

💻 Code example

WITH product_revenue AS ( SELECT p.product_id, p.product_name, p.category, COALESCE(SUM( CASE WHEN o.status <> 'Cancelled' THEN oi.quantity * oi.unit_price ELSE 0 END ), 0) AS revenue FROM products p LEFT JOIN order_items oi ON p.product_id = oi.product_id LEFT JOIN orders o ON oi.order_id = o.order_id GROUP BY p.product_id, p.product_name, p.category ) SELECT * FROM ( SELECT pr.*, AVG(revenue) OVER (PARTITION BY category) AS category_avg FROM product_revenue pr ) x WHERE revenue > category_avg;

P95 — Customer Spending Rank  ·  Advanced

Rank customers by total spending.

SQL

WITH spending AS ( SELECT c.customer_id, c.customer_name, COALESCE(SUM( CASE WHEN o.status <> 'Cancelled' THEN o.total_amount ELSE 0 END ), 0) AS total_spent FROM customers c LEFT JOIN orders o ON c.customer_id = o.customer_id GROUP BY c.customer_id, c.customer_name ) SELECT *, RANK() OVER (ORDER BY total_spent DESC) AS spending_rank FROM spending;

Output (computed against this section's live 9-table dataset)

customer_idcustomer_nametotal_spentspending_rank
5Vikas Rana316,8101
7Rajesh Pandey301,1212
3Amit Bhatia272,0083
2Sneha Nair257,9154
4Pooja Sinha213,1375
1Rahul Agarwal194,9866
6Neha Ghosh168,3067
8Swati Yadav146,4338
19Kunal Malhotra136,6609
21John Smith122,78310

(showing 10 of 30 rows)

Explanation

This wraps Problem 70's lifetime-spending calculation in a RANK() OVER (ORDER BY total_spent DESC) — since total_spent is computed first inside the spending CTE, the ranking operates on the already-aggregated per-customer totals rather than raw order rows. Vikas Rana ranks #1 at ₹316,810, matching the top of Problem 70's list exactly, since both queries compute lifetime value the same way.

💻 Code example

WITH spending AS ( SELECT c.customer_id, c.customer_name, COALESCE(SUM( CASE WHEN o.status <> 'Cancelled' THEN o.total_amount ELSE 0 END ), 0) AS total_spent FROM customers c LEFT JOIN orders o ON c.customer_id = o.customer_id GROUP BY c.customer_id, c.customer_name ) SELECT *, RANK() OVER (ORDER BY total_spent DESC) AS spending_rank FROM spending;

P96 — Top 3 Customers Per Country  ·  Advanced

Find the three highest-spending customers in each country.

SQL

WITH spending AS ( SELECT c.customer_id, c.customer_name, c.country, COALESCE(SUM( CASE WHEN o.status <> 'Cancelled' THEN o.total_amount ELSE 0 END ), 0) AS total_spent FROM customers c LEFT JOIN orders o ON c.customer_id = o.customer_id GROUP BY c.customer_id, c.customer_name, c.country ), ranked AS ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY country ORDER BY total_spent DESC ) AS rn FROM spending ) SELECT * FROM ranked WHERE rn <= 3;

Output (computed against this section's live 9-table dataset)

customer_idcustomer_namecountrytotal_spentrn
23Michael JohnsonCanada5,7941
30Grace ClarkCanada02
21John SmithUSA122,7831
28Sofia RossiUSA02
26Fatima AliSingapore36,9891
22Emma BrownUK120,8771
29David MillerUK02
5Vikas RanaIndia316,8101
7Rajesh PandeyIndia301,1212
3Amit BhatiaIndia272,0083

(showing 10 of 13 rows)

Explanation

Layering PARTITION BY country onto the ranking from Problem 95 restarts the rank at 1 within every country, and filtering rn <= 3 keeps each country's top 3 spenders — small countries with only 1-2 customers (like Canada or the UK) simply return all of their customers, correctly ranked, since there aren't three to compete for the podium. Notably, some customers with ₹0 in lifetime spending still appear here (rank 2 in Canada, rank 2 in the UK) — they have zero spend but are still their country's second-highest, which is a fair answer, just a low bar.

💻 Code example

WITH spending AS ( SELECT c.customer_id, c.customer_name, c.country, COALESCE(SUM( CASE WHEN o.status <> 'Cancelled' THEN o.total_amount ELSE 0 END ), 0) AS total_spent FROM customers c LEFT JOIN orders o ON c.customer_id = o.customer_id GROUP BY c.customer_id, c.customer_name, c.country ), ranked AS ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY country ORDER BY total_spent DESC ) AS rn FROM spending ) SELECT * FROM ranked WHERE rn <= 3;

P97 — Employee Project Hours  ·  Advanced

Calculate total project hours for each employee.

SQL

SELECT e.employee_id, e.first_name, COALESCE(SUM(ep.hours_worked), 0) AS total_hours FROM employees e LEFT JOIN employee_projects ep ON e.employee_id = ep.employee_id GROUP BY e.employee_id, e.first_name;

Output (computed against this section's live 9-table dataset)

employee_idfirst_nametotal_hours
14Rohan400
38Riya390
22Arjun0
6Anil60
1Arvind0
19Vikram0
30Manish55
13Riya0
35Varun0
4Priya130

(showing 10 of 40 rows)

Explanation

LEFT JOIN plus COALESCE(SUM(...), 0) — the same pattern from Problem 36 — ensures every employee appears with a real number, including the many who were never assigned to any project at all (they show 0.0 total hours rather than being silently dropped). Rohan (employee 14) leads with 400 hours logged across his project assignments.

💻 Code example

SELECT e.employee_id, e.first_name, COALESCE(SUM(ep.hours_worked), 0) AS total_hours FROM employees e LEFT JOIN employee_projects ep ON e.employee_id = ep.employee_id GROUP BY e.employee_id, e.first_name;

P98 — Most Active Project  ·  Advanced

Find the project with the highest total employee hours.

SQL

SELECT p.project_id, p.project_name, SUM(ep.hours_worked) AS total_hours FROM projects p JOIN employee_projects ep ON p.project_id = ep.project_id GROUP BY p.project_id, p.project_name ORDER BY total_hours DESC LIMIT 1;

Output (computed against this section's live 9-table dataset)

project_idproject_nametotal_hours
2Data Warehouse Migration1,190

(1 row total)

Explanation

Grouping employee_projects by project_id (via a join to projects for the name) and summing hours_worked, then sorting descending and keeping just the top row, finds the single project that consumed the most person-hours. The Data Warehouse Migration project leads by a wide margin at 1,190 total hours — unsurprising given it also carries the dataset's largest team.

💻 Code example

SELECT p.project_id, p.project_name, SUM(ep.hours_worked) AS total_hours FROM projects p JOIN employee_projects ep ON p.project_id = ep.project_id GROUP BY p.project_id, p.project_name ORDER BY total_hours DESC LIMIT 1;

P99 — Employees Working on Multiple Projects  ·  Advanced

Find employees assigned to at least two projects.

SQL

SELECT employee_id, COUNT(DISTINCT project_id) AS project_count FROM employee_projects GROUP BY employee_id HAVING COUNT(DISTINCT project_id) >= 2;

Output (computed against this section's live 9-table dataset)

employee_idproject_count
322
62
22
382
142
92
82
42

(8 rows total)

Explanation

COUNT(DISTINCT project_id) per employee (note: DISTINCT matters in principle, though employee_projects has no duplicate employee/project pairs here) filtered by HAVING >= 2 finds employees stretched across multiple projects at once. 8 employees qualify, each juggling exactly two project assignments in this dataset — a realistic cross-section of a company's most in-demand engineers and managers.

💻 Code example

SELECT employee_id, COUNT(DISTINCT project_id) AS project_count FROM employee_projects GROUP BY employee_id HAVING COUNT(DISTINCT project_id) >= 2;

P100 — Projects With No Employees  ·  Advanced

Find projects that have no employee assignments.

SQL

SELECT p.project_id, p.project_name FROM projects p LEFT JOIN employee_projects ep ON p.project_id = ep.project_id WHERE ep.employee_id IS NULL;

Output (computed against this section's live 9-table dataset)

project_idproject_name
8Legacy System Decommission

(1 row total)

Explanation

The same LEFT JOIN ... WHERE ... IS NULL 'find unmatched rows' pattern used for products (P35) and customers (P33), now applied to projects against employee_projects. Exactly one project — Legacy System Decommission — has no employee assignments at all, deliberately left that way in this dataset as a completed, already-wound-down initiative.

💻 Code example

SELECT p.project_id, p.project_name FROM projects p LEFT JOIN employee_projects ep ON p.project_id = ep.project_id WHERE ep.employee_id IS NULL;

P101 — Employees Who Worked on the Largest Project  ·  Advanced

Find employees assigned to the project with the highest budget.

SQL

SELECT DISTINCT e.employee_id, e.first_name, p.project_name, p.budget FROM employees e JOIN employee_projects ep ON e.employee_id = ep.employee_id JOIN projects p ON ep.project_id = p.project_id WHERE p.budget = ( SELECT MAX(budget) FROM projects );

Output (computed against this section's live 9-table dataset)

employee_idfirst_nameproject_namebudget
9AnanyaFraud Detection Engine3,200,000
38RiyaFraud Detection Engine3,200,000
6AnilFraud Detection Engine3,200,000
32SaanviFraud Detection Engine3,200,000

(4 rows total)

Explanation

The subquery SELECT MAX(budget) FROM projects finds the single largest budget (₹3,200,000, belonging to the Fraud Detection Engine), and the outer query then finds every employee who worked on that specific project — SELECT DISTINCT matters because an employee could in principle have multiple employee_projects rows for the same project, which would otherwise duplicate them in the output. 4 employees worked on it, including manager Ananya Singh.

💻 Code example

SELECT DISTINCT e.employee_id, e.first_name, p.project_name, p.budget FROM employees e JOIN employee_projects ep ON e.employee_id = ep.employee_id JOIN projects p ON ep.project_id = p.project_id WHERE p.budget = ( SELECT MAX(budget) FROM projects );

P102 — Employee Count Per Project  ·  Advanced

Show every project and the number of assigned employees.

SQL

SELECT p.project_id, p.project_name, COUNT(DISTINCT ep.employee_id) AS employee_count FROM projects p LEFT JOIN employee_projects ep ON p.project_id = ep.project_id GROUP BY p.project_id, p.project_name;

Output (computed against this section's live 9-table dataset)

project_idproject_nameemployee_count
5Internal HR Portal3
1Customer Portal Revamp4
2Data Warehouse Migration5
3Mobile App Launch4
6Fraud Detection Engine4
8Legacy System Decommission0
4Marketing Automation3
7Vendor Payment Automation2

(8 rows total)

Explanation

LEFT JOIN plus COUNT(DISTINCT ep.employee_id) ensures every project appears in the result, even Legacy System Decommission (Problem 100's zero-employee project) — for that row, the COUNT correctly returns 0 rather than the row disappearing entirely, because COUNT (unlike SUM) counts non-null values, and COUNT(*) would incorrectly count 1 due to the single unmatched NULL row the LEFT JOIN produces.

💻 Code example

SELECT p.project_id, p.project_name, COUNT(DISTINCT ep.employee_id) AS employee_count FROM projects p LEFT JOIN employee_projects ep ON p.project_id = ep.project_id GROUP BY p.project_id, p.project_name;

P103 — Department With Highest Average Salary  ·  Advanced

Find the department with the highest average employee salary.

SQL

SELECT department_id, AVG(salary) AS average_salary FROM employees GROUP BY department_id ORDER BY average_salary DESC LIMIT 1;

Output (computed against this section's live 9-table dataset)

department_idaverage_salary
1138,588.51

(1 row total)

Explanation

This reuses Problem 26's per-department average salary calculation, sorts it descending, and keeps only the top row — Engineering has both the highest average salary (~₹138,589) and, as seen in Problem 67, the widest range, since it houses both the CEO and several of the newest, lowest-paid hires.

💻 Code example

SELECT department_id, AVG(salary) AS average_salary FROM employees GROUP BY department_id ORDER BY average_salary DESC LIMIT 1;

P104 — Customer's First and Last Order  ·  Advanced

Show first order date, last order date, and order count per customer.

SQL

SELECT customer_id, MIN(order_date) AS first_order, MAX(order_date) AS last_order, COUNT(*) AS order_count FROM orders GROUP BY customer_id;

Output (computed against this section's live 9-table dataset)

customer_idfirst_orderlast_orderorder_count
12024-02-032025-09-0214
172024-09-122025-11-172
122024-12-012025-08-032
272025-09-122025-09-121
162024-01-212025-11-174
232024-07-112024-07-111
62024-03-222025-06-0912
102024-03-062025-03-113
152024-09-172025-12-064
182024-08-252025-06-173

(showing 10 of 28 rows)

Explanation

Three aggregates computed together — MIN, MAX, and COUNT — summarize each customer's entire order history in one row: when they first ordered, when they most recently ordered, and how many orders they've placed in between. Customer 1 stands out with 14 orders spanning from February 2024 to September 2025, one of this dataset's most active buyers.

💻 Code example

SELECT customer_id, MIN(order_date) AS first_order, MAX(order_date) AS last_order, COUNT(*) AS order_count FROM orders GROUP BY customer_id;

P105 — Customer Retention After First Order  ·  Advanced

Find customers who placed another order after their first order.

SQL

WITH order_history AS ( SELECT customer_id, order_date, ROW_NUMBER() OVER ( PARTITION BY customer_id ORDER BY order_date, order_id ) AS rn FROM orders ) SELECT customer_id FROM order_history GROUP BY customer_id HAVING COUNT(*) > 1;

Output (computed against this section's live 9-table dataset)

customer_id
17
1
12
8
3
2
15
6
10
18

(showing 10 of 22 rows)

Explanation

The CTE numbers each customer's orders chronologically with ROW_NUMBER() (identical setup to Problem 84), and the outer query groups by customer and keeps anyone with more than one row — meaning they placed at least a second order after their first. 22 of the 28 customers with any order history qualify as 'retained' by this simple definition.

💻 Code example

WITH order_history AS ( SELECT customer_id, order_date, ROW_NUMBER() OVER ( PARTITION BY customer_id ORDER BY order_date, order_id ) AS rn FROM orders ) SELECT customer_id FROM order_history GROUP BY customer_id HAVING COUNT(*) > 1;

P106 — Cancellation Rate  ·  Advanced

Calculate the percentage of orders that were cancelled.

SQL

SELECT ROUND( 100.0 * SUM(CASE WHEN status = 'Cancelled' THEN 1 ELSE 0 END) / COUNT(*), 2 ) AS cancellation_rate FROM orders;

Output (computed against this section's live 9-table dataset)

cancellation_rate
16.78

(1 row total)

Explanation

The same conditional-aggregation pattern as Problem 57's payment success rate, applied to order status instead of payment status: SUM(CASE WHEN status = 'Cancelled' THEN 1 ELSE 0 END) / COUNT(*). 16.78% of all 149 orders in this dataset were cancelled — close to, but not exactly, the ~20% weighting used when the data was generated, since randomness doesn't hit its target exactly over a finite sample.

💻 Code example

SELECT ROUND( 100.0 * SUM(CASE WHEN status = 'Cancelled' THEN 1 ELSE 0 END) / COUNT(*), 2 ) AS cancellation_rate FROM orders;

P107 — Revenue by Customer Country  ·  Advanced

Calculate total non-cancelled revenue for each customer country.

SQL

SELECT c.country, SUM(o.total_amount) AS revenue FROM customers c JOIN orders o ON c.customer_id = o.customer_id WHERE o.status <> 'Cancelled' GROUP BY c.country ORDER BY revenue DESC;

Output (computed against this section's live 9-table dataset)

countryrevenue
India2,348,500
USA122,783
UK120,877
Germany60,289
Singapore36,989
Canada5,794
Australia3,897
UAE3,397

(8 rows total)

Explanation

Joining customers to orders and excluding cancelled orders before grouping by country shows where real revenue actually comes from geographically. India dominates at ₹2,348,500 — completely unsurprising given 20 of the 30 customers are based there — with the USA (₹122,783) a distant second among the international markets.

💻 Code example

SELECT c.country, SUM(o.total_amount) AS revenue FROM customers c JOIN orders o ON c.customer_id = o.customer_id WHERE o.status <> 'Cancelled' GROUP BY c.country ORDER BY revenue DESC;

P108 — Revenue Contribution Percentage  ·  Advanced

Find each customer's percentage contribution to total revenue.

SQL

WITH spending AS ( SELECT customer_id, SUM(total_amount) AS revenue FROM orders WHERE status <> 'Cancelled' GROUP BY customer_id ) SELECT customer_id, revenue, ROUND( 100.0 * revenue / SUM(revenue) OVER (), 2 ) AS revenue_percentage FROM spending;

Output (computed against this section's live 9-table dataset)

customer_idrevenuerevenue_percentage
1194,9867.21
1710,3880.38
1224,3880.90
273,3970.13
1632,3791.20
235,7940.21
6168,3066.22
1017,5830.65
1564,7802.39
1832,5841.20

(showing 10 of 28 rows)

Explanation

SUM(revenue) OVER () with empty parentheses — no PARTITION BY, no ORDER BY — computes one grand total across the entire result set and repeats that same value on every row, which is exactly what's needed as the denominator for a percentage-of-total calculation. Customer 1 alone contributes 7.21% of all revenue in this dataset, the single largest share of any individual customer.

💻 Code example

WITH spending AS ( SELECT customer_id, SUM(total_amount) AS revenue FROM orders WHERE status <> 'Cancelled' GROUP BY customer_id ) SELECT customer_id, revenue, ROUND( 100.0 * revenue / SUM(revenue) OVER (), 2 ) AS revenue_percentage FROM spending;

P109 — Products With More Than 100 Units Sold  ·  Advanced

Find products whose total quantity sold exceeds 100 units.

SQL

SELECT product_id, SUM(quantity) AS units_sold FROM order_items GROUP BY product_id HAVING SUM(quantity) > 100;

Output (computed against this section's live 9-table dataset)

product_idunits_sold
1207
4145

(2 rows total)

Explanation

Summing quantity from order_items grouped by product_id and filtering HAVING SUM(quantity) > 100 finds genuinely high-volume movers — only 2 products clear that bar: the Wireless Mouse (207 units) and the USB-C Hub (145 units), both of which this dataset deliberately biases toward being purchased together far more often than other products (see Problem 113).

💻 Code example

SELECT product_id, SUM(quantity) AS units_sold FROM order_items GROUP BY product_id HAVING SUM(quantity) > 100;

P110 — Order Value Reconciliation  ·  Advanced

Find orders where the stored total amount differs from the calculated item total.

SQL

SELECT o.order_id, o.total_amount AS stored_total, SUM(oi.quantity * oi.unit_price) AS calculated_total FROM orders o JOIN order_items oi ON o.order_id = oi.order_id GROUP BY o.order_id, o.total_amount HAVING o.total_amount <> SUM(oi.quantity * oi.unit_price);

Output (computed against this section's live 9-table dataset)

order_idstored_totalcalculated_total
7610,99110,491
583,1473,397
14862,89161,892
75-49914,190
6910,2969,996
1154,4954,995
3687,79086,791
68013,693

(8 rows total)

Explanation

Comparing the orders.total_amount stored on the parent row against a freshly recalculated SUM(quantity * unit_price) from order_items catches exactly the kind of silent data drift that happens in real systems when a price changes after an order was placed, or when a total gets updated in one place but not another. 8 orders show a mismatch in this dataset — 6 of them were deliberately corrupted by a small random amount for this exercise, and 2 more are the same orders Problem 149 flags for having a zero or negative total_amount, since those, too, obviously disagree with their real item total.

💻 Code example

SELECT o.order_id, o.total_amount AS stored_total, SUM(oi.quantity * oi.unit_price) AS calculated_total FROM orders o JOIN order_items oi ON o.order_id = oi.order_id GROUP BY o.order_id, o.total_amount HAVING o.total_amount <> SUM(oi.quantity * oi.unit_price);

Want a visual for this concept?

Generate a diagram tailored to “SQL Practice — Advanced (Problems 71-110)” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

Continue to SQL Practice — Expert & Real-World (Problems 111-150)← Back to all SQL Practice Problems chapters