COUNT DISTINCT 是数据库统计分析中非常常见的写法,但 DB2 优化器对去重结果行数的估算并不总是准确。例如在订单表中统计不同客户和地区的组合数量时,如果优化器把两个字段当成相互独立的事件,就会用两个列各自基数的乘积推导组合基数,结果是高估或低估。opt_distinct_stats 这一选项可以从统计信息层面改善这类去重基数估计,帮助 DB2 在执行计划选择时更贴近真实数据分布。

一、opt_distinct_stats 主要解决什么问题
DB2 优化器在生成执行计划时,会使用系统目录中保存的列级统计信息,例如 COLCARD、NUM_DISTINCT、HIGH2KEY、LOW2KEY 等。单列的去重值数量通常可以通过 RUNSTATS 比较准确地收集,优化器也能基于这些信息估算 WHERE col = value 这类简单谓词的选择率。但一旦查询涉及 COUNT(DISTINCT col1, col2) 或多个列分组统计,单列统计就暴露出明显的不足。
多列去重的典型问题是列与列之间往往存在相关性。比如 region 和 city 两个字段,城市必然属于某个地区,如果用地区基数乘以城市基数,就会高估组合后的去重值数量;相反,如果字段之间存在强约束关系,也可能因为优化器没有捕捉到约束而导致低估。opt_distinct_stats 通过引入更细粒度的统计采集,让 DB2 能够识别这些列组层面的真实数据分布,而不是简单采用独立概率模型。
该选项并不是一个孤立的统计项,而是一组影响 RUNSTATS 采集行为以及优化器读取统计方式的参数。不同 DB2 版本中的暴露方式可能略有差异,有的版本直接使用 opt_distinct_stats,有的则以 DB2_OPT_DISTINCT_STATS 注册变量形式出现。启用前建议先用 db2set -all 或查看对应版本的数据库配置确认名称。
二、开启 opt_distinct_stats 并采集列组统计
要让优化器使用更准确的去重统计,首先需要启用 opt_distinct_stats。该参数通常通过 db2set 注册变量来设置,修改后必须重启 DB2 实例才能生效。启用之后,RUNSTATS 在采集统计信息时会根据该开关决定是否额外维护可用于去重基数估计的列组分布数据。
下面的示例展示了从启用参数到重新收集统计的完整操作。示例中先设置 DB2_OPT_DISTINCT_STATS=ON,然后重启实例,再连接 sample 数据库执行 RUNSTATS。对于多列去重场景,建议显式使用 ON COLUMNS 指定参与组合的字段,并使用 WITH DISTRIBUTION 收集频率分布信息。
db2set DB2_OPT_DISTINCT_STATS=ON
db2stop force
db2start
db2 connect to sample
-- 常规方式:收集全表统计和分布信息
db2 RUNSTATS ON TABLE myschema.orders WITH DISTRIBUTION AND DETAILED INDEXES ALL
-- 针对多列去重:显式收集列组分布
db2 RUNSTATS ON TABLE myschema.orders
ON COLUMNS (customer_id, region, status)
WITH DISTRIBUTION ON COLUMNS (customer_id, region, status)
AND DETAILED INDEXES ALL
如果表数据量特别大,直接全表扫描统计的成本会比较高。此时可以在 RUNSTATS 中加入采样选项,例如 USE SAMPLING,但采样比例不能设置得过低,否则列组分布统计可能出现偏差。对于频繁变更的大表,通常需要在业务低峰期定期执行 RUNSTATS,以维持统计信息的新鲜度。
需要注意的是,仅仅开启参数但不重新收集统计,优化器通常无法立即得到改善。原有统计信息里没有列组级别的去重数据,参数打开后必须重新执行 RUNSTATS,系统才能生成可供优化器使用的新统计。
三、查看去重统计与执行计划变化
执行完 RUNSTATS 之后,可以通过系统目录视图查看列级统计是否已经更新。SYSCAT.COLUMNS 中的 NUM_DISTINCT 表示该列的去重值数量,COLCARD 表示列基数,两者结合起来可以判断单列统计是否合理。对于多列去重查询,更重要的是查看列组分布统计。
SELECT TABSCHEMA, TABNAME, COLNAME, COLCARD, NUM_DISTINCT FROM SYSCAT.COLUMNS WHERE TABSCHEMA = 'MYSCHEMA' AND TABNAME = 'ORDERS' ORDER BY COLNO; SELECT TABSCHEMA, TABNAME, COLGROUPCOLNO, NUM_DISTINCT, FREQUENCY FROM SYSSTAT.COLGROUPDIST WHERE TABSCHEMA = 'MYSCHEMA' AND TABNAME = 'ORDERS' ORDER BY COLGROUPCOLNO, SEQNO;
其中 SYSSTAT.COLGROUPDIST 存储的是列组级别的分布数据,计算组合去重值时,优化器会优先参考这类信息。如果该视图中出现了目标列组记录,说明 RUNSTATS 已经成功采集了额外统计。不同版本的列名可能有差异,建议根据实际 DB2 版本查阅系统目录说明。
除了查看系统目录,还可以使用 db2expln 工具对比优化前后的执行计划。重点观察查询中 COUNT DISTINCT 或 GROUP BY 节点返回行数的估计值是否更接近实际数据。如果估计值明显改善,但执行计划没有变化,说明该 SQL 的主要成本并不在去重这个环节,此时不必过度调整参数。
四、优化效果与适用场景分析
opt_distinct_stats 对以下场景的帮助尤为明显:多个列组成的 GROUP BY 查询、COUNT(DISTINCT col1, col2) 形式的统计、星型模型中维度表与事实表关联后的去重、以及包含表达式去重的报表类 SQL。这些查询的共同点是最终结果集通常远小于参与运算的原始行数,如果优化器高估了中间结果集,就会错误地选择嵌套循环连接或全表扫描。
以一个订单表为例,假设表中有 5000 万行数据,customer_id 的去重值为 200 万,region 为 20,status 为 4。如果优化器认为这些字段相互独立,就会将组合基数估算为 200 万乘以 20 再乘以 4,结果是 1.6 亿,甚至超过表行数,这显然不合理。真实组合可能只有 800 万左右,启用 opt_distinct_stats 后,优化器可以根据列组分布把估计值拉回到接近真实值的范围,进而选择更合适的索引和连接方式。
但也要注意,opt_distinct_stats 不是解决所有性能问题的万能开关。如果表本身很小、查询响应时间本来就短,或者 SQL 的主要成本在排序和 IO 而不是基数估计,开启该参数带来的收益可能有限。统计采集和存储也会占用额外空间与维护成本,因此更适合数据量大且多列去重查询频繁的系统。
五、常见问题与调优建议
开启 opt_distinct_stats 后,如果 SQL 执行计划没有任何变化,首先应确认参数是否真正生效,例如通过 db2set -all 查看当前值,并确认实例已重启。然后检查 RUNSTATS 是否在参数生效后重新执行过,以及是否针对正确的列组收集了 WITH DISTRIBUTION。旧统计信息不会因为参数打开而自动升级,这是实际操作中容易忽略的地方。
另一个常见问题是统计信息过期。opt_distinct_stats 依赖 RUNSTATS 采集的数据,如果表频繁插入、删除或更新,统计信息会逐渐失去代表性。建议把 RUNSTATS 纳入定期维护任务,例如每天或每周对关键表执行一次。对于 DPF 或分区表环境,还需要确保所有分区上的统计都得到更新,避免优化器因为局部统计缺失而退化为默认估计。
总的来说,opt_distinct_stats 的核心价值在于提升多列去重查询的基数估计精度,让优化器少走弯路。使用时应先找到确实存在基数估计偏差的 SQL,再开启参数并重新收集统计,最后通过执行计划和实际返回行数验证效果。这种基于数据分布和查询特征的点对点优化,比盲目开启参数更稳妥,也更容易在复杂数据库环境中取得稳定收益。

