這是 Oracle 效能調校最常見的困惑。優化器基於成本模型(CBO)決策,「不走索引」通常意味著優化器認為全表掃描成本更低,而非優化器有 Bug。以下系統梳理七大核心原因與對應策略。
| 因子 | 影響 | 檢查指令 | 解決方案 |
|---|---|---|---|
實例級 OPTIMIZER_MODE | ALL_ROWS 傾向全表掃描;FIRST_ROWS 傾向索引 | SHOW PARAMETER optimizer_mode | Session 級 ALTER SESSION SET optimizer_mode=FIRST_ROWS 或 Hint |
| 表/索引統計資訊過期/缺失 | CBO 依賴統計估算基數,錯誤統計→錯誤計畫 | SELECT last_analyzed FROM user_tables WHERE table_name='T' | EXEC DBMS_STATS.GATHER_TABLE_STATS('SCOTT','T',CASCADE=>TRUE) |
| 表極小(< 數百區塊) | 優化器判斷 FTS 多區塊讀比索引範圍掃描+回表更快 | SELECT blocks FROM user_tables... | 接受 FTS 或用 INDEX(t idx) Hint 強制(測試環境) |
-- 索引定義
CREATE INDEX idx_emp_dept_job ON emp(deptno, job);
-- 查詢條件缺少首欄 deptno
SELECT * FROM emp WHERE job = 'CLERK';
-- ❌ 通常不走索引(除非啟用 INDEX SKIP SCAN 且首欄基數極低)
-- 正確用法
SELECT * FROM emp WHERE deptno = 20 AND job = 'CLERK'; -- ✅ 索引範圍掃描
SELECT * FROM emp WHERE deptno = 20; -- ✅ 索引範圍掃描
補救:若業務頻繁單獨查
job,建立單獨索引CREATE INDEX idx_emp_job ON emp(job)或調整複合索引欄位順序(高選擇性欄位前置)。
-- 含 NULL 欄位的索引
CREATE INDEX idx_emp_comm ON emp(comm); -- comm 允許 NULL
-- 不走索引
SELECT COUNT(*) FROM emp; -- 全表掃描
SELECT COUNT(comm) FROM emp; -- 走索引(只數非 NULL)
SELECT COUNT(*) FROM emp WHERE comm IS NOT NULL; -- 走索引
原理:B-Tree 索引不存儲全 NULL 的鍵值。
COUNT(*)需統計所有列,優化器無法確信索引覆蓋全表,故選 FTS。
| 錯誤寫法 | 正確寫法 | 替代方案 |
|---|---|---|
WHERE UPPER(name) = 'SMITH' | WHERE name = 'SMITH'(資料已標準化) | 函數索引 CREATE INDEX idx_upper_name ON emp(UPPER(name)) |
WHERE TRUNC(hiredate) = TRUNC(SYSDATE) | WHERE hiredate >= TRUNC(SYSDATE) AND hiredate < TRUNC(SYSDATE+1) | 函數索引 CREATE INDEX idx_trunc_hiredate ON emp(TRUNC(hiredate)) |
WHERE sal + 100 > 2000 | WHERE sal > 1900 | 函數索引 CREATE INDEX idx_sal_plus ON emp(sal+100) |
WHERE TO_CHAR(id) = '123' | WHERE id = 123(避免隱性轉換) | 確保應用端參數綁定型別正確 |
-- 欄位 VARCHAR2,傳入數字
SELECT * FROM emp WHERE empno = 7369;
-- Oracle 隱性轉換:TO_NUMBER(empno) = 7369 → 索引失效
-- 正確:傳入字串
SELECT * FROM emp WHERE empno = '7369'; -- 或使用參數綁定
-- 日期隱性轉換陷阱
SELECT * FROM emp WHERE hiredate = '1980-12-17'; -- 依賴 NLS_DATE_FORMAT
-- 安全寫法
SELECT * FROM emp WHERE hiredate = DATE '1980-12-17'; -- ANSI 日期字面值
SELECT * FROM emp WHERE hiredate = TO_DATE('1980-12-17','YYYY-MM-DD');
-- 不等號通常不走索引(範圍太大)
SELECT * FROM emp WHERE deptno <> 20;
-- 改寫:UNION ALL 兩個範圍
SELECT * FROM emp WHERE deptno < 20
UNION ALL
SELECT * FROM emp WHERE deptno > 20;
-- LIKE '%pattern' 開頭萬用字元 → 索引失效
SELECT * FROM emp WHERE ename LIKE '%SMITH'; -- 全表掃描
-- 對策:Oracle Text / 反向索引 / 函數索引 REVERSE(ename) / 應用層前綴搜尋
當查詢回傳資料佔表總量 > 5%-10% 時,FTS 多區塊讀(
DB_FILE_MULTIBLOCK_READ_COUNT)通常比「索引範圍掃描 + 大量單區塊回表」更快。不要盲目強制索引,先驗證實際執行時間與邏輯讀(SET AUTOTRACE TRACEONLY)。
1. 確認統計資訊最新
EXEC DBMS_STATS.GATHER_TABLE_STATS(ownname=>'SCOTT', tabname=>'EMP', cascade=>TRUE);
2. 觀察實際執行計畫(非 EXPLAIN PLAN)
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(FORMAT=>'ALLSTATS LAST'));
3. 對比邏輯讀
SET AUTOTRACE TRACEONLY STATISTICS
-- 執行原 SQL 與強制索引 Hint 版本,對比 consistent gets
4. 若統計正確但計畫仍差 → 考慮
• SQL Profile / SQL Plan Baseline 固定好計畫
• 擴展統計資訊(欄位群組相關性)
• 直方圖(高傾斜欄位)
• 重寫 SQL(避開函數/隱性轉換)
5. 最後手段:Hint 強制(註解說明原因)
SELECT /*+ INDEX(emp idx_emp_deptno) */ * FROM emp WHERE deptno = 20;
「不走索引」90% 歸因於統計資訊不準、SQL 寫法阻礙索引使用(函數/轉換/首欄缺失)、優化器正確選擇 FTS。建立「統計資訊定時收集 → SQL 規範避開函數/隱性轉換 → 執行計畫定期巡檢」三層防線,比事後強制 Hint 更根本、更穩健。