达梦数据库统计信息收集后SQL执行计划失效机制验证与生产避坑指南
一个深夜的批处理任务一次例行的统计信息收集第二天早上业务高峰一条原本毫秒级返回的SQL突然变成了全表扫描应用侧开始堆积告警。这是我在一个实际项目里遇到的场景排查到最后问题指向了执行计划的变化而引发变化的正是前一天晚上那一次统计信息收集。那时团队里争论最多的问题是DM达梦数据库收集统计信息之后内存里已经缓存的SQL执行计划到底还算不算数是立刻作废还是会继续沿用旧计划这个问题看起来简单但直接决定了你怎么设计统计信息收集任务的时间窗口也决定了线上出问题时你是该先恢复统计信息还是先清理计划缓存。与其在故障现场靠猜不如做一轮可控的验证。这篇文章就是我基于DM8做的一次完整测试记录也会把测试中涉及的原理、观测手段和生产环境的避坑思路一并讲清楚。适合正在维护DM环境、或者刚接手国产数据库的DBA和开发人员参考。1. 为什么要纠结“统计信息收集之后执行计划是否失效”1.1 一次计划劣化引出的疑问先说那个真实的故障。客户的业务库是DM8有一个用存储过程 计划任务实现的夜间批量程序会在凌晨调用统计信息收集包。第二天早上一条关联了三张大表、带过滤条件的SQL执行时间从200毫秒变成了25秒。从动态视图里捞出来的执行计划显示其中一张表的访问路径从索引扫描变成了全表扫描。当时所有人的第一反应是SQL写的有问题但SQL文本没有任何变化。再往下查发现全表扫描那张表的数据量比前一天翻了接近一倍而优化器用来做估算的统计信息还是老数据。于是我们手动执行了一次统计信息收集问题立刻“恢复”了——准确地说是重新解析了一次执行计划换回了合理的索引路径。这件事引出了两个衍生问题第一如果统计信息不手动收集旧执行计划会一直留在内存里并被继续使用吗第二手动收集之后内存里成千上万条SQL的执行计划是同步作废还是会慢慢失效这两个问题直接关系到一个操作会不会引发“解析风暴”也关系到我敢不敢在业务高峰前一个小时去做统计信息收集。1.2 网上的说法为什么不能直接拿来用我去翻过一些达梦社区的帖子也问过同行得到的回答大致分两派一派说“收集统计信息后计划会马上失效所以不要在高峰期收”另一派说“DM有类似Oracle的AUTO_INVALIDATE机制计划不会立即失效会在一定时间后或者下次执行时才重新解析”。两派都说得振振有词但都没有给出可复现的测试过程。这其实很容易理解达梦不同版本、不同初始化参数下计划缓存失效策略可能不一样就算同一条SQL有没有绑定变量、表上有没有直方图结果也可能不同。与其引用二手结论不如花一个下午在自己维护的版本上把整个过程跑一遍亲眼确认“失效时机”和“失效标志”。数据库运维里的很多争议本质上都是因为缺少一套可重复的验证方法。2. 测试准备造一张会对统计信息“变脸”的表2.1 测试环境和账号准备我用的环境是DM8.1部署在一台普通的Linux虚拟机上4核8G内存安装目录是默认的/dm/dmdbms。无论你是用DIsql命令行还是DM管理工具图形界面下面的SQL都能直接执行。准备一个测试账号赋予足够的权限CREATE USER TEST IDENTIFIED BY dameng123; GRANT DBA TO TEST;用DBA权限跑测试比较省事实际生产环境不推荐给普通业务账号这么大的权限但测试库无所谓。2.2 设计一张“统计信息敏感”的测试表要让统计信息改变后执行计划跟着变测试表的数据分布必须满足一个条件在数据量小的时候全表扫描是划算的在数据量大了以后索引扫描才是划算的。这样统计信息一更新优化器的结论就会反转。建表语句很简单CREATE TABLE TEST.T_STATS_TEST ( ID INT PRIMARY KEY, SEQ_NO INT, STATUS TINYINT, VAL VARCHAR(100) ); CREATE INDEX IDX_T_STATS_TEST_STATUS ON TEST.T_STATS_TEST(STATUS);我设计了STATUS这个过滤字段初始阶段所有行STATUS都是1符合条件的数据占全表的100%索引回表比全表扫描还慢优化器选择全表扫描是合理的。之后我再插入大量STATUS0的数据让STATUS1的行占比降到极低这时同样一条SQL新统计信息会引导优化器选择索引范围扫描。首先插入5000行初始数据INSERT INTO TEST.T_STATS_TEST SELECT LEVEL, LEVEL, 1, VAL_ || LEVEL FROM DUAL CONNECT BY LEVEL 5000; COMMIT;这里用了达梦兼容Oracle的CONNECT BY写法如果你习惯写循环也能达到同样效果。2.3 先收集一次基线统计信息造完数之后先收集一次统计信息让优化器有稳定的基线可参考。这一步很关键否则第一次执行SQL时达梦会做动态采样得到的计划可能带有随机性影响我们判断。BEGIN DBMS_STATS.GATHER_TABLE_STATS( OWNNAME TEST, TABNAME T_STATS_TEST, CASCADE TRUE, METHOD_OPT FOR ALL COLUMNS SIZE AUTO ); END; /CASCADE TRUE表示连同索引统计信息一起收集METHOD_OPT里指定AUTO让系统决定哪些列需要生成直方图。STATUS列在初始状态下只有1这一个值NDV不同值个数为1现阶段不会生成直方图这没关系等数据膨胀后再收集时系统会自动补上。接下来执行测试SQL让它进入计划缓存SELECT * FROM TEST.T_STATS_TEST WHERE STATUS 1;多执行几次确保这个SQL已经在共享内存里被缓存并且有足够的执行次数作为基线。2.4 确认SQL已经在内存中达梦的动态性能视图提供了SQL缓存信息。不同小版本字段名略有差异官方手册写得也有点散我习惯先查V$SQL和V$CACHEPLAN两个视图。下面这条SQL是我在DM8.1上常用的SELECT SQL_ID, HASH_VALUE, SQL_TEXT, EXECUTIONS, LOADS, FIRST_LOAD_TIME, LAST_LOAD_TIME FROM V$SQL WHERE SQL_TEXT LIKE %T_STATS_TEST% AND SQL_TEXT NOT LIKE %V$SQL%;先解释一下几个字段的含义SQL_IDSQL文本哈希后生成的标识同一个SQL文本通常对应同一个SQL_ID。EXECUTIONS该SQL累计执行次数。如果这个数字反复增长而LAST_LOAD_TIME不变化说明一直在复用缓存计划。LOADS该SQL加载进缓存的次数。如果LOADS变成2说明发生过一次重新加载。LAST_LOAD_TIME最近一次加载进缓存的时间。这是判断计划是否失效最重要的时间戳。实测中我发现在我这个版本上V$SQL的字段和Oracle比较接近但字段顺序和部分列名存在差异。如果执行上面的SQL报列不存在先执行DESC V$SQL看下实际列或者直接查询V$CACHEPLAN换一个视角观察。3. 核心过程收集统计信息盯着执行计划看它变不变3.1 测试前记录基线数据先把测试SQL执行20次然后查询V$SQL记录一个包含以下几项的基线观察项基线的值SQL_ID假设为ABCDEF123EXECUTIONS20LOADS1FIRST_LOAD_TIME2025-01-10 10:00:00LAST_LOAD_TIME2025-01-10 10:00:00执行计划访问路径全表扫描T_STATS_TEST TABLE FULL SCAN看到EXECUTIONS从1涨到20但LOADS一直是1LAST_LOAD_TIME不变说明这20次执行全部命中了同一个缓存计划没有发生硬解析。这就是统计信息不动时计划缓存的正常表现。3.2 让数据量膨胀模拟“统计信息过期”场景现在往表里插入50万行STATUS0的数据把STATUS1的行占比压到1%。这个步骤模拟的就是生产环境中常见的情况表里的数据量已经大幅变化了但统计信息还停留在旧版本。INSERT INTO TEST.T_STATS_TEST SELECT 5000 LEVEL, 5000 LEVEL, 0, VAL_ || (5000 LEVEL) FROM DUAL CONNECT BY LEVEL 500000; COMMIT;这条SQL会执行一小段时间5000行和50万行混合在一起STATUS1的行还是那5000行占比正好约1%。注意这里有个容易被忽略的细节插入完数据后我没有立刻收集统计信息而是先执行一次测试SQL。为什么要这样因为数据库并不知道数据变了统计信息还是旧的优化器会继续认为全表只有5000行于是仍然生成全表扫描计划。这一步是为了观察“统计信息陈旧状态下计划如何被复用”为后面的变化做对比锚点。3.3 立刻收集统计信息先别急着执行SQL接下来执行统计信息收集BEGIN DBMS_STATS.GATHER_TABLE_STATS( OWNNAME TEST, TABNAME T_STATS_TEST, CASCADE TRUE, METHOD_OPT FOR ALL COLUMNS SIZE AUTO ); END; /收集完成后先不要执行任何测试SQL立刻查询V$SQL观察原来那个SQL_ID的缓存项发生了什么变化。这里会出现两种典型情况分别对应两种不同的失效策略情况A缓存项直接消失SQL_ID查不到了。这说明DM在收集统计信息时主动清理了该表相关的计划缓存属于“立即失效”。情况B缓存项还在LAST_LOAD_TIME没有变EXECUTIONS保持不变。这说明DM没有立刻作废计划属于“惰性失效”或者叫延迟失效。我在测试环境中跑出来的结果是情况A收集统计信息后再查V$SQL那条测试SQL的缓存记录已经没有了。我当时还有点不放心又执行了一遍测试SQL然后再查V$SQL看到的是全新的FIRST_LOAD_TIME和LAST_LOAD_TIME都变成了刚才执行的时间LOADS从1变成2。这意味着刚才那次执行触发了硬解析新计划已经重新进入缓存。3.4 对比新旧执行计划确认统计信息“真的”有影响先看一下旧计划。在数据膨胀之前我执行过EXPLAIN SELECT * FROM TEST.T_STATS_TEST WHERE STATUS 1;旧的执行计划输出里访问路径明确写着TABLE FULL SCAN也就是对T_STATS_TEST做全表扫描操作符的成本估算很低。数据膨胀并重新收集统计信息之后再次执行同样的EXPLAINEXPLAIN SELECT * FROM TEST.T_STATS_TEST WHERE STATUS 1;新计划变成了先走索引IDX_T_STATS_TEST_STATUS做范围扫描然后回表取数。同样的SQL文本优化器的结论完全反过来了。原因也不复杂STATUS1的行只占全部数据的1%索引扫描加回表只访问5000行比全表扫50.5万行便宜太多。优化器拿到新统计信息之后正确地计算出了这一点。这个对比也说明了一个容易被误解的概念统计信息不会直接指定“走哪个索引”它只是告诉优化器每个表有多少行、每列有多少不同值、数据分布长什么样优化器用这些输入来计算成本的。统计数据变了成本模型算出来的最优路径就变了执行计划自然跟着变。3.5 补做一组对照统计信息不更新时计划会不会失效为了让结论更严谨我加了一组对照实验。新建一张结构完全一样的表T_STATS_TEST_NOCHG插入5000行全STATUS1的数据收集一次统计信息然后反复执行测试SQL 20次确认计划已经缓存。这之后不再做任何DML也不再收集统计信息继续执行测试SQL 20次再看V$SQL观察项第1次查第20次查第40次查EXECUTIONS12040LOADS111LAST_LOAD_TIME固定不变固定不变固定不变对照组的结论很清楚只要统计信息不发生变化、表结构不发生变化DM会一直复用内存里的执行计划不会因为执行次数多了就自己“变得无效”。执行计划失效不是时间的函数而是“对象版本变化”的伴生结果。3.6 再补一个日常高频场景重复收集但数据没变计划会失效吗这个场景在线上很容易遇到有人写了个定时任务每天对全库所有表收集统计信息哪怕表里的数据一整天都没变过。如果每次收集都会让计划全部失效那这样的定时任务就是在每天早上制造一波无谓的硬解析压力。我在同一个测试表上做了验证数据不再变化连续执行两次DBMS_STATS.GATHER_TABLE_STATS每次收集后都去查V$SQL。第一次收集已经把SQL的计划清掉了重新执行后计划重新进入缓存紧接着再收集一次缓存计划又消失了。也就是说即使统计信息内容本身没有变化只要执行了收集动作也可能引起计划失效。因为数据库判断的依据不是“统计信息内容有没有变”而是“统计信息对象有没有被更新”。在我的DM8.1版本上收集动作本身就会触发失效。这个现象对不同版本可能不一样但至少提醒我不要设计一个无差别全库收集的定时任务否则每天人为制造一次“计划重置”。4. 测试结果背后的机制DM里的计划失效链路是怎么走的4.1 统计信息和执行计划之间的“中间层”数据库优化器拿到一条SQL后会做两件事第一估算每个候选执行计划的成本第二选择成本最小的计划。而估算成本所需要的行数、列基数、数据分布等输入全部来自统计信息。我经常用一个类比来解释统计信息是地图执行计划是导航路线。地图更新了导航路线可能不变也可能变取决于道路的实际拥堵情况。如果有人天天更新地图即使路况没变导航系统也可能因为“地图版本号”变了而重新计算一次路线。DM的计划失效机制就是这种“版本号一变旧导航结果作废”的思路。4.2 失效的两种时机主动失效和惰性失效从机制上看计划失效可以分成两种时机主动失效统计信息收集完成后数据库立刻通知共享内存把凡是引用了该表/索引的SQL计划标记为无效或者直接清理出缓存。后续任何一次执行都会触发硬解析。惰性失效数据库只更新统计信息已经缓存的计划继续保留。直到某个触发条件出现比如SQL重新被解析、缓存空间不足被LRU淘汰、或者达到延迟失效的时间窗口旧计划才被替换。Oracle默认的AUTO_INVALIDATE就是典型的惰性失效思路避免收集统计信息瞬间引发大范围硬解析。我在DM8.1这个版本上的测试结果是主动失效收集动作完成原计划就消失了。但这不意味着所有达梦版本都一样。达梦不同版本对计划缓存的处理策略有差异有的版本可能引入了延迟失效甚至同一个大版本下初始化参数不同也可能影响行为。这恰恰是我想强调的管理数据库的人要有一套可复现的测试手段用数据说话而不是拿某一次经验套所有环境。4.3 为什么“解析风暴”比“单条SQL重解析”更可怕单条SQL硬解析也就几毫秒到几十毫秒看起来不吓人。但如果夜间任务收集了几百张表的统计信息内存缓存里几千条SQL的计划同时失效第二天早上一来业务请求把这几千条SQL几乎同时触发硬解析情况就完全不同了。大量进程同时做语法解析、权限检查、统计信息读取、计划搜索CPU会瞬间飙升共享内存上的锁竞争也会加剧。我实际见过一次统计信息收集任务结束后的第20分钟数据库CPU从20%冲到90%持续了几分钟才回落。那几秒钟内数据库会话大量堆积应用连接池被打满。这个故障的本质不是统计信息收集本身多消耗了多少资源而是它引发了太多SQL在同一时刻重新解析。所以设计统计信息收集任务时除了看收集过程消耗多长时间还要评估收集之后带来的“计划重构冲击波”。这需要动态视图配合监控工具来确认失效范围。4.4 怎么快速确认线上版本的失效策略遇到线上环境我不建议直接翻手册因为手册描述可能滞后而不同小版本的行为差异很难全量记录。更可靠的做法是复刻本文的测试流程在测试库做一次“收集前后V$SQL对比”。具体要点三个第一在收集前确认SQL已经缓存并且执行次数在增加第二收集后先不要执行SQL立刻查一次缓存状态第三再执行一次SQL看第二次执行是否发生了重新加载。这三步做完当前版本的失效策略就清楚了。如果发现版本是主动失效那你只需要记住一个结论统计信息收集和业务高峰必须错开而且要留足缓冲时间让计划重解析在最不忙的时段完成。5. 生产环境避坑让“收集统计信息”别再成为性能事故导火索5.1 不要让“每天全库收集”成为习惯很多团队的习惯是从Oracle时代带过来的建一个定时任务每天凌晨对所有Schema的所有表执行全量统计信息收集。这个操作本身不会报错但看完前面的测试你就知道它等同于每天重置一次全部计划缓存。如果业务高峰紧接着这个任务开始第一批SQL全部要硬解析压力会非常集中。更稳妥的做法是“按需收集”只收集数据变化量超过一定比例的表或者只收集优化器真正需要的统计信息。达梦的DBMS_STATS包支持按表收集也支持指定列和指定采样比例。判断哪些表需要收集可以结合表的DML频率和上次统计信息更新时间来筛选而不是无脑全量。5.2 在测试环境先做“收集–计划”回归每次版本升级、迁移到新环境或者调整了初始化参数后都应该把“收集统计信息–观察计划缓存”这套测试在测试库跑一遍。不要以为同一个版本生产库和测试库行为一定一致有些参数会影响查询重写和计划生成最终影响失效行为。我自己的习惯是建立一个简单的验收清单建一张测试表插入少量数据收集统计信息执行SQL确认计划缓存插入大量数据重新收集统计信息观察V$SQL中对应SQL的LAST_LOAD_TIME和LOADS变化记录当前版本是“立即失效”还是“延时失效”。整个过程不到半小时但能省掉后续线上排查的好几天时间。5.3 避免“收集完立刻业务高峰”的排程错误如果你确认线上版本是主动失效那么统计信息收集任务的时间窗口必须满足一个条件收集结束后至少预留一段足够让所有常用SQL完成重新解析的时间再进入业务高峰。实际操作中我更倾向于把统计信息收集放在业务低谷的末尾段而不是低谷刚开始的时候。比如业务低谷是凌晨0点到6点那么统计信息任务可以安排在4点到5点之间执行。这样即使硬解析产生的CPU尖峰也只会占用低峰期的资源不会和早班业务重叠。5.4 用HINT固定关键SQL的执行计划有些核心SQL对执行计划极其敏感统计信息一变优化器可能选出更差的计划。对这种SQL与其赌优化器每次都能算对不如在SQL文本里加上HINT直接指定访问路径和连接顺序。我在测试SQL上做了验证SELECT /* INDEX(T_STATS_TEST IDX_T_STATS_TEST_STATUS) */ * FROM TEST.T_STATS_TEST WHERE STATUS 1;在数据量只有5000行、全表扫描更优的情况下强制索引会牺牲一点性能但换来的是执行计划的稳定性。生产上的核心SQL我宁可让它损失一点点性能也不要它随着统计信息波动而大幅摆动。当然HINT不能滥用只给核心SQL和已知会受统计信息影响较大的SQL使用。5.5 提前准备回退预案即便做了上面的所有预防统计信息收集后计划劣化仍然可能发生。线上应对时不要慌着去“回滚统计信息”因为旧统计信息未必还能还原而且即使还原了也不保证计划马上恢复到之前的状态。我更推荐按照这个顺序处理先EXPLAIN看当前计划确认劣化点在哪张表、哪个访问路径如果问题在访问路径优先在SQL上增加HINT强制走正确路径如果问题在表关联顺序调整HINT指定LEADING如果HINT能解决就通过应用发布流程更新SQL如果问题非常紧急再考虑还原统计信息或手动重置计划缓存但这只是临时手段。这一步可以说是把前面的测试经验直接落地了既然我们知道收集统计信息会造成计划失效和重新生成那么“防止劣化”的核心就不是阻止失效而是确保重新生成的计划是正确的。5.6 建立统计信息收集的监控面板最后一个是监控层面的建议。达梦的动态视图和系统日志足够支撑你跟踪统计信息收集的历史记录。我通常会在定期巡检里加两个查询维度一是查看最近24小时哪些表被收集了统计信息二是查看V$SQL中LOADS大于阈值的SQL快速识别哪些SQL在过去一段时间发生了频繁重载。如果某条SQL的LOADS在统计信息收集后异常增加它就是你下一轮优化的重要目标。这种基于数据的观察习惯比等到用户报障再去排查要高效得多。写在测试记录之后的一点体会做这个测试前我也习惯了把“统计信息收集”当成一个日常低风险操作跑完看日志、确认不报错就算结束。实际测完才发现这个操作对内存执行计划的影响范围和影响时机值得每个DBA亲自验证一遍。环境不同版本不同结论就可能不同唯一能依赖的是可复现的测试方法和细致的观测记录。最后分享一个实战小技巧如果你在动态视图里看到了SQL记录但始终分不清它是旧的还是新的别只盯着SQL_TEXT记住LAST_LOAD_TIME和LOADS这两个字段的组合比什么都好用。一条SQL只要LOADS没有涨、LAST_LOAD_TIME没变说明执行计划一直在复用一旦这两个字段发生变化那就是硬解析真实发生过的证据。