LEFT JOIN keeps every row from the left table, and RIGHT JOIN keeps every row from the right table. FULL OUTER JOIN keeps every row from both tables, so no row is missed. A row that exists on only one side is padded with NULL on the other side.
-- Comparing join types (left: A={1,2,3}, right: B={1,3,4}) LEFT JOIN → 1, 2, 3 (2 is absent from B, so the B side is NULL) RIGHT JOIN → 1, 3, 4 (4 is absent from A, so the A side is NULL) FULL JOIN → 1, 2, 3, 4 (B is NULL for 2; A is NULL for 4)
COALESCE(lm.col, tm.col) to fill the NULL.LEFT JOIN … UNION ALL … RIGHT JOIN WHERE A.id IS NULL as an alternative. PostgreSQL, BigQuery, and SQL Server support it.An e-commerce site has product-sales tables for last month (last_month) and this month (this_month). Some products sold only last month, some only this month, and some in both months.
Cover every product and place last month's sales (last_amount) next to this month's sales (this_amount). A product missing from one month should have NULL for that month.
| product_id | product_name | amount |
|---|---|---|
| 1 | PC | 500 |
| 2 | Mouse | 120 |
| 3 | Keyboard | 280 |
| product_id | product_name | amount |
|---|---|---|
| 1 | PC | 630 |
| 3 | Keyboard | 210 |
| 4 | Monitor | 450 |
| product_id | product_name | last_amount | this_amount |
|---|---|---|---|
| 1 | PC | 500 | 630 |
| 2 | Mouse | 120 | NULL |
| 3 | Keyboard | 280 | 210 |
| 4 | Monitor | NULL | 450 |
- 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
The operation of retrieving “rows that exist in table A but not in table B” is called an anti-JOIN (set difference). It is common in practical work for detecting churned users, finding unprocessed tickets, and identifying data that is missing from a master table.
There are two main ways to implement it.
-- ① LEFT JOIN + WHERE IS NULL (the method used in this question) SELECT a.* FROM A LEFT JOIN B ON a.id = b.id WHERE b.id IS NULL; -- ② NOT EXISTS (more explicit intent) SELECT a.* FROM A WHERE NOT EXISTS (SELECT 1 FROM B WHERE b.id = a.id);
WHERE a.id NOT IN (SELECT id FROM B) contains even one NULL, every row becomes UNKNOWN and the result is empty. Use LEFT JOIN or NOT EXISTS for an anti-JOIN.There are tables of customers who purchased last month (last_buyers) and customers who purchased this month (this_buyers).
Retrieve the user_id and user_name of customers who purchased last month but not this month (churned customers).
| user_id | user_name |
|---|---|
| 1 | Tanaka |
| 2 | Sato |
| 3 | Suzuki |
| 4 | Takahashi |
| user_id |
|---|
| 1 |
| 3 |
| user_id | user_name |
|---|---|
| 2 | Sato |
| 4 | Takahashi |
- 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
A many-to-many (M:N) relationship—where one article can have multiple tags and one tag can belong to multiple articles—is modeled with a junction table containing two foreign keys. This is one of the most common structures in web-application database design.
-- A typical many-to-many database structure articles (article_id, title) ← entity A tags (tag_id, tag_name) ← entity B article_tags (article_id, tag_id) ← junction table (bridge) -- Retrieval pattern: traverse articles → article_tags → tags in two steps FROM articles AS a INNER JOIN article_tags AS at ON a.article_id = at.article_id INNER JOIN tags AS t ON at.tag_id = t.tag_id
SELECT DISTINCT.There are an article table (articles), a tag table (tags), and a junction table (article_tags) that links them.
Retrieve the article_id and title of articles tagged with the tag name 'React'.
| article_id | title |
|---|---|
| 1 | Build an SPA with React |
| 2 | Complete Guide to SQL JOIN |
| 3 | Compare Vue and React |
| tag_id | tag_name |
|---|---|
| 1 | React |
| 2 | SQL |
| 3 | Vue |
| article_id | tag_id |
|---|---|
| 1 | 1 |
| 2 | 2 |
| 3 | 1 |
| 3 | 3 |
| article_id | title |
|---|---|
| 1 | Build an SPA with React |
| 3 | Compare Vue and React |
- 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
Packing a complex aggregation into one SELECT statement causes nested subqueries to pile up, sharply reducing readability and maintainability. A WITH clause (CTE: Common Table Expression) lets you break the aggregation into named virtual tables step by step, then JOIN them in the main query.
-- Basic CTE syntax WITH cte_name AS ( SELECT ... -- write the aggregation query here FROM source_table GROUP BY ... ) SELECT * FROM main_table LEFT JOIN cte_name ON ...; -- a CTE can be joined as a table
fo.first_order_date <= u.register_date + INTERVAL '30 days'. Putting the condition in ON rather than WHERE applies it without removing rows from the left side of the LEFT JOIN.There are tables for user registrations (users) and order history (orders). A user may place multiple orders.
For each registration month (cohort month), count users who made their first purchase within 30 days of registration (converted_users) and all registered users (total_users).
| user_id | register_date |
|---|---|
| 1 | 2024-04-01 |
| 2 | 2024-04-20 |
| 3 | 2024-05-05 |
| 4 | 2024-05-10 |
| order_id | user_id | order_date |
|---|---|---|
| 101 | 1 | 2024-04-10 |
| 102 | 1 | 2024-06-01 |
| 103 | 2 | 2024-07-01 |
| 104 | 3 | 2024-05-15 |
| cohort_month | total_users | converted_users |
|---|---|---|
| 2024-04 | 2 | 1 |
| 2024-05 | 2 | 1 |
- 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
The problem “find users who have purchased both product A and product B” cannot be solved with a simple WHERE … IN. Each row represents only one purchase, so asking one row to be product A and product B at the same time is contradictory.
There are two solutions.
-- ① SELF JOIN the same table twice to place p1=product A and p2=product B side by side FROM purchases AS p1 INNER JOIN purchases AS p2 ON p1.user_id = p2.user_id WHERE p1.product_code = 'A' AND p2.product_code = 'B' -- ② Aggregate first and filter with HAVING (alternative; useful with many products) SELECT user_id FROM purchases WHERE product_code IN ('A', 'B') GROUP BY user_id HAVING COUNT(DISTINCT product_code) = 2
There is a purchase-history table (purchases). Each row records one purchase of a specific product by a user.
Retrieve the user_id of users who have purchased both product code 'A' and product code 'B'.
| user_id | product_code |
|---|---|
| 1 | A |
| 1 | B |
| 1 | C |
| 2 | A |
| 3 | B |
| 4 | A |
| 4 | B |
| user_id |
|---|
| 1 |
| 4 |
- 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