Oracle 全表扫(Full Table Scan)何时、为何、以及如何发生


“为什么我的 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) */

二、全表扫内部机制:一次读懂多块读 & 高水位线

  1. 顺序扫描高水位线以下所有已格式化的块
    • 未格式化的块直接跳过(见图 8-2)。
  2. 多块读(Multiblock Read)
    • 单次 I/O 读取 1 到 DB_FILE_MULTIBLOCK_READ_COUNT 个块。
    • 示例:参数设为 4,一次最多读 4 块,减少 I/O 调用次数。
  3. 缓存策略
    • 表大小、缓存池(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 并验证成本。
  • 全表扫 ≠ 洪水猛兽,在小表、低选择性、并行场景下往往是最优解。
  • 真正的问题是无谓的全表扫:缺索引、错类型、统计信息陈旧。
  • 用正确的工具(执行计划、统计信息、索引设计)定位并修正,而不是“一刀切”地抵制全表扫。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部