一、子查詢四大核心模式邏輯差異
| 模式 | 語義 | NULL 處理 | 典型用途 | 效能特性 |
| `IN` | 值在集合中 | 子查詢含 NULL → 結果為 UNKNOWN (不匹配) | 小型靜態集合、代碼表查找 | 半聯結優化,大集合轉 Hash Match |
| `NOT IN` | 值不在集合中 | **子查詢含 NULL → 全部不匹配 (陷阱!)** | 排除清單 | 同 IN,但 NULL 語義風險極高 |
| `EXISTS` | 存在至少一列 | 忽略 NULL,只檢查列存在性 | 關聯存在性、半聯結 | **最穩健**,找到即停止,索引求精確 |
| `NOT EXISTS` | 不存在任何列 | 同 EXISTS | 反向關聯、找孤兒資料 | 同 EXISTS,極佳 |
二、NOT IN 陷阱實測
-- 假設 SubQuery 回傳 (1, 2, NULL)
SELECT * FROM Main WHERE ID NOT IN (SELECT Val FROM SubQuery);
-- 結果:空集合!因為 NULL 導致所有比較為 UNKNOWN
-- 修正:加 WHERE 過濾 NULL
SELECT * FROM Main WHERE ID NOT IN (SELECT Val FROM SubQuery WHERE Val IS NOT NULL);
-- 最佳:改用 NOT EXISTS
SELECT * FROM Main m
WHERE NOT EXISTS (SELECT 1 FROM SubQuery s WHERE s.Val = m.ID);
三、實戰範例:專輯/歌詞/翻唱三表關聯
1. IN:找出含特定歌詞的專輯歌曲
SELECT SongName FROM Album
WHERE BandName = 'Metallica'
AND SongName IN (
SELECT SongName FROM Lyric
WHERE SongLyric LIKE '%justice%'
);
2. NOT IN:找出專輯中無歌詞包含關鍵字的歌曲 (修正版)
SELECT SongName FROM Album
WHERE AlbumName = 'And Justice for All'
AND BandName = 'Metallica'
AND SongName NOT IN (
SELECT SongName FROM Lyric
WHERE SongLyric LIKE '%justice%'
AND SongName IS NOT NULL -- 關鍵修正
);
3. EXISTS:找出被翻唱樂團翻唱過的原唱歌曲
SELECT a.SongName FROM Album a
WHERE a.BandName = 'Metallica'
AND EXISTS (
SELECT 1 FROM Cover c
WHERE c.BandName = 'Damage, Inc.'
AND c.SongName = a.SongName
);
4. NOT EXISTS:找出從未被翻唱的歌曲
SELECT a.SongName FROM Album a
WHERE a.BandName = 'Metallica'
AND NOT EXISTS (
SELECT 1 FROM Cover c
WHERE c.SongName = a.SongName
);
四、彙總函數在子查詢中的應用
驗證專輯曲目數是否完整
SELECT ai.AlbumName
FROM AlbumInfo ai
WHERE ai.AlbumTracks <> (
SELECT COUNT(*) FROM Album a
WHERE a.AlbumName = ai.AlbumName
AND a.BandName = ai.BandName
);
等效 JOIN (通常更快):
SELECT ai.AlbumName
FROM AlbumInfo ai
LEFT JOIN (
SELECT AlbumName, BandName, COUNT(*) AS ActualTracks
FROM Album
GROUP BY AlbumName, BandName
) a ON ai.AlbumName = a.AlbumName AND ai.BandName = a.BandName
WHERE ai.AlbumTracks <> a.ActualTracks OR a.ActualTracks IS NULL;
五、窗口函數替代子查詢模式 (2005+)
| 子查詢模式 | 窗口函數替代 | 優勢 |
| 每群組前 N 名 (相關子查詢) | `ROW_NUMBER() OVER (PARTITION BY grp ORDER BY val DESC)` | 單次掃描、無相關執行、索引支援 |
| 每群組最大/最小值 | `MAX() OVER (PARTITION BY grp)` / `FIRST_VALUE()` | 避免自我聯結 |
| 累計合計/移動平均 | `SUM() OVER (PARTITION BY grp ORDER BY dt ROWS UNBOUNDED PRECEDING)` | 標準語法、平行處理 |
| 排名/去重 | `RANK()` / `DENSE_RANK()` / `ROW_NUMBER()` | 取代 `DISTINCT` + 排序 |
範例:每位歌手最高銷售專輯 (子查詢 vs 窗口函數)
-- 子查詢版 (相關,慢)
SELECT BandName, AlbumName, Sales
FROM AlbumInfo ai
WHERE Sales = (
SELECT MAX(Sales) FROM AlbumInfo WHERE BandName = ai.BandName
);
-- 窗口函數版 (單掃描,快)
WITH Ranked AS (
SELECT BandName, AlbumName, Sales,
ROW_NUMBER() OVER (PARTITION BY BandName ORDER BY Sales DESC) AS RN
FROM AlbumInfo
)
SELECT BandName, AlbumName, Sales
FROM Ranked WHERE RN = 1;
六、子查詢效能優化鐵律
- 優先 `EXISTS`/`NOT EXISTS`:語義清晰、NULL 安全、半聯結優化
- 避免 `NOT IN` 含 NULL 子查詢:必加 `IS NOT NULL` 或改 `NOT EXISTS`
- 大結果集 `IN` 改 `JOIN`/`EXISTS`:`IN` 可能產生大量排序/雜湊
- 標量子查詢移出 SELECT 列表:改 `APPLY`/`JOIN`/`CTE`,避免逐列執行
- 彙總子查詢預先分組:衍生表/CTE 先 `GROUP BY`,再聯結
- 2005+ 優先窗口函數:`ROW_NUMBER`/`RANK`/`SUM OVER` 替代相關子查詢
- 檢查執行計劃:尋找 `Lazy Spool`、`Table Spool`、`Nested Loops` 高估基數
七、常見錯誤修正速查
| 錯誤現象 | 根因 | 標準修正 |
| `NOT IN` 回傳空結果 | 子查詢含 NULL | 改 `NOT EXISTS` 或加 `WHERE col IS NOT NULL` |
| 相關子查詢極慢 | 逐外層列執行,無索引求 | 改 `JOIN`/`APPLY`/`窗口函數` |
| SELECT 列表標量子查詢重複計算 | 每輸出列執行一次 | 移至 `CROSS APPLY` 或 `LEFT JOIN` 衍生表 |
| `IN` 子查詢效能差 | 大集合 Hash Match 溢寫 tempdb | 改 `EXISTS` 或預先篩選縮小集合 |
| 彙總子查詢每次重複 GROUP BY | 未使用衍生表/CTE | 提取為 `WITH Agg AS (...) SELECT ... JOIN Agg` |
下一篇將講解 SELECT 陳述式進階選項:`ORDER BY` 多欄位排序、`DISTINCT` 去重邏輯、彙總函數 `COUNT/SUM/AVG/MIN/MAX`、`GROUP BY` 分組、`HAVING` 分組篩選、窗口函數進階應用。