“为什么我的 SQL 走了全表扫?”——这可能是 DBA 和开发者在优化过程中被问得最多的问题之一。 本文基于 Oracle 官方文档 8.2.2 章节,结合一线案例,用工程师的语言把全表扫的触发条件、内部机制以及调优思路讲清楚。
一、什么时候优化器会选择全表扫? 一句话总结:当优化器认为其他访问路径更贵或者根本不存在时。 官方给出的 9 大典型场景,整理成思维导图方便记忆:
触发场景 | 一句话解读 | 快速检查手段 |
---|---|---|
1. 没有可用索引 | “巧妇难为无米之炊” | SELECT index_name FROM user_indexes WHERE table_name='EMP'; |
2. 对索引列使用了函数或隐式转换 | 函数杀死索引 | WHERE TO_CHAR(col)=… 或 WHERE col=1 (字符列) |
3. SELECT COUNT(*) 且索引列含 NULL | 索引不存 NULL,无法计数 | SELECT column_name FROM user_tab_columns WHERE nullable='Y' AND … |
4. 跳过索引前导列 | INDEX SKIP SCAN 不是万金油 | 查看谓词是否覆盖前导列 |
5. 谓词过滤性差(返回大部分行) | “反正都要读 80% 块,何必走索引?” | 计算 num_rows / cardinality 预估 |
6. 统计信息过期 | 优化器活在“旧时代” | DBMS_STATS.LOCK_TABLE_STATS / GATHER_TABLE_STATS |
7. 表很小(≤ DB_FILE_MULTIBLOCK_READ_COUNT 块) | 直接扫完比索引还快 | SELECT blocks FROM user_tables WHERE … |
8. 表并行度高 | 并行让全表扫更香 | SELECT degree FROM all_tables WHERE … |
9. HINT 强制 | 人为干预 | /*+ FULL(t) */ |
二、全表扫内部机制:一次读懂多块读 & 高水位线
- 顺序扫描高水位线以下所有已格式化的块
- 未格式化的块直接跳过(见图 8-2)。
- 多块读(Multiblock Read)
- 单次 I/O 读取 1 到
DB_FILE_MULTIBLOCK_READ_COUNT
个块。 - 示例:参数设为 4,一次最多读 4 块,减少 I/O 调用次数。
- 单次 I/O 读取 1 到
- 缓存策略
- 表大小、缓存池(KEEP、DEFAULT、RECYCLE)共同决定块在 buffer cache 的驻留时长。
- 小表可能一次性被“钉”在缓存,大表则可能被冲刷。
三、实战:一条 SQL 如何走上全表扫
示例语句
SELECT salary
FROM hr.employees
WHERE salary > 4000;
执行计划
----------------------------------------------------------------
| Id | Operation | Name | Rows | Cost |
----------------------------------------------------------------
| 0 | SELECT STATEMENT | | 98 | 3 |
|* 1 | TABLE ACCESS FULL| EMPLOYEES | 98 | 3 |
----------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter("SALARY">4000)
诊断要点
SALARY
列无索引 → 优化器别无选择。- 预估 98 行,成本 3,表块数 ≤
DB_FILE_MULTIBLOCK_READ_COUNT
→ 小表场景,FTS 反而更高效。
四、如何“治”全表扫?三步走
Step 1:确认是否真的需要治
- 若表 < 1000 行、返回行数 > 80%,FTS 往往是正确选择,无需折腾。
- 若 SQL 执行耗时、逻辑读爆表,继续下一步。
Step 2:找出根因
-- 统计信息是否过期
SELECT last_analyzed, num_rows, blocks
FROM user_tables
WHERE table_name='EMPLOYEES';
-- 索引是否存在、可用
SELECT index_name, column_name, num_rows, leaf_blocks
FROM user_ind_columns ic
JOIN user_indexes i ON ic.index_name=i.index_name
WHERE ic.table_name='EMPLOYEES';
Step 3:对症下药
根因 | 治疗方案 |
---|---|
无索引 | 创建 合适 的索引,避免在索引列使用函数。 |
统计信息过期 | DBMS_STATS.GATHER_TABLE_STATS 一键刷新。 |
隐式转换 | 统一数据类型,WHERE char_col='1' 。 |
并行度太高 | ALTER TABLE employees PARALLEL 1; 关闭并行。 |
HINT 强制 | 移除 /*+ FULL */ 或改用 INDEX Hint 并验证成本。 |
- 全表扫 ≠ 洪水猛兽,在小表、低选择性、并行场景下往往是最优解。
- 真正的问题是无谓的全表扫:缺索引、错类型、统计信息陈旧。
- 用正确的工具(执行计划、统计信息、索引设计)定位并修正,而不是“一刀切”地抵制全表扫。