A CTE (Common Table Expression) is a temporary named query defined with WITH name AS (...). Extracting logic into a CTE instead of nesting a subquery makes the SQL readable from top to bottom.
WITH cte_name AS ( -- Write a SELECT statement here (think of it as a temporary table) SELECT col1, SUM(col2) AS total FROM some_table GROUP BY col1 ) -- Write the main SELECT after the CTE definition SELECT * FROM cte_name WHERE total > 10000;
CTEs can be used with SELECT, INSERT, UPDATE, and DELETE. This quiz covers SELECT only.
Using the orders table below, write a CTE query for an API that calculates the total sales, order count, and average order value for each month.
Return only months whose average order value is greater than 40,000.
| order_id | order_month | amount |
|---|---|---|
| 1 | 2024-01 | 30000 |
| 2 | 2024-01 | 45000 |
| 3 | 2024-01 | 20000 |
| 4 | 2024-02 | 55000 |
| 5 | 2024-02 | 60000 |
| 6 | 2024-03 | 80000 |
| 7 | 2024-03 | 15000 |
| 8 | 2024-03 | 25000 |
| order_month | total_sales | order_count | avg_order |
|---|---|---|---|
| 2024-02 | 115000 | 2 | 57500 |
2024-01 (avg ≈ 31,667) and 2024-03 (avg ≈ 40,000) are excluded because neither satisfies “greater than.” Only 2024-02 has avg > 40000.
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
You can define multiple comma-separated CTEs after WITH, and each later CTE can reference an earlier one. This expresses a SQL pipeline in which “step ② uses the result of step ①.”
WITH step1 AS ( SELECT user_id, SUM(amount) AS total FROM orders GROUP BY user_id ), -- step2 can reference step1 (define consecutive CTEs separated by commas) step2 AS ( SELECT *, NTILE(4) OVER (ORDER BY total DESC) AS quartile FROM step1 -- Reference the preceding CTE like a table ) SELECT * FROM step2;
Using the users and orders tables below, build an API query that calculates each user's total purchases and ranks users from highest to lowest into quartiles with NTILE.
quartile=1 is the highest rank. Also join user_name from the users table into the final output.
| user_id | user_name |
|---|---|
| U01 | Alice |
| U02 | Bob |
| U03 | Carol |
| U04 | Dave |
| U05 | Eve |
| U06 | Frank |
| U07 | Grace |
| U08 | Hank |
| order_id | user_id | amount |
|---|---|---|
| 1 | U01 | 120000 |
| 2 | U01 | 50000 |
| 3 | U02 | 30000 |
| 4 | U03 | 200000 |
| 5 | U04 | 75000 |
| 6 | U05 | 10000 |
| 7 | U06 | 95000 |
| 8 | U07 | 40000 |
| 9 | U08 | 160000 |
| 10 | U06 | 20000 |
| user_id | user_name | total_amount | quartile |
|---|---|---|---|
| U03 | Carol | 200000 | 1 |
| U01 | Alice | 170000 | 1 |
| U08 | Hank | 160000 | 2 |
| U06 | Frank | 115000 | 2 |
| U04 | Dave | 75000 | 3 |
| U07 | Grace | 40000 | 3 |
| U02 | Bob | 30000 | 4 |
| U05 | Eve | 10000 | 4 |
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
APIs that retrieve only the top N rows in each category are extremely common. ROW_NUMBER() combined with a CTE is the simplest and most versatile pattern.
WITH ranked AS ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY category -- Number rows independently within each category ORDER BY sales DESC -- Number from 1 in descending sales order ) AS rn FROM products ) SELECT * FROM ranked WHERE rn <= 2; -- Keep only the top two rows in each category
From the product_sales table below, extract the two products with the highest sales in each category.
Even when values tie, use ROW_NUMBER to force the result to exactly two products.
| product_id | category | product_name | sales |
|---|---|---|---|
| P01 | Food | Apple | 85000 |
| P02 | Food | Banana | 62000 |
| P03 | Food | Orange | 62000 |
| P04 | Food | Grapes | 41000 |
| P05 | Drink | Green Tea | 95000 |
| P06 | Drink | Coffee | 78000 |
| P07 | Drink | Juice | 78000 |
| P08 | Drink | Water | 55000 |
| category | product_name | sales | rn |
|---|---|---|---|
| Drink | Green Tea | 95000 | 1 |
| Drink | Coffee | 78000 | 2 |
| Food | Apple | 85000 | 1 |
| Food | Banana | 62000 | 2 |
※ Banana and Orange tie at 62000, but ROW_NUMBER makes only one of them second (the choice depends on the database’s internal order unless a tiebreaker such as product_id is added)
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
LAG(col, n) is a window function that returns the value n rows before the current row. It is essential for time-series comparisons such as month-over-month, year-over-year, and day-over-day changes.
LAG(amount, 1) OVER (ORDER BY order_month) -- Return amount from the preceding row when ordered by order_month -- The first row has no preceding row, so it returns NULL
LAG(col, 1, 0) returns 0 instead of NULL. Use it when an API response should avoid NULL.The corresponding
LEAD function references a following row (covered in Q9).Month-over-month formula: (current month − previous month) ÷ previous month × 100. Use NULLIF to prevent division by zero when the previous month is NULL or 0.
Using the monthly_sales table below, create an API query that calculates each month’s sales, previous-month sales, and month-over-month growth rate (%).
For the first month, which has no preceding month, output NULL for both prev_sales and mom_rate.
| order_month | sales |
|---|---|
| 2024-01 | 100000 |
| 2024-02 | 120000 |
| 2024-03 | 108000 |
| 2024-04 | 135000 |
| 2024-05 | 135000 |
| 2024-06 | 150000 |
| order_month | sales | prev_sales | mom_rate |
|---|---|---|---|
| 2024-01 | 100000 | NULL | NULL |
| 2024-02 | 120000 | 100000 | +20.0 |
| 2024-03 | 108000 | 120000 | -10.0 |
| 2024-04 | 135000 | 108000 | +25.0 |
| 2024-05 | 135000 | 135000 | 0.0 |
| 2024-06 | 150000 | 135000 | +11.1 |
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
A recursive CTE uses WITH RECURSIVE to process a hierarchy iteratively by referencing itself. Common uses include organization charts, category hierarchies, and bills of materials (BOMs).
WITH RECURSIVE org_tree AS ( -- ① Anchor member: recursion starting point (select the top row) SELECT id, name, manager_id, 0 AS depth FROM employees WHERE manager_id IS NULL -- No manager means the top level UNION ALL -- Vertically combine anchor and recursive rows, including duplicates -- ② Recursive member: keep adding direct reports to the preceding result SELECT e.id, e.name, e.manager_id, t.depth + 1 FROM employees e JOIN org_tree t ON e.manager_id = t.id ) SELECT * FROM org_tree;
WHERE depth < 10 to prevent infinite loops.From the self-referencing employees table below, expand the hierarchy of every employee starting from the CEO, whose manager_id is NULL.
For each employee, also output depth (hierarchy depth, with CEO = 0) and path (for example, "CEO→Director→Manager").
| emp_id | name | manager_id |
|---|---|---|
| 1 | Alex (CEO) | NULL |
| 2 | Blake (Sales Director) | 1 |
| 3 | Casey (Engineering Director) | 1 |
| 4 | Drew (Sales Manager) | 2 |
| 5 | Emery (Sales Rep) | 4 |
| 6 | Finley (Tech Lead) | 3 |
| emp_id | name | manager_id | depth | path |
|---|---|---|---|---|
| 1 | Alex (CEO) | NULL | 0 | Alex (CEO) |
| 2 | Blake (Sales Director) | 1 | 1 | Alex (CEO)→Blake (Sales Director) |
| 3 | Casey (Engineering Director) | 1 | 1 | Alex (CEO)→Casey (Engineering Director) |
| 4 | Drew (Sales Manager) | 2 | 2 | Alex (CEO)→Blake (Sales Director)→Drew (Sales Manager) |
| 6 | Finley (Tech Lead) | 3 | 2 | Alex (CEO)→Casey (Engineering Director)→Finley (Tech Lead) |
| 5 | Emery (Sales Rep) | 4 | 3 | Alex (CEO)→Blake (Sales Director)→Drew (Sales Manager)→Emery (Sales Rep) |
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free