DB2的查询优化器是数据库引擎中最核心的组件之一,它接收一条SQL语句后,会在众多可能的执行方案中挑选出代价最低的一个。这个过程的关键环节就是访问路径的选择,也就是决定如何从基表中取出数据:是老老实实做全表扫描,还是利用索引快速定位。很多性能问题的根源,恰恰是优化器选了一条看似意外实则有其逻辑依据的路径。理解这套决策机制,是做DB2性能调优绕不开的一步。

优化器的决策基础:基于代价的选择模型
DB2优化器属于典型的基于代价的优化器(CBO)。它不会简单地说索引一定比全表扫描快,而是对每一种候选方案进行量化估算,比较各自的总代价,代价通常由CPU成本和I/O成本两部分加权构成。优化器会枚举出各种可能的访问路径、连接顺序和连接方法,逐一估算,最终选择估算代价最小的组合。
这个估算过程高度依赖统计信息。DB2在系统编目表中维护着每个表的行数(CARD)、页数(NPAGES)、每个索引的不同键值数量(FULLKEYCARD)、列的高频值分布(FREQUENCYCOL)等信息。如果统计信息陈旧或者缺失,优化器的估算就会严重偏离现实,做出错误选择也就不奇怪了。这也是为什么执行RUNSTATS命令更新统计信息,往往是解决执行计划突变问题的第一招。
另一个重要概念是过滤因子,即谓词能够过滤掉多少数据。比如WHERE STATUS = 'A'这个谓词,如果STATUS列只有A和B两个值且各占一半,过滤因子就是0.5,意味着会返回一半的行数。过滤因子越小,索引扫描的优势越明显;过滤因子接近1时,索引反而可能拖慢查询,因为随机I/O的代价远高于顺序读取。
常见的访问路径类型及选择条件
DB2中最基础的访问路径是表扫描。优化器从头到尾顺序读取表的所有数据页,逐行应用谓词过滤。这种方式在以下场景反而更优:查询需要返回表中大部分数据、表本身很小(只有几页)、或者没有合适的索引可用。很多人一看到表扫描就认为是问题,其实返回大结果集时表扫描是最高效的方式,顺序预取的I/O效率远高于通过索引做大量随机读取。
第二种是索引扫描,通过索引定位满足条件的行,再回表取数据。索引扫描又细分为匹配索引扫描和筛查索引扫描。匹配扫描意味着谓词能匹配索引的前导列,比如索引建立在(LASTNAME, FIRSTNAME)上,谓词LASTNAME = 'ZHANG'就能做匹配扫描。而如果只有FIRSTNAME = 'MING'这样的谓词,索引虽然也能用,但只能扫描整个索引做筛查,过滤能力大打折扣。这就是常说的最左前缀原则,索引列的顺序直接决定了它能否被有效利用。
第三种是仅索引访问。如果查询需要的所有列都包含在索引里,DB2可以直接从索引返回结果,完全不用回表。这种方式的效率极高,尤其配合索引ANDing和ORing技术,可以让多个索引协同工作,各自过滤出ROWID之后再做交集或并集。
此外还有列表预取的考虑。当索引扫描后回表的行比较分散时,DB2可能选择先把ROWID收集起来排序,再按页的顺序回表,把随机I/O转化为顺序I/O。是否走这条路,取决于表行的聚类程度,也就是索引的CLUSTER_RATIO统计信息。
如何查看优化器实际选择的访问路径
理论说得再多,不如亲自看一次执行计划。DB2提供了EXPLAIN工具来展示优化器的决策结果。最常用的方式是通过EXPLAIN命令捕获执行计划,再查询EXPLAIN表查看详情。基本用法如下:
-- 先创建EXPLAIN表(只需一次)
CALL SYSPROC.SYSINSTALLOBJECTS('EXPLAIN','C',NULL,CURRENT SCHEMA)
-- 捕获指定SQL的执行计划
EXPLAIN ALL FOR SELECT ORDER_ID, CUSTOMER_ID
FROM ORDERS
WHERE ORDER_DATE BETWEEN '2024-01-01' AND '2024-03-31';
-- 查看访问路径的核心信息
SELECT * FROM EXPLAIN_INSTANCE;
SELECT OPERATOR_TYPE, OBJECT_NAME, INDEX_NAME
FROM EXPLAIN_OPERATOR O, EXPLAIN_STREAM S
WHERE O.EXPLAIN_REQUESTER = S.EXPLAIN_REQUESTER;
也可以使用db2expln命令行工具直接输出文本格式的访问路径,例如db2expln -d SAMPLE -t -f query.sql,输出中会明确标注使用的是关系扫描还是索引扫描、命中了哪个索引、扫描方式是定位还是全索引。新版DB2还支持db2exfmt工具,能把计划格式化成树状结构,父操作在上、子操作在下,阅读起来更直观。
阅读执行计划时要重点关注几个信号:出现了预期的索引扫描说明索引被利用;如果显示关系扫描,就要核对谓词写法是否阻止了索引匹配,比如对索引列使用了函数WHERE UPPER(NAME) = 'ABC',或者发生了隐式类型转换,都会导致索引失效。
引导优化器做出更好选择的实用手段
当优化器选错了路径,可以从几个方向入手。第一是确保统计信息准确,定期执行RUNSTATS,对大表建议带上WITH DISTRIBUTION选项,让优化器了解数据分布的偏斜情况。第二是调整优化级别,DB2提供了从0到9的优化级别,级别越高优化器枚举的方案越多,编译时间也越长。对于复杂查询可以尝试提高到5或7,对简单查询保持默认的5即可,避免编译开销得不偿失。
-- 更新表和索引的统计信息,包含数据分布
RUNSTATS ON TABLE DB2INST1.ORDERS
WITH DISTRIBUTION AND DETAILED INDEXES ALL;
-- 设置当前会话的优化级别
SET CURRENT QUERY OPTIMIZATION 7;
第三是审视索引设计本身。检查现有索引的顺序是否与查询谓词匹配,是否有必要创建覆盖索引来支持仅索引访问,或者利用INCLUDE子句把额外列加进索引避免回表。第四是利用优化概要文件锁定执行计划,在统计信息暂时无法改善、又急需稳定性能的场景下,可以通过概要文件显式指定访问路径,不过这种方式属于应急手段,不宜长期依赖。
最后要强调的是,优化器的选择永远是当时统计信息和代价模型下的理性结果。与其质疑优化器,不如把功夫花在提供准确的统计信息、合理的索引结构和干净的谓词写法上。当这三样东西都到位时,DB2优化器绝大多数时候都会给出一条代价合理的访问路径。