本系列第三篇專講 Nested Loops Join (NLJ),前篇覆蓋單表掃描,後續將講 Hash Join、Merge Join、子查詢轉換。達夢、Oracle、SQL Server 在執行計畫邏輯上無本質差異。本文採 Oracle 10g 實測,解析基於執行計畫、10053 Trace 反推,大方向準確。
SET AUTOTRACE ON EXPLAIN:執行 SQL 並顯示計畫與統計,**會真正跑完 SQL**,慢查詢慎用。EXPLAIN PLAN FOR ...; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);:不執行 SQL,僅生成計畫,適合慢查詢分析。每個操作有 ID 與 Parent ID。NESTED LOOPS 為父節點,第一個 Child 為 Outer Table (驅動表/外部表),第二個 Child 為 Inner Table (被驅動表/內部表)。控制權在父節點與子節點間流轉,類似雙層迴圈。
Predicate Information 區區分 Access (索引/雜湊鍵值定位) 與 Filter (取得行後再過濾)。
SELECT /*+ USE_NL(t2) */ c1,c2 FROM t1 INNER JOIN t2 ON c1=d2;
計畫:NESTED LOOPS → TABLE ACCESS FULL T1 (Outer) → TABLE ACCESS FULL T2 (Inner, Filter: C1=D2)
邏輯等同:
FOR rec1 IN (SELECT * FROM t1) LOOP
FOR rec2 IN (SELECT * FROM t2) LOOP
IF rec1.c1 = rec2.d2 THEN PIPE ROW(rec1.c1, rec1.c2); END IF;
END LOOP;
END LOOP;
控制流:Outer 取一行 → 交給 NLJ → NLJ 驅動 Inner 全掃描 → 符合 Filter 者上傳 → 直到 Inner 耗盡 → Outer 取下一行。Inner 表重複全掃描 Outer 行數次。
SELECT /*+ INDEX(t2) */ c1,c2,d1 FROM t1 INNER JOIN t2 ON c1=d1;
計畫:NESTED LOOPS → TABLE ACCESS FULL T1 → INDEX RANGE SCAN IT2D1 (Access: C1=D1)
關鍵差異:Inner 端改用索引範圍掃描,Access("C1"="D1") 表示用 Outer 當前行的 C1 值去 B-Tree 定位。因非唯一索引,可能返回多行,**NLJ 需重複呼叫 Index Range Scan 直到鍵值不匹配**,控制權在 NLJ 與 Index Scan 間多次切換。
DROP INDEX it2d1; CREATE UNIQUE INDEX iut2d1 ON t2(d1);
SELECT /*+ INDEX(t2) */ c1,c2,d1 FROM t1 INNER JOIN t2 ON c1=d1;
計畫:NESTED LOOPS → TABLE ACCESS FULL T1 → INDEX UNIQUE SCAN IUT2D1 (Access: C1=D1, Cost=0)
唯一索引保證至多 1 行匹配,**Index Unique Scan 只需呼叫一次即返回或結束**,NLJ 不再重複驅動 Inner,直接把控制權還給 Outer 取下一行。Cost 從 4 降至 2,效能顯著提升。
| 維度 | 無索引 | 非唯一索引 | 唯一索引 |
|---|---|---|---|
| Inner 存取 | Full Scan + Filter | Index Range Scan (多次) | Index Unique Scan (單次) |
| 適用場景 | 小表驅動小表、無索引 | Outer 少、Inner 有非唯一索引 | Outer 少、Inner 有唯一索引/PK |
| Cost 特徵 | 最高 | 中等 | 最低 (Cost=0 索引存取) |
Predicate Information 中的 Access vs Filter,Access 才是真正的索引定位鍵。DBMS_XPLAN.DISPLAY_CURSOR 可顯示實際執行時的 Starts (啟動次數),NLJ 的 Inner Starts 應 ≈ Outer 返回行數,驗證驅動關係。