☰
达梦数据库统计信息维护:从执行计划漂移到慢查询优化实战
2026/10/17 9:45:36 网站建设 项目流程

1. 先说一个让我熬夜到凌晨两点的慢查询

去年在客户现场处理一个生产系统问题,业务侧反馈一条报表查询从原来的几百毫秒直接飙升到三十多秒,DBA群里炸了锅。我接手以后第一反应是DBA_TABLES里看一眼统计信息,代码还没敲完,就发现主表STATS的LAST_ANALYZED停留在三个月之前,而表的行数已经翻了四倍。问题根源几乎明牌了:优化器拿着过期的人口数据去估算,走错执行计划是必然的。

那条SQL本身不复杂,双表关联加范围过滤,索引和主键都在,表结构也没有变化。问题就出在达梦数据库的优化器完全依赖统计信息来推断"从表里读多少行最划算"。统计信息一旦失真,优化器就像一个人拿着过期地图开车,哪怕路况再好,也容易绕到岔路上去。

这个案例让我决定把这几年倒腾达梦数据库统计信息的经验完整写下来。不是从官方文档抄概念,而是把优化器怎么用统计信息、统计信息怎么收、收完怎么验证、过期了怎么发现这一整条链路讲清楚。适合正在用达梦数据库做生产维护的DBA、刚接触国产数据库想搞懂执行计划逻辑的开发同学,以及那些已经被"统计信息过期"坑过但还没找到系统排查思路的人。

2. 统计信息到底是什么:优化器的"盲人拐杖"

2.1 没有统计信息,优化器就是闭着眼睛猜

达梦数据库支持基于代价的优化方式。所谓代价,简单说就是优化器评估一个执行计划要消耗多少IO、多少CPU、多少内存,然后挑一个它认为最便宜的方案。可问题来了:优化器怎么知道一张表有多少行?一个字段有多少个不同的值?一个索引列的值是怎么分布的?

这些它全不知道。它能依赖的只有统计信息。

统计信息就是描述数据库对象"长相"的数据:表有多少行、占多少页、列上有多少不同取值、最大值最小值是多少、数据分布是否倾斜、索引的叶子块数和聚类程度如何。优化器拿到这些数字以后,才能估算做一次过滤后大概剩多少行,走索引省事还是扫全表省事。

你可以把优化器想象成拿着拐杖走夜路的人,统计信息就是那根拐杖的触感。拐杖指对了方向,路就好走;拐杖本身是歪的,走歪路就是常态。这也是为什么在达梦数据库运维里,统计信息的重要性从来不亚于索引设计。

2.2 达梦统计信息的三个层次:表级、列级、索引级

达梦的统计信息不是一块铁板,而是分层次的。日常维护时得搞清楚自己动的是哪一层,不然会出现"收了表统计但列统计还是旧的"这种半吊子状态。

表级统计信息是最基础的那一层,核心内容是表的总行数(NUM_ROWS)和占用的数据页数量(BLOCKS)。优化器计算全表扫描代价的时候,主要就看BLOCKS;估算过滤后的返回行数,则要看NUM_ROWS。一张千万行的表和一张千行表,全表扫描代价完全是两个量级。

列级统计信息则细化到每一列。对一张表来说,每个列都有自己独立的数据画像:这一列的最小值、最大值、空值个数、平均长度,以及不同值的个数。优化器计算WHERE条件的选择率时,就是拿这些数据来推算。最典型的就是等值条件,如果某个列有100个不同值,那优化器默认过滤后大约能剩1/100的行,这个推算全靠列统计。

索引统计信息描述索引本身的结构:索引的层数、叶子块数量、簇因子。这决定优化器判断"走这个索引要扫多少个数据块"。簇因子如果很高,说明索引顺序和数据存储顺序差别很大,用索引回表可能还不如直接扫全表。

2.3 数据字典里的统计信息:这些视图要烂熟于心

在达梦数据库中,统计信息最终都落在系统表里,对外体现为一组数据字典视图。排查问题的时候我最常用的就是这几个:

视图名里面能看到什么典型用途
DBA_TAB_STATISTICS表级统计:行数、块数、最后分析时间确认统计信息是否过期
DBA_TAB_COL_STATISTICS列级统计:NDV、空值、最大最小、平均长度确认某个谓词列是否有统计
DBA_HISTOGRAMS直方图桶信息:端点值、频次看高频值分布是否已记录
DBA_TAB_MODIFICATIONS对象的DML变化量(需开启监控)判断统计信息失效程度

排查统计信息问题,第一步永远是先看LAST_ANALYZED。如果一张频繁变更的表连着几个星期没有更新记录,同时DBA_TAB_MODIFICATIONS里监测到的增删改行数已经超过了表总行数的一定比例,那基本可以判定统计信息处于失真状态。还有一种极端情况,表结构刚加了一个新列,但列统计信息完全缺失,优化器对那个列的过滤只能靠猜,执行计划出问题的概率极大。

3. 统计信息是怎么"指挥"执行计划的:两个关键动作拆开看

3.1 优化器的估算流水线:从选择率到基数,再到代价

达梦的优化器在决定一个访问路径之前,会做一套完整的估算动作。拿最简单的查询来举例:

SELECT * FROM T1 WHERE STATUS = 'VALID' AND CREATE_DATE > DATE '2024-01-01';

优化器看到STATUS和CREATE_DATE两个条件,会先翻列统计信息,拿到这两个字段的NDV(不同值数量)和最大最小值。假设STATUS的NDV是2,那它默认等值过滤后数据会缩到1/2;假设CREATE_DATE在统计信息里的最大值是2023年12月31日,而查询条件是大于2024年1月1日,那么优化器会根据统计的最大最小值算出过滤后几乎没有行。

每一层条件都会计算出一个"选择率",乘到表的总行数上,得到最终"基数"(Cardinality)。基数再代入访问路径的代价公式:走索引要读多少叶子块、回表多少块,走全表扫描要读多少个数据页。哪个便宜选哪个。

整套逻辑听起来很理性,但有个致命前提:统计信息必须是当前数据分布的真实写照。一旦列统计里的最大值早就过时了,高于它的数据不断插入,优化器会错判范围条件的过滤效果,把明明会返回大量行的SQL估成只返回一行,从而选错索引。

3.2 均匀分布假设:优化器最容易翻车的地方

默认情况下,如果列上没有直方图,优化器假设列的值是均匀分布的。也就是说,一个列有100个不同值,任何一个值出现的频率都被视为1%。绝大多数业务表并不满足这个假设,最常见的就是状态字段:比如订单表里99.9%的数据都是"已完成",只有0.1%是"待支付"。

当SQL对STATUS='待支付'做等值过滤时,实际返回的行数极少,走索引非常合适。但因为统计信息只记录NDV=2,优化器认为每个状态各占50%,估算出返回一半数据,觉得走索引不划算,果断选择全表扫描。这种因为均匀分布假设导致的执行计划偏差,几乎是所有数据库都会遇到的经典问题,达梦也不例外。解决方案就是直方图,后面我会专门写一节。

3.3 统计信息缺失的另一个症状:动态的"猜"

还有一种情况比统计信息过时更麻烦,那就是统计信息完全缺失。新导入的表没跑收集,或者TRUNCATE之后没有重新收集,优化器查不到该表的统计信息时,达梦会退回到动态采样的机制,在SQL执行时临时抽一点数据来估算。

动态采样是兜底方案,不是最优解。它最大的问题在于每次采样的估算结果可能有波动,同样的SQL不同时间执行,优化器可能给出不同的执行计划。更麻烦的是,采样阶段本身要消耗资源,在并发高的生产环境里等于每条SQL进来都先做一次"预查询",整体性能损耗不小。

所以我的原则是:靠人工和自动维护任务把统计信息保持新鲜,让优化器尽量少走动态采样这条兜底路径。动态采样适合做临时排查,不适合当长期依赖。

4. 一次真实排查:统计信息陈旧导致执行计划"漂移"的全过程

4.1 现象与第一轮检查

还是开篇提到的那套环境。故障期间的TOP SQL是一条分组统计查询,核心结构是:

SELECT REGION, COUNT(*), SUM(AMOUNT) FROM ORDERS WHERE ORDER_DATE >= SYSDATE - 30 GROUP BY REGION;

表上ORDER_DATE有索引,数据量大约8000万行。从AWR里看这条SQL平均执行时长30秒,执行计划显示走了索引范围扫描,但代价评估的返回行数只有1200行,而实际上ORDER_DATE过滤后要扫出400多万行。

第一轮检查我做了三件事:看表行数是否正确、看ORDER_DATE列统计的最大最小值、看索引统计的叶子块是否合理。三条SQL查下来,发现表统计里的行数还停留在2000万,ORDERS列统计里ORDER_DATE的最大值比当前时间晚了三个月,索引叶子块数量也和当前索引实际大小对不上。统计信息全面滞后。

4.2 对比实际数据量:确认失真程度

为了量化失真程度,我直接跑了一条COUNT验证实际行数:

SELECT COUNT(*) FROM ORDERS;

最后统计出来是8200万行。统计信息里记录2000万行,偏差超过四倍,但真正让优化器"漂移"的还不是总行数,而是ORDER_DATE列的最大值滞后。因为优化器估算SYSDATE-30这个范围条件时,用旧的最大值判断选择性,以为过滤后只剩零点几个百分点,就错误地认为索引范围扫描非常划算。实际数据分布完全不是那样,ORDER_DATE条件过滤后还有大量数据,索引范围扫描加上回表访问,代价远高于全表扫描。

4.3 修复动作:全量收集加验证执行计划

确认根因以后,我没有直接改SQL,而是在维护窗口里对ORDERS表做了全量统计信息收集:

CALL DBMS_STATS.GATHER_TABLE_STATS('SALES', 'ORDERS', NULL, 100);

这里第四个参数是采样比例,100表示全量收集。收集过程大概跑了半小时,完成后再次查看DBA_TAB_STATISTICS,行数已经更新到8200万,ORDER_DATE列的最大值也刷新到了当前时间。

重新执行那条SQL,执行计划从索引范围扫描变成了全表扫描,因为代价估算下来全表扫描明显更便宜。实际执行时间从30秒降到4秒。执行计划变了不代表变坏,关键是这个计划终于符合数据的真实情况。

4.4 如果统计信息没问题,下一步该查什么

这里也补充一句,统计信息只是执行计划失真的常见原因之一,不是唯一原因。如果LAST_ANALYZED是新鲜的,列统计和直方图也都正常,但执行计划还是怪,就要往下查索引使用、表连接顺序、连接方式以及是否有不必要的类型转换。很多时候数据列上隐式转换会导致索引失效,这类问题在统计信息正常的情况下反而是首要排查对象。我在排查流程里通常按这个顺序走:统计信息新鲜度、列分布准确性、索引结构、SQL写法、系统负载。

5. 达梦收集统计信息的完整实操:别只会跑一条GATHER

5.1 先激活系统包

达梦把统计信息收集能力封装在DBMS_STATS包里,但这个包不是默认就可用的。在数据库初始化环境里,需要先执行系统包创建,否则调用会直接报错。激活方式通常是用系统管理员账号执行:

SP_CREATE_SYSTEM_PACKAGES(1, 'DBMS_STATS');

具体版本可能有细微差异,有些环境是通过初始化参数AUTO_COVER_SYSTEM_PACKAGE来控制的。建议先跑这句确认无报错,用SYSDBA登录后查看包是否存在。这一步很容易被新手忽略,一上来就CALL DBMS_STATS.GATHER_TABLE_STATS,结果提示包不存在,其实是系统包没激活。

5.2 四种最常用的收集调用

达梦的DBMS_STATS包提供的收集接口和Oracle高度相似,日常最常用的是这四个方向:

收集单表统计信息:

CALL DBMS_STATS.GATHER_TABLE_STATS('SALES', 'ORDERS');

收集整个方案下所有对象的统计信息:

CALL DBMS_STATS.GATHER_SCHEMA_STATS('SALES');

收集索引统计信息:

CALL DBMS_STATS.GATHER_INDEX_STATS('SALES', 'IDX_ORDERS_ORDER_DATE');

删除表的统计信息:

CALL DBMS_STATS.DELETE_TABLE_STATS('SALES', 'ORDERS');

删除统计信息这个操作在一些特殊场景下很有用。比如你打算对某张超大的历史导出表长期做静态查询,不需要优化器频繁重新估算,或者你要测试"无统计信息时优化器的表现",可以主动删除。但生产环境不要乱删,删了以后动态采样带来的计划不稳定风险很高。

5.3 关键参数怎么设置:采样率、并行度、直方图

GATHER_TABLE_STATS的参数比大多数人想象的灵活,这里按实战经验聊聊几个关键项。

第一个是采样率。参数ESTIMATE_PERCENT控制收集时扫描多少数据来推算统计信息。100代表全量扫描,统计信息最精确,代价是耗时更长、消耗更多IO;低于100是采样模式,速度快但统计信息有一定误差。我的建议是:千万行以内的表直接全量收集,准确度优先;上亿级别的大表,可以先用30到50的采样率试探,观察收集耗时和统计信息准确性之间的平衡。对于每天DML量很大的核心表,可以稳定在80到100的区间,配合业务低谷窗口执行。

第二个是并行度。DEGREE参数可以让收集过程并行执行,缩短大表收集时间。比如测试机器是16核,设置DEGREE=>8通常能获得不错的加速。但并行收集会加大系统资源竞争,在OLTP高峰时段要克制,一般建议在维护窗口内用。

第三个是直方图。METHOD_OPT参数控制哪些列要收集直方图。最省心的写法是:

CALL DBMS_STATS.GATHER_TABLE_STATS('SALES', 'ORDERS', METHOD_OPT=>'FOR ALL COLUMNS SIZE AUTO');

SIZE AUTO表示由数据库自动判断哪些列需要直方图以及桶的数量。这个值是推荐优先使用的,它会让达梦参考列的数据分布和SQL使用情况,决定是否值得为某列记录分布细节。如果开发反馈某条SQL的WHERE条件列分布特别倾斜,也可以强制指定:

CALL DBMS_STATS.GATHER_TABLE_STATS('SALES', 'ORDERS', METHOD_OPT=>'FOR COLUMNS STATUS SIZE 254');

这里STATUS就是倾斜列,SIZE 254表示直方图桶数。桶数越大,分布描述越精细,但统计信息存储和更新开销也越大,一般254够用,不是越大越好。

5.4 自动收集任务:别全靠手工

生产环境不可能每天记得手动收集,我看到身边有不少人都是等出问题了才补收,这是最被动的运维方式。达梦提供调度系统和自动维护作业能力,可以按窗口周期自动刷新统计信息。标准做法是在业务低谷时段(比如每天凌晨1点)跑一次全库或关键方案的统计信息收集作业。

实务上我不建议做全库每天无差别收集。有的表一天变更几十行,有的表一月变更几千万行,张弛要有度。核心业务表的自动收集频率可以低一些但采样比例高一些;关键大表单独设置每周收集一次,采样率控制在可接受范围内;冷表一个季度收集一次都够。自动作业跑完以后,最好在运维后台加一个检查脚本,拉取LAST_ANALYZED不合规对象的清单,这样才能兜底。

6. 直方图:对付"二八分布"列的关键武器

6.1 为什么有时候只有直方图能救场

前面提到均匀分布假设的坑。如果一个列只有两个值,但99.9%集中在一个值上,没有直方图时优化器永远以为两个值各占一半。前面那个STATUS='待支付'的场景,没有直方图之前,执行计划很可能倾向于扫描大量数据。加了直方图之后,优化器才知道"待支付"实际占比极低,走索引才是最优解。

这个现象我在达梦环境里遇过不止一次。有一回某业务表的审核状态字段只有三个取值,SQL是查"未审核"的记录,实际只有几百行。表本身几百万行,因为没有直方图,优化器估出三分之一的行数,拒绝走索引,每次查询都把全表读一遍。收集直方图以后,执行计划换成索引快速路径,查询从几百毫秒变成个位数毫秒。

6.2 达梦直方图的基本形态与含义

达梦里直方图用桶来记录列值的分布。常见的形式和Oracle类似:频率直方图和等高直方图。频率直方图适合NDV较低且每个值出现次数不平均的场景,比如状态字段;等高直方图适合NDV很高、数据范围跨度大的数值或日期列。具体的生成规则由数据库在SIZE AUTO模式下自动选择,DBA不需要手工指定类型,但要能看懂视图结果里的端点值和累计频次。

如果你想确认某列是否真的被记录了直方图,查DBA_HISTOGRAMS,里面有表名、列名、桶号、端点值、频次等字段。如果查询结果为空,说明该列没有直方图,分布信息缺失。

6.3 添加直方图后必须回看执行计划

收集直方图不是做完就完事了,一定要回来重新验证SQL执行计划。我见过有人很兴奋地给倾斜列加了直方图,结果执行计划反而变了。这时候要先看Estimate的行数是否接近实际值,其次看访问路径是否合理。如果行数估准了但访问路径还是怪,那问题可能在索引本身,比如聚簇因子太差,索引回表开销大,优化器选择全表扫描是正确的决定。

直方图的信息也不是一劳永逸的。业务数据分布会变,比如某个状态的比例从99.9%变成80%,原来的直方图就会失真。所以直方图的刷新要和统计信息刷新同步进行,这也是为什么我始终推荐使用SIZE AUTO而不是长期固定手工参数的原因:让数据库在每次收集时重新评估分布,避免人为设置成为新的瓶颈。

7. 维护统计信息的那点经验:几个坑和几条铁律

7.1 从LAST_ANALYZED和表变化量判断过期

统计信息维护最核心的问题是"什么时候该收"。我的判断依据有两个:一个是LAST_ANALYZED距今的时间,另一个是表的DML变化量。对于OLTP核心表,如果单日增删改比例超过10%,建议当晚就重新收集;如果一张表在一个统计周期内的变化行数超过了上次统计行数的20%到30%,就算时间没到也要收。达梦支持对象修改监测,开启后可以查询DBA_TAB_MODIFICATIONS来准确判断表的数据变化量。

边界条件也要注意。TRUNCATE表以后,表数据变了但统计信息不会自动清空,旧的统计信息会让优化器以为表里还有海量数据,这种操作之后一定要手动收集一次。批量导入导出的ETL作业结束后,也必须把统计信息更新纳入作业发布流程。

7.2 大表收集的锁与IO问题

统计信息全量收集要扫描全表,副作用就是产生IO压力,还可能对表上的并发DML造成影响。达梦的统计信息收集机制在收集过程中并不能完全保证无锁,表数据持续发生变化时还可能产生不一致的中间状态。所以大表统计信息收集一定安排在维护窗口:晚上的时间窗口优先,周末大窗口用于超大表的全量刷新。如果实在没有窗口,可以退而求其次用低采样率加限流方式,至少保证统计信息不会烂到不可用。

另一个被忽视的点是历史归档表。有些归档表已经很久不变了,统计信息其实非常稳定。但自动任务如果天天全量扫描,浪费大量资源。建议把这类表设置较长的统计信息刷新间隔,或者直接排除出自动收集名单。

7.3 统计信息收集失败后的补救链路

实际运维中总有收集失败的时候。常见原因有执行权限不足、系统包未激活、表处于DDL变更的中间状态、磁盘空间不足导致临时排序失败等等。我的排查顺序是:先看日志定位是权限问题还是资源问题,再确认表结构是否处于正常状态,最后检查是否有正在执行的DDL。

如果统计信息收集失败且业务SQL已经出现性能问题,可以先用紧急手段缓解:针对该表使用低采样率快速收集,比如ESTIMATE_PERCENT=>10,先让优化器有一个大概正确的量级估算,然后等到窗口再补全量收集。这比让系统一直错误估算下去要安全得多。

7.4 和Oracle对比:迁移DBA要注意什么

用过Oracle的人上手达梦统计信息会比较顺畅,很多思路是通的,但细节上有差别。Oracle的DBMS_STATS有很多细粒度的锁定、保留策略和自动任务配置,达梦的体系没那么复杂,但基本能力和使用手感还是对得上。迁移过来的DBA最容易犯的错是拿Oracle全套参数原样套到达梦上,比如某些参数名在达梦里不支持或语义不同。

我的建议是迁移项目里先小范围验证,把统计信息收集脚本在测试环境跑通了再上生产,不要在没验证的情况下直接复用原Oracle的自动化菜单。语法接近不等于完全等价,命令之间潜在参数差异很大,测试环境的执行计划对比是最后的保险。

8. 最后再分享一个小习惯

我给自己定了一条规矩:任何一次慢SQL优化的最终汇报里,必须要附带"统计信息检查结果"这一节。哪怕最后定位到的根因是代码逻辑问题,也要写明统计信息状态是否正常。这习惯帮我养成了先看统计、再猜理由的思维方式,也避免了很多次"SQL优化了半天、其实只要刷新一下统计信息就能解决"的无效加班。

如果你的系统也频繁出现执行计划时好时坏,不妨先把统计信息的健康检查脚本固化下来。每天巡检一次LAST_ANALYZED,每周比对一次大表行数和统计值差异,把异常对象拉到清单里。这套动作花不了多少时间,但能让你在被业务拉去救火之前,先把火苗灭掉。

数据库这行,慢SQL的源头往往不是SQL写得多差,而是数据画像失真了。统计信息就是那张画像,维护好它,优化器才能正常发挥。希望这篇东西能帮你在达梦数据库上少踩几个坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询