在DB2数据库的日常运维中,SQL语句执行缓慢往往是最常见的问题之一。很多看似相同的查询,在不同数据分布或参数取值下性能差异巨大,根源通常在于优化器选择的访问路径与实际情况不匹配。要系统化地调优DB2 SQL,首先需要理解执行计划如何生成,以及哪些因素会导致优化器做出错误判断。

一、先读懂DB2的执行计划
执行计划是优化器根据统计信息和SQL语句结构生成的一组操作步骤,它决定了DB2如何访问表、以什么顺序连接以及使用何种连接算法。查看执行计划最常用的工具是EXPLAIN和db2exfmt。通过EXPLAIN命令可以为一条SQL生成访问计划,再用db2exfmt将计划中的操作符按树形结构展开,能够直观看到表扫描(TBSCAN)、索引扫描(IXSCAN)、嵌套循环连接(NLJOIN)和排序(SORT)等核心节点。
判断一条SQL是否需要调优,首先看执行计划里是否出现了不必要的全表扫描。对于过滤条件选择性较高的列,如果计划中显示TBSCAN,说明优化器没有选择理想索引,这时需要进一步检查该列上是否有索引,以及统计信息是否过期。另一个常见问题是连接顺序颠倒,导致中间结果集无限膨胀。通过对比不同连接方式的估算成本,可以快速定位问题环节。
-- 查看SQL执行计划 EXPLAIN PLAN FOR SELECT E.EMPNO, E.LASTNAME, D.DEPTNAME FROM EMPLOYEE E JOIN DEPARTMENT D ON E.WORKDEPT = D.DEPTNO WHERE E.SALARY > 50000;
执行完EXPLAIN后,使用db2exfmt工具可以输出更友好的计划文本。在Linux或Unix环境下,通常执行 db2exfmt -d sample -g TIC -w -1 -n % -s % -o exfmt.out 生成报告。该报告中的每个操作符都会附带累计成本和结果集基数估算,这些数字是优化器决策的依据,也是调优的起点。
二、索引设计要避免让谓词失效
很多SQL调优问题都出在索引没有被使用上。DB2优化器只有在谓词形式与索引键列完全兼容时才会考虑IXSCAN。例如在索引列上使用函数、做算术运算或发生隐式类型转换,都会导致索引失效。假设WORKDEPT列上有索引,但编写了 WHERE UPPER(WORKDEPT) = 'A00',优化器无法直接利用该索引,除非创建表达式索引或改写SQL。
复合索引则要遵循最左前缀原则。对于索引(DEPTNO, JOB, SALARY),如果查询条件是 WHERE JOB = 'MANAGER',没有带上DEPTNO,则该复合索引通常不会被选择。因此设计索引时需要结合应用实际查询模式,将等值过滤且选择性较好的列放在前面。另一个常见误判是在低选择性的列上建索引,例如性别列只有M和F两个值,顺序扫描可能反而更快。优化器会根据统计信息中的基数估算做出判断,所以保持统计信息准确同样重要。
-- 索引失效示例:函数作用于索引列 SELECT EMPNO, LASTNAME FROM EMPLOYEE WHERE UPPER(WORKDEPT) = 'A00'; -- 改进写法:保持列原始值过滤 SELECT EMPNO, LASTNAME FROM EMPLOYEE WHERE WORKDEPT = 'A00';
还有一种典型情况是隐式类型转换。如果T.SALARY列定义为DECIMAL,而过滤条件写成 SALARY = '50000',DB2可能先将SALARY列转换为字符再比较,导致索引失效。在代码中应始终使用与列定义一致的数据类型进行参数绑定,避免优化器额外引入转换步骤。
三、统计信息与重新绑定决定优化器判断
DB2优化器严重依赖系统目录中的统计信息来估算每个操作的成本。如果表数据量发生了显著变化,或者数据分布出现倾斜,而统计信息没有更新,优化器很可能继续使用过时的基数和分布信息,做出错误的连接顺序或索引选择。对核心业务表应周期性执行RUNSTATS命令,收集表、索引和列的基本统计信息。
RUNSTATS可以指定采样比例和分布收集选项。例如 RUNSTATS ON TABLE schema.employee WITH DISTRIBUTION AND DETAILED INDEXES ALL 不仅能收集表级和索引级基础统计,还能收集列值分布,有助于优化器处理数据倾斜场景。执行完成后,与这些表相关的动态SQL会在下一次执行时重新优化,而静态SQL则需要重新绑定包才能让新统计信息生效。
-- 收集表和索引统计信息 RUNSTATS ON TABLE MYSCHEMA.EMPLOYEE WITH DISTRIBUTION AND DETAILED INDEXES ALL; -- 重新绑定包,使静态SQL使用新统计信息 REBIND PACKAGE MYSCHEMA.MYPKG RESOLVE ANY;
在涉及参数标记的SQL中,优化器使用默认的通用规则估算过滤率,可能忽略参数值的实际选择性。如果某些参数值出现严重倾斜,可以通过在SQL语句中加入 OPTIMIZE FOR 1 ROW 或使用 REOPT ALWAYS 绑定选项来让DB2在每次执行时根据实际参数值重新优化。这种方式适合执行频率不高但参数选择性差异巨大的查询。
四、借助包缓存定位慢SQL
DB2的包缓存(Package Cache)中记录了最近执行过的SQL语句及其执行时间、CPU消耗、执行次数和对应的执行计划。通过查询包缓存相关表函数,可以快速找到消耗资源最高的语句,而不必在应用日志中乱找。常用的表函数包括 MON_GET_PKG_CACHE_STMT 和 MON_GET_PKG_CACHE_STMT_DETAILS。
例如下面的SQL可以按总CPU时间排序,列出前10条最耗CPU的动态语句,同时返回语句文本和已用执行计划,方便和当前表上的索引设计对照排查。
SELECT S.STMT_TEXT, S.TOTAL_CPU_TIME, S.NUM_EXECUTIONS, S.ROWS_READ, S.ROWS_RETURNED FROM TABLE(MON_GET_PKG_CACHE_STMT(NULL, NULL, NULL, -2)) AS S ORDER BY S.TOTAL_CPU_TIME DESC FETCH FIRST 10 ROWS ONLY;
拿到语句文本后,将其代入前文提到的EXPLAIN和db2exfmt流程,就能看到该语句在当前统计信息和绑定参数下的真实访问路径。如果发现计划中出现了预估基数与实际返回行数相差几十倍以上的情况,通常说明统计信息已经不适合当前数据分布,需要先执行RUNSTATS再重新观测。对于执行频率极高的语句,还应关注每次执行的平均延迟和锁等待时间,结合事件监视器分析是否存在锁竞争导致的性能抖动。
定位到问题SQL后,不要急于加索引。先评估该语句的执行频率、返回结果集大小以及业务上是否真的需要返回全部数据。有时通过增加过滤条件、限制返回行数或改写为更高效的集合操作,比单纯增加索引效果更明显。索引虽然能加速查询,但也会增加插入、更新和删除操作的成本,因此需要在读写负载之间做出平衡。