ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

达梦DM8执行计划全解析:从原理到SQL优化实战,彻底告别慢查询

达梦DM8执行计划全解析:从原理到SQL优化实战,彻底告别慢查询 做达梦数据库调优的如果不懂执行计划基本等于闭着眼开车。SQL慢不慢、为什么慢、改哪里能变快答案全都藏在执行计划里。今天我就以达梦DM8为背景结合我实际踩过的一些坑把这个执行计划彻底掰开揉碎讲清楚从怎么获取、怎么读到怎么用来优化一条真实慢SQL一次聊透。这次分享适合三类人刚接手达梦、被领导点名优化慢查询的运维同学从MySQL或Oracle转过来、还不熟悉达梦执行计划布局的开发以及已经在用达梦、但总觉得执行计划看得懂却用不上的朋友。我会尽量用说人话的方式把那些看起来复杂的代号和数字讲明白。1. 达梦DM8执行计划是什么先弄懂它再谈优化1.1 执行计划到底长什么样执行计划就是数据库优化器对一条SQL语句的执行方式做出的具体安排。它告诉你这张表是全部读一遍全表扫描还是通过索引定位两张表先连接谁再连接谁数据是按什么顺序排列的每一步估算处理多少行这些决策组合在一起就形成了我们看到的执行计划。我第一次接触达梦时执行计划是在disql里用EXPLAIN打出来的那输出格式和Oracle很像树状结构一层一层缩进。举个例子EXPLAIN SELECT * FROM T_USER WHERE AGE 60;输出大概长这个德行1 #NSET2: [1, 501, 300] 2 #PRJT2: [1, 501, 300] 3 #CSCN2: [1, 501, 300] SYSUSER这个CSCN2就是全表扫描Cluster Scan 的缩写在达梦里经常用来表示按聚簇扫描整张表[1, 501, 300]三个数字分别是估算代价、估算行数、估算字节数。这块内容后面我会专门拆解。1.2 为什么执行计划决定SQL命运同样一条SQL写法一字不改执行计划不同性能可能差几十倍甚至几百倍。比如一张千万级用户表WHERE status 1这句话如果status上有索引并且区分度好可能只扫几千行如果没索引那就是千万行全量扫描数据量大点磁盘IO直接飙到撑不住。有同学问数据库不是会自动选择最优计划吗理论上优化器会基于统计信息计算代价选出它认为最优的计划。但现实里统计信息过期、数据分布不均、参数设置不合理都会导致优化器做“错误决策”。这就是我们必须学会读执行计划的原因——优化器选的只是“它认为”的好计划不代表“实际上”最好的方案。看懂计划才能判断它是真聪明还是自作聪明。1.3 执行计划与统计信息的关系达梦的优化器是典型的CBOCost-Based Optimization也就是基于代价。代价的估算依赖两个核心数据表的行数和列的分布情况。这些数据存储在统计信息里通过DBMS_STATS包或STAT命令收集。我有一个亲身教训某生产库一张表数据从100万涨到2000万统计信息还停留在三个月前。优化器以为这张表只有100万行于是一张三表连接选择了嵌套循环Nested Loop结果实际扫描的行数翻了20倍一条本来应该秒出的SQL跑了40秒。后来刷新统计信息执行计划自动改成了哈希连接耗时降到300毫秒。这说明统计信息就是执行计划的“眼睛”眼睛近视了走路自然会撞墙。2. 怎么拿到执行计划三个方法一次讲透2.1 方法一EXPLAIN 命令直接看最简单的方式就是EXPLAIN加SQL。在disql里执行EXPLAIN SELECT U.USER_NAME, O.ORDER_ID FROM T_USER U, T_ORDER O WHERE U.USER_ID O.USER_ID AND U.CITY_ID 100;执行后返回的结果就是当前SQL在这个会话环境下的估算执行计划。注意这里的执行计划是“估算”的也就是它没有真正跑这条SQL它只是根据统计信息模拟走了一遍给出预期代价和行数。EXPLAIN的优点是快、无副作用不产生真实查询结果。缺点是它不反映SQL运行时的真实情况比如内存排序溢出、实际行数偏差等问题它都看不到。适合做初步判断比如看看有没有全表扫描、连接顺序是否正确。如果你用的是图形化工具比如达梦自带的数据库管理工具DM Manager选中SQL点“解释计划”按钮也会得到类似结果。图形界面里看树状缩进更直观新手建议先用工具看等熟悉了再回到命令模式。2.2 方法二EXPLAIN PLAN 配合系统表达梦也支持类似Oracle的EXPLAIN PLAN语法把执行计划写入一张计划表然后再查。操作分两步EXPLAIN PLAN SET STATEMENT_IDPLAN_001 INTO PLAN_TABLE FOR SELECT * FROM T_USER WHERE AGE 60; SELECT * FROM PLAN_TABLE WHERE STATEMENT_IDPLAN_001 ORDER BY ID;这里有个小坑达梦默认可能没有PLAN_TABLE你需要先创建。达梦提供了一个系统脚本通常在数据库安装目录的script子目录下叫plansql.sql或类似名字用disql执行就能创建好计划表。如果懒得找脚本也可以自己手建一张字段名参考达梦系统视图V$SQL_PLAN来设计。但说实话日常用第一种方法已经足够EXPLAIN PLAN适合在需要把计划保存下来做对比、或者嵌入到自动化分析脚本时使用。2.3 方法三开启会话级日志抓取真实计划有时候估算计划和实际运行计划差很多我们得抓真实执行计划。达梦里可以通过开启会话的SVR_LOG或ENABLE_MONITOR来记录SQL的执行细节。就我的经验最常用也最有效的做法是在当前会话执行ALTER SESSION SET EVENTSIMPROVE_OPT_PLAN;这个写法在达梦里不一定通用不同版本支持的事件不一样。我更推荐用系统动态性能视图查真实执行计划。先定位到SQL的SQL_IDSELECT SQL_ID, SQL_TEXT, COST, ROWS FROM V$SQL WHERE SQL_TEXT LIKE %T_USER%;然后通过V$SQL_PLAN查看该SQL的真实执行计划SELECT * FROM V$SQL_PLAN WHERE SQL_ID 上面的SQL_ID;真实计划反映的是这条SQL上次实际执行的路径能看到真实的行数、时间等统计信息。注意如果SQL还没执行过V$SQL里是查不到的。可以先跑一遍SQL再去查。2.4 用工具看执行计划DBMS_XPLAN 与图形化工具达梦在某些版本中提供了类似Oracle的DBMS_XPLAN包可以格式化输出计划。日常最常用的还是第三方的数据库客户端比如Navicat连到达梦后可以用“解释执行计划”功能。不过要注意Navicat连接达梦本质上是走ODBC/JDBC驱动解释计划的功能依赖驱动和数据库支持我在实际使用中发现不同版本的Navicat对达梦的解释计划显示支持程度不一样有的可以显示有的会报错。所以我建议不要过度依赖图形客户端disql命令行的结果最可靠。还有一个很实用的办法用达梦自带的DM Monitor或者DM Enterprise Manager去看会话的SQL执行详情。在生产环境中如果发现某条SQL在跑可以直接查动态性能视图拿到它的执行计划这是最贴近现实的方式。3. 执行计划核心要素解读操作符、行数与代价3.1 读懂操作符扫描与连接执行计划里最刺眼的永远是各种大写缩写。我把达梦里常见的操作符梳理成一张表方便对照查询操作符写法或缩写含义风险点全表扫描CSCN2 / CSCR2把整张表的数据块全部读一遍大表上出现要警惕索引扫描ISCN2 / ISSE2 / SSCN通过索引定位数据一般是好事但注意回表次数聚簇扫描CSCN带cluster按聚簇索引顺序扫描有时等价于全表扫描嵌套循环连接NLJ2 / NESTED LOOP外表驱动内表逐行匹配内外表顺序与内表索引很关键哈希连接HJ2 / HASH JOIN两表分别做哈希后匹配通常用于大表等值连接排序合并连接SMJ / SORT MERGE JOIN两边排序后归并非等值连接时可选过滤FILTER / PRJT2谓词过滤或投影出现条件过滤顺序问题排序SORT / OSGA排序操作数据量大可能内存溢出分组AAGR / SAGR聚合操作重点看是否走哈希聚合视图VRGN / VNED视图嵌套或内联视图复杂的视图可能导致性能差具体到代码里比如CSCN2是达梦8里常见的全表扫描操作符如果你看到大表上挂着它基本可以判断这条SQL有问题。NLJ2是嵌套循环连接如果驱动表外层循环比较大内层表又没有索引那代价会是惊天数字。3.2 行数估算与代价真假成本怎么看执行计划里每一行都有类似[1, 501, 300]这样的数字。拿我执行过的真实SQL举例1 #NSET2: [1, 501, 300] 2 #PRJT2: [1, 501, 300] 3 #CSCN2: [1, 501, 300] SYSUSER这里的三个数字第一个是代价Cost第二个是估算返回行数Rows第三个是估算返回字节数Bytes。代价越小理论越好但代价只是一个相对值不同执行计划之间比较才有意义。比如一个计划代价10另一个计划代价100通常选10。关于行数我特别提醒一点估算行数很可能不准。如果你看计划里写的是501行实际跑了却发现扫了50万行那最大的嫌疑是统计信息过期或者是SQL里有隐式类型转换导致优化器对谓词的区分度判断完全跑偏。读计划时真正重要的一列是“实际行数”与“估算行数”的对比。使用真实执行计划视图如V$SQL_PLAN时一般能看到OUTPUT_ROWS这样的真实行数。这两个数字如果差一个数量级以上恭喜你找到了优化方向的大线索。3.3 谓词信息与过滤条件执行计划中还会显示谓词比如ACCESS PREDICATE和FILTER PREDICATE。这两个的区别非常关键ACCESS PREDICATE表示通过索引直接定位到对应数据块的匹配条件这是好事相当于按图索骥。FILTER PREDICATE表示把数据块取出来后再做一次条件过滤的动作相当于把整筐菜搬回家再挑出能吃的。在达梦的执行计划输出中你可能会看到类似WHERE AGE 60被标识为FILTER CONDITION如果这个条件是在全表扫描后过滤那就意味着存储层没办法直接跳过无效数据只能硬扫。举一个生活化例子如果你要找一本图书馆里“红色封面、书名含SQL”的书ACCESS是直接按分类索引走到“数据库”书架再快速扫一眼红皮书FILTER则是把图书馆每本书都拿下来看封面颜色。一个走索引范围一个全量遍历代价天差地别。3.4 排序、分组、去重等特殊操作符排序和分组也是慢SQL重灾区。执行计划里如果出现SORT ORDER BY意味着优化器选择把结果整体排序后再输出如果数据量大排序会用到临时表空间磁盘交换就可能发生。我曾经遇到一条SQLORDER BY一个没索引的字段数据只有几万行结果执行计划里出现排序操作跑了2秒多。加上索引后SORT消失代价直接降一个数量级。分组操作符在达梦里可能是AGR或HASH GROUP BY。重点看它是不是基于哈希做的分组如果是SORT GROUP BY那通常意味着要先把数据排序再分组代价会高很多。去重操作符DISTINCT也是同理涉及排序的话要警惕。一句话总结执行计划里出现的每个操作符都对应真实的CPU、内存、磁盘消耗。你不需要背下所有缩写但你至少要能分辨“扫描类”“连接类”“排序类”三大类然后根据每类特点去追问题。4. 真实案例复盘通过执行计划消除全表扫描4.1 案例背景与SQL某业务系统报表模块反馈一条统计SQL跑得很慢在达梦8上原来只要600毫秒最近突然变成12秒。表结构简化一下是这样CREATE TABLE T_ORDER ( ORDER_ID BIGINT PRIMARY KEY, USER_ID BIGINT, STATUS TINYINT, ORDER_TIME DATETIME, AMOUNT DECIMAL(10,2) ); CREATE TABLE T_USER ( USER_ID BIGINT PRIMARY KEY, USER_NAME VARCHAR(50), CITY_ID INT );慢SQL是一张订单表和用户表的关联统计SELECT U.CITY_ID, COUNT(*), SUM(O.AMOUNT) FROM T_ORDER O, T_USER U WHERE O.USER_ID U.USER_ID AND O.STATUS 1 AND O.ORDER_TIME 2024-01-01 GROUP BY U.CITY_ID;表数据量T_ORDER约800万行T_USER约20万行。这个量级在达梦里不算大跑12秒明显异常。4.2 拿到初始执行计划在disql里执行EXPLAIN SELECT U.CITY_ID, COUNT(*), SUM(O.AMOUNT) FROM T_ORDER O, T_USER U WHERE O.USER_ID U.USER_ID AND O.STATUS 1 AND O.ORDER_TIME 2024-01-01 GROUP BY U.CITY_ID;结果大致如下简化关键行1 #NSET2: [73654, 3, 24] 2 #AGR2: [73654, 3, 24] 3 #PRJT2: [73654, 44288, 264] 4 #NLJ2: [73654, 44288, 264] 5 #CSCN2: [21352, 44288, 352] T_ORDER O 6 #CSCN2: [10, 1, 32] T_USER U这个计划表面看似乎合理但注意NLJ2也就是嵌套循环连接外层扫描T_ORDER预估返回44288行内层T_USER全表扫描。嵌套循环每处理一行外表数据就要去扫一次内表如果内表没有索引等于44288次全表扫描。这是最典型的“看起来行数不多、实际代价爆炸”的情况。4.3 定位问题FILTER与全表扫描进一步看明细发现T_ORDER实际上走的是全表扫描CSCN2但过滤条件STATUS1 AND ORDER_TIME2024-01-01不是ACCESS而是FILTER。也就是说达梦把800万行订单数据全部读了出来然后一行行判断状态和时间。这就是第一层性能瓶颈全表扫描加上大面积过滤。第二层瓶颈是连接方式。内层T_USER也是全表扫描嵌套循环下要对20万用户表反复全表扫描44288次。优化器之所以选这个方案大概率是统计信息里T_ORDER的STATUS和ORDER_TIME列没有直方图导致优化器严重低估了过滤后的行数以为只要扫4万多行就够然后搭配嵌套循环也够快。实际上STATUS1在业务里占了80%的数据完全不是一个小过滤集。4.4 创建索引后效果对比既然找到问题动手就很简单。T_ORDER上已有的主键索引跟这个SQL没关系需要创建复合索引CREATE INDEX IDX_ORDER_STATUS_TIME ON T_ORDER(STATUS, ORDER_TIME);这个索引的选择是有讲究的我解释一下因为WHERE里同时有STATUS 1和ORDER_TIME ...都是过滤条件所以把两者放入联合索引。STATUS放在前面因为等值条件比范围条件更适合做索引前导列。如果把ORDER_TIME放前面等值列STATUS反而没法高效定位。查询中不涉及ORDER_ID以外的列不需要覆盖索引但如果查询经常要回表取AMOUNT回表次数多也是一个成本。我这里把AMOUNT纳入索引形成覆盖索引可以避免回表。实际生产里如果查询字段固定建议直接建一个(STATUS, ORDER_TIME, USER_ID, AMOUNT)的覆盖索引。建完索引后再看执行计划1 #NSET2: [1236, 3, 24] 2 #AGR2: [1236, 3, 24] 3 #PRJT2: [1236, 44288, 264] 4 #HJ2: [1236, 44288, 264] 5 #BLKUP2: [896, 44288, 256] T_ORDER O 6 #ISCN2: [896, 44288, 180] IDX_ORDER_STATUS_TIME 7 #CSCN2: [105, 200000, 32] T_USER U优化器把连接方式从嵌套循环改成了哈希连接HJ2排序消失全表扫描CSCN2依然在T_USER上但内层现在只扫描一次。整体代价从73654降到了1236这条SQL执行时间降到700毫秒左右。这里面的底层逻辑是当过滤后T_ORDER的行数从估算的4万多涨到实际可能几百万的时候嵌套循环的劣势就完全暴露了而哈希连接只需要把两个输入各读一遍然后构建哈希表匹配对于过滤集比较大的场景明显更稳。优化器的选择变化也验证了统计信息和索引的威力。4.5 更复杂的案例连接顺序调整还有一次某开发反馈一条三表连接的SQL很慢。我看执行计划发现三个表连接顺序是A-B-C其中A和B的大表先做嵌套循环导致中间结果集膨胀。理论上如果先过滤C表再连接或者A先跟C连接结果会好很多。这种场景下我推荐两种手段改写SQL把过滤条件更明确地表达到子查询或WITH子句里引导优化器。使用HINT强制指定连接顺序。后面第5节我会专门讲HINT的用法。5. 执行计划优化实战常见改法与调优思路5.1 优先检查过滤因子与索引拿到一条慢SQL第一步不是急着加索引而是先看执行计划里每个表的驱动方式尤其关注大表的扫描类型。如果出现CSCN2全表扫描而表又比较大先看WHERE条件里有没有可以走索引的字段。检查顺序建议WHERE条件中有哪些列是等值匹配、IN有哪些列是范围匹配、、BETWEEN、LIKE 前缀%这些列上有没有现成索引索引前导列是否和条件匹配查询需要返回哪些列能不能用覆盖索引免回表比如WHERE STATUS1 AND ORDER_TIME...就应该创建(STATUS, ORDER_TIME)索引。前导列是等值列效率最高。如果发现条件是WHERE ORDER_TIME... AND STATUS1创建(STATUS, ORDER_TIME)依然没问题优化器大概率会调整谓词顺序来匹配索引。5.2 连接方式的选择NL、HJ、SMJ达梦支持三种主流表连接方式各有各的适用场景。我在实际调优时是这样判断的嵌套循环连接NL。适合驱动表小、被驱动表连接列有唯一索引或高选择性索引的场景。比如十行的小表连接一张千万大表大表上有索引走NL可能比走HJ更快因为不需要构建哈希表和全表扫描大表。哈希连接HJ。适合两表都比较大、连接方式是等值连接而且过滤后数据量较大。哈希连接的代价主要取决于能否把内表哈希表放进内存。达梦有HAGR_HASH_SIZE等参数可以调整哈希内存。如果计划里出现HJ2并且同时有大量SORT要注意是否哈希内存不足导致排序。排序合并连接SMJ。适合非等值连接比如、因为哈希连接只适用于等值连接。SMJ需要两边都按连接列排序如果连接列本身有序也还可以。平时用得少但一旦出现别傻眼。有一个实用经验当你看到NL里内表是全表扫描时要么是统计信息误导要么是缺少索引。加索引一般能解决问题。如果你看到HJ出现一般不用担心大问题重点要检查哈希内存是否充足。5.3 如何用HINT引导执行计划有时候你很清楚应该怎么走但优化器就是不听话。这时可以借助达梦的HINT直接在SQL里加注释去引导。示例强制走哈希连接SELECT /* HASH_JOIN(U O) */ U.CITY_ID, COUNT(*) FROM T_USER U, T_ORDER O WHERE U.USER_ID O.USER_ID GROUP BY U.CITY_ID;示例强制连接顺序SELECT /* ORDERED */ U.CITY_ID, COUNT(*) FROM T_ORDER O, T_USER U WHERE U.USER_ID O.USER_ID GROUP BY U.CITY_ID;ORDERED表示按SQL里表出现的顺序从左到右进行连接。如果想让某个表先驱动就用LEADING提示。HINT虽好用我建议不要滥用。我踩过一个坑上线前加了一个HINT当时统计信息是准的强制走了NL半年后数据量翻了10倍NL彻底废了但因为有HINT锁死SQL又慢到报表超时。后来去掉HINT优化器自己选择了HJ问题迎刃而解。所以HINT适合短期干预、测试验证长期方案一定要回归统计信息和索引治理。5.4 统计信息更新与直方图达梦收集统计信息的标准方法是DBMS_STATS.GATHER_TABLE_STATS(SCHEMA, T_ORDER);或者库里有个SP_PREPARE_DB_STATS之类的包也可以用图形工具里的“更新统计信息”。重点说直方图当列数据分布不均匀比如STATUS列90%都是1仅靠基数估计会得出“过滤后行数很少”的结论这会让优化器低估扫描量。这时需要为列收集直方图。达梦自动直方图的策略一般由参数控制如果发现列分布严重倾斜可以手动指定桶数重新收集DBMS_STATS.GATHER_TABLE_STATS(SCHEMA, T_ORDER, METHOD_OPT FOR COLUMNS STATUS SIZE 100);收集完再重新EXPLAIN看行数估算是否接近实际。这一招在电信、金融等数据分布极不均匀的场景特别有效能解决一批莫名其妙的执行计划偏差问题。6. 常见问题与排查技巧实录6.1 获取执行计划时报错无PLAN_TABLE我在环境迁移时遇到过执行EXPLAIN PLAN提示表或视图不存在。原因就是没有创建计划表。解决方式去达梦安装目录的script文件夹找plansql.sql用disql执行disql SYSDBA/SYSDBAlocalhost:5236 start /opt/dmdbms/script/plansql.sql如果你的数据库是图形化安装通常默认已经建好。还有另一种情况当前用户不是表的主人需要授权或者改成PLAN_TABLE所在的用户去执行。6.2 执行计划与真实表现严重不符发现EXPLAIN出来的计划显示代价很小但实际跑出来奇慢无比。这通常有三种原因统计信息过期收集统计信息。有隐式类型转换导致索引失效。比如字段是VARCHAR传入数字123驱动可能把字段转成数字从而不让用索引。检查方式是对比执行计划里FILTER谓词是否引用了函数。参数配置异常比如并行度、内存设置过低导致哈希连接或排序在磁盘溢出。遇到这个问题我建议优先查V$SQL_PLAN看真实执行计划的实际行数如果估算行数和实际行数差太多直接动手更新统计信息。6.3 计划缓存问题与固定执行计划达梦和Oracle类似共享SQL会缓存执行计划。有时候统计信息更新了缓存里的旧计划还是没更新SQL继续按老计划跑。这时可以用系统调用刷掉该SQL的计划缓存DBMS_FLUSH_SQL_PLAN(SQL_ID);如果业务需要保持计划稳定也可以用达梦的固定执行计划功能但我不建议动不动就固定。固定计划等于你放弃了优化器根据新环境做调整的能力除非你对某个计划非常有信心且长期不变否则还是让优化器自己选比较好。6.4 排查技巧速查表我把自己常用的排查套路整理成一张速查表方便大家实际工作时对照现象最可能原因优先动作大表计划显示CSCN2缺少索引或统计信息误导查看WHERE条件创建合适索引NL连接的内表是全表扫描内表连接列无索引给内表连接列加索引估算行数严重偏小统计信息过期/直方图缺失收集统计信息对倾斜列做直方图出现大量SORT排序字段无索引/哈希内存不足检查GROUP BY、ORDER BY列索引调大增内存参数有HINT但没生效HINT语法错误或版本不支持用正确FULL/INDEX/HASH_JOIN写法确认版本SQL简单却慢隐式类型转换/函数包裹列重写SQL避免函数包裹列计划已经变化但SQL没变绑定变量窥视问题查看V$SQL参数考虑重置计划缓存关于绑定变量问题多说一句达梦里使用绑定变量可以避免SQL硬解析但如果列数据分布极度不均衡那个“窥视”到的变量可能代表一个极少数情况从而选出偏离实际的大多数情况的计划。这是Oracle和达梦都存在的经典问题。解决思路是引入直方图并让优化器对BIND参数做动态采样或者拆分SQL按特殊值单独处理。最后分享一点个人经验我刚开始用达梦时总习惯把执行计划和MySQL对比结果被它的各种缩写绕晕了好一阵。后来我悟了执行计划说到底就是优化器的“决策说明书”你不用记住所有细节只需要抓三个关键点大表是不是全表扫了连接方式合不合理估算行数和实际差多少这三个点盯住了80%的慢查询问题都能暴露出来。另一点体会是调优慢SQL时先问统计信息更新没再检查索引合不合理最后才轮到HINT和改写SQL。这个顺序千万别弄反。我有好几次上来就重写SQL结果后面发现是统计信息过期白折腾半天。先看数据底数再看索引设计执行计划会自己“变好”。希望这篇分享能帮你少走一些弯路。如果你也在达梦执行计划上遇过什么奇葩问题欢迎带着例子来交流踩坑经验往往比文档更有用。
返回列表