JOIN — Learn Practical Joins and Subqueries in Practice
AdvancedAdvanced JOINSubqueriesWeb developmentPostgreSQL-ready5 questions
QUESTION 6
JOIN + GROUP BY Aggregation — Aggregate Sales by Customer with COUNT and SUM After an INNER JOIN
INNER JOINGROUP BYCOUNT / SUMSales aggregation
Background

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;
List every non-aggregate column from SELECT in GROUP BY: A column used without an aggregate function, such as 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).
Problem

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.

Tables used
▸ customers
customer_idnamecity
1TanakaTokyo
2SatoOsaka
3YamadaTokyo
4SuzukiNagoya
▸ orders
order_idcustomer_idamount
115000
213000
327000
432000
Expected Output
nameorder_counttotal_amount
Tanaka28000
Sato17000
Yamada12000
QUESTION 7
FULL OUTER JOIN — Include Rows Found on Either Side and Classify Employees as Retired, Continuing, or New
FULL OUTER JOINCOALESCECASE WHENDifference detection
Background

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
Combine the id with COALESCE: In a FULL JOIN, either side's id may be NULL, so COALESCE(a.id, b.id) is the standard idiom for retrieving whichever id is not NULL.
MySQL does not support FULL OUTER JOIN: PostgreSQL, SQL Server, and Oracle support it as standard. In MySQL, use LEFT JOIN UNION RIGHT JOIN as an alternative.
Problem

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.

Tables used
▸ employees_2023 (previous year)
emp_idname
1Tanaka
2Sato
3Yamada
▸ employees_2024 (current year)
emp_idname
2Sato
3Yamada
4Suzuki
Expected Output
emp_idname_2023name_2024status
1TanakaNULLRetired
2SatoSatoContinuing
3YamadaYamadaContinuing
4NULLSuzukiNew hire
QUESTION 8
Many-to-Many JOIN (M:N) — Connect Users and Tags by INNER JOINing Through a Junction Table Twice
INNER JOIN × 2Junction tableM:N relationshipTag system
Background

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
The junction table is the bridge: The user_tags table contains only user_id and tag_id and serves as the bridge between users and tags. The combination of the two foreign keys is typically the primary key.
Problem

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.

Tables used
▸ users
user_idname
1Tanaka
2Sato
3Yamada
▸ user_tags (junction table)
user_idtag_id
11
12
22
23
31
▸ tags
tag_idtag_name
1Frontend
2Backend
3DevOps
Expected Output
nametag_name
TanakaFrontend
TanakaBackend
SatoBackend
SatoDevOps
YamadaFrontend
QUESTION 9
CTE (WITH) + JOIN — Turn Per-Product Sales Aggregates into a Virtual Table, Then Add Details with a JOIN
WITH / CTEINNER JOINGROUP BY + SUMQuery readability
Background

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;
A CTE is a “temporary named subquery”: Rewriting an inline subquery in the FROM clause as a CTE improves readability. The same CTE can be referenced multiple times, making it useful for organizing complex aggregate queries (supported by PostgreSQL, MySQL 8+, SQL Server, and others).
Problem

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.

Tables used
▸ products
product_idnameprice
1Notebook PC80000
2Mouse3000
3Keyboard8000
▸ order_items
item_idproduct_idquantity
112
225
323
431
511
Expected Output
nametotal_qtyorder_counttotal_revenue
Notebook PC32240000
Mouse8224000
Keyboard118000
QUESTION 10
ROW_NUMBER() + Subquery JOIN — Number Rows Within Each Group and Retrieve Only the Latest Row
ROW_NUMBER()PARTITION BYSubquery JOINLatest-row retrieval
Background

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
Unlike GROUP BY, a window function does not remove rows: GROUP BY aggregates multiple rows into one, while ROW_NUMBER() only assigns a number to each row and does not change the row count. Even when PARTITION BY defines groups, every row is retained and receives its within-group rank as rn.
A window function cannot be used directly in WHERE: Writing 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.
Problem

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.

Tables used
▸ users
user_idname
1Tanaka
2Sato
3Yamada
▸ orders
order_iduser_idamountordered_at
1150002024-01-10
2180002024-02-15
3230002024-01-20
4260002024-03-01
5340002024-02-05
Expected Output
nameorder_idamountordered_at
Sato460002024-03-01
Tanaka280002024-02-15
Yamada540002024-02-05