Databases 15 min read

SQL JOIN Mastery: INNER, LEFT, RIGHT Joins Explained with Live Examples

This tutorial explains SQL INNER, LEFT, and RIGHT JOINs through practical examples with a user-order-product schema, demonstrating each join type's behavior, result sets, and common pitfalls like Cartesian products and missing indexes.

Engineer Tang's Implementation Study Notes
Engineer Tang's Implementation Study Notes
Engineer Tang's Implementation Study Notes
SQL JOIN Mastery: INNER, LEFT, RIGHT Joins Explained with Live Examples

Why JOIN Queries Matter

JOIN queries act as the "relationship master" and "data bridge engineer" in databases. They connect related tables (e.g., users and orders via user_id) in a single statement, retrieving all needed information at once. Without JOINs, querying "what did Zhang San buy" requires three manual steps: find Zhang San's ID in the user table, filter orders by that ID, then look up product names — turning you into an "Excel data porter." With a well-structured SELECT ... FROM user JOIN orders JOIN goods ..., the same result takes seconds.

Test Environment: Three-Table Schema

The article sets up a classic "user-order-goods" scenario with runnable DDL and DML:

1. Table Creation

-- Create user table (stores basic user info)
CREATE TABLE `user` (
  `user_id` INT PRIMARY KEY AUTO_INCREMENT, -- Unique user ID (auto-increment PK)
  `name` VARCHAR(20) NOT NULL,              -- Name
  `age` INT,                                -- Age
  `phone` VARCHAR(11),                      -- Phone
  `register_time` DATETIME                  -- Registration time
);

-- Create goods table (stores product info)
CREATE TABLE `goods` (
  `goods_id` INT PRIMARY KEY AUTO_INCREMENT, -- Unique product ID
  `goods_name` VARCHAR(50) NOT NULL,         -- Product name
  `price` DECIMAL(10,2) NOT NULL,            -- Price (2 decimal places)
  `category` VARCHAR(20)                     -- Product category
);

-- Create orders table (core link table connecting users and goods)
CREATE TABLE `orders` (
  `order_id` INT PRIMARY KEY AUTO_INCREMENT,
  `user_id` INT,                             -- Links to user.user_id
  `goods_id` INT,                            -- Links to goods.goods_id
  `order_num` VARCHAR(30) NOT NULL UNIQUE,   -- Order number
  `create_time` DATETIME NOT NULL,           -- Order time
  `pay_status` TINYINT DEFAULT 0,            -- Payment status: 0 unpaid, 1 paid
  FOREIGN KEY (`user_id`) REFERENCES `user`(`user_id`),
  FOREIGN KEY (`goods_id`) REFERENCES `goods`(`goods_id`)
);

2. Sample Data

-- Insert 5 users
INSERT INTO `user` (`name`, `age`, `phone`, `register_time`) VALUES
('Zhang San', 22, '13800138001', '2025-01-10 09:30:00'),
('Li Si', 17, '13800138002', '2025-02-15 14:20:00'),
('Wang Wu', 28, '13800138003', '2025-03-20 10:10:00'),
('Zhao Liu', 35, NULL, '2025-04-05 16:40:00'), -- Phone is NULL
('Sun Qi', 25, '13800138005', '2025-05-12 08:50:00');

-- Insert 4 products
INSERT INTO `goods` (`goods_name`, `price`, `category`) VALUES
('Wireless Bluetooth Earphones', 199.99, 'Digital'),
('Mechanical Keyboard', 299.99, 'Digital'),
('Pure Cotton T-shirt', 99.99, 'Clothing'),
('Running Shoes', 399.99, 'Footwear');

-- Insert 6 orders
INSERT INTO `orders` (`user_id`, `goods_id`, `order_num`, `create_time`, `pay_status`) VALUES
(1, 1, 'OD20250601001', '2025-06-01 11:20:00', 1), -- Zhang San buys earphones (paid)
(1, 2, 'OD20250602001', '2025-06-02 15:30:00', 1), -- Zhang San buys keyboard (paid)
(3, 4, 'OD20250603001', '2025-06-03 13:10:00', 1), -- Wang Wu buys shoes (paid)
(5, 3, 'OD20250604001', '2025-06-04 10:00:00', 0), -- Sun Qi buys T-shirt (unpaid)
(2, 1, 'OD20250605001', '2025-06-05 09:40:00', 1), -- Li Si buys earphones (paid)
(6, 2, 'OD20250606001', '2025-06-06 14:50:00', 1); -- Ghost order: user_id=6 (non-existent)

Three Join Types: Core Logic & Analogies

INNER JOIN (Inner Join): Returns only rows where join columns match in both tables. Analogy: "Tomato meets egg — only when both are present do you get the classic dish."

LEFT JOIN (Left Join): Keeps all rows from the left table (FROM table); right table columns become NULL where no match. Analogy: "Family combo meal — the main (left) must appear; sides (right) fill in if available, otherwise empty."

RIGHT JOIN (Right Join): Keeps all rows from the right table; left table columns become NULL where no match. Analogy: "Side dish gets its own serving — even if the main doesn't show up, the side is still plated."

Visual cheat sheet:

LEFT JOIN        INNER JOIN        RIGHT JOIN
[Left all]       [Overlap only]    [Right all]
□□□■■          ■■                ■■□□□
■■□□□          ■■                □□□■■
(■ = matched, □ = unmatched)

Practical Examples

Example 1: INNER JOIN — "Strict Matchmaker"

Use case: Query confirmed facts like "all paid order details."

-- Query paid order details
SELECT
  u.name AS 'User',
  g.goods_name AS 'Product',
  o.order_num AS 'Order No',
  g.price AS 'Amount',
  DATE(o.create_time) AS 'Order Date'
FROM `user` u
INNER JOIN `orders` o ON u.user_id = o.user_id
INNER JOIN `goods` g ON o.goods_id = g.goods_id
WHERE o.pay_status = 1;

Result: 4 rows (Zhang San 2, Wang Wu 1, Li Si 1). Sun Qi's unpaid order, Zhao Liu (no orders), and the ghost order (user_id=6) are excluded because they fail the "both sides match" rule.

Example 2: LEFT JOIN — "Left Table Leads"

Use case: Statistics covering all entities even with zero related records, e.g., "order count per user."

-- All users' order counts, zero for non-buyers
SELECT
  u.name AS 'User',
  u.age AS 'Age',
  IFNULL(u.phone, 'Not provided') AS 'Phone',
  COUNT(o.order_id) AS 'Order Count'
FROM `user` u
LEFT JOIN `orders` o ON u.user_id = o.user_id
GROUP BY u.user_id;

Result: All 5 users appear. Zhang San 2, Li Si 1, Wang Wu 1, Sun Qi 1 (unpaid counts as order action), Zhao Liu 0. Zhao Liu's phone shows "Not provided."

Example 3: RIGHT JOIN — "Right Table Leads"

Use case: Less common; can be rewritten as LEFT JOIN by swapping tables. Example: "List all orders with buyer name, marking orphan orders as 'Anonymous'."

-- All orders with buyer, ghost orders marked anonymous
SELECT
  o.order_num AS 'Order No',
  g.goods_name AS 'Product',
  IFNULL(u.name, 'Anonymous') AS 'User',
  CASE o.pay_status WHEN 1 THEN 'Paid' ELSE 'Unpaid' END AS 'Status',
  DATE(o.create_time) AS 'Order Date'
FROM `user` u
RIGHT JOIN `orders` o ON u.user_id = o.user_id
RIGHT JOIN `goods` g ON o.goods_id = g.goods_id
ORDER BY o.create_time DESC;

Result: All 6 orders appear. The ghost order (user_id=6) shows "Anonymous." Sun Qi's order shows "Unpaid."

Example 4: Advanced Combo — Filtering, Formatting, Concatenation

Combines JOIN with WHERE, date formatting, and string concatenation.

-- High-value orders after June 1, price > 200
SELECT
  u.name AS 'User',
  g.goods_name AS 'Product',
  g.price AS 'Price',
  DATE_FORMAT(o.create_time, '%Y-%m-%d %H:%i') AS 'Order Time',
  g.category AS 'Category',
  CONCAT(u.name, ' bought ', g.goods_name) AS 'Summary'
FROM `user` u
INNER JOIN `orders` o ON u.user_id = o.user_id
INNER JOIN `goods` g ON o.goods_id = g.goods_id
WHERE o.create_time >= '2025-06-01 00:00:00'
  AND g.price > 200
ORDER BY g.price DESC;

Result: 2 rows — Wang Wu's Running Shoes (399.99) and Zhang San's Mechanical Keyboard (299.99). Times formatted as "2025-06-03 13:10".

Pitfall Guide (Battle-Tested Lessons)

Never omit the ON clause! Missing ON produces a Cartesian product. Example: 10,000 users × 100,000 orders = 1 billion intermediate rows — memory exhaustion, CPU spike, query hangs, DBA hunts you down.

Qualify column names. When multiple tables share column names (e.g., user_id), always prefix with table name or alias ( u.user_id, o.order_id) or the engine throws "column ambiguous" errors.

Use short aliases. orders o (AS optional) keeps SQL readable and faster to write.

Index join columns. On million/ten-million row tables, indexes on user_id, goods_id turn minute-long queries into seconds.

All code samples are copy-paste runnable. Best practice: execute them, then tweak — swap LEFT for INNER to see what disappears, add age > 20 in WHERE, rewrite RIGHT JOIN as LEFT JOIN. Hands-on beats reading.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

SQLdatabaseQuery OptimizationMySQLJOININNER JOINLEFT JOINRIGHT JOIN
Engineer Tang's Implementation Study Notes
Written by

Engineer Tang's Implementation Study Notes

Old Tang | Pseudo-full-stack tech player: writes code without going bald, manages projects without going crazy, delivers without staying up late. No formal tutorials here, only a "slacking-off" practical manual that cures system pain points. Focuses on Shell, SQL, Java, and delivery, turning complexity into simplicity and making efficiency fun.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.