核心模式:LEFT JOIN + IS NULL
SELECT A.*
FROM `A`
LEFT JOIN `B` ON A.pid = B.pid
WHERE B.pid IS NULL;
原理圖解
A 表 (主表) B 表 (從表) LEFT JOIN 結果
pid | data pid | info A.pid | A.data | B.pid | B.info
----+----- ----+----- ------+--------+-------+-------
1 | a1 1 | b1 1 | a1 | 1 | b1
2 | a2 3 | b3 2 | a2 | NULL | NULL ← 目標行
3 | a3 3 | a3 | 3 | b3
4 | a4 4 | a4 | NULL | NULL ← 目標行
LEFT JOIN 保證左表 (A) 所有行輸出。
- 右表 (B) 無匹配行時,其列填充
NULL。
WHERE B.pid IS NULL 精確過濾出「A 有、B 無」的差集。
實戰案例:抽獎系統 - 找出未中獎用戶
-- 參與表:一用戶多條記錄
CREATE TABLE `wall_list` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`openid` VARCHAR(64),
`weid` INT
);
-- 中獎表
CREATE TABLE `wall_lottery` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`openid` VARCHAR(64)
);
-- 查詢 weid=63 活動中未中獎的用戶 (去重 openid)
SELECT DISTINCT l.`openid`
FROM `wall_list` AS l
LEFT JOIN `wall_lottery` AS lottery ON l.`openid` = lottery.`openid`
WHERE l.`weid` = 63
AND lottery.`id` IS NULL;
關鍵知識點
- LEFT JOIN 語義:左表全量,右表匹配或 NULL。
- 判斷欄位選擇:用右表主鍵或非空列做
IS NULL 判斷,避免業務數據本身含 NULL 導致誤判。
- 去重:一對多關聯時,左表一行匹配右表多行會展開,
DISTINCT 或 GROUP BY 去重。
- 性能:右表關聯列 (B.pid) 必須有索引,否則全表掃描 B 表,極慢。
對比:NOT IN / NOT EXISTS
| 寫法 | 示例 | 優點 | 缺點/風險 |
| LEFT JOIN + IS NULL | SELECT A.* FROM A LEFT JOIN B ON A.id=B.id WHERE B.id IS NULL | 標準 SQL、可選右表列、優化器友好 | 需注意右表列非空性 |
| NOT IN | SELECT * FROM A WHERE id NOT IN (SELECT id FROM B) | 語意直觀 | 子查詢結果含 NULL 會導致全空結果集、優化器難優化 |
| NOT EXISTS | SELECT * FROM A WHERE NOT EXISTS (SELECT 1 FROM B WHERE A.id=B.id) | 語義清晰、無 NULL 陷阱、支援相關子查詢 | 舊版本優化器可能轉為 Dependent Subquery 慢 |
推薦:NOT EXISTS(語義最清晰、無 NULL 坑、現代優化器優秀)
SELECT A.*
FROM `A`
WHERE NOT EXISTS (
SELECT 1 FROM `B` WHERE B.pid = A.pid
);
擴展:求交集、對稱差集
-- 交集 (A ∩ B)
SELECT A.* FROM A INNER JOIN B ON A.pid = B.pid;
-- 對稱差集 (A Δ B) = (A-B) ∪ (B-A)
SELECT A.* FROM A LEFT JOIN B ON A.pid=B.pid WHERE B.pid IS NULL
UNION ALL
SELECT B.* FROM B LEFT JOIN A ON B.pid=A.pid WHERE A.pid IS NULL;