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待支付做等值过滤时实际返回的行数极少走索引非常合适。但因为统计信息只记录NDV2优化器认为每个状态各占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. 达梦收集统计信息的完整实操别只会跑一条GATHER5.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核设置DEGREE8通常能获得不错的加速。但并行收集会加大系统资源竞争在OLTP高峰时段要克制一般建议在维护窗口内用。第三个是直方图。METHOD_OPT参数控制哪些列要收集直方图。最省心的写法是CALL DBMS_STATS.GATHER_TABLE_STATS(SALES, ORDERS, METHOD_OPTFOR ALL COLUMNS SIZE AUTO);SIZE AUTO表示由数据库自动判断哪些列需要直方图以及桶的数量。这个值是推荐优先使用的它会让达梦参考列的数据分布和SQL使用情况决定是否值得为某列记录分布细节。如果开发反馈某条SQL的WHERE条件列分布特别倾斜也可以强制指定:CALL DBMS_STATS.GATHER_TABLE_STATS(SALES, ORDERS, METHOD_OPTFOR 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_PERCENT10先让优化器有一个大概正确的量级估算然后等到窗口再补全量收集。这比让系统一直错误估算下去要安全得多。7.4 和Oracle对比迁移DBA要注意什么用过Oracle的人上手达梦统计信息会比较顺畅很多思路是通的但细节上有差别。Oracle的DBMS_STATS有很多细粒度的锁定、保留策略和自动任务配置达梦的体系没那么复杂但基本能力和使用手感还是对得上。迁移过来的DBA最容易犯的错是拿Oracle全套参数原样套到达梦上比如某些参数名在达梦里不支持或语义不同。我的建议是迁移项目里先小范围验证把统计信息收集脚本在测试环境跑通了再上生产不要在没验证的情况下直接复用原Oracle的自动化菜单。语法接近不等于完全等价命令之间潜在参数差异很大测试环境的执行计划对比是最后的保险。8. 最后再分享一个小习惯我给自己定了一条规矩任何一次慢SQL优化的最终汇报里必须要附带统计信息检查结果这一节。哪怕最后定位到的根因是代码逻辑问题也要写明统计信息状态是否正常。这习惯帮我养成了先看统计、再猜理由的思维方式也避免了很多次SQL优化了半天、其实只要刷新一下统计信息就能解决的无效加班。如果你的系统也频繁出现执行计划时好时坏不妨先把统计信息的健康检查脚本固化下来。每天巡检一次LAST_ANALYZED每周比对一次大表行数和统计值差异把异常对象拉到清单里。这套动作花不了多少时间但能让你在被业务拉去救火之前先把火苗灭掉。数据库这行慢SQL的源头往往不是SQL写得多差而是数据画像失真了。统计信息就是那张画像维护好它优化器才能正常发挥。希望这篇东西能帮你在达梦数据库上少踩几个坑。