The same person may access a site many times — in such data, total events and actual people are different things. Use COUNT(DISTINCT column) to count the number of distinct values within a group. The difference between COUNT(*) (number of rows), COUNT(column) (non-NULL values), and COUNT(DISTINCT column) is the key.
-- Compare three kinds of COUNT COUNT(*) -- Number of rows (all rows, including NULLs) COUNT(user_id) -- Number of rows where user_id is not NULL COUNT(DISTINCT user_id) -- Number of distinct user_id values (duplicates collapse to one)
COUNT(DISTINCT user_id) returns the number of distinct user_id values within each group. Even when total accesses (COUNT(*)) are the same, frequent revisits by the same users make the number of actual people (COUNT(DISTINCT user_id)) smaller. NULL is not counted.From the page_views table, calculate the daily total view count and unique visitor count. Output columns: view_date, total_views, unique_users. Return rows in ascending view_date order.
| view_id | view_date | user_id |
|---|---|---|
| 1 | 2024-05-01 | U1 |
| 2 | 2024-05-01 | U1 |
| 3 | 2024-05-01 | U2 |
| 4 | 2024-05-02 | U2 |
| 5 | 2024-05-02 | U3 |
| 6 | 2024-05-02 | U3 |
| 7 | 2024-05-02 | U1 |
※ On 5/1, U1 views twice (3 total views, 2 actual people); on 5/2, U3 views twice (4 total views, 3 actual people).
Expected output (view_date ascending):
| view_date | total_views | unique_users |
|---|---|---|
| 2024-05-01 | 3 | 2 |
| 2024-05-02 | 4 | 3 |
total_views is the number of rows, while unique_users is the number of distinct user_id values. Revisits make total_views larger than unique_users.
- 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
Want to put counts for each status on one row? A pivot (wide format) expands the vertically stacked status values into columns. With conditional aggregation, you can calculate multiple metrics in one table scan. In PostgreSQL, the aggregate FILTER (WHERE …) syntax is an elegant way to do this.
-- FILTER is a WHERE that applies only to one aggregate COUNT(*) FILTER (WHERE status = 'paid') -- Count only paid rows SUM(amount) FILTER (WHERE status = 'paid') -- Sum only paid amounts -- For portability, CASE can express the same logic SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END)
FILTER, on the other hand, applies its condition to only that one aggregate function. That is why “total count” and “paid-only count” can appear on the same row. When no rows match, COUNT returns 0 while SUM returns NULL.From the orders table, produce one row per category containing the total order count, counts by status, and the total amount for paid orders. Output columns: category, total_orders, paid_orders, pending_orders, cancelled_orders, paid_amount. Sort by total_orders descending, then category ascending for ties.
| order_id | category | status | amount |
|---|---|---|---|
| 1 | Books | paid | 1200 |
| 2 | Books | paid | 800 |
| 3 | Books | paid | 600 |
| 4 | Books | pending | 500 |
| 5 | Books | cancelled | 300 |
| 6 | Toys | paid | 2000 |
| 7 | Toys | cancelled | 1500 |
| 8 | Toys | pending | 700 |
※ Books = 5 orders (paid 3 / pending 1 / cancelled 1); Toys = 3 orders (paid 1 / pending 1 / cancelled 1). paid_amount sums only the amounts of paid rows.
Expected output (total_orders descending):
| category | total_orders | paid_orders | pending_orders | cancelled_orders | paid_amount |
|---|---|---|---|---|---|
| Books | 5 | 3 | 1 | 1 | 2600 |
| Toys | 3 | 1 | 1 | 1 | 2000 |
Books paid_amount = 1200 + 800 + 600 = 2600; Toys = 2000 (only order 6 is paid). The vertical statuses have been expanded into horizontal columns.
- 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 you want regional subtotals and an overall grand total alongside detail aggregates, use GROUP BY ROLLUP(…). It automatically adds higher-level summary rows to ordinary grouping. GROUPING() tells you whether a row is a summary row.
-- ROLLUP(a, b) generates these three aggregation levels at once GROUP BY ROLLUP(region, product) -- (region, product) … detail -- (region) … region subtotal (product is collapsed) -- () … grand total (everything is collapsed) GROUPING(product) -- 1 when product is collapsed on the row, 0 for detail
GROUPING(column)=1 (the column was rolled up) and replace it with a label such as '(All)' using CASE.From the sales table, create a table of regional × product sales with a regional subtotal and an overall grand total. Output columns: region, product, total_amount. Show (All) for collapsed columns on subtotal and grand-total rows, and place each summary row at the end of its block (details → subtotal, with the grand total last).
| region | product | amount |
|---|---|---|
| East | Apple | 100 |
| East | Banana | 150 |
| West | Apple | 200 |
| West | Banana | 250 |
※ 4 detail rows + East subtotal (250) + West subtotal (450) + grand total (700) = 7 rows.
Expected output (subtotals and grand total at the end):
| region | product | total_amount |
|---|---|---|
| East | Apple | 100 |
| East | Banana | 150 |
| East | (All) | 250 |
| West | Apple | 200 |
| West | Banana | 250 |
| West | (All) | 450 |
| (All) | (All) | 700 |
Rows labeled (All) are subtotals or the grand total. East = 100 + 150 = 250, West = 200 + 250 = 450, and grand total = 250 + 450 = 700.
- 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 intend to calculate “average purchase amount per customer,” but before you know it you have calculated “average per order” instead — a classic mistake in aggregation grain. To aggregate the result of an aggregation, separate the stages with a subquery (derived table) or CTE. One GROUP BY alone cannot express it.
-- ① First, total each customer (grain = customer) WITH customer_totals AS ( SELECT customer_id, SUM(amount) AS customer_total FROM orders GROUP BY customer_id ) -- ② Average that result (one row = one customer) SELECT AVG(customer_total) FROM customer_totals;
AVG(amount) on raw orders is the average per order. First collapsing to a customer total with GROUP BY customer_id and then applying AVG gives the average per customer. They are different numbers. Always keep “average per what?” in mind.From the orders table, calculate the customer count and average purchase amount per customer by region (first total each customer, then average those totals by region). Output columns: region, customer_count, avg_customer_spend (round the average to an integer). Sort by avg_customer_spend descending, then region ascending for ties.
| order_id | region | customer_id | amount |
|---|---|---|---|
| 1 | East | C1 | 1000 |
| 2 | East | C1 | 2000 |
| 3 | East | C2 | 3000 |
| 4 | West | C3 | 500 |
| 5 | West | C3 | 500 |
| 6 | West | C4 | 2000 |
※ East has 2 customers, C1 (3000) and C2 (3000); West has 2, C3 (1000) and C4 (2000). Average by customer count, not order count.
Expected output (avg_customer_spend descending):
| region | customer_count | avg_customer_spend |
|---|---|---|
| East | 2 | 3000 |
| West | 2 | 1500 |
East = (3000 + 3000) / 2 = 3000; West = (1000 + 2000) / 2 = 1500. ※ The order average would be East 2000 and West 1000 — a different metric.
- 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 each group’s total together with “what percentage of the whole it represents,” use the window function SUM(…) OVER (). It keeps the result rows from GROUP BY intact while broadcasting the overall aggregate to every row. You can calculate shares in one statement without a subquery or join.
-- Divide each group total by the grand total of all groups SUM(amount) -- ① After GROUP BY: total for this group SUM(SUM(amount)) OVER () -- ② Sum ① across groups = grand total (same value on every row) 100.0 * SUM(amount) / SUM(SUM(amount)) OVER () -- ③ Share (%)
SUM(amount) is the total for each group after GROUP BY. The outer window SUM(…) OVER () sums across groups and broadcasts the grand total to every row. Because window functions run after aggregation, they can add another aggregation layer over the grouped result. OVER () means that all rows form one window.From the sales table, calculate the total sales by category and each category’s share of the whole (%). Output columns: category, category_total, pct_of_total (round the share to one decimal place). Sort by category_total descending, then category ascending for ties.
| sale_id | category | amount |
|---|---|---|
| 1 | Electronics | 4000 |
| 2 | Electronics | 2000 |
| 3 | Clothing | 1500 |
| 4 | Clothing | 500 |
| 5 | Food | 2000 |
※ Electronics = 6000, Clothing = 2000, Food = 2000; grand total = 10000. Each share is its category total ÷ 10000 × 100.
Expected output (category_total descending):
| category | category_total | pct_of_total |
|---|---|---|
| Electronics | 6000 | 60.0 |
| Clothing | 2000 | 20.0 |
| Food | 2000 | 20.0 |
6000/10000 = 60.0%, and 2000/10000 = 20.0%. The shares sum to 100%.
- 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