CTEs, window functions, upserts, subqueries, UNION — the SQL that separates proficient engineers from beginners.
P-1 — Advanced SQL: The Patterns You Will Use Every Week
Who this module is for: You completed Phase 1 and can write fundamental SQL. Phase 2 assumes you are building real applications and need the SQL patterns that separate engineers who can write queries from engineers who can write good queries. This module covers the constructs that appear in almost every production PostgreSQL codebase.
Common Table Expressions (CTEs) — WITH Clauses
A CTE creates a named, temporary result set within a query. It makes complex queries readable by breaking them into named steps.
-- Without CTE: hard to read
SELECT u.display_name, task_counts.total
FROM users u
JOIN (
SELECT assigned_to, COUNT(*) AS total
FROM tasks
WHERE status != 'done'
GROUP BY assigned_to
) task_counts ON u.id = task_counts.assigned_to
ORDER BY task_counts.total DESC;
-- With CTE: same query, readable
WITH open_task_counts AS (
SELECT assigned_to, COUNT(*) AS total
FROM tasks
WHERE status != 'done'
GROUP BY assigned_to
)
SELECT u.display_name, otc.total
FROM users u
JOIN open_task_counts otc ON u.id = otc.assigned_to
ORDER BY otc.total DESC;
Chaining Multiple CTEs
WITH
-- Step 1: get all orders in the last 30 days
recent_orders AS (
SELECT customer_id, product_id, quantity, unit_price
FROM orders
WHERE ordered_at > NOW() - INTERVAL '30 days'
),
-- Step 2: calculate revenue per customer from those orders
customer_revenue AS (
SELECT customer_id, SUM(quantity * unit_price) AS revenue
FROM recent_orders
GROUP BY customer_id
),
-- Step 3: rank customers by revenue
ranked_customers AS (
SELECT
customer_id,
revenue,
RANK() OVER (ORDER BY revenue DESC) AS rank
FROM customer_revenue
)
SELECT
c.name,
rc.revenue,
rc.rank
FROM ranked_customers rc
JOIN customers c ON rc.customer_id = c.id
WHERE rc.rank <= 10; -- top 10 customers
Since PostgreSQL 12, a non-recursive CTE referenced exactly once (with no side effects) is inlined by the planner — treated like a subquery, subject to predicate pushdown and join reordering, not run as a separate fenced step. Every CTE above is referenced once, so on PG12+ they're inlined. A CTE only materializes — executes once, result cached, opaque to the planner — when it's referenced more than once, or when you force it with AS MATERIALIZED. You can force inlining the other way with AS NOT MATERIALIZED, even on a multiply-referenced CTE.
Recursive CTEs — Traversing Hierarchies
Recursive CTEs can traverse tree structures (org charts, categories with subcategories, file systems):
-- Find all members of a team hierarchy, starting from a given manager
WITH RECURSIVE team_hierarchy AS (
-- Base case: the starting employee
SELECT id, name, manager_id, 0 AS depth
FROM employees
WHERE id = 3 -- start from VP Eng (id=3)
UNION ALL
-- Recursive case: find direct reports of everyone already in the result
SELECT e.id, e.name, e.manager_id, th.depth + 1
FROM employees e
JOIN team_hierarchy th ON e.manager_id = th.id
)
SELECT
REPEAT(' ', depth) || name AS indented_name, -- indent by depth
depth
FROM team_hierarchy
ORDER BY depth, name;
-- indented_name | depth
-- -----------------+-------
-- VP Eng | 0
-- Engineer 1 | 1
-- Engineer 2 | 1
-- Junior Dev | 2
Window Functions — Computation Without Collapsing Rows
Aggregate functions collapse many rows into one. Window functions compute across a set of rows but keep every row in the output.
-- Aggregate: collapses rows — one row per category
SELECT category, AVG(price) AS avg_price FROM products GROUP BY category;
-- Window function: keeps every row, adds the avg alongside each product
SELECT
name,
category,
price,
AVG(price) OVER (PARTITION BY category) AS category_avg,
price - AVG(price) OVER (PARTITION BY category) AS diff_from_avg
FROM products;
-- name | category | price | category_avg | diff_from_avg
-- --------------------+-----------+--------+--------------+---------------
-- Keyboard | peripherals| 129.99 | 89.99 | 40.00
-- Mouse | peripherals| 49.99 | 89.99 | -40.00
-- Webcam | peripherals| 89.99 | 89.99 | 0.00
-- Monitor Stand | accessories| 39.99 | 43.32 | -3.33
-- USB-C Hub | accessories| 64.99 | 43.32 | 21.67
-- Laptop Sleeve | accessories| 24.99 | 43.32 | -18.33
The OVER() Clause
OVER() defines the "window" — the set of rows each calculation sees:
-- PARTITION BY: like GROUP BY but doesn't collapse rows
OVER (PARTITION BY category) -- each row sees its category's rows
-- ORDER BY: gives rows an order within the partition
OVER (PARTITION BY category ORDER BY price DESC)
-- No partition (all rows): each row sees the entire result set
OVER ()
Essential Window Functions
Ranking:
SELECT
name,
price,
category,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY price DESC) AS row_num,
RANK() OVER (PARTITION BY category ORDER BY price DESC) AS rank,
DENSE_RANK() OVER (PARTITION BY category ORDER BY price DESC) AS dense_rank
FROM products;
| Difference | When there is a tie |
|---|---|
ROW_NUMBER() | Assigns unique numbers (arbitrary tie-breaking) |
RANK() | Tied rows get same rank; next rank skips numbers (1,1,3) |
DENSE_RANK() | Tied rows get same rank; next rank does not skip (1,1,2) |
Sign in to keep reading
The rest of this module is free — sign in with Google to unlock it and track your progress.
Sign in & RegisterDiscussion
0Join the discussion