A CTE (Common Table Expression) is a temporary named query defined as WITH name AS (...). In batch processing, a standard pattern is to split the three stages—aggregation, filtering, and formatting—into CTEs.
WITH aggregate_name AS ( -- ① Define the aggregation (GROUP BY) here SELECT col1, SUM(col2) AS total FROM table_name GROUP BY col1 -- Aggregate rows for each col1 value ) -- ② Filter and sort the aggregated result SELECT * FROM aggregate_name WHERE total > 10000 -- A CTE lets WHERE reference the aggregated total ORDER BY col1; -- Sort using the default ASC order
WHERE SUM(...) and must use HAVING. Moving the aggregation into a CTE lets you treat its result as an ordinary column. As batch SQL grows, naming and separating processing stages becomes increasingly valuable.From the orders table below, calculate the total sales, order count, and average order amount for each order_date.
Return only dates whose total sales are at least 50,000 yen, sorted by total sales in descending order.
| order_id | order_date | amount |
|---|---|---|
| 1 | 2024-04-01 | 20000 |
| 2 | 2024-04-01 | 35000 |
| 3 | 2024-04-02 | 60000 |
| 4 | 2024-04-02 | 15000 |
| 5 | 2024-04-03 | 80000 |
| 6 | 2024-04-03 | 40000 |
| 7 | 2024-04-04 | 12000 |
| 8 | 2024-04-04 | 18000 |
| order_date | total_sales | order_count | avg_order |
|---|---|---|---|
| 2024-04-03 | 120000 | 2 | 60000 |
| 2024-04-02 | 75000 | 2 | 37500 |
| 2024-04-01 | 55000 | 2 | 27500 |
04-04 is excluded because its total is 30,000 yen.
- 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() is a window function that brings a value from one row earlier (or n rows earlier) onto the current row. It is essential for month-over-month and day-over-day calculations.
LAG(value_column, 1, 0) OVER ( PARTITION BY group_column -- Reference the previous row independently within each group ORDER BY sort_column -- This order determines which row is previous ) AS previous_value
The arguments are LAG(column, offset, default). The offset defaults to 1 (one row earlier), and the default value is used when no previous row exists, as on the first row.
From the monthly_sales table below, calculate each month’s previous-month sales, difference from the previous month, and month-over-month growth rate (%).
| month | sales |
|---|---|
| 2024-01 | 100000 |
| 2024-02 | 130000 |
| 2024-03 | 120000 |
| 2024-04 | 160000 |
| 2024-05 | 145000 |
| month | sales | prev_sales | diff | growth_rate |
|---|---|---|---|---|
| 2024-01 | 100000 | 0 | NULL | NULL |
| 2024-02 | 130000 | 100000 | +30000 | 30.00 |
| 2024-03 | 120000 | 130000 | -10000 | -7.69 |
| 2024-04 | 160000 | 120000 | +40000 | 33.33 |
| 2024-05 | 145000 | 160000 | -15000 | -9.38 |
- 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
The ROWS BETWEEN clause of a window function precisely specifies which range of rows to aggregate for each current row.
SUM(sales) OVER ( ORDER BY month ROWS BETWEEN UNBOUNDED PRECEDING -- From the first row AND CURRENT ROW -- Through the current row → running total ) AS running_total AVG(sales) OVER ( ORDER BY month ROWS BETWEEN 2 PRECEDING -- From two rows earlier AND CURRENT ROW -- Through the current row → three-month moving average ) AS moving_avg_3m
From the monthly_sales table below, calculate each month’s running sales total (running_total) and moving average for the latest three months (moving_avg_3m).
| month | sales |
|---|---|
| 2024-01 | 80000 |
| 2024-02 | 120000 |
| 2024-03 | 100000 |
| 2024-04 | 150000 |
| 2024-05 | 130000 |
| 2024-06 | 170000 |
| month | sales | running_total | moving_avg_3m |
|---|---|---|---|
| 2024-01 | 80000 | 80000 | 80000.0 |
| 2024-02 | 120000 | 200000 | 100000.0 |
| 2024-03 | 100000 | 300000 | 100000.0 |
| 2024-04 | 150000 | 450000 | 123333.3 |
| 2024-05 | 130000 | 580000 | 126666.7 |
| 2024-06 | 170000 | 750000 | 150000.0 |
- 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 often need only the top N rows in each category. A standard pattern assigns each row a rank within its group using ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...), isolates that result in a CTE, and then filters with WHERE rn <= N.
WITH ranked AS ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY category -- Number rows independently within each category ORDER BY sales DESC, product_id -- Break ties by product_id ascending ) 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 top two products by sales within each category.
| product_id | category | product_name | sales |
|---|---|---|---|
| P01 | Food | Apple | 85000 |
| P02 | Food | Banana | 62000 |
| P03 | Food | Mandarin | 62000 |
| P04 | Food | Grape | 41000 |
| P05 | Refreshments | Green tea | 95000 |
| P06 | Refreshments | Coffee | 78000 |
| P07 | Refreshments | Juice | 78000 |
| P08 | Refreshments | Water | 55000 |
| category | product_name | sales | rn |
|---|---|---|---|
| Food | Apple | 85000 | 1 |
| Food | Banana | 62000 | 2 |
| Refreshments | Green tea | 95000 | 1 |
| Refreshments | Coffee | 78000 | 2 |
Ties are resolved deterministically by product_id in ascending order.
- 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
Pivot aggregation, which expands row-oriented data into columns, is common in dashboard APIs. Because many SQL dialects lack a dedicated PIVOT statement, it can be implemented with CASE WHEN + SUM.
SELECT category, SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) AS completed, SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) AS pending FROM tasks GROUP BY category;
COUNT(*) FILTER (WHERE status = 'done') produces the same result. CASE WHEN is more portable because some databases, including BigQuery, do not support this FILTER syntax.From the tasks table below, return one row per assignee containing counts by status (done, in progress, and not started) plus the total count.
| task_id | assignee | status |
|---|---|---|
| T01 | Baker | done |
| T02 | Baker | done |
| T03 | Baker | in progress |
| T04 | Baker | not started |
| T05 | Clark | done |
| T06 | Clark | in progress |
| T07 | Clark | in progress |
| T08 | Adams | not started |
| T09 | Adams | not started |
| T10 | Adams | done |
| assignee | done | in_progress | not_started | total |
|---|---|---|---|---|
| Adams | 1 | 0 | 2 | 3 |
| Baker | 2 | 1 | 1 | 4 |
| Clark | 1 | 2 | 0 | 3 |
- 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