Correlated subquery — retrieve only each user’s “latest order”
A correlated subquery references a column from the outer query and is evaluated for each outer row, making it useful for finding a maximum or minimum under the same condition as the current row.
SELECT * FROM orders o1 WHERE o1.ordered_at = ( SELECT MAX(o2.ordered_at) -- The inner query references the outer o1.user_id → correlated subquery FROM orders o2 WHERE o2.user_id = o1.user_id -- ← This is the core of the correlation: evaluated for each outer row );
Retrieving the latest row in a group—such as each user’s latest login or each product’s last price change—is one of the most common API and batch-processing patterns.
ROW_NUMBER() window-function version may be faster for large data sets. It remains a readable choice for small data sets and straightforward requirements.From the orders table, retrieve the row for each user (user_id) with the maximum ordered_at. If a user has multiple orders, return only the latest row. Sort the result by user_id ascending.
| order_id | user_id | amount | ordered_at |
|---|---|---|---|
| 1001 | U01 | 3200 | 2024-03-10 |
| 1002 | U01 | 1500 | 2024-05-20 |
| 1003 | U02 | 8000 | 2024-04-01 |
| 1004 | U02 | 2100 | 2024-06-15 |
| 1005 | U03 | 500 | 2024-02-28 |
| 1006 | U01 | 4700 | 2024-06-01 |
| order_id | user_id | amount | ordered_at |
|---|---|---|---|
| 1006 | U01 | 4700 | 2024-06-01 |
| 1004 | U02 | 2100 | 2024-06-15 |
| 1005 | U03 | 500 | 2024-02-28 |
WITH clause (CTE) — Build a monthly sales report step by step
A WITH clause (CTE, or Common Table Expression) defines a temporary named result set inside a query. Instead of nesting complex subqueries, you can separate the work into step-by-step blocks, greatly improving readability and maintainability.
WITH cte_name1 AS ( -- Block 1: define the first temporary table SELECT ... FROM some_table ), cte_name2 AS ( -- Block 2: cte_name1 is available here SELECT ... FROM cte_name1 ) SELECT * FROM cte_name2; -- The final SELECT produces the actual output
MATERIALIZEDthe MATERIALIZED option can force one computation), and 3) when writing recursive queries.From the sales table, retrieve sales totals by month and product category, plus each category’s percentage of the monthly total. Use CTEs to build it in three stages: “monthly category aggregation” → “monthly total” → “ratio calculation”. Sort by month and category ascending.
| sale_id | category | amount | sold_at |
|---|---|---|---|
| 1 | food | 3000 | 2024-05-03 |
| 2 | drink | 1500 | 2024-05-10 |
| 3 | food | 2000 | 2024-05-20 |
| 4 | drink | 2500 | 2024-06-05 |
| 5 | food | 4000 | 2024-06-12 |
| 6 | drink | 1000 | 2024-06-20 |
| month | category | cat_total | month_total | ratio_pct |
|---|---|---|---|---|
| 2024-05 | drink | 1500 | 6500 | 23.08 |
| 2024-05 | food | 5000 | 6500 | 76.92 |
| 2024-06 | drink | 3500 | 7500 | 46.67 |
| 2024-06 | food | 4000 | 7500 | 53.33 |
CASE WHEN + Conditional aggregation — inventory-status pivot aggregation with FILTER
CASE WHEN is SQL’s conditional expression. Embedding it in an aggregate function enables conditional aggregation (pivot aggregation), where only rows meeting a condition are aggregated. PostgreSQL also supports the dedicated FILTER (WHERE ...) syntax.
-- Conditional aggregation with CASE WHEN (standard SQL) SUM(CASE WHEN status = 'active' THEN amount ELSE 0 END) AS active_sum -- Conditional aggregation with FILTER (PostgreSQL) SUM(amount) FILTER (WHERE status = 'active') AS active_sum
From the inventory table, retrieve the count of in-stock items (quantity > 0), out-of-stock items (quantity = 0), and all items on one row per warehouse as a pivot aggregation.
| item_id | warehouse | quantity |
|---|---|---|
| A01 | Tokyo | 50 |
| A02 | Tokyo | 0 |
| A03 | Tokyo | 20 |
| B01 | Osaka | 0 |
| B02 | Osaka | 0 |
| B03 | Osaka | 15 |
| C01 | Fukuoka | 100 |
| C02 | Fukuoka | 30 |
| warehouse | in_stock | out_of_stock | total_items |
|---|---|---|---|
| Tokyo | 2 | 1 | 3 |
| Osaka | 1 | 2 | 3 |
| Fukuoka | 2 | 0 | 2 |
LAG / LEAD — Visualize trends with month-over-month and row-over-row changes
LAG(column, n) is a window function that returns the value n rows before the current row. LEAD(column, n) returns the value n rows after it. Its key feature is adding the difference from a previous row to the same row without aggregating rows with GROUP BY.
LAG(col, 1, 0) OVER ( -- Value one row before col; default 0 when there is no previous row PARTITION BY group_col -- Keep an independent order within each group ORDER BY sort_col -- Define “previous” and “next” by this order (required) ) LEAD(col, 1) OVER ( -- Value one row after col (the next row) ORDER BY sort_col )
ORDER BY inside the OVER clause Without a defined order, “the previous row” is undefined.monthly_sales table, retrieve monthly sales, previous-month sales, and month-over-month change (%) for each product (product_id). When there is no previous-month data (the first month), previous-month sales and month-over-month change may be NULL. Sort by product_id and month ascending.
| product_id | month | revenue |
|---|---|---|
| P01 | 2024-03 | 10000 |
| P01 | 2024-04 | 12000 |
| P01 | 2024-05 | 9000 |
| P02 | 2024-03 | 5000 |
| P02 | 2024-04 | 7500 |
| P02 | 2024-05 | 7500 |
| product_id | month | revenue | prev_revenue | mom_pct |
|---|---|---|---|---|
| P01 | 2024-03 | 10000 | NULL | NULL |
| P01 | 2024-04 | 12000 | 10000 | +20.00 |
| P01 | 2024-05 | 9000 | 12000 | -25.00 |
| P02 | 2024-03 | 5000 | NULL | NULL |
| P02 | 2024-04 | 7500 | 5000 | +50.00 |
| P02 | 2024-05 | 7500 | 7500 | 0.00 |
Recursive CTE (WITH RECURSIVE) — retrieve all descendants in a category hierarchy
WITH RECURSIVE is syntax for writing a recursive query in which a CTE references itself. It is used to traverse hierarchical data such as category trees, organization charts, and bills of materials.
WITH RECURSIVE cte AS ( -- ① Anchor member (the recursion starting point; runs once at the beginning) SELECT id, parent_id, name, 0 AS depth FROM categories WHERE id = 1 -- Select one starting row UNION ALL -- Connect the anchor and recursive members (fast because duplicates are not removed) -- ② Recursive member (cte references the previous iteration; repeat until it returns zero rows) SELECT c.id, c.parent_id, c.name, r.depth + 1 FROM categories c JOIN cte r ON c.parent_id = r.id -- Retrieve rows whose parent ID is the previous iteration’s id ) SELECT * FROM cte;
depth < 10 or loop detection using a path column.categories is a self-referencing hierarchy through parent_id. Retrieve all descendant categories starting from category_id = 1, including grandchildren and great-grandchildren, not just direct children with their depth (hierarchy level). Treat the starting point as depth 0.
| category_id | parent_id | name |
|---|---|---|
| 1 | NULL | Food |
| 2 | 1 | Drinks |
| 3 | 1 | Snacks |
| 4 | 2 | Coffee |
| 5 | 2 | Juice |
| 6 | 3 | Chocolate |
| 7 | 10 | Miscellaneous (separate tree) |
| category_id | parent_id | name | depth |
|---|---|---|---|
| 1 | NULL | Food | 0 |
| 2 | 1 | Drinks | 1 |
| 3 | 1 | Snacks | 1 |
| 4 | 2 | Coffee | 2 |
| 5 | 2 | Juice | 2 |
| 6 | 3 | Chocolate | 2 |