JOIN and GROUP BY can be combined in the flow “join → group → aggregate.” JOIN widens rows horizontally, while GROUP BY consolidates rows vertically. Combining these two operations lets you write aggregate queries across multiple tables.
SELECT c.name, COUNT(o.order_id) AS order_count, -- count the orders SUM(o.amount) AS total_amount -- calculate the total amount FROM customers AS c INNER JOIN orders AS o ON c.customer_id = o.customer_id GROUP BY c.customer_id, c.name ORDER BY total_amount DESC;
SELECT c.name, must be included in GROUP BY. With only GROUP BY c.customer_id, you cannot select c.name (PostgreSQL and standard SQL).INNER JOIN the customers and orders tables, then retrieve each customer's order count (order_count) and total amount (total_amount). Exclude customers with no orders (Suzuki) and sort by total amount in descending order.
| customer_id | name | city |
|---|---|---|
| 1 | Tanaka | Tokyo |
| 2 | Sato | Osaka |
| 3 | Yamada | Tokyo |
| 4 | Suzuki | Nagoya |
| order_id | customer_id | amount |
|---|---|---|
| 1 | 1 | 5000 |
| 2 | 1 | 3000 |
| 3 | 2 | 7000 |
| 4 | 3 | 2000 |
| name | order_count | total_amount |
|---|---|---|
| Tanaka | 2 | 8000 |
| Sato | 1 | 7000 |
| Yamada | 1 | 2000 |
- 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
FULL OUTER JOIN includes all rows from both tables, including rows that exist in only one of the two tables. Columns from the side with no match are filled with NULL.
SELECT ... FROM table_a AS a FULL OUTER JOIN table_b AS b ON a.id = b.id; -- a.id IS NULL → row exists only in B -- b.id IS NULL → row exists only in A -- neither is NULL → row exists in both
COALESCE(a.id, b.id) is the standard idiom for retrieving whichever id is not NULL.LEFT JOIN UNION RIGHT JOIN as an alternative.FULL OUTER JOIN the employee tables for 2023 and 2024, then classify every employee's status as retired, continuing, or new. Use CASE WHEN to classify rows according to whether one side's emp_id is NULL.
| emp_id | name |
|---|---|
| 1 | Tanaka |
| 2 | Sato |
| 3 | Yamada |
| emp_id | name |
|---|---|
| 2 | Sato |
| 3 | Yamada |
| 4 | Suzuki |
| emp_id | name_2023 | name_2024 | status |
|---|---|---|---|
| 1 | Tanaka | NULL | Retired |
| 2 | Sato | Sato | Continuing |
| 3 | Yamada | Yamada | Continuing |
| 4 | NULL | Suzuki | New hire |
- 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
Web applications frequently have many-to-many (M:N) relationships: one user can have multiple tags, and one tag can belong to multiple users. In an RDB, this relationship is represented with a junction table.
SELECT u.name, t.tag_name FROM users AS u INNER JOIN user_tags AS ut ON u.user_id = ut.user_id -- ① users → junction table INNER JOIN tags AS t ON ut.tag_id = t.tag_id; -- ② junction table → tags
INNER JOIN the three tables users, user_tags (the junction table), and tags twice to retrieve the list of tag names held by each user. Sort by user_id, then tag_id.
| user_id | name |
|---|---|
| 1 | Tanaka |
| 2 | Sato |
| 3 | Yamada |
| user_id | tag_id |
|---|---|
| 1 | 1 |
| 1 | 2 |
| 2 | 2 |
| 2 | 3 |
| 3 | 1 |
| tag_id | tag_name |
|---|---|
| 1 | Frontend |
| 2 | Backend |
| 3 | DevOps |
| name | tag_name |
|---|---|
| Tanaka | Frontend |
| Tanaka | Backend |
| Sato | Backend |
| Sato | DevOps |
| Yamada | Frontend |
- 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 CTE (Common Table Expression, or WITH clause) gives a complex subquery a name so it can be reused as a “virtual table.” Because a query can be written as multiple steps, readability and maintainability improve significantly.
WITH cte_name AS ( -- subquery (aggregation, filtering, etc.) SELECT col1, SUM(col2) AS total FROM some_table GROUP BY col1 ) SELECT m.name, c.total FROM master_table AS m INNER JOIN cte_name AS c ON m.id = c.col1;
Use a CTE to aggregate the order_items table by product (total quantity and order count), then INNER JOIN it with the products table to retrieve the product name, total quantity, order count, and total revenue (price × total_qty). Sort by total revenue in descending order.
| product_id | name | price |
|---|---|---|
| 1 | Notebook PC | 80000 |
| 2 | Mouse | 3000 |
| 3 | Keyboard | 8000 |
| item_id | product_id | quantity |
|---|---|---|
| 1 | 1 | 2 |
| 2 | 2 | 5 |
| 3 | 2 | 3 |
| 4 | 3 | 1 |
| 5 | 1 | 1 |
| name | total_qty | order_count | total_revenue |
|---|---|---|---|
| Notebook PC | 3 | 2 | 240000 |
| Mouse | 8 | 2 | 24000 |
| Keyboard | 1 | 1 | 8000 |
- 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 window function ROW_NUMBER() numbers rows within each group (partition). Using this number to filter for only a specific row within each group (such as the latest or maximum-valued row) is one of the most common advanced patterns in practical work.
ROW_NUMBER() OVER ( PARTITION BY column_name -- define groups (unlike GROUP BY, keep the rows) ORDER BY column_name DESC -- define the order within each group ) AS rn -- the latest or maximum row becomes rn=1
WHERE ROW_NUMBER() OVER (...) = 1 causes an error. Calculate the window function inside a subquery (or CTE), then filter rn in the outer WHERE or JOIN ON clause.Retrieve exactly one latest order (the order with the newest ordered_at) for each user from the orders table, then INNER JOIN it with the users table to add user names. Use ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY ordered_at DESC) and select only rows where rn = 1.
| user_id | name |
|---|---|
| 1 | Tanaka |
| 2 | Sato |
| 3 | Yamada |
| order_id | user_id | amount | ordered_at |
|---|---|---|---|
| 1 | 1 | 5000 | 2024-01-10 |
| 2 | 1 | 8000 | 2024-02-15 |
| 3 | 2 | 3000 | 2024-01-20 |
| 4 | 2 | 6000 | 2024-03-01 |
| 5 | 3 | 4000 | 2024-02-05 |
| name | order_id | amount | ordered_at |
|---|---|---|---|
| Sato | 4 | 6000 | 2024-03-01 |
| Tanaka | 2 | 8000 | 2024-02-15 |
| Yamada | 5 | 4000 | 2024-02-05 |
- 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