advanced~7h

SQL Practice — Expert & Real-World (Problems 111-150)

40 real-interview-style problems on recursive CTEs, cohort analysis, retention, Pareto analysis, medians, and data-quality validation — every one solved, run against real data, and explained, including two problems where the given solution has a real limitation worth understanding.

Learning objectives

  • Build and read a recursive CTE for an org chart or any other self-referencing hierarchy.
  • Run cohort, retention, and streak analysis (3 consecutive months, Pareto 80/20) with window functions.
  • Compute medians and percentiles with PERCENTILE_CONT and know why they resist skew better than AVG.
  • Spot the specific failure mode of LEFT JOIN ... IS NULL against a one-to-many relationship, and fix it with NOT EXISTS.

P111 — Customers Who Bought Every Product in a Category  ·  Expert

Find customers who purchased every product in the Electronics category.

SQL

SELECT o.customer_id FROM orders o JOIN order_items oi ON o.order_id = oi.order_id JOIN products p ON oi.product_id = p.product_id WHERE p.category = 'Electronics' AND o.status <> 'Cancelled' GROUP BY o.customer_id HAVING COUNT(DISTINCT p.product_id) = ( SELECT COUNT(*) FROM products WHERE category = 'Electronics' );

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

customer_id
1

(1 row total)

Explanation

This checks a strict 'bought absolutely everything' condition: HAVING COUNT(DISTINCT p.product_id) = (SELECT COUNT(*) FROM products WHERE category = 'Electronics') only passes if a customer's distinct Electronics purchases equal the entire size of that category (8 products). Only customer 1 clears that bar in this dataset — via a dedicated order deliberately built to cover all 8 Electronics products in one go, since hitting every single item in a category by ordinary chance across a handful of orders would be extremely unlikely.

💻 Code example

SELECT o.customer_id FROM orders o JOIN order_items oi ON o.order_id = oi.order_id JOIN products p ON oi.product_id = p.product_id WHERE p.category = 'Electronics' AND o.status <> 'Cancelled' GROUP BY o.customer_id HAVING COUNT(DISTINCT p.product_id) = ( SELECT COUNT(*) FROM products WHERE category = 'Electronics' );

P112 — Customers Who Bought at Least One Product From Every Category  ·  Expert

Find customers who purchased products from every product category.

SQL

WITH customer_categories AS ( SELECT o.customer_id, COUNT(DISTINCT p.category) AS category_count FROM orders o JOIN order_items oi ON o.order_id = oi.order_id JOIN products p ON oi.product_id = p.product_id WHERE o.status <> 'Cancelled' GROUP BY o.customer_id ) SELECT customer_id FROM customer_categories WHERE category_count = ( SELECT COUNT(DISTINCT category) FROM products );

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

customer_id
6
3
9
1
8
7
4

(7 rows total)

Explanation

This relaxes Problem 111's bar considerably: instead of buying every product in one category, a customer just needs to have bought something from every category (5 total: Electronics, Sports, Home & Kitchen, Clothing, Books). That's a much easier target for a customer with several varied orders to hit naturally, and 7 of the 30 customers manage it in this dataset.

💻 Code example

WITH customer_categories AS ( SELECT o.customer_id, COUNT(DISTINCT p.category) AS category_count FROM orders o JOIN order_items oi ON o.order_id = oi.order_id JOIN products p ON oi.product_id = p.product_id WHERE o.status <> 'Cancelled' GROUP BY o.customer_id ) SELECT customer_id FROM customer_categories WHERE category_count = ( SELECT COUNT(DISTINCT category) FROM products );

P113 — Most Popular Product Pair  ·  Expert

Find the pair of products most frequently purchased together in the same order.

SQL

SELECT oi1.product_id AS product_a, oi2.product_id AS product_b, COUNT(*) AS pair_count FROM order_items oi1 JOIN order_items oi2 ON oi1.order_id = oi2.order_id AND oi1.product_id < oi2.product_id GROUP BY oi1.product_id, oi2.product_id ORDER BY pair_count DESC LIMIT 1;

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

product_aproduct_bpair_count
1440

(1 row total)

Explanation

The self-join oi1.order_id = oi2.order_id AND oi1.product_id < oi2.product_id pairs up every two different products that appeared together in the same order, counts how often each specific pair recurs, and the < (rather than <>) is what prevents each pair from being counted twice in both directions (product 1 with product 4, and product 4 with product 1). Products 1 and 4 — the Wireless Mouse and the USB-C Hub — come out as the single most frequent pairing, appearing together in 40 separate orders, a strong affinity deliberately built into this dataset's order-generation logic.

💻 Code example

SELECT oi1.product_id AS product_a, oi2.product_id AS product_b, COUNT(*) AS pair_count FROM order_items oi1 JOIN order_items oi2 ON oi1.order_id = oi2.order_id AND oi1.product_id < oi2.product_id GROUP BY oi1.product_id, oi2.product_id ORDER BY pair_count DESC LIMIT 1;

P114 — Customers Who Bought a Product but Never Again  ·  Expert

Find customers whose last order was exactly their first order.

SQL

SELECT customer_id FROM orders GROUP BY customer_id HAVING MIN(order_date) = MAX(order_date);

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

customer_id
23
24
25
9,999
27
26

(6 rows total)

Explanation

MIN(order_date) = MAX(order_date) per customer is only true when a customer has exactly one order — if they had two or more, their earliest and latest order dates would necessarily differ. 6 customers in this dataset placed exactly one order and never returned, including customers 23-25, three accounts deliberately generated as single-order buyers.

💻 Code example

SELECT customer_id FROM orders GROUP BY customer_id HAVING MIN(order_date) = MAX(order_date);

P115 — Longest Gap Between Customer Orders  ·  Expert

Find the maximum number of days between consecutive orders for each customer.

SQL

WITH gaps AS ( SELECT customer_id, order_date, order_date - LAG(order_date) OVER ( PARTITION BY customer_id ORDER BY order_date ) AS gap_days FROM orders ) SELECT customer_id, MAX(gap_days) AS longest_gap FROM gaps GROUP BY customer_id;

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

customer_idlongest_gap
17431
1177
12245
27NULL
23NULL
16381
10250
18181
662
15220

(showing 10 of 28 rows)

Explanation

LAG(order_date) OVER (PARTITION BY customer_id ORDER BY order_date) computes the days since the previous order for every order a customer placed, and MAX(gap_days) per customer then finds their single longest quiet stretch. Customer 17 has the largest gap in the dataset at 431 days between two orders — more than a year of inactivity before they came back.

💻 Code example

WITH gaps AS ( SELECT customer_id, order_date, order_date - LAG(order_date) OVER ( PARTITION BY customer_id ORDER BY order_date ) AS gap_days FROM orders ) SELECT customer_id, MAX(gap_days) AS longest_gap FROM gaps GROUP BY customer_id;

P116 — Customers With Increasing Order Values  ·  Expert

Find customers whose order amount increased compared with their previous order every time after the first order.

SQL

WITH x AS ( SELECT customer_id, order_id, total_amount, LAG(total_amount) OVER ( PARTITION BY customer_id ORDER BY order_date, order_id ) AS previous_amount FROM orders ), summary AS ( SELECT customer_id, COUNT(*) FILTER ( WHERE previous_amount IS NOT NULL AND total_amount > previous_amount ) AS increases, COUNT(*) FILTER ( WHERE previous_amount IS NOT NULL ) AS comparisons FROM x GROUP BY customer_id ) SELECT customer_id FROM summary WHERE comparisons > 0 AND increases = comparisons;

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

customer_id
17
11
21

(3 rows total)

Explanation

The FILTER (WHERE ...) clause lets a single COUNT(*) aggregate only rows matching an extra condition, without needing separate CASE WHEN expressions — increases counts comparisons where the order grew, comparisons counts all valid (non-first) comparisons, and a customer only qualifies if every single comparison was an increase (increases = comparisons, with comparisons > 0 guarding against customers with only one order, who'd otherwise trivially pass with 0 = 0). Only 3 customers in this dataset had every one of their orders strictly larger than the one before it.

💻 Code example

WITH x AS ( SELECT customer_id, order_id, total_amount, LAG(total_amount) OVER ( PARTITION BY customer_id ORDER BY order_date, order_id ) AS previous_amount FROM orders ), summary AS ( SELECT customer_id, COUNT(*) FILTER ( WHERE previous_amount IS NOT NULL AND total_amount > previous_amount ) AS increases, COUNT(*) FILTER ( WHERE previous_amount IS NOT NULL ) AS comparisons FROM x GROUP BY customer_id ) SELECT customer_id FROM summary WHERE comparisons > 0 AND increases = comparisons;

P117 — Employees With No Projects in the Last Year  ·  Expert

Assuming the reference date is 2026-01-01, find employees who worked on no projects during 2025.

SQL

SELECT e.employee_id, e.first_name FROM employees e LEFT JOIN employee_projects ep ON e.employee_id = ep.employee_id LEFT JOIN projects p ON ep.project_id = p.project_id AND p.start_date >= '2025-01-01' AND p.start_date < '2026-01-01' WHERE p.project_id IS NULL;

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

employee_idfirst_name
2Meera
5Suresh
8Rohan
14Rohan
18Sanjay
20Deepak
24Rahul
26Suresh
2Meera
4Priya

(showing 10 of 39 rows)

Explanation

This query has a real, worth-knowing limitation. Its LEFT JOIN ... WHERE p.project_id IS NULL pattern correctly finds employees with zero project rows at all, but for employees with multiple project assignments, it filters row by row rather than per employee — so an employee who worked on one project in 2025 and one project before 2025 still has a row where the date-filtered join fails to match (their pre-2025 project), and that row alone is enough to let them slip into the 'no projects in 2025' result. In this dataset, employees 4, 6, 9, 32, and 38 all genuinely worked on a 2025 project but leak into the 39-row result anyway for exactly this reason — only employees 11, 12, 17, and 30 (who happened to have only a single, 2025-dated project) are excluded correctly. A reliable version of this check would use NOT EXISTS (SELECT 1 FROM employee_projects ep JOIN projects p ON ... WHERE ep.employee_id = e.employee_id AND p.start_date >= '2025-01-01' AND p.start_date < '2026-01-01') instead, which evaluates the condition per employee rather than per matched row.

💻 Code example

SELECT e.employee_id, e.first_name FROM employees e LEFT JOIN employee_projects ep ON e.employee_id = ep.employee_id LEFT JOIN projects p ON ep.project_id = p.project_id AND p.start_date >= '2025-01-01' AND p.start_date < '2026-01-01' WHERE p.project_id IS NULL;

P118 — Project Utilization  ·  Expert

Calculate each project's percentage of the total hours worked across all projects.

SQL

WITH project_hours AS ( SELECT p.project_id, p.project_name, COALESCE(SUM(ep.hours_worked), 0) AS hours FROM projects p LEFT JOIN employee_projects ep ON p.project_id = ep.project_id GROUP BY p.project_id, p.project_name ) SELECT *, ROUND( 100.0 * hours / NULLIF(SUM(hours) OVER (), 0), 2 ) AS utilization_percentage FROM project_hours;

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

project_idproject_namehoursutilization_percentage
5Internal HR Portal2706.79
1Customer Portal Revamp81020.38
3Mobile App Launch67016.86
2Data Warehouse Migration1,19029.94
6Fraud Detection Engine51012.83
7Vendor Payment Automation1152.89
8Legacy System Decommission00
4Marketing Automation41010.31

(8 rows total)

Explanation

SUM(hours) OVER () (no partition) totals hours across every project to use as a shared denominator, the same 'grand total window function' trick as Problem 108's revenue-share calculation — NULLIF guards against division by zero if the grand total were ever 0. Data Warehouse Migration, the project with the most hours logged, would claim the largest utilization share, while the Internal HR Portal (already completed, with a smaller team) sits at the low end around 6.79%.

💻 Code example

WITH project_hours AS ( SELECT p.project_id, p.project_name, COALESCE(SUM(ep.hours_worked), 0) AS hours FROM projects p LEFT JOIN employee_projects ep ON p.project_id = ep.project_id GROUP BY p.project_id, p.project_name ) SELECT *, ROUND( 100.0 * hours / NULLIF(SUM(hours) OVER (), 0), 2 ) AS utilization_percentage FROM project_hours;

P119 — Over-Budget Projects by Hourly Cost  ·  Expert

Assume each project hour costs 100. Find projects where calculated labor cost exceeds the budget.

SQL

SELECT p.project_id, p.project_name, p.budget, SUM(ep.hours_worked) * 100 AS labor_cost FROM projects p JOIN employee_projects ep ON p.project_id = ep.project_id GROUP BY p.project_id, p.project_name, p.budget HAVING SUM(ep.hours_worked) * 100 > p.budget;

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

project_idproject_namebudgetlabor_cost
7Vendor Payment Automation8,00011,500

(1 row total)

Explanation

Multiplying total logged hours by a flat ₹100/hour rate and comparing that estimate against each project's actual budget is a simple way to flag projects burning more labor than they were funded for — the Vendor Payment Automation project is the one case in this dataset where that happens (115 hours × ₹100 = ₹11,500 against an ₹8,000 budget), because it was deliberately given a low budget relative to the ongoing work logged against it.

💻 Code example

SELECT p.project_id, p.project_name, p.budget, SUM(ep.hours_worked) * 100 AS labor_cost FROM projects p JOIN employee_projects ep ON p.project_id = ep.project_id GROUP BY p.project_id, p.project_name, p.budget HAVING SUM(ep.hours_worked) * 100 > p.budget;

P120 — Manager With Most Direct Reports  ·  Expert

Find the manager who has the most direct reports.

SQL

SELECT m.employee_id, m.first_name, COUNT(e.employee_id) AS direct_reports FROM employees m JOIN employees e ON e.manager_id = m.employee_id GROUP BY m.employee_id, m.first_name ORDER BY direct_reports DESC LIMIT 1;

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

employee_idfirst_namedirect_reports
1Arvind6

(1 row total)

Explanation

The self-join e.manager_id = m.employee_id pairs every employee with their manager, and grouping by manager while counting matched employees finds who has the largest team reporting directly to them. Arvind Krishnan, the CEO, has the most direct reports at 6 — exactly the six department heads who report straight to him, before the hierarchy fans out further at the manager and IC levels.

💻 Code example

SELECT m.employee_id, m.first_name, COUNT(e.employee_id) AS direct_reports FROM employees m JOIN employees e ON e.manager_id = m.employee_id GROUP BY m.employee_id, m.first_name ORDER BY direct_reports DESC LIMIT 1;

P121 — Managers With Average Team Salary  ·  Expert

Calculate the average salary of each manager's direct reports.

SQL

SELECT m.employee_id AS manager_id, m.first_name AS manager_name, AVG(e.salary) AS average_team_salary FROM employees m JOIN employees e ON e.manager_id = m.employee_id GROUP BY m.employee_id, m.first_name;

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

manager_idmanager_nameaverage_team_salary
12Deepak58,374.15
4Priya118,000
2Meera150,000
3Karan75,163.12
1Arvind186,833.33
6Anil142,000
7Sunita74,417.14
5Suresh53,063.97
11Kavya80,165.24
8Rohan89,459.31

(10 rows total)

Explanation

This is the same self-join as Problem 120, but averaging salary instead of counting rows — it reveals each manager's team pay level, not just team size. Meera Nair's direct reports average ₹150,000 (she manages senior engineering managers), while Deepak Saxena's team averages a more modest ~₹58,374, reflecting the more junior finance ICs reporting to him.

💻 Code example

SELECT m.employee_id AS manager_id, m.first_name AS manager_name, AVG(e.salary) AS average_team_salary FROM employees m JOIN employees e ON e.manager_id = m.employee_id GROUP BY m.employee_id, m.first_name;

P122 — Employees Earning More Than Their Manager  ·  Expert

Find employees whose salary is greater than their manager's salary.

SQL

SELECT e.employee_id, e.first_name, e.salary, m.first_name AS manager_name, m.salary AS manager_salary FROM employees e JOIN employees m ON e.manager_id = m.employee_id WHERE e.salary > m.salary;

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

employee_idfirst_namesalarymanager_namemanager_salary
14Rohan167,000Rohan152,000

(1 row total)

Explanation

Comparing e.salary > m.salary across the same self-join surfaces an organizational anomaly: an individual contributor out-earning their own manager. Exactly one such case exists in this dataset — employee Rohan (id 14, ₹167,000) earns more than his manager, also confusingly named Rohan (id 8, ₹152,000) — a scenario deliberately built into the data, since this kind of compression is a real (if awkward) thing that happens in growing companies when a specialist's market rate outpaces their manager's.

💻 Code example

SELECT e.employee_id, e.first_name, e.salary, m.first_name AS manager_name, m.salary AS manager_salary FROM employees e JOIN employees m ON e.manager_id = m.employee_id WHERE e.salary > m.salary;

P123 — Department With Highest Revenue  ·  Expert

Calculate sales revenue attributed to employees by department and find the highest-revenue department.

SQL

SELECT d.department_id, d.department_name, SUM(o.total_amount) AS revenue FROM departments d JOIN employees e ON d.department_id = e.department_id JOIN orders o ON e.employee_id = o.employee_id WHERE o.status <> 'Cancelled' GROUP BY d.department_id, d.department_name ORDER BY revenue DESC LIMIT 1;

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

department_iddepartment_namerevenue
2Sales2,705,025

(1 row total)

Explanation

Chaining three tables — departmentsemployeesorders — attributes each non-cancelled order's revenue to the department of the employee who processed the sale (not the customer's department, since customers don't have one). Because orders.employee_id in this schema always references a Sales-team employee (the "sales employee" role in this schema), every single order traces back to the Sales department — so Sales is trivially both the only department with any attributed revenue at all, and therefore also the department with the highest.

💻 Code example

SELECT d.department_id, d.department_name, SUM(o.total_amount) AS revenue FROM departments d JOIN employees e ON d.department_id = e.department_id JOIN orders o ON e.employee_id = o.employee_id WHERE o.status <> 'Cancelled' GROUP BY d.department_id, d.department_name ORDER BY revenue DESC LIMIT 1;

P124 — Employee Sales Ranking  ·  Expert

Rank employees by total sales revenue.

SQL

WITH sales AS ( SELECT e.employee_id, e.first_name, COALESCE(SUM( CASE WHEN o.status <> 'Cancelled' THEN o.total_amount ELSE 0 END ), 0) AS revenue FROM employees e LEFT JOIN orders o ON e.employee_id = o.employee_id GROUP BY e.employee_id, e.first_name ) SELECT *, RANK() OVER (ORDER BY revenue DESC) AS sales_rank FROM sales;

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

employee_idfirst_namerevenuesales_rank
27Neha635,5391
15Vihaan533,5172
33Amit492,0453
3Karan491,2414
21Diya228,5895
39Kiara171,1786
10Vikram152,9167
25Meera08
24Rahul08
20Deepak08

(showing 10 of 40 rows)

Explanation

This mirrors Problem 95's customer-spending rank, but for employees and sales revenue instead: the sales CTE sums each employee's non-cancelled order revenue (defaulting to 0 via COALESCE for anyone with none), and RANK() orders the result. Neha (employee 27) leads the whole company at ₹635,539 in attributed sales revenue — notably, only employees in the Sales department can have any nonzero revenue at all here, for the same reason as Problem 123.

💻 Code example

WITH sales AS ( SELECT e.employee_id, e.first_name, COALESCE(SUM( CASE WHEN o.status <> 'Cancelled' THEN o.total_amount ELSE 0 END ), 0) AS revenue FROM employees e LEFT JOIN orders o ON e.employee_id = o.employee_id GROUP BY e.employee_id, e.first_name ) SELECT *, RANK() OVER (ORDER BY revenue DESC) AS sales_rank FROM sales;

P125 — Employees With Sales Above Department Average  ·  Expert

Find employees whose sales revenue is above the average employee revenue in their department.

SQL

WITH sales AS ( SELECT e.employee_id, e.first_name, e.department_id, COALESCE(SUM( CASE WHEN o.status <> 'Cancelled' THEN o.total_amount ELSE 0 END ), 0) AS revenue FROM employees e LEFT JOIN orders o ON e.employee_id = o.employee_id GROUP BY e.employee_id, e.first_name, e.department_id ) SELECT * FROM ( SELECT s.*, AVG(revenue) OVER ( PARTITION BY department_id ) AS department_avg FROM sales s ) x WHERE revenue > department_avg;

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

employee_idfirst_namedepartment_idrevenuedepartment_avg
27Neha2635,539386,432.14
33Amit2492,045386,432.14
3Karan2491,241386,432.14
15Vihaan2533,517386,432.14

(4 rows total)

Explanation

Because orders.employee_id only ever points to a Sales-department employee, the department_avg window average computed here is only ever nonzero for department_id 2 (Sales) — every other department's employees uniformly show 0 revenue against a 0 average, and 0 > 0 is false, so they correctly never appear. All 4 employees who clear this bar are Sales-team members outperforming their own team's average.

💻 Code example

WITH sales AS ( SELECT e.employee_id, e.first_name, e.department_id, COALESCE(SUM( CASE WHEN o.status <> 'Cancelled' THEN o.total_amount ELSE 0 END ), 0) AS revenue FROM employees e LEFT JOIN orders o ON e.employee_id = o.employee_id GROUP BY e.employee_id, e.first_name, e.department_id ) SELECT * FROM ( SELECT s.*, AVG(revenue) OVER ( PARTITION BY department_id ) AS department_avg FROM sales s ) x WHERE revenue > department_avg;

P126 — Recursive Employee Hierarchy  ·  Expert

Build an employee hierarchy starting from top-level employees.

SQL

WITH RECURSIVE hierarchy AS ( SELECT employee_id, first_name, manager_id, 1 AS level FROM employees WHERE manager_id IS NULL UNION ALL SELECT e.employee_id, e.first_name, e.manager_id, h.level + 1 FROM employees e JOIN hierarchy h ON e.manager_id = h.employee_id ) SELECT * FROM hierarchy ORDER BY level, employee_id;

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

employee_idfirst_namemanager_idlevel
1ArvindNULL1
2Meera12
3Karan12
4Priya12
5Suresh12
6Anil12
7Sunita12
8Rohan23
9Ananya23
10Vikram33

(showing 10 of 40 rows)

Explanation

WITH RECURSIVE works in two parts: the 'anchor' half (WHERE manager_id IS NULL) seeds the recursion with the top of the hierarchy — Arvind Krishnan, at level 1 — and the 'recursive' half then repeatedly joins employees back onto the growing hierarchy result, one level at a time, until no more matches are found. All 40 employees eventually appear, each correctly tagged with their depth in the org chart, from Arvind at level 1 down to individual contributors at level 4.

💻 Code example

WITH RECURSIVE hierarchy AS ( SELECT employee_id, first_name, manager_id, 1 AS level FROM employees WHERE manager_id IS NULL UNION ALL SELECT e.employee_id, e.first_name, e.manager_id, h.level + 1 FROM employees e JOIN hierarchy h ON e.manager_id = h.employee_id ) SELECT * FROM hierarchy ORDER BY level, employee_id;

P127 — Employee Hierarchy Path  ·  Expert

Build a text path showing each employee's hierarchy chain.

SQL

WITH RECURSIVE hierarchy AS ( SELECT employee_id, first_name, manager_id, first_name::TEXT AS path FROM employees WHERE manager_id IS NULL UNION ALL SELECT e.employee_id, e.first_name, e.manager_id, h.path || ' > ' || e.first_name FROM employees e JOIN hierarchy h ON e.manager_id = h.employee_id ) SELECT * FROM hierarchy;

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

employee_idfirst_namemanager_idpath
1ArvindNULLArvind
2Meera1Arvind > Meera
3Karan1Arvind > Karan
4Priya1Arvind > Priya
5Suresh1Arvind > Suresh
6Anil1Arvind > Anil
7Sunita1Arvind > Sunita
8Rohan2Arvind > Meera > Rohan
9Ananya2Arvind > Meera > Ananya
10Vikram3Arvind > Karan > Vikram

(showing 10 of 40 rows)

Explanation

This is structurally the same recursive CTE as Problem 126, but instead of tracking a numeric level, it builds up a readable text trail with h.path || ' > ' || e.first_name at each recursive step — so by the time the recursion reaches a leaf-level individual contributor, their path column reads as a full chain like 'Arvind > Meera > Rohan > ...', showing every manager between them and the CEO in one string.

💻 Code example

WITH RECURSIVE hierarchy AS ( SELECT employee_id, first_name, manager_id, first_name::TEXT AS path FROM employees WHERE manager_id IS NULL UNION ALL SELECT e.employee_id, e.first_name, e.manager_id, h.path || ' > ' || e.first_name FROM employees e JOIN hierarchy h ON e.manager_id = h.employee_id ) SELECT * FROM hierarchy;

P128 — Customers With Orders in Three Consecutive Months  ·  Expert

Find customers who placed at least one order in three consecutive calendar months.

SQL

WITH months AS ( SELECT DISTINCT customer_id, DATE_TRUNC('month', order_date) AS month FROM orders WHERE status <> 'Cancelled' ), x AS ( SELECT customer_id, month, LAG(month, 1) OVER ( PARTITION BY customer_id ORDER BY month ) AS prev1, LAG(month, 2) OVER ( PARTITION BY customer_id ORDER BY month ) AS prev2 FROM months ) SELECT DISTINCT customer_id FROM x WHERE month = prev1 + INTERVAL '1 month' AND prev1 = prev2 + INTERVAL '1 month';

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

customer_id
1
6
3
2
5
7
8

(7 rows total)

Explanation

This chains two LAG calls (LAG(month, 1) and LAG(month, 2)) to look back one and two months respectively within each customer's distinct list of active months, then checks that the current month, the month before it, and the month before that are all exactly one calendar month apart — i.e., a genuine unbroken 3-month streak, not just any three months scattered across the year. 7 customers achieve this in the dataset, several of them deliberately given a guaranteed 4-month consecutive run of orders when the data was generated.

💻 Code example

WITH months AS ( SELECT DISTINCT customer_id, DATE_TRUNC('month', order_date) AS month FROM orders WHERE status <> 'Cancelled' ), x AS ( SELECT customer_id, month, LAG(month, 1) OVER ( PARTITION BY customer_id ORDER BY month ) AS prev1, LAG(month, 2) OVER ( PARTITION BY customer_id ORDER BY month ) AS prev2 FROM months ) SELECT DISTINCT customer_id FROM x WHERE month = prev1 + INTERVAL '1 month' AND prev1 = prev2 + INTERVAL '1 month';

P129 — Customer Cohort by Signup Month  ·  Expert

Group customers into signup-month cohorts and count them.

SQL

SELECT DATE_TRUNC('month', signup_date) AS cohort_month, COUNT(*) AS customer_count FROM customers GROUP BY DATE_TRUNC('month', signup_date) ORDER BY cohort_month;

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

cohort_monthcustomer_count
2023-01-011
2023-02-013
2023-07-011
2023-10-011
2024-01-011
2024-02-015
2024-03-012
2024-04-012
2024-05-012
2024-06-011

(showing 10 of 19 rows)

Explanation

DATE_TRUNC('month', signup_date) groups every customer into the calendar month they signed up in — a 'cohort' in growth-analytics terms — and counting rows per group shows how signups were distributed over time. Signups trickle in from as early as January 2023 (this dataset's earliest customer) through the most recent months, with no single month dominating.

💻 Code example

SELECT DATE_TRUNC('month', signup_date) AS cohort_month, COUNT(*) AS customer_count FROM customers GROUP BY DATE_TRUNC('month', signup_date) ORDER BY cohort_month;

P130 — Cohort Revenue  ·  Expert

Calculate total revenue generated by each customer signup cohort.

SQL

SELECT DATE_TRUNC('month', c.signup_date) AS cohort_month, 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 DATE_TRUNC('month', c.signup_date) ORDER BY cohort_month;

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

cohort_monthrevenue
2023-01-01301,121
2023-02-01355,386
2023-07-01136,660
2023-10-01272,008
2024-02-01665,719
2024-03-01120,877
2024-04-01140,366
2024-05-0148,261
2024-06-0164,780
2024-07-0159,792

(showing 10 of 18 rows)

Explanation

Joining each customer's signup cohort to their (non-cancelled) order history and summing revenue per cohort answers a slightly different question than raw signup counts: not 'how many people signed up in a given month', but 'how much lifetime revenue has that whole cohort generated since'. Early cohorts like January and February 2023 already show substantial revenue (₹301,121 and ₹355,386 respectively), since those customers have had the most time to place orders.

💻 Code example

SELECT DATE_TRUNC('month', c.signup_date) AS cohort_month, 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 DATE_TRUNC('month', c.signup_date) ORDER BY cohort_month;

P131 — Customers With No Orders in Their Signup Month  ·  Expert

Find customers who did not place an order during the month they signed up.

SQL

SELECT c.customer_id, c.customer_name FROM customers c WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id AND DATE_TRUNC('month', o.order_date) = DATE_TRUNC('month', c.signup_date) );

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

customer_idcustomer_name
3Amit Bhatia
4Pooja Sinha
5Vikas Rana
7Rajesh Pandey
8Swati Yadav
9Manoj Kulkarni
10Kirti Shetty
11Alok Bose
13Chetan Chauhan
14Divya Naidu

(showing 10 of 25 rows)

Explanation

NOT EXISTS checks, for each customer, whether any row in orders matches both their customer_id and a DATE_TRUNC('month', ...) equal to their own signup month — if no such row exists, the customer is flagged. 25 of the 30 customers never ordered during their actual signup month (most likely ordering for the first time some weeks or months after registering instead), which is a very common real-world pattern: signing up and buying immediately is the exception, not the rule.

💻 Code example

SELECT c.customer_id, c.customer_name FROM customers c WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id AND DATE_TRUNC('month', o.order_date) = DATE_TRUNC('month', c.signup_date) );

P132 — Customers With Repeat Purchase Within 30 Days  ·  Expert

Find customers who placed another order within 30 days of a previous order.

SQL

WITH x AS ( SELECT customer_id, order_date, LEAD(order_date) OVER ( PARTITION BY customer_id ORDER BY order_date ) AS next_order_date FROM orders ) SELECT DISTINCT customer_id FROM x WHERE next_order_date <= order_date + INTERVAL '30 days';

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

customer_id
1
15
6
3
19
9
14
2
7
4

(showing 10 of 12 rows)

Explanation

LEAD(order_date) OVER (PARTITION BY customer_id ORDER BY order_date) looks ahead to each customer's next order, and the filter checks whether that next order landed within 30 days. 12 customers show this kind of quick repeat-purchase behavior, including customer 9, deliberately given two orders 19 days apart when the data was generated specifically to exercise this check.

💻 Code example

WITH x AS ( SELECT customer_id, order_date, LEAD(order_date) OVER ( PARTITION BY customer_id ORDER BY order_date ) AS next_order_date FROM orders ) SELECT DISTINCT customer_id FROM x WHERE next_order_date <= order_date + INTERVAL '30 days';

P133 — Customers With Increasing Order Frequency  ·  Expert

Compare each customer's order count in the first and second half of 2025.

SQL

SELECT customer_id, SUM(CASE WHEN order_date >= '2025-01-01' AND order_date < '2025-07-01' THEN 1 ELSE 0 END) AS first_half, SUM(CASE WHEN order_date >= '2025-07-01' AND order_date < '2026-01-01' THEN 1 ELSE 0 END) AS second_half FROM orders GROUP BY customer_id HAVING SUM(CASE WHEN order_date >= '2025-07-01' AND order_date < '2026-01-01' THEN 1 ELSE 0 END) > SUM(CASE WHEN order_date >= '2025-01-01' AND order_date < '2025-07-01' THEN 1 ELSE 0 END);

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

customer_idfirst_halfsecond_half
1512
825
535
2501
1701
1201
2701
712
9,99901
1902

(10 rows total)

Explanation

Two SUM(CASE WHEN ... THEN 1 ELSE 0 END) expressions in the same query count each customer's order volume in the first half of 2025 (Jan-Jun) and second half (Jul-Dec) side by side, and the HAVING clause keeps only customers whose second-half count exceeds their first-half count. 10 customers ordered more often in the back half of the year than the front half — customer 8 nearly tripled their pace, from 2 orders in H1 to 5 in H2.

💻 Code example

SELECT customer_id, SUM(CASE WHEN order_date >= '2025-01-01' AND order_date < '2025-07-01' THEN 1 ELSE 0 END) AS first_half, SUM(CASE WHEN order_date >= '2025-07-01' AND order_date < '2026-01-01' THEN 1 ELSE 0 END) AS second_half FROM orders GROUP BY customer_id HAVING SUM(CASE WHEN order_date >= '2025-07-01' AND order_date < '2026-01-01' THEN 1 ELSE 0 END) > SUM(CASE WHEN order_date >= '2025-01-01' AND order_date < '2025-07-01' THEN 1 ELSE 0 END);

P134 — Find the Median Employee Salary  ·  Expert

Calculate the median salary of employees.

SQL

SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY salary) AS median_salary FROM employees;

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

median_salary
85,078.57

(1 row total)

Explanation

PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY salary) computes the continuous median — for an even number of rows (40 employees here) it interpolates between the two middle values rather than picking one of them arbitrarily. At ₹85,078.57, the median sits well below the ₹102,073 average from Problem 22, confirming what that earlier problem already hinted at: the average is being pulled upward by a handful of very high leadership salaries, while the median reflects where the 'typical' employee actually sits.

💻 Code example

SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY salary) AS median_salary FROM employees;

P135 — Median Order Value by Country  ·  Expert

Find the median order value for each customer country.

SQL

SELECT c.country, PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY o.total_amount) AS median_order_value FROM customers c JOIN orders o ON c.customer_id = o.customer_id GROUP BY c.country;

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

countrymedian_order_value
UK7,043.50
India11,794
UAE3,397
USA61,391.50
Canada5,794
Australia3,897
Singapore36,989
Germany60,289

(8 rows total)

Explanation

The same PERCENTILE_CONT function as Problem 134, now computed once per country group via GROUP BY. Median order values vary meaningfully by market — the UAE sits at just ₹3,397 (a single customer with modest orders) while India's median of ₹11,794 reflects both a much larger and more varied set of orders.

💻 Code example

SELECT c.country, PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY o.total_amount) AS median_order_value FROM customers c JOIN orders o ON c.customer_id = o.customer_id GROUP BY c.country;

P136 — Pareto Revenue Customers  ·  Expert

Find the smallest set of customers contributing to approximately the first 80% of total revenue.

SQL

WITH spending AS ( SELECT customer_id, SUM(total_amount) AS revenue FROM orders WHERE status <> 'Cancelled' GROUP BY customer_id ), ranked AS ( SELECT *, SUM(revenue) OVER ( ORDER BY revenue DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS cumulative_revenue, SUM(revenue) OVER () AS total_revenue FROM spending ) SELECT * FROM ranked WHERE cumulative_revenue <= total_revenue * 0.80;

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

customer_idrevenuecumulative_revenuetotal_revenue
5316,810316,8102,705,025
7301,121617,9312,705,025
3272,008889,9392,705,025
2257,9151,147,8542,705,025
4213,1371,360,9912,705,025
1194,9861,555,9772,705,025
6168,3061,724,2832,705,025
8146,4331,870,7162,705,025
19136,6602,007,3762,705,025
21122,7832,130,1592,705,025

(10 rows total)

Explanation

This builds a running cumulative-revenue total ordered from the highest customer down to the lowest (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW), compares it against the grand total computed separately with an unpartitioned SUM(revenue) OVER (), and keeps every customer up to the point where the running total first reaches 80% of all revenue — a direct SQL implementation of the Pareto ('80/20') principle. It takes only 10 of this dataset's highest-spending customers to account for that first 80% of total revenue, a concentration that's common in real customer bases.

💻 Code example

WITH spending AS ( SELECT customer_id, SUM(total_amount) AS revenue FROM orders WHERE status <> 'Cancelled' GROUP BY customer_id ), ranked AS ( SELECT *, SUM(revenue) OVER ( ORDER BY revenue DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS cumulative_revenue, SUM(revenue) OVER () AS total_revenue FROM spending ) SELECT * FROM ranked WHERE cumulative_revenue <= total_revenue * 0.80;

P137 — Product Market Share  ·  Expert

Calculate each product's percentage share of total product revenue.

SQL

WITH product_sales AS ( SELECT p.product_id, p.product_name, SUM(oi.quantity * oi.unit_price) AS revenue FROM products p JOIN order_items oi ON p.product_id = oi.product_id JOIN orders o ON oi.order_id = o.order_id WHERE o.status <> 'Cancelled' GROUP BY p.product_id, p.product_name ) SELECT *, ROUND( 100.0 * revenue / SUM(revenue) OVER (), 2 ) AS market_share FROM product_sales ORDER BY market_share DESC;

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

product_idproduct_namerevenuemarket_share
327-inch 4K Monitor849,96631.47
17Air Fryer215,9648
5Noise Cancelling Headphones206,9777.66
7Portable SSD 1TB202,9717.52
4USB-C Hub158,4785.87
1Wireless Mouse131,8354.88
2Mechanical Keyboard125,9644.66
11Dumbbell Set 10kg96,1633.56
19Mixer Grinder95,6713.54
20Ceramic Dinner Set85,7613.18

(showing 10 of 27 rows)

Explanation

Revenue per product is computed from order_items (excluding cancelled orders), and SUM(revenue) OVER () again supplies the shared grand-total denominator for a percentage-of-total 'market share' figure. The 27-inch 4K Monitor alone claims 31.47% of all product revenue in this dataset — a single high-price, high-volume item dominating the entire catalog's earnings.

💻 Code example

WITH product_sales AS ( SELECT p.product_id, p.product_name, SUM(oi.quantity * oi.unit_price) AS revenue FROM products p JOIN order_items oi ON p.product_id = oi.product_id JOIN orders o ON oi.order_id = o.order_id WHERE o.status <> 'Cancelled' GROUP BY p.product_id, p.product_name ) SELECT *, ROUND( 100.0 * revenue / SUM(revenue) OVER (), 2 ) AS market_share FROM product_sales ORDER BY market_share DESC;

P138 — Category Revenue Ranking  ·  Expert

Rank product categories by revenue.

SQL

WITH category_sales AS ( SELECT p.category, SUM(oi.quantity * oi.unit_price) AS revenue FROM products p JOIN order_items oi ON p.product_id = oi.product_id JOIN orders o ON oi.order_id = o.order_id WHERE o.status <> 'Cancelled' GROUP BY p.category ) SELECT *, DENSE_RANK() OVER (ORDER BY revenue DESC) AS category_rank FROM category_sales;

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

categoryrevenuecategory_rank
Electronics1,758,1601
Home & Kitchen477,0272
Sports226,6753
Clothing154,8044
Books83,8125

(5 rows total)

Explanation

This groups product-level revenue up one more level, into category, and uses DENSE_RANK() rather than RANK() so that (in principle) tied categories wouldn't produce a gap in the ranking. Electronics leads by a wide margin at ₹1,758,160 — more than triple the second-place category (Home & Kitchen at ₹477,027), driven largely by that same dominant 4K Monitor from Problem 137.

💻 Code example

WITH category_sales AS ( SELECT p.category, SUM(oi.quantity * oi.unit_price) AS revenue FROM products p JOIN order_items oi ON p.product_id = oi.product_id JOIN orders o ON oi.order_id = o.order_id WHERE o.status <> 'Cancelled' GROUP BY p.category ) SELECT *, DENSE_RANK() OVER (ORDER BY revenue DESC) AS category_rank FROM category_sales;

P139 — Highest Revenue Product Per Category  ·  Expert

Find the top revenue-generating product in every category.

SQL

WITH product_sales 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 ), ranked AS ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY category ORDER BY revenue DESC ) AS rn FROM product_sales ) SELECT * FROM ranked WHERE rn = 1;

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

product_idproduct_namecategoryrevenuern
23Denim JacketClothing82,4671
27Designing Data-Intensive ApplicationsBooks40,4731
17Air FryerHome & Kitchen215,9641
11Dumbbell Set 10kgSports96,1631
327-inch 4K MonitorElectronics849,9661

(5 rows total)

Explanation

Filtering ROW_NUMBER() OVER (PARTITION BY category ORDER BY revenue DESC) = 1 after computing every product's revenue (with LEFT JOINs and COALESCE so even a product with zero sales still gets a 0 and stays in contention) picks the single best-selling product within each of the 5 categories. Interestingly, Clothing's top product is the Denim Jacket (₹82,467), not the cheaper, higher-volume items in that category — reflecting its higher unit price relative to how many units of it actually sold.

💻 Code example

WITH product_sales 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 ), ranked AS ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY category ORDER BY revenue DESC ) AS rn FROM product_sales ) SELECT * FROM ranked WHERE rn = 1;

P140 — Products With Declining Monthly Sales  ·  Expert

Find products whose sales decreased from one month to the next.

SQL

WITH monthly AS ( SELECT oi.product_id, DATE_TRUNC('month', o.order_date) AS month, SUM(oi.quantity) AS units FROM order_items oi JOIN orders o ON oi.order_id = o.order_id WHERE o.status <> 'Cancelled' GROUP BY oi.product_id, DATE_TRUNC('month', o.order_date) ), x AS ( SELECT *, LAG(units) OVER ( PARTITION BY product_id ORDER BY month ) AS previous_units FROM monthly ) SELECT * FROM x WHERE previous_units IS NOT NULL AND units < previous_units;

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

product_idmonthunitsprevious_units
102024-08-0125
102025-02-0135
102025-12-0138
182024-11-0114
182025-06-0145
182025-09-0124
212024-06-0124
212024-11-0112
212025-12-0134
252024-08-0145

(showing 10 of 83 rows)

Explanation

The monthly CTE first collapses order_items into total units sold per product per month, and LAG(units) OVER (PARTITION BY product_id ORDER BY month) then compares each month against the one before it for that same product — the outer query keeps every month where sales dropped versus the prior month. This is a broad, expected-to-be-noisy signal by design (83 rows match here), since almost every product will have some month where sales dipped relative to the month before; it's a starting point for investigation, not by itself proof of a real declining trend (Problem 141 tackles that stricter, more meaningful version).

💻 Code example

WITH monthly AS ( SELECT oi.product_id, DATE_TRUNC('month', o.order_date) AS month, SUM(oi.quantity) AS units FROM order_items oi JOIN orders o ON oi.order_id = o.order_id WHERE o.status <> 'Cancelled' GROUP BY oi.product_id, DATE_TRUNC('month', o.order_date) ), x AS ( SELECT *, LAG(units) OVER ( PARTITION BY product_id ORDER BY month ) AS previous_units FROM monthly ) SELECT * FROM x WHERE previous_units IS NOT NULL AND units < previous_units;

P141 — Three Consecutive Months of Revenue Growth  ·  Expert

Find categories whose revenue increased for three consecutive months.

SQL

WITH monthly AS ( SELECT p.category, DATE_TRUNC('month', o.order_date) AS month, SUM(oi.quantity * oi.unit_price) AS revenue FROM products p JOIN order_items oi ON p.product_id = oi.product_id JOIN orders o ON oi.order_id = o.order_id WHERE o.status <> 'Cancelled' GROUP BY p.category, DATE_TRUNC('month', o.order_date) ), x AS ( SELECT *, LAG(revenue, 1) OVER ( PARTITION BY category ORDER BY month ) AS prev1, LAG(revenue, 2) OVER ( PARTITION BY category ORDER BY month ) AS prev2 FROM monthly ) SELECT DISTINCT category FROM x WHERE revenue > prev1 AND prev1 > prev2;

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

category
Books
Sports
Home & Kitchen
Electronics

(4 rows total)

Explanation

This is Problem 128's '3 consecutive periods' pattern (two chained LAG calls) applied to category-level monthly revenue instead of customer order months — a category only qualifies if its revenue was strictly higher than the previous month, which was itself strictly higher than the month before that: a genuine 3-month growth streak, not just an isolated good month. 4 of the 5 categories achieve this at some point in the two-year history, showing that sustained growth streaks aren't rare in this dataset's overall upward order-volume trend.

💻 Code example

WITH monthly AS ( SELECT p.category, DATE_TRUNC('month', o.order_date) AS month, SUM(oi.quantity * oi.unit_price) AS revenue FROM products p JOIN order_items oi ON p.product_id = oi.product_id JOIN orders o ON oi.order_id = o.order_id WHERE o.status <> 'Cancelled' GROUP BY p.category, DATE_TRUNC('month', o.order_date) ), x AS ( SELECT *, LAG(revenue, 1) OVER ( PARTITION BY category ORDER BY month ) AS prev1, LAG(revenue, 2) OVER ( PARTITION BY category ORDER BY month ) AS prev2 FROM monthly ) SELECT DISTINCT category FROM x WHERE revenue > prev1 AND prev1 > prev2;

P142 — Orders With Multiple Payments  ·  Expert

Find orders that have more than one payment record.

SQL

SELECT order_id, COUNT(*) AS payment_count, SUM(amount) AS total_paid FROM payments GROUP BY order_id HAVING COUNT(*) > 1;

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

order_idpayment_counttotal_paid
282128,178
6327,791
7421,499
76221,982
88211,588
89279,782
95235,578
9827,891
100224,590
10424,796

(showing 10 of 16 rows)

Explanation

Grouping payments by order_id and filtering HAVING COUNT(*) > 1 finds orders with more than one payment row attached — in this dataset that covers both the deliberately-generated installment/two-payment orders and any order where an initial failed payment attempt was followed by a successful retry, both entirely plausible real-world reasons for multiple payment records against one order.

💻 Code example

SELECT order_id, COUNT(*) AS payment_count, SUM(amount) AS total_paid FROM payments GROUP BY order_id HAVING COUNT(*) > 1;

P143 — Underpaid Orders  ·  Expert

Find orders where successful payments total less than the order amount.

SQL

SELECT o.order_id, o.total_amount, COALESCE(SUM( CASE WHEN p.payment_status = 'Success' THEN p.amount ELSE 0 END ), 0) AS paid_amount FROM orders o LEFT JOIN payments p ON o.order_id = p.order_id GROUP BY o.order_id, o.total_amount HAVING COALESCE(SUM( CASE WHEN p.payment_status = 'Success' THEN p.amount ELSE 0 END ), 0) < o.total_amount;

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

order_idtotal_amountpaid_amount
438,8920
27,7974,582.01
497,7970
3313,5920
406,3960
1813,1960
913,8910
475,7940
1076,38545,370.22
13157,8900

(showing 10 of 41 rows)

Explanation

This flags every order where successfully-collected payments (COALESCE(SUM(... WHERE payment_status = 'Success' ...), 0)) fall short of the order's total_amount — but it's worth reading the 41-row result carefully: 23 of those are Cancelled orders that were simply never expected to be paid in full, and only the remaining 18 (15 Delivered, 3 Pending) represent orders that actually should have been paid but weren't. A tighter, more production-ready version of this check would add WHERE o.status <> 'Cancelled' to avoid flagging cancellations as a payment problem.

💻 Code example

SELECT o.order_id, o.total_amount, COALESCE(SUM( CASE WHEN p.payment_status = 'Success' THEN p.amount ELSE 0 END ), 0) AS paid_amount FROM orders o LEFT JOIN payments p ON o.order_id = p.order_id GROUP BY o.order_id, o.total_amount HAVING COALESCE(SUM( CASE WHEN p.payment_status = 'Success' THEN p.amount ELSE 0 END ), 0) < o.total_amount;

P144 — Fully Paid Orders  ·  Expert

Find orders where successful payments exactly equal the order total.

SQL

SELECT o.order_id, o.total_amount, SUM( CASE WHEN p.payment_status = 'Success' THEN p.amount ELSE 0 END ) AS paid_amount FROM orders o LEFT JOIN payments p ON o.order_id = p.order_id GROUP BY o.order_id, o.total_amount HAVING SUM( CASE WHEN p.payment_status = 'Success' THEN p.amount ELSE 0 END ) = o.total_amount;

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

order_idtotal_amountpaid_amount
714,49414,494
2338,58838,588
79155,589155,589
976,5986,598
1172,0982,098
12032,38432,384
13217,48917,489
6115,286115,286
1223,9953,995
12945,59045,590

(showing 10 of 107 rows)

Explanation

The mirror image of Problem 143: orders where successful payments add up to exactly the order total. 107 of this dataset's 149 orders are fully paid this way — the expected common case, since most orders that aren't cancelled, underpaid, or still pending do eventually get paid in full.

💻 Code example

SELECT o.order_id, o.total_amount, SUM( CASE WHEN p.payment_status = 'Success' THEN p.amount ELSE 0 END ) AS paid_amount FROM orders o LEFT JOIN payments p ON o.order_id = p.order_id GROUP BY o.order_id, o.total_amount HAVING SUM( CASE WHEN p.payment_status = 'Success' THEN p.amount ELSE 0 END ) = o.total_amount;

P145 — Refund Amount by Customer  ·  Expert

Calculate the total refunded amount for each customer.

SQL

SELECT c.customer_id, c.customer_name, COALESCE(SUM( CASE WHEN p.payment_status = 'Refunded' THEN p.amount ELSE 0 END ), 0) AS refunded_amount FROM customers c LEFT JOIN orders o ON c.customer_id = o.customer_id LEFT JOIN payments p ON o.order_id = p.order_id GROUP BY c.customer_id, c.customer_name ORDER BY refunded_amount DESC;

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

customer_idcustomer_namerefunded_amount
5Vikas Rana31,584
2Sneha Nair22,782
4Pooja Sinha13,088
17Imran Khan8,093
8Swati Yadav6,294
20Lata Pillai2,397
14Divya Naidu1,598
26Fatima Ali0
25Wei Zhang0
6Neha Ghosh0

(showing 10 of 30 rows)

Explanation

Chaining customers → orders → payments and summing only rows where payment_status = 'Refunded' (via conditional aggregation, defaulting to 0 for customers with none) totals how much money has actually been returned to each customer. Vikas Rana — this dataset's single highest-spending customer overall — also has the largest refunded amount at ₹31,584, a reminder that a customer's highest lifetime value and their highest refund exposure can be the same person simply because they transact the most.

💻 Code example

SELECT c.customer_id, c.customer_name, COALESCE(SUM( CASE WHEN p.payment_status = 'Refunded' THEN p.amount ELSE 0 END ), 0) AS refunded_amount FROM customers c LEFT JOIN orders o ON c.customer_id = o.customer_id LEFT JOIN payments p ON o.order_id = p.order_id GROUP BY c.customer_id, c.customer_name ORDER BY refunded_amount DESC;

P146 — Customer Churn Candidates  ·  Expert

Assuming 2026-01-01 as the reference date, find customers whose latest order was more than 365 days ago.

SQL

SELECT c.customer_id, c.customer_name, MAX(o.order_date) AS last_order_date FROM customers c JOIN orders o ON c.customer_id = o.customer_id GROUP BY c.customer_id, c.customer_name HAVING MAX(o.order_date) < DATE '2026-01-01' - INTERVAL '365 days';

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

customer_idcustomer_namelast_order_date
24Olivia Davis2024-07-16
11Alok Bose2024-04-19
26Fatima Ali2024-01-16
23Michael Johnson2024-07-11

(4 rows total)

Explanation

The same 'reference date minus an interval' pattern as Problem 52's 180-day inactivity check, extended to 365 days to define real customer churn. Only 4 customers in this dataset haven't ordered anything in over a year as of the 2026-01-01 reference date — a notably smaller list than Problem 52's 180-day version, since a full year of silence is a much higher bar to clear.

💻 Code example

SELECT c.customer_id, c.customer_name, MAX(o.order_date) AS last_order_date FROM customers c JOIN orders o ON c.customer_id = o.customer_id GROUP BY c.customer_id, c.customer_name HAVING MAX(o.order_date) < DATE '2026-01-01' - INTERVAL '365 days';

P147 — Customer Reactivation  ·  Expert

Find customers who returned after being inactive for at least 180 days.

SQL

WITH gaps AS ( SELECT customer_id, order_date, LAG(order_date) OVER ( PARTITION BY customer_id ORDER BY order_date ) AS previous_order_date FROM orders ) SELECT DISTINCT customer_id FROM gaps WHERE order_date - previous_order_date >= 180;

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

customer_id
12
17
16
15
10
18
19
20
14
22

(showing 10 of 12 rows)

Explanation

LAG(order_date) OVER (PARTITION BY customer_id ORDER BY order_date) computes the gap before every order (same mechanism as Problem 115's longest-gap calculation), and this query flags every individual order that came after a 180+ day silence — meaning a customer can appear here more than once if they've 'reactivated' from a long gap on multiple separate occasions. 12 such reactivation events show up in this dataset.

💻 Code example

WITH gaps AS ( SELECT customer_id, order_date, LAG(order_date) OVER ( PARTITION BY customer_id ORDER BY order_date ) AS previous_order_date FROM orders ) SELECT DISTINCT customer_id FROM gaps WHERE order_date - previous_order_date >= 180;

P148 — Employee Productivity Score  ·  Expert

Create a simple productivity score using sales revenue and project hours: revenue divided by total project hours.

SQL

WITH sales AS ( SELECT employee_id, SUM(total_amount) AS revenue FROM orders WHERE status <> 'Cancelled' GROUP BY employee_id ), hours AS ( SELECT employee_id, SUM(hours_worked) AS total_hours FROM employee_projects GROUP BY employee_id ) SELECT e.employee_id, e.first_name, COALESCE(s.revenue, 0) AS revenue, COALESCE(h.total_hours, 0) AS total_hours, COALESCE( s.revenue / NULLIF(h.total_hours, 0), 0 ) AS productivity_score FROM employees e LEFT JOIN sales s ON e.employee_id = s.employee_id LEFT JOIN hours h ON e.employee_id = h.employee_id;

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

employee_idfirst_namerevenuetotal_hoursproductivity_score
2Meera01500
4Priya01300
5Suresh0300
6Anil0600
8Rohan05000
9Ananya06300
11Kavya02000
12Deepak0600
14Rohan04000
17Rakesh01500

(showing 10 of 40 rows)

Explanation

Two independent CTEs — one summing sales revenue per employee, one summing project hours per employee — are joined back onto the full employees table with LEFT JOINs so nobody is dropped, and NULLIF(h.total_hours, 0) prevents a division-by-zero for anyone with hours logged but, in principle, zero total (a defensive habit worth keeping even when it can't currently happen). Because only Sales-department employees ever have order-derived revenue in this schema, every non-Sales employee's productivity score correctly comes out to exactly 0, regardless of how many project hours they logged — a limitation of the metric itself, not a bug: 'revenue per hour' simply isn't a meaningful measure for people who were never credited with any sales.

💻 Code example

WITH sales AS ( SELECT employee_id, SUM(total_amount) AS revenue FROM orders WHERE status <> 'Cancelled' GROUP BY employee_id ), hours AS ( SELECT employee_id, SUM(hours_worked) AS total_hours FROM employee_projects GROUP BY employee_id ) SELECT e.employee_id, e.first_name, COALESCE(s.revenue, 0) AS revenue, COALESCE(h.total_hours, 0) AS total_hours, COALESCE( s.revenue / NULLIF(h.total_hours, 0), 0 ) AS productivity_score FROM employees e LEFT JOIN sales s ON e.employee_id = s.employee_id LEFT JOIN hours h ON e.employee_id = h.employee_id;

P149 — Identify Data Quality Issues in Orders  ·  Expert

Find orders with any of these issues: negative amount, zero amount, invalid status, or missing customer.

SQL

SELECT o.* FROM orders o LEFT JOIN customers c ON o.customer_id = c.customer_id WHERE o.total_amount <= 0 OR o.status NOT IN ( 'Pending', 'Shipped', 'Delivered', 'Cancelled' ) OR c.customer_id IS NULL;

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

order_idcustomer_idemployee_idorder_datestatustotal_amount
153332024-11-25processing52,588
6812152025-08-03Cancelled0
751102024-02-03Cancelled-499
964332024-02-20Returned14,392
1499,99932025-08-05Pending2,499

(5 rows total)

Explanation

Three independent conditions — total_amount <= 0, an invalid status value (checked with NOT IN against the four legitimate statuses), or a customer_id with no matching row in customers (via LEFT JOIN ... IS NULL) — are combined with OR, so an order needs to trip just one of them to be flagged as suspect. All 5 of this dataset's deliberately planted data-quality problems surface here in one query: two orders with an invalid free-text status ('processing', 'Returned'), one with a zero total, one with a negative total, and the orphan order referencing a customer_id (9999) that doesn't exist in customers at all — exactly the kind of validation sweep a real data-quality job would run on this table nightly.

💻 Code example

SELECT o.* FROM orders o LEFT JOIN customers c ON o.customer_id = c.customer_id WHERE o.total_amount <= 0 OR o.status NOT IN ( 'Pending', 'Shipped', 'Delivered', 'Cancelled' ) OR c.customer_id IS NULL;

P150 — Comprehensive Customer Analytics  ·  Expert

Build a customer-level report containing total orders, total spending, average order value, first order, latest order, and spending rank.

SQL

WITH customer_metrics AS ( SELECT c.customer_id, c.customer_name, COUNT(o.order_id) AS total_orders, COALESCE(SUM( CASE WHEN o.status <> 'Cancelled' THEN o.total_amount ELSE 0 END ), 0) AS total_spending, COALESCE(AVG( CASE WHEN o.status <> 'Cancelled' THEN o.total_amount END ), 0) AS average_order_value, MIN(o.order_date) AS first_order_date, MAX(o.order_date) AS latest_order_date 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_spending DESC ) AS spending_rank FROM customer_metrics ORDER BY spending_rank, customer_id;

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

customer_idcustomer_nametotal_orderstotal_spendingaverage_order_valuefirst_order_datelatest_order_datespending_rank
5Vikas Rana15316,81026,400.832024-02-102025-12-061
7Rajesh Pandey10301,12130,112.102024-01-112025-10-202
3Amit Bhatia11272,00827,200.802024-03-042025-11-033
2Sneha Nair17257,91519,839.622024-02-252025-11-184
4Pooja Sinha10213,13730,448.142024-02-202025-10-165
1Rahul Agarwal14194,98619,498.602024-02-032025-09-026
6Neha Ghosh12168,30614,025.502024-03-222025-06-097
8Swati Yadav10146,43316,270.332024-10-252025-12-158
19Kunal Malhotra4136,66034,1652024-07-132025-09-269
21John Smith2122,78361,391.502024-09-092025-04-0710

(showing 10 of 30 rows)

Explanation

This is the capstone problem of this practice set: one CTE builds a complete per-customer profile — order count, non-cancelled total spending, average non-cancelled order value, first and last order dates — using LEFT JOIN and COALESCE/conditional AVG so every customer appears even with zero orders, and the outer query then ranks everyone by total_spending with RANK(). The result is effectively Problems 36, 51, 69, 70, and 95 all fused into a single report: Vikas Rana again tops the list at ₹316,810 across 15 orders, exactly consistent with every other spending-based ranking computed earlier in this section.

💻 Code example

WITH customer_metrics AS ( SELECT c.customer_id, c.customer_name, COUNT(o.order_id) AS total_orders, COALESCE(SUM( CASE WHEN o.status <> 'Cancelled' THEN o.total_amount ELSE 0 END ), 0) AS total_spending, COALESCE(AVG( CASE WHEN o.status <> 'Cancelled' THEN o.total_amount END ), 0) AS average_order_value, MIN(o.order_date) AS first_order_date, MAX(o.order_date) AS latest_order_date 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_spending DESC ) AS spending_rank FROM customer_metrics ORDER BY spending_rank, customer_id;

Want a visual for this concept?

Generate a diagram tailored to “SQL Practice — Expert & Real-World (Problems 111-150)” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

← Back to all SQL Practice Problems chapters