Applying FILTER from the basic level lets you expand long-format data into a wide cross-tabulation (pivot). Place each status aggregate in its own column to create a summary table with one row per category.
-- Create a "column" for each status value (pivot) SUM(amount) FILTER (WHERE status = 'completed') -- completed column SUM(amount) FILTER (WHERE status = 'pending') -- pending column COALESCE(SUM(...) FILTER (...), 0) -- turn NULL for no match into 0
SUM(...) FILTER(...) returns NULL, not 0. In a cross-tabulation, you often want an empty cell to display as 0, so use COALESCE(aggregate, 0) to convert it. The CASE expression form, SUM(CASE WHEN status='completed' THEN amount ELSE 0 END), produces the same table.From orders, create a pivot table with categories as rows and statuses as columns. Output columns: category, completed_amt, pending_amt, cancelled_amt, total_amt. Use 0 for a cell with no matching rows, and sort by total_amt descending.
| order_id | category | status | amount |
|---|---|---|---|
| 1 | Books | completed | 1200 |
| 2 | Books | completed | 800 |
| 3 | Books | cancelled | 500 |
| 4 | Toys | completed | 3000 |
| 5 | Toys | pending | 1500 |
| 6 | Toys | cancelled | 1000 |
| 7 | Food | completed | 600 |
| 8 | Food | pending | 400 |
Note: Books has no pending order, so pending_amt is 0; Food has no cancelled order, so cancelled_amt is 0.
Expected output (total_amt descending):
| category | completed_amt | pending_amt | cancelled_amt | total_amt |
|---|---|---|---|---|
| Toys | 3000 | 1500 | 1000 | 5500 |
| Books | 2000 | 0 | 500 | 2500 |
| Food | 600 | 400 | 0 | 1000 |
The three status columns are expanded while the rows remain the three category groups. total_amt is the sum across all statuses.
- 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
When aggregating time-series data, truncate dates to units such as months or weeks before grouping. PostgreSQL's DATE_TRUNC('month', ts) rounds a timestamp to the first day of that month at 00:00, so rows from the same month naturally fall into one group.
-- Truncate to the month start and group by "month" DATE_TRUNC('month', order_date) -- 2024-02-15 → 2024-02-01 GROUP BY DATE_TRUNC('month', order_date)
::date makes the result easier to read.From orders, calculate the monthly order count and total amount. Output columns: month, order_count, total_amount. Represent month as the first day of the month (date type), and sort by month ascending.
| order_id | order_date | amount |
|---|---|---|
| 1 | 2024-01-05 | 1000 |
| 2 | 2024-01-20 | 1500 |
| 3 | 2024-02-03 | 2000 |
| 4 | 2024-02-15 | 500 |
| 5 | 2024-02-28 | 1000 |
| 6 | 2024-03-10 | 3000 |
| 7 | 2024-03-22 | 2000 |
Note: data covers January–March 2024: January = 2 rows, February = 3, March = 2.
Expected output (month ascending):
| month | order_count | total_amount |
|---|---|---|
| 2024-01-01 | 2 | 2500 |
| 2024-02-01 | 3 | 3500 |
| 2024-03-01 | 2 | 5000 |
The 7 detail rows collapse into three month groups keyed by the first day of the month.
- 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
For reports with total rows, GROUP BY ROLLUP is the shortest route. In addition to detail (a,b), ROLLUP(a, b) outputs the subtotal (a) and the grand total () together in one query.
-- Generate detail + category subtotals + grand total at once GROUP BY ROLLUP (category, status) -- Aggregate levels generated: (cat,status) / (cat) / () GROUPING(status) -- 1 means a subtotal/grand-total row with status rolled up
GROUPING(column) (1 for a subtotal or total at that column) to distinguish it from a real NULL and to label or sort the rows.From orders, return the category × status details, category subtotals, and overall grand total in one query. Display subtotal rows with status "(Subtotal)"; display the grand-total row with category "[All Categories]" and status "(Grand Total)". Return the rows in report order (category → details → subtotal → grand total last).
| order_id | category | status | amount |
|---|---|---|---|
| 1 | Books | completed | 1200 |
| 2 | Books | cancelled | 800 |
| 3 | Toys | completed | 3000 |
| 4 | Toys | completed | 1000 |
| 5 | Toys | cancelled | 500 |
| 6 | Food | completed | 600 |
Note: 5 detail rows + 3 category subtotal rows + 1 grand-total row = 9 rows. The grand total is 1200+800+3000+1000+500+600 = 7100.
Expected output (detail → subtotal → grand total report order):
| category | status | total_amount |
|---|---|---|
| Books | cancelled | 800 |
| Books | completed | 1200 |
| Books | (Subtotal) | 2000 |
| Food | completed | 600 |
| Food | (Subtotal) | 600 |
| Toys | cancelled | 500 |
| Toys | completed | 4000 |
| Toys | (Subtotal) | 4500 |
| [All Categories] | (Grand Total) | 7100 |
Within each category, details come before the subtotal; the grand total follows all categories.
- 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
To show what percentage of the whole each category represents, layer a window function on top of the grouped result. In SUM(SUM(amount)) OVER (), the inner SUM is the group total and the outer SUM is the grand total across all groups: this is two-level aggregation.
-- share = group total / grand total SUM(amount) * 100.0 / SUM(SUM(amount)) OVER () -- cumulative share (accumulated from largest total downward) SUM(SUM(amount)) OVER (ORDER BY SUM(amount) DESC)
SUM(amount) can be referenced inside a window and why the nested SUM(SUM(...)) works. OVER () creates one window over all rows; OVER (ORDER BY ...) creates a cumulative (running) total.From orders, calculate the category total, share of the whole (%), and cumulative share (%). Output columns: category, total_amount, pct_of_total, cumulative_pct. Sort by total descending (the ABC-analysis order), and round percentages to one decimal place.
| order_id | category | amount |
|---|---|---|
| 1 | Books | 1000 |
| 2 | Books | 1000 |
| 3 | Toys | 3000 |
| 4 | Toys | 2000 |
| 5 | Food | 2000 |
| 6 | Food | 1000 |
Note: Books=2000, Toys=5000, Food=3000, for a grand total of 10000. The shares are 20% / 50% / 30%.
Expected output (total_amount descending):
| category | total_amount | pct_of_total | cumulative_pct |
|---|---|---|---|
| Toys | 5000 | 50.0 | 50.0 |
| Food | 3000 | 30.0 | 80.0 |
| Books | 2000 | 20.0 | 100.0 |
Cumulative from the top: 50.0 → 50.0+30.0=80.0 → +20.0=100.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
Use STRING_AGG (string concatenation) or ARRAY_AGG (array construction) when you want to combine values within a group into one list. A defining feature is that ORDER BY and DISTINCT can be written inside the aggregate function.
-- Concatenate group values with commas (deduplicated and sorted) STRING_AGG(DISTINCT product, ', ' ORDER BY product) -- Aggregate into an array ARRAY_AGG(DISTINCT product ORDER BY product)
STRING_AGG(expr, delimiter ORDER BY ...) determines the order of list elements; it is different from the ORDER BY at the end of the query. Adding DISTINCT combines duplicate elements into one (and the ORDER BY expression must match the DISTINCT expression).From sales, calculate the number of distinct products, a product-name list, and the total quantity per category. Remove duplicate names and sort the product list alphabetically. Output columns: category, product_count, products, total_qty. Return only categories with at least 2 distinct products, sorted by total_qty descending.
| id | category | product | qty |
|---|---|---|---|
| 1 | Books | SQL Guide | 3 |
| 2 | Books | Python | 1 |
| 3 | Books | SQL Guide | 2 |
| 4 | Toys | Blocks | 5 |
| 5 | Toys | Puzzle | 2 |
| 6 | Toys | Blocks | 1 |
| 7 | Food | Coffee | 4 |
Note: Books has two SQL Guide rows, but it is one product type. Food has only one product type, Coffee.
Expected output (total_qty descending):
| category | product_count | products | total_qty |
|---|---|---|---|
| Toys | 2 | Blocks, Puzzle | 8 |
| Books | 2 | Python, SQL Guide | 6 |
Food is excluded by HAVING because it has only one distinct product.
- 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