After this lesson, you will be able to: Use correlated and non-correlated subqueries, EXISTS, and CTEs (WITH) to express complex queries readably.
Subqueries unlock multi-step questions. CTEs (Common Table Expressions) are the modern, readable version, write your queries in stages instead of nested mess.
Inner query runs once; result feeds the outer.
-- IN (subquery)SELECT * FROM usersWHERE id IN (SELECT DISTINCT user_id FROM posts WHERE published);-- = (subquery returning one value)SELECT * FROM posts-- Subquery in FROM (derived table)SELECT post_count_bucket, COUNT(*) AS usersFROM (SELECT user_id, COUNT(*) / 10 AS post_count_bucketFROM posts GROUP BY user_id) tGROUP BY post_count_bucket;
Inner query references outer row; runs once per outer row.
-- For each user, count their posts (in a subquery)SELECT u.id, u.name,(SELECT COUNT(*) FROM posts p WHERE p.user_id = u.id) AS post_countFROM users u;-- Often better: JOIN + GROUP BY (faster + cleaner).-- Use correlated subqueries when GROUP BY would be awkward.
Often faster than IN for 'does any row exist'.
-- Users who have at least one postSELECT * FROM users uWHERE EXISTS (SELECT 1 FROM posts p WHERE p.user_id = u.id);-- Users with NO postsSELECT * FROM users uWHERE NOT EXISTS (SELECT 1 FROM posts p WHERE p.user_id = u.id);-- EXISTS stops at first match, efficient for big inner tables-- IN materializes the full inner result, usually slower
WITH clauses let you name intermediate results.
WITH user_post_counts AS (SELECT user_id, COUNT(*) AS post_countFROM postsWHERE published = TRUEGROUP BY user_id),top_posters AS (SELECT user_idFROM user_post_countsWHERE post_count >= 5)SELECT u.id, u.name, upc.post_countFROM users uJOIN user_post_counts upc ON upc.user_id = u.idWHERE u.id IN (SELECT user_id FROM top_posters);
For trees / graphs / org charts / category hierarchies.
CREATE TABLE categories (id SERIAL PRIMARY KEY,name TEXT,parent_id INT REFERENCES categories(id));-- Get the full ancestor chain for category id = 42WITH RECURSIVE ancestors AS (SELECT id, name, parent_id, 0 AS depthFROM categories WHERE id = 42UNION ALLSELECT c.id, c.name, c.parent_id, a.depth + 1FROM categories cJOIN ancestors a ON c.id = a.parent_id)SELECT * FROM ancestors;
IN (subquery) on huge tables (slow, use EXISTS or JOIN). Correlated subqueries when GROUP BY would be faster. Treating CTEs as optimization hints, in old Postgres (< 12) CTEs were optimization fences. Modern Postgres inlines them. Forgetting RECURSIVE keyword on recursive CTEs.
Sign in and purchase access to unlock this lesson.