从KWR报告到优化器内核:一次性能调优的全链路复盘

发布时间:2026/8/5 8:16:12
从KWR报告到优化器内核:一次性能调优的全链路复盘 文章目录先说个事儿先把家伙什准备好KWR你的数据库行车记录仪KSH秒级采样的放大镜KDDM自动诊断的老专家开始破案第一步KWR报告找大盘第二步KSH定位精确时刻第三步KDDM要个建议找到问题了然后呢——优化器能帮你多少DISTINCT优化优化器开始推理了标量子查询消除把子查询变外连接外连接消除优化器的好心可能办坏事WHERE里函数执行顺序一个更隐蔽的坑Hint优化器不靠谱的时候你得自己来总结几句兼容是对前人努力的尊重是确保业务平稳过渡的基石然而这仅仅是故事的起点先说个事儿去年十一月吧我记得特别清楚那天下午三点多正摸鱼呢运维群突然炸了。业务方说系统卡死页面转圈转了快两分钟打不开。我赶紧登上去看CPU直接干到95%活跃连接数飙到三四百。当时第一反应是——完了又是哪条SQL在作妖。说实话这种场景我经历过不下十次了每次排查都跟破案似的先看操作系统top再看数据库日志然后一条条翻慢SQL。问题是这次翻了一圈啥明显异常都没发现日志里也没有锁等待的报错。就很难受你明知道有问题但抓不到证据。后来花了差不多一个多小时才定位到——一条报表SQL的子查询写的有问题全表扫描了一张八百多万行的表。但这一个小时里业务方电话打了三四个领导也在问啥时候能恢复那个压力真是服了。事后复盘的时候我就在想要是能有个工具提前把性能数据存下来出了事直接看报告定位问题而不是从头开始翻日志那该多好。后来去翻文档发现KES自带的KWR性能诊断体系还真是干这个的而且不止KWR还有KSH和KDDM三个工具串起来用从宏观到微观都能覆盖。今天就把这套东西从头到尾唠一遍不光说工具怎么用更关键的是——找到问题SQL之后优化器层面到底能帮你做些什么你又该在什么时候介入。顺便提一嘴金仓社区在搞征文大赛同时还有个智能运维工具开发大赛的联动报名参加开发大赛再投一篇开发大赛心得方向的文章审核通过就能拿金仓币和定制T恤。搞运维的同学们可以关注下反正写都写了不如多拿个奖。先把家伙什准备好KWR你的数据库行车记录仪KWR全称叫Kingbase Auto Workload Repertories说白了就是数据库的行车记录仪。它通过周期性快照自动采集性能数据——SQL执行时间、等待事件、IO信息、内存命中率这些——存到快照库里。出了事你调两个快照之间的报告出来看啥时候开始变的、哪条SQL在搞事、等的是什么资源一清二楚。安装很简单一条SQL的事-- 创建KWR扩展CREATEEXTENSION sys_kwr;但光装上不行还得配参数。这里我把我踩过坑之后的推荐配置贴出来你们直接抄# kingbase.conf 中添加以下配置 # 信息收集相关参数这些不开的话KWR报告里会缺东西 track_sql on track_instance on track_wait_timing on track_counts on track_io_timing on track_functions all sys_stat_statements.track top # KWR管理参数 sys_kwr.enable on # 开启自动快照默认是关的这个坑我踩过 sys_kwr.topn 20 # 报告里每个类别显示前20条 sys_kwr.history_days 8 # 快照保留8天跟Oracle AWR默认一样 sys_kwr.interval 60 # 每60分钟自动采集一次快照 sys_kwr.language chinese # 报告用中文看英文头疼的福音 sys_kwr.track_os on # 采集操作系统数据建议开 sys_kwr.database kingbase # 指定监控的目标数据库有个事儿得特别注意——改完kingbase.conf之后要reload才能生效不用重启数据库。我当时不知道直接restart了生产环境闪断了几秒被运维经理瞪了一眼。后来才知道sys_ctl reload就行了# 重载配置不用重启sys_ctl reload配置好之后KWR会自动每小时采集一次快照。但有时候你想在特定时间点手动采集比如你刚改完一个参数想看效果可以手动创建快照-- 手动创建快照SELECT*FROMperf.create_snapshot();-- 看看现在有哪些快照SELECTsnap_id,snap_timeFROMperf.kwr_snapshotsORDERBYsnap_idDESC;KSH秒级采样的放大镜KWR的问题是粒度太粗——默认一小时一个快照如果故障只持续了三五分钟在这个小时的累积数据里可能就被稀释掉了看不出来。这时候就需要KSH了。KSH全称Kingbase Session History每秒采样一次活跃会话状态存到内存环形缓冲区里。说白了就是KWR的放大镜版本。KWR看趋势KSH看瞬间。配置也很简单在KWR基础上加几个参数就行# KSH相关配置 track_activities on # KSH依赖这个开关默认是开的 sys_stat_statements.max 10000 # 跟踪的最大语句数 sys_kwr.collect_ksh on # 开启自动采集KSH sys_kwr.ringbuf_size 200000 # 内存环形缓冲区大小默认100000这里有个坑——改这些参数需要重启数据库生效不像KWR的参数reload就行。所以建议在初始部署的时候就配好别等出问题了再临时加。KDDM自动诊断的老专家KDDM全称Kingbase Diagnostic Manager基于KWR快照数据自动分析等待事件、IO、网络、内存和SQL执行时间直接给你吐出优化建议。类似于你请了个老DBA帮你看报告只不过这个老DBA是程序化的。KDDM跟KWR在同一个插件里不用单独安装。生成报告也很简单-- 基于两个快照生成KDDM诊断报告SELECT*FROMperf.kddm_report(90,101);注意KDDM目前只支持TEXT格式不支持HTML。一开始我还以为是我参数配错了折腾了好久才发现就是不支持HTML输出。这玩意儿生成的建议包括数据库时间分解、CPU相关建议、TOP SQL建议、使用索引建议、等待事件建议等等还挺全面的。开始破案第一步KWR报告找大盘工具准备好了回到开头那个场景。系统突然变慢我需要先看大盘——到底是CPU瓶颈、IO瓶颈还是锁等待。先找到故障前后的两个快照ID生成KWR报告-- 假设故障发生在下午3点左右-- 快照90是下午2点的快照91是下午3点的SELECT*FROMperf.kwr_report(90,91,html);生成的HTML报告可以导出到本地看-- 导出到文件\copy(SELECT*FROMperf.kwr_report(90,91,html))TO/tmp/kwr_report.htmlWITH(FORMATTEXT);报告打开之后我一般先看几个核心指标。先是数据库时间分解——DB Time花在哪儿了。如果大部分时间花在CPU上说明是计算密集型的SQL在搞事如果花在IO等待上说明有大表扫描在啃磁盘如果是Lock等待那就是锁冲突。然后看TOP SQL部分。KWR报告里会列出消耗资源最多的SQL按执行时间、CPU时间、IO读取量等维度排序。那天我看到的TOP SQL长这样-- 报表SQL简化后的样子SELECTDISTINCTt1.dept_name,t2.report_date,t3.metric_valueFROMdept_info t1LEFTJOINdaily_report t2ONt1.dept_idt2.dept_idLEFTJOINmetric_data t3ONt2.report_idt3.report_idWHEREt2.report_date2025-11-15ANDt3.metric_typeSALESORDERBYt1.dept_name;执行时间排第一单次执行47秒物理读8个G。看看执行计划EXPLAINANALYZESELECTDISTINCTt1.dept_name,t2.report_date,t3.metric_valueFROMdept_info t1LEFTJOINdaily_report t2ONt1.dept_idt2.dept_idLEFTJOINmetric_data t3ONt2.report_idt3.report_idWHEREt2.report_date2025-11-15ANDt3.metric_typeSALESORDERBYt1.dept_name;执行计划出来我一看就发现问题了——最外层的LEFT JOIN在执行计划里变成了Hash Join而不是Left Join。而且metric_data那张八百多万行的大表走的是Seq Scan全表扫描没有命中索引。Hash Join本身倒不是坏事问题在于全表扫描八百多万行数据每行都要读磁盘物理读8个G这能不慢吗。报告里还有个实例效率百分比的section我也看了一眼缓冲区命中率只有85%说明大量数据页都在走磁盘IO而不是内存缓存。这个数字按理说应该98%以上才正常。再加上TOP 10前台等待事件里排第一的是DataFileRead类的IO等待基本上可以确定——这条SQL的瓶颈在磁盘IO上全表扫描是罪魁祸首。第二步KSH定位精确时刻KWR告诉我这一小时里这条SQL最耗时但我想知道具体是哪个时间点开始变的。这时候上KSH-- 生成指定时间段的KSH报告15分钟时长SELECT*FROMperf.ksh_report(2025-11-15 14:50:00,15,0,html);KSH报告里有个TOP SQL等待事件的section能看到每条SQL在不同时刻的等待事件分布。我一看14:55之前系统很平静14:56开始突然冒出来大量IO类等待事件对应的Query ID正好就是那条报表SQL。而且KSH里还有个TOP阻塞会话事件的section能看到有没有锁阻塞。那天确认了没有锁冲突纯粹就是这条SQL自己在啃磁盘。第三步KDDM要个建议最后再跑个KDDM看看系统给什么建议SELECT*FROMperf.kddm_report(90,91);KDDM吐出来的建议还挺有意思的它不光告诉你哪儿有问题还把建议动作按优先级排好了。我那天拿到的报告里排在最前面的是TOP SQL建议——直接告诉你哪几条SQL最吃资源建议你优先处理。然后是使用索引建议不仅指出了metric_data表缺少metric_type字段的索引还把建议的DDL给出来了-- KDDM建议的索引CREATEINDEXidx_metric_data_typeONmetric_data(metric_type);另外还有个GUC参数建议功能这个我觉得特别实用-- 让KDDM根据你的硬件配置给参数建议SELECT*FROMperf.kddm_guc_advisor();-- 也可以指定参数SELECT*FROMperf.kddm_guc_advisor(conn :300,-- 最大连接数service_type :oltp,-- 业务类型cpu :96,-- CPU核心数memory :262144-- 内存大小MB);输出大概长这样建议参数列表 max_connections 300 shared_buffers 64GB effective_cache_size 192GB work_mem 55MB maintenance_work_mem 2GB max_parallel_workers_per_gather 4 max_parallel_workers 24这个功能对新手特别友好不用记那么多参数让系统自己算。当然老DBA可能觉得这些建议比较保守但作为一个参考基线还是挺好用的。找到问题了然后呢——优化器能帮你多少定位到问题SQL之后下一步就是优化。很多人第一反应是加索引、改参数但其实KES的优化器在内部做了很多自动优化有些你根本不用手动干预它自己就帮你改了。关键是你得知道它在做什么不然出了问题你都不知道为啥。下面我把几个比较有意思的优化器自动改写机制过一遍都是我在实际项目中碰到过的。DISTINCT优化优化器开始推理了先说个让我血压上来的事。之前在群里看到有人贴了这么条SQLSELECTDISTINCTa,bFROMs1WHEREa1ANDb1;WHERE已经把a和b都钉死了结果集里每条记录都长一样DISTINCT去重个寂寞啊。但数据库呢老老实实全表扫一遍排序去重几十毫秒才出来。后来翻KES的更新日志——V9R4C19版本加了DISTINCT优化分两层。第一层把DISTINCT改写成GROUP BY因为GROUP BY有现成的键值消除和并行处理能力通用场景下耗时差不多砍半-- 你写的SELECTDISTINCTa,bFROMs1;-- KES内部改写成SELECTa,bFROMs1GROUPBYa,b;第二层更狠。如果目标列被常值固定了结果最多一条直接用LIMIT 1替代去重-- 你写的SELECTDISTINCTa,bFROMs1WHEREa1ANDb1;-- KES内部改写成SELECTa,bFROMs1WHEREa1ANDb1LIMIT1;实测效果30毫秒变0.03毫秒一千倍。什么索引优化什么参数调优能给你一千倍做不到的。因为这不是在优化执行效率是在消除一个根本不需要存在的操作。开启方式-- 开启DISTINCT优化SETkdb_rbo.rbo_ruleon;SETkdb_rbo.enable_distinct_optimizationon;默认是关的建议先session级别试试水再全局开。标量子查询消除把子查询变外连接这个也是优化器自动改写的典型案例。啥是标量子查询就是SELECT里嵌了个返回单值的子查询-- 典型的标量子查询SELECTt1.id,(SELECTt2.nameFROMt2WHEREt2.idt1.fk_id)ASnameFROMt1WHEREt1.statusACTIVE;这种写法的问题在于子查询会对外层每行数据执行一次。t1有一万行就执行一万次子查询有十万行就十万次。数据量大了直接卡死。KES在V009R002C014版本加了标量子查询消除功能分三个阶段先判断子查询和主查询之间的等价性然后把子查询改写成外连接最后把相似的子查询合并。-- 优化器内部把上面那个改写成SELECTt1.id,t2.nameFROMt1LEFTJOINt2ONt2.idt1.fk_idWHEREt1.statusACTIVE;改成JOIN之后就可以用Hash Join或者Merge Join了不用一行一行去查。实测t1和t2各一万行数据优化前32秒优化后24毫秒一千三百多倍的提升。开启方式SETkdb_rbo.rbo_ruleon;SETkdb_rbo.enable_scalar_subquery_optimizationon;外连接消除优化器的好心可能办坏事上面两个都是优化器帮你做减法的好例子但外连接消除这个就不一样了——它有时候会好心办坏事。啥是外连接消除就是你写了LEFT JOIN但优化器发现WHERE条件会过滤掉外连接产生的所有NULL行于是在数学逻辑上LEFT JOIN WHERE跟INNER JOIN WHERE是等价的为了性能它就把LEFT JOIN改成了INNER JOIN。看个例子就明白了-- 开发者想查所有t1的记录关联t2中name2cc的信息SELECT*FROMt1LEFTJOINt2ONt1.id1t2.id2WHEREt2.name2cc;你以为会返回t1的所有行t2没匹配到的显示NULL。但实际上呢NULL cc的结果是UnknownWHERE会把NULL行全部过滤掉所以LEFT JOIN在执行计划里变成了INNER JOINt1中没匹配到t2的行直接消失了。这事儿要是在迁移场景下就更要命了。你从Oracle迁过来原来跑的好好的SQL结果到了KES上数据少了你查了半天找不到原因最后发现是外连接被消除了。解决方法是把过滤条件从WHERE移到ON里面-- 正确写法先过滤t2再做外连接SELECT*FROMt1LEFTJOINt2ONt1.id1t2.id2ANDt2.name2cc;这样系统会先对t2做过滤然后跟t1做外连接t1的所有记录都能返回。还有一种情况要注意——如果你用的是Oracle的()语法过滤条件不带()的话同样会触发外连接消除-- 这个会触发外连接消除SELECT*FROMt1,t2WHEREt1.id1t2.id2()ANDt2.name2cc;-- 这个不会因为过滤条件带了()SELECT*FROMt1,t2WHEREt1.id1t2.id2()ANDt2.name2()cc;所以迁移的时候凡是涉及外连接的SQL一定要看一眼执行计划确认连接类型没被偷偷改掉。WHERE里函数执行顺序一个更隐蔽的坑这个严格来说不是优化器的锅但它跟SQL执行逻辑密切相关性能调优的时候经常碰到。有些开发者喜欢在WHERE里放有副作用的函数——一个函数负责设置状态另一个负责读取状态而且读取必须依赖设置先执行完-- 预期先执行set_id赋值再用get_id获取进行过滤SELECT*FROMmy_tableWHEREid1pkg_abc.get_id()-- 获取值ANDpkg_abc.set_id(10)1;-- 设置值这种写法在测试环境可能跑的好好的因为上一次测试残留的变量值还在。但到了生产环境新开会话变量是空的get_id()返回NULLid1 NULL直接false整条SQL返回空集。而且这个问题是间歇性的——连接池复用了之前设置过变量的连接就能查出来新建的连接就查不出来。排查起来简直要命。KES对WHERE中的函数条件默认按从左到右的顺序执行但这只是当前版本的实现不代表以后不会变。SQL是声明式语言你写的是要什么而不是怎么做执行顺序理论上优化器有权调整。所以记住一个原则——严禁在WHERE里放有副作用的函数。状态设置逻辑应该放到SQL外面先调用set_id再发独立的SELECT。如果函数确实是纯读取的在KES里声明成IMMUTABLE或STABLE这样优化器能更好地理解它的行为-- 声明函数属性CREATEORREPLACEFUNCTIONget_id()RETURNSINTSTABLEAS$$-- 声明为STABLE表示在一次查询中结果不变BEGINRETURNcurrent_setting(app.current_id)::INT;END;$$LANGUAGEplpgsql;Hint优化器不靠谱的时候你得自己来优化器再聪明也有翻车的时候。统计信息不准、数据分布倾斜、多表连接路径选择错误这些情况都可能导致优化器选了条烂计划。这时候就得用Hint强制干预了。KES的Hint用法跟Oracle基本一致通过特殊注释来指定执行计划。先装插件CREATEEXTENSION sys_hint_plan;然后在SQL里加Hint注释-- 强制使用索引扫描SELECT/*IndexScan(t1 idx_t1_status)*/*FROMt1WHEREstatusACTIVE;-- 强制全表扫描测试用SELECT/*SeqScan(t1)*/*FROMt1WHEREstatusACTIVE;-- 强制Hash JoinSELECT/*HashJoin(t1 t2)*/*FROMt1,t2WHEREt1.idt2.fk_id;-- 指定连接顺序SELECT/*Leading(t1 t2)*/*FROMt1,t2WHEREt1.idt2.fk_id;-- 开启并行查询并行度4SELECT/*Parallel(t1 4)*/count(*)FROMt1;-- 在Hint里临时设置参数SELECT/*Set(work_mem 256MB)*/DISTINCTaFROMbig_tableORDERBYa;Hint混合使用也行下面这个例子同时指定了连接顺序、连接方式和聚合方式SELECT/*Leading(((t2 t3) t1)) NestLoop(t2 t3) hashagg*/t2.idFROMt1,t2,t3WHEREt1.idt3.idANDt1.id3ANDt3.valt2.idGROUPBYt2.id;有个特别实用的功能叫Hint Table——你把Hint规则写到一张表里数据库匹配到对应的SQL就自动加载规则不用改应用代码。这个对不能改SQL的场景简直是救星-- 开启Hint Table功能SETsys_hint_plan.enable_hint_tableon;-- 往hint表里写规则INSERTINTOhint_plan.hintsVALUES(SELECT * FROM t1 WHERE status $1,IndexScan(t1 idx_t1_status));不过说实话Hint这东西是双刃剑。它能救场也能埋雷——你在当前数据分布下加的Hint等数据量变了可能反而变成负优化。所以核心原则是只在必要时用用了要定期review统计信息要记得ANALYZE。还有个跟Hint配合使用的叫Query Mapping这个更底层——它可以直接把一条SQL重写成另一条。比如你有些老SQL写的很烂但又不能改 vendor的代码啥的可以用Query Mapping在数据库层面做替换。不过这个功能用起来比较复杂实际项目中我见过的用例不多这里就不展开了。总结几句整个过程走下来我觉得KES的性能调优可以分三个层次。第一层是工具层——KWR看大盘找趋势KSH看细节找瞬间KDDM给建议省脑力。这三个工具搭配使用基本能覆盖绝大多数性能诊断场景。而且它们跟Oracle的AWR/ASH体系很像有Oracle经验的DBA上手很快。第二层是优化器层——DISTINCT优化、标量子查询消除、外连接消除这些自动改写机制能帮你消掉很多不必要的开销。但前提是你得知道它们在做什么尤其外连接消除这种可能改变结果集的迁移的时候一定要看执行计划。第三层是人工干预层——Hint、索引设计、参数调优这些是优化器搞不定的时候你需要出手的地方。Hint是最后手段不是第一手段用之前先确认统计信息是不是准的。还有一点就是——性能调优不是一次性的活是个持续的过程。KWR的快照对比功能特别适合做这个优化前拍个快照优化后再拍一个diff一下看效果。养成这个习惯比什么调优技巧都管用。