
数据库慢SQL处理这件事几乎每个后端团队都躲不过。我印象最深的一次是一条统计报表SQL在线上跑了二十多秒开发组第一反应是“加索引”DBA第一反应是“看执行计划”两边吵了半天最后发现是统计信息太久没更新优化器选了一条离谱的路径。这种场景见多了以后我慢慢意识到不管是金仓还是其他数据库优化的核心不在于“你动了什么”而在于“你凭什么动”。这也是我今天想认真聊一聊“先判定后评估”框架的原因。先说明一下定位。这里说的“先判定后评估”不是金仓官方手册里某个按钮也不是一行命令而是一套把数据库优化流程化的方法——先判定就是用可观测的数据把慢SQL的根因锁死后评估就是优化动作做完后用客观指标回答“到底有没有变快”。这套框架尤其适合跑在金仓V8这类兼容PostgreSQL生态的国产数据库上也适合那些被“盲目加索引”“重启大法”折腾怕了的开发者和DBA。下面我按“为什么需要这套框架、判定怎么做、评估怎么做、金仓上怎么落地、常见坑有哪些”的顺序一点一点拆开来讲。1. 为什么传统优化套路越来越不够用先判定后评估的出发点1.1 你还在用“三板斧”救火吗我早些年处理线上数据库性能问题动作相当固定查慢日志、看执行计划、加索引。听起来挺专业实际上大部分时间是在猜。系统卡了就加索引加了没用就重建统计信息再不行就让开发改SQL。这种“三板斧”式的处理运气好能解决七八成问题但剩下两三成会让你在凌晨的工位上怀疑人生。问题出在哪我总结下来有三个典型坑。第一索引不是越多越好。每多一个索引就多一份写入维护成本而且优化器不一定会用你加的索引很多时候费劲建的索引只是摆设。第二统计信息不准的时候执行计划是“拿着错误的地图找餐厅”怎么走都到不了目的地。第三很多团队把优化等同于改SQL但压根没区分SQL本身的问题和数据库资源层面的瓶颈。明明是锁等待把SQL拖慢了你还在埋头改写关联逻辑典型的南辕北辙。1.2 “先判定后评估”到底在讲什么这套框架的精髓用一句话概括用数据代替感觉让优化动作有依据、有验证。它把一次性能优化拆成两个环节。判定环节要做的是“诊断”通过执行计划、统计信息、等待事件、资源利用率这些客观信号把问题定性到具体层面——是优化器选错了路径是SQL结构本身有硬伤还是参数配置不合理。评估环节要做的是“复查”优化动作落地后用执行时间、扫描行数、缓冲区命中率等指标做前后对比确认这次改动真的有效而不是换个写法图个心理安慰。之所以强调“先判定”是因为我做数据库优化这些年最大的体会是大部分无效优化都是因为没搞清楚问题在哪就动手。判断一条SQL慢必须先回答几个问题——慢是偶发还是持续慢在哪个阶段优化器估算的行数和实际返回的行数差多少有没有锁在排队这些问题有了答案优化动作往往就只剩一两个方向可选了根本不需要广撒网。2. 判定环节拆解把问题定位从“猜”变成“算”2.1 第一件事确认统计信息是否“新鲜”判定SQL为什么慢我第一步永远不是看SQL写法而是看表上的统计信息。这个道理在金仓这类数据库里尤其重要——优化器决定走索引还是全表扫描、选择哪种join算法靠的全是统计信息里的数据分布。统计信息过期或缺失优化器就像戴墨镜开车方向感再好也会跑偏。我在金仓V8上排查过一个典型场景订单表有5000万行按订单时间建的索引清清楚楚可explain一看优化器坚持走全表扫描。原因是什么表是昨晚批量导完数据之后直接开放的自动统计任务还没来得及触发analyze统计信息里的“行数”还停留在导入前的几十万行。优化器一算全表扫描才3万行索引扫描要回表再排一次序怎么算都“全表更划算”——它压根不知道表已经膨胀到5000万行。所以判定环节的第一步就是确认统计信息的新鲜度。在金仓里可以通过系统表或者管理工具查看表的上次analyze时间也可以用类似analyze table的命令主动触发收集。这个动作成本极低却常常能直接消除“莫名其妙变慢”的问题。统计信息新鲜度不对后面的所有判定都可以先暂停先把这关过了再说。2.2 吃透执行计划里的三个信号统计信息没问题之后下一步就是把慢SQL的执行计划完整explain出来。这里给新手一个建议别只看“有没有走索引”这一项。执行计划里真正有价值的信息远不止一个索引标记至少要看三个信号。第一个是大表上的Seq Scan。几百万行以上的表出现全表扫描本身就是高风险信号但也要看过滤条件能不能用上索引不是所有Seq Scan都得消灭。第二个是优化器的“估算行数”和实际行数之间的偏差。比如explain输出里估算10行实际执行却扫出100万行这说明统计信息或SQL写法导致优化器严重误判这种偏差往往是选错执行计划的最大元凶。第三个是Nested Loop的循环次数。当内层表被循环扫描了几十万次即使单次很快累计起来也会慢得离谱。怎么看这些信号我习惯用explain analyze而不是光explain。explain只给估算值explain analyze会真实执行一遍告诉你每个节点实际扫描了多少行、耗时多少。两条放在一起看估算和实际的差距一目了然。注意explain analyze会真的跑SQL生产环境要谨慎最好在备库或业务低峰期做或者包一层事务再回滚。我见过有人白天高峰期在核心库上直接explain analyze结果把本就不稳的业务雪上加霜这种事一定要避免。2.3 把“SQL慢”和“系统慢”分开这个维度特别容易被忽略。很多时候SQL本身没问题执行计划选得也对但就是在线上跑得慢。这种情况我直接去看数据库的等待事件和系统资源占用而不是继续纠结SQL文本。具体三步检查。第一步看有没有锁等待。在金仓里可以通过类似pg_stat_activity的视图找那些state卡在等待状态的会话看看是不是有长事务占着行锁不放导致后续SQL全部排队。这种“慢”不是SQL的错你写多漂亮的SQL都没用把锁源头找出来、事务提交掉速度立刻恢复。如果看到死锁报告优先把互相等待的会话日志拉出来先解决锁问题再谈优化。第二步看CPU和IO。如果数据库所在主机的CPU已打满或磁盘IO延迟明显升高那是资源层面的瓶颈先扩容或限流再谈SQL优化。第三步看连接数和活跃会话。连接池被打满之后新请求都在排队拿连接SQL本身再快也出不来。这三步走完基本能把问题从“SQL问题”和“资源问题”之间切割开。SQL问题继续深挖执行计划和写法资源问题直接走运维手段。先判定后评估能落地的关键就在这里——每个动作都有明确依据而不是看到慢就加索引。3. 评估环节拆解优化做完怎么知道真的赢了3.1 别被“执行时间”一叶障目优化动作做完之后最常见的评估方式是“看一眼执行时间变快了”。但执行时间这个指标非常容易被缓存、并发、机器负载干扰。举个例子同一句SQL在热数据上跑比冷数据快几十倍是常有的事。你优化完刚好碰上热数据时间从3秒降到200毫秒以为是大成功第二天一复盘又回到3秒心态直接崩。我自己的评估习惯是至少同时记录四个指标执行耗时、实际扫描行数、缓冲区命中情况、IO次数。在金仓这种走PostgreSQL生态的数据库里explain analyze输出的信息基本都能看到这些数据不用额外搭监控。这里有个很容易忽略的点如果优化后执行时间下降不明显但扫描行数从几百万降到了几千这个优化同样有价值——它降低了数据库整体负载只是还没在单条SQL上体现出来。反过来执行时间下降但扫描行数没变那大概率只是缓存或并发层面的改善不是真正的优化。评估指标说明为什么重要执行耗时SQL从开始到返回结果的总时间最直观但易受缓存和负载干扰扫描行数每个节点实际扫描的行数反映优化是否真正减少工作量缓冲区命中命中共享缓冲区的次数高命中可能只是结果被缓存IO次数磁盘读写次数最能反映数据库实际压力3.2 控制变量的正确姿势既然要评估就得有做实验的样子。我最早吃过的亏是优化前测了3次取最快优化后测了1次然后得出结论“优化失败”。后来被反复教育才老老实实按控制变量的套路来。第一冷热缓存分开看。同一个SQL我先跑一次让数据进缓存再跑第二次和第三次取三次的平均或中位数。如果第一次差距特别大说明缓存影响明显别拿第一次说事。第二同样的参数、同样的数据量、同样的并发下做对比最好连执行计划也保存下来方便到时候diff。第三能用只读查询做验证就尽量别拿写事务来测。写事务涉及锁和日志写入干扰因素太多不适合作为优化效果的标尺。我在实际项目里会把优化前后的explain analyze输出存成文本放进代码仓库的PR描述里或者写进优化工单。这看起来麻烦但好处极大下次有人再问“这个优化到底有没有用”直接把两份输出贴出来比任何口头的“我觉得快了很多”都有说服力。3.3 评估结果要“回流”到判定先判定后评估这套框架和一次性调优最大的不同在于它是个闭环。评估完不是画句号而是要把结果带回判定环节看看刚才的判定是否准确优化动作是否真的命中了根因。举个例子你判定问题是统计信息过期于是analyze了一把然后评估发现执行时间确实下降了但还没达到预期量级。这时候不能就这么收工而是要进一步判定是不是SQL写法本身的join顺序有问题是不是参数配置限制了优化器发挥这才是闭环的完整走法。我见过太多人做优化做了一步就到处说“搞定了”结果上线一周又被打回原形。闭环还要沉淀成机制。每次优化完我会顺手写一条简短记录问题现象、判定结论、优化动作、评估数据、是否回退。三个月后翻记录这些都是宝贵的经验资产远比“这次运气好加了索引就快了”这种模糊记忆靠谱。4. 金仓环境下的落地实操从SpringBoot集成到一条慢SQL的完整优化4.1 先把手上的环境跑起来聊完方法论必须上点能直接复用的实操。现在很多团队用SpringBoot接金仓V8我就从这一步开始顺便把一些容易踩的坑说一下。Maven依赖里引入金仓的JDBC驱动驱动类名一般是com.kingbase8.Driver连接串格式是jdbc:kingbase8://ip:端口/数据库名。配置在application.yml里就几行spring: datasource: driver-class-name: com.kingbase8.Driver url: jdbc:kingbase8://192.168.1.100:54321/mydb username: user password: pass hikari: maximum-pool-size: 20 minimum-idle: 5这里有几个坑值得说。第一连接串里的端口不要按习惯写成5432很多金仓实例默认端口是54321连不上先查这个。第二不要一上来配很大的连接池。HikariCP官方建议maximum-pool-size控制在CPU核心数×2再加一点就够连接池越大并发越高反而更容易把数据库打满。第三集成之后第一件事是确认驱动版本和数据库版本是否匹配。版本不对会有各种诡异行为比如日期类型返回异常、批量插入报错排查起来非常浪费时间。4.2 一条慢SQL的“先判定后评估”全流程环境通了之后我用一个接近真实的案例把前面两章的方法串起来走一遍。业务场景是三张表关联订单表、客户表、商品表查询条件是订单时间范围加客户等级过滤返回最近一段时间高等级客户的订单列表。刚开始这条SQL线上跑一次要800毫秒左右高峰期能到2秒以上。开发组已经试过给订单表的订单时间建索引没效果。我接手后没有急着写优化方案先做判定。第一步确认统计信息。查了一下表的上次analyze时间发现订单表已经一个多月没更新而数据量每天都在涨最新统计信息里的预估行数和实际行数差了快100倍。顺手做了analyze再explain执行计划立刻从Seq Scan变成Index Scan执行时间从800毫秒降到200毫秒。你看还没开始改SQL仅仅刷新统计信息就解决了大半问题。第二步继续深挖剩下的200毫秒花在哪。explain analyze显示虽然走了索引但索引过滤之后还要回表回表次数高达20万次而且客户表的关联用的是Nested Loop导致循环次数暴涨。到这一步判定结论就清楚了统计信息过期是主因join顺序和索引覆盖度还有优化空间。第三步针对判定结果做优化动作。我在订单时间索引的基础上改成一个覆盖索引把订单金额、客户ID、商品ID都一起带上减少回表次数。同时把客户等级这个高频过滤条件单独建索引。SQL本身几乎没有改只是让查询里用到的列和过滤条件都被索引覆盖。第四步评估。优化后的explain analyze对比指标优化前优化后执行耗时800ms28ms扫描行数40万2万回表次数20万0覆盖索引Nested Loop循环次数20万5000主要瓶颈统计信息过期 回表 嵌套循环无到这里整个优化才算真正完成。注意每一步改动我都保留了explain analyze的输出第四步的对比才有依据。4.3 顺手调一调这些参数做完一次SQL优化很多人就收工了。但如果你的金仓实例跑的是高并发业务有几个参数值得一并检查因为它们会直接影响优化器的“评估”质量。第一个是shared_buffers数据库共享缓冲区大小决定内存里能缓存多少数据页和索引页。第二个是work_mem每个排序或哈希操作能用的内存设置太小会让优化器宁可走性能差的全表排序也不走索引。第三个是effective_cache_size它告诉优化器操作系统层面大概能留多少缓存给数据库用设置太低会导致优化器低估走索引的收益、高估全表扫描的成本。这三个参数没有绝对标准我给一个经验性的起点shared_buffers设成物理内存的25%左右effective_cache_size设成物理内存的50%到75%work_mem按连接池最大连接数反推保证并发时总内存不爆。这里的计算逻辑很简单数据库可用总内存减去shared_buffers再除以最大并发连接数就是每个连接能分到的work_mem上限。比如物理内存64GBshared_buffers设16GB左右留给数据库可用的内存约48GB连接池最大20个连接平均每个连接能分到约2.4GB排序操作大的时候单独调大就好。算出来的值不要拍脑袋填改完要重启实例并重新评估线上所有核心SQL的执行计划。5. 常见问题与排查技巧实录5.1 统计信息不准优化做了等于白做这是出现频率最高的问题症状特别有迷惑性——SQL突然变慢重启数据库就好了但过几天又慢。很多人以为是缓存问题实际上是统计信息太旧优化器一直在用错误的数据做决策。我踩过最重的一次坑是给一张大表加了索引后发现完全没效果最后才查出索引是在analyze之前建的统计信息里压根没有新索引的踪迹优化器自然当它不存在。排查技巧遇到“加索引无效”“执行计划诡异”的情况先别怀疑索引本身先去收集统计信息。金仓里执行表级别的统计信息收集命令即可低峰期跑几秒钟到几分钟不等。收集完直接重新explain八成以上的“诡异执行计划”都会恢复正常。5.2 执行计划突变昨天快今天慢另一种高发问题是同一句SQL昨天执行5毫秒今天执行500毫秒SQL没改、数据量没暴涨但就是慢。这个问题的根因通常是统计信息变化导致优化器“改了主意”——可能是某个索引的选择性下降可能是一个新数据分布让优化器觉得全表扫描更划算。这种时候我一般做三件事第一把两个时间点的执行计划拿出来diff看看优化器到底换了什么思考路径第二如果旧的计划确实更好可以尝试通过SQL改写或参数调整把优化器“教育”回正确方向第三如果压根不想让执行计划波动就要考虑绑定执行计划把验证过的最优计划固定下来。这三种手段里最简单的是SQL改写比如把改成IN、拆掉OR条件、调整join顺序都能影响优化器的判断。5.3 评估时被缓存干扰一个反直觉的验证坑最后提醒一个评估环节特有的坑。我曾在对比优化效果时发现优化后的SQL比优化前慢了气得差点回退代码。后来冷静下来一想第一次测的是冷缓存第二次测的SQL刚好命中热数据区这才导致结果倒挂。这就是典型的评估方法错误。正确的验证套路是同一个SQL按“冷执行、预热、再执行”的顺序来取第二次和第三次的结果为准。如果两次的波动依然很大说明还有并发或系统负载干扰那就选业务低峰期把并发降到最低之后重新测。这一步虽然繁琐但能让你避免把一次成功的优化误判成失败也能避免把一个失败的改动误判成成功。这套“先判定后评估”的框架说到底并不是什么高深理论它就是把数据库优化这件看起来很玄的事变成了一个有据可查、有章可循的工程流程。我在金仓上实践这几年最大的收获不是学会几个命令而是养成了一个习惯动手优化之前先逼自己回答三个问题——它为什么慢这个结论的证据是什么我改完之后拿什么指标来证明它真的变快了只要这三个问题都能回答上来再复杂的性能问题大多都能一步步拆解到可以落地的方案。最后再分享一个小技巧我现在做每次SQL优化都会把优化前后的explain analyze输出截图或存成文本连同变更记录一起归档。哪天同事跑来问我“这个SQL为什么改成了这样”我只需要把两份执行计划往桌上一放所有争议当场结束。这个方法不花一分钱却能省下无数沟通成本强烈建议正在用金仓或其他兼容PostgreSQL生态数据库的团队都试一下。