YashanDB慢查询诊断:从执行计划到索引优化的实战之路
1. 一次慢查询诊断从几十秒到几十毫秒问题到底出在哪前阵子有个同事跑过来找我说系统有个报表页面打开要三十多秒客户已经催了两天他的数据库刚迁到YashanDB上之前用别的库时顶多两秒。我第一反应是这又是个迁移后变慢的经典案例但等我把执行计划拉出来一看发现根子根本不在YashanDB身上而是SQL写法一直在跟优化器对着干。先说结论YashanDB作为一款兼容Oracle和PostgreSQL syntax的国产关系型数据库查询优化器的很多行为逻辑跟Oracle一脉相承——基于成本的估算、索引选择、谓词下推、连接顺序调整这些机制你理解了基本上就能解决九成以上的查询性能问题。它跟老牌数据库的区别主要在外围工具链和部分参数语义上核心优化思路是通用的。这篇文章我不打算泛泛讲要建索引、要分页这种正确的废话而是把一次真实的慢查询从发现、分析到改造的完整链路拆开顺带把我在YashanDB上踩过的坑和验证过的手法一起说出来。适合正在用YashanDB或者准备从其他数据库迁移过来的朋友尤其是那些已经会写SQL但还没系统梳理过性能优化思路的开发者和DBA。2. 慢查询的定位方法先看执行计划不要上来就改SQL2.1 用EXPLAIN PLAN看清楚优化器的真实选择很多人一遇到查询慢习惯先给WHERE条件的字段加索引或者盲目把SQL拆成几条分开跑。这其实是在拿猜赌性能。正确做法是先让数据库告诉你它到底是怎么执行的。YashanDB的SQL命令行工具支持标准EXPLAIN PLAN语法直接执行EXPLAIN PLAN FOR SELECT o.order_no, c.customer_name, o.order_amount FROM orders o JOIN customer c ON o.customer_id c.customer_id WHERE o.create_time TO_DATE(2024-06-01, YYYY-MM-DD) AND o.order_status PAID;然后查看输出计划。我那次诊断看到的执行计划大概是这样的orders表走了全表扫描TABLE ACCESS FULLcustomer表作为驱动表被反复访问了N次代价估算高得吓人。orders表当时已经有两千多万行全表扫描意味着每次查询都要把两千多万行数据从磁盘捞出来过滤一遍三十秒一点都不冤枉。执行计划就是优化器的内心独白。你写SQL的时候觉得我这么写挺清晰的但在优化器眼里它关注的是哪个执行路径成本最低而不是你的SQL看起来好不好读。所以第一步永远是确认优化器选择了哪个驱动表走了什么访问路径有没有做笛卡尔积排序和哈希操作发生在哪一层2.2 抓取实际运行时的等待事件执行计划能告诉你怎么执行但不会自动告诉你卡在哪。如果一张报表页面的SQL执行计划看起来还行、cost也不算离谱但实际就是慢那就要看等什么了。YashanDB的AWR类报告和会话视图能查到类似Oracle的等待事件。我当时在排查那个报表时执行计划显示走的索引没错cost也只有几百但响应时间依旧超过五秒。后来查了会话等待发现大量时间花在了buffer busy wait和enq: TX - row lock contention上——说白了就是有人在堵它。这类问题不是SQL本身的问题而是并发和锁的问题。所以慢查询排查有个固定顺序执行计划先行等待事件兜底。先确认计划形状是否合理再确认有没有被锁、有没有在等IO、有没有因为统计信息错误导致估算偏差。顺序反了很容易在错误的方向上浪费大半天。3. 最常见的隐式类型转换陷阱索引明明建了就是不走3.1 一次索引失效的现场还原回到我那个同事的案例。orders表上其实已经建了idx_create_time索引create_time这个字段也是查询条件之一但优化器就是不走。我把SQL里的条件掰开看发现问题出在写法上——业务代码传参的时候把时间参数当字符串传了WHERE o.create_time 2024-06-01YashanDB的优化器在处理create_time 2024-06-01时如果create_time是DATE/TIMESTAMP类型而字符串是VARCHAR类型就会发生隐式转换。为了完成比较优化器会选择把create_time列做一次TO_CHAR或类似运算而一旦索引列被函数包裹索引就失效了——这是一个非常基础的规律但生产环境里出现频率高得离谱。我在YashanDB上反复验证过日期字段跟字符串常量比较虽然结果不会错但执行计划就是会从索引范围扫描退化成全表扫描。你加再多索引也没用因为索引存的是原始的DATE值没法直接跟字符串做二分定位。3.2 解法与验证参数类型对齐是第一步解决办法也简单把SQL改成用TO_DATE显式转换常量让类型先对齐再比较WHERE o.create_time TO_DATE(2024-06-01, YYYY-MM-DD)改完之后再EXPLAIN计划从TABLE ACCESS FULL变成了INDEX RANGE SCAN查询耗时从三十多秒降到了一点二秒。这一步没加任何新索引没改任何表结构纯粹是让SQL的写法尊重了数据类型的本来面目。我建议凡是遇到索引建了但执行计划不用索引的情况都优先排查三件事WHERE条件里索引列是否被函数或表达式包裹索引列与常量的数据类型是否一致VARCHAR为什么非要跟NUMBER比是否出现了隐式TO_CHAR/TO_NUMBER转换这三件事不查清楚建一百个索引都是白搭。4. 分页深翻页的隐形代价LIMIT/OFFSET越往后越慢的数学原理4.1 OFFSET 100000的代价为什么不是线性的报表系统里有一个很常见的需求前端表格分页用户点击第5000页。如果SQL写成SELECT * FROM orders ORDER BY create_time DESC OFFSET 100000 ROWS FETCH NEXT 20 ROWS ONLY;你会发现页数越深查询越慢而且慢得不成比例。原因在于数据库并不知道你要的只是最后那20行它得先把前100000行全部找出来、排好序然后才能跳过它们。OFFSET 200000不是OFFSET 100000的两倍耗时因为排序和扫描的IO开销在放大——越翻越深前面积累的无用数据越多被白白排序和丢弃的工作量就越大。4.2 改写方案用游标定位或基于排序键的分页我在YashanDB上实践下来最靠谱的深分页改法是键值分页或游标分页。比如以create_time和order_no作为排序键客户端把上一页最后一条记录的排序键值带回来SELECT * FROM orders WHERE (create_time, order_no) (LAST_SEE_CREATE_TIME, LAST_SEE_ORDER_NO) ORDER BY create_time DESC, order_no DESC FETCH NEXT 20 ROWS ONLY;这种写法可以走索引直接定位到目标区间数据库只需要扫描20行左右的数据跟页数深浅没有关系。代价是你不能随便跳页只能一页一页往后翻。但绝大多数真实业务场景下用户根本不会从第1页跳到第5000页真正需要的是下一页和上一页所以游标分页在实践中的可用性很高。如果你是做管理后台的确实需要支持任意页跳转那就别把全表数据都拿出来翻给查询条件加一层更严格的时间范围过滤先把总数据量控制在一个量级内然后再OFFSET这样浅翻页的代价也可控。5. 统计信息与执行计划漂移为什么同样的SQL今天快明天慢5.1 统计信息过期导致优化器瞎了眼YashanDB的优化器跟Oracle一样是基于成本的。成本估算依赖表上的统计信息表的行数、列的基数、数据分布、直方图等等。如果统计信息是三个月前收集的而这三个月里表数据从一百万涨到了两千万那优化器估算的cardinality就跟现实差了十万八千里。我有个真实的经历有一条SQL在测试环境跑得飞快上了一套数据量大的生产库就变成全表扫描。对比执行计划后发现问题就出在优化器估算的成本上——它认为orders表只有十万行所以全表扫描挺便宜实际这个表有两千万行。等手动收集一遍统计信息后优化器立刻换回了索引路径耗时下降了不止一个数量级。5.2 在YashanDB里什么时候需要手动收集自动收集统计信息不是所有场景都靠得住三条经验你可以直接拿去用大量数据导入比如ETL批量灌数之后建议立刻收集统计信息不要等自动任务表数据发生剧烈变化的小表比如从零涨到百万行自动收集的采样频率可能跟不上某些查询条件涉及多表关联且关联列的基数估算明显离谱时直接重建统计信息统计信息收集本身也要控制频率和采样率大表每次100%采样会很伤性能。一般生产实践是大表用默认采样率关键维度表做100%采样批量任务结束后定点收集。5.3 执行计划漂移的应对思路统计信息一旦变化执行计划就可能跟着变——有时变好有时变坏。对于核心SQL我推荐的做法是先确认SQL在统计信息更新后是否依然使用合理的计划如果出现回退可以考虑像固定执行计划或大纲提示这类手段。YashanDB在这方面的生态兼容性做得不错使用Oracle风格提示的方式也能对执行计划做引导。但我不建议一上来就固定计划。优先还是让优化器通过准确的统计信息做出正确选择。固定计划只是兜底方案因为数据量的增长不会停下一个现在看似乎好到飞起的固定计划等表涨到十倍时可能恰恰是灾难。6. 连接与并发查询慢不一定怪SQL也可能是被堵了6.1 锁等待怎么快速确认前面提到的那个报表系统有一类慢查询其实不是执行计划的问题。某天一条SELECT明明走了索引但延迟非常高。我盯着会话视图看发现它在等row lock也就是有另一个会话已经修改了某一行还没提交而SELECT因为开启了读已提交或更高级别的隔离被阻塞住了。在YashanDB里排查锁阻塞的思路是找到那个卡住的会话往上追它的等待类型如果是TX锁或者buffer busy基本就是行锁竞争。再查是哪条没提交的事务在阻塞。最直接的方法是看持锁会话的SQL文本、启动时间和等待对象。实际业务中这种问题多半出现在先更新后提交的代码路径和长事务长时间持有锁这两类场景。有时候就是一个开发人员在测试环境跑了一个事务忘了提交就能把整张表的查询拖垮。6.2 连接池参数与并发控制建议连接池配置也是查询慢的一个隐蔽放大器。如果你的中间件给YashanDB的连接池设置得过大每个连接各自带着排序区、缓存区在并发高的时候会产生大量内存和锁竞争。反过来连接池太小请求排队等连接的时间也会被算进查询耗时里。我一般建议从这几点开始调初始连接数不要等于最大连接数让连接按需增长空闲连接超时设置适当地短避免一堆空闲连接占用资源最大活跃连接数要根据数据库CPU核心数和业务峰值来算不是越大越好优先检查应用是不是存在连接泄漏即查询完了没有归还连接说到并发锁还有一个经常被问到的点叫死锁。两个事务各自持有对方需要的行锁就会造成死锁。YashanDB的锁检测机制会自动回滚其中一个事务但是业务日志里如果频繁出现死锁报错你就要审视代码的更新顺序是不是统一了——所有事务都按相同的顺序去更新表A再更新表B死锁概率就大幅下降。这是个成本极低收益极高的代码规范。7. 索引设计进阶联合索引与覆盖索引的落地取舍7.1 联合索引怎么排字段顺序才不会白建很多开发者知道要建索引但建索引时要么一个表建了七八个单列索引要么把经常变化的字段放前面导致索引利用率低下。联合索引的字段顺序有一个朴素原则先等值条件后范围条件区分度高的字段排前面区分度低的排后面。比如这样一个查询WHERE order_status PAID AND create_time TO_DATE(2024-06-01, YYYY-MM-DD)order_status是等值条件create_time是范围条件。索引设计成(order_status, create_time)比(create_time, order_status)更合理因为前者的索引可以先精确匹配到status对应的数据段再在其中有界地扫描时间范围。后者需要处理很宽的create_time范围再过滤status低效得多。7.2 覆盖索引的价值让查询不用回表如果SELECT的字段恰好都包含在索引里那查询就完全不需要回表取数据这通常能带来数倍的性能提升。我遇到过一个案例报表查询要select几个订单关键字段WHERE条件只有customer_id。原来执行计划是INDEX RANGE SCAN之后一一行行回表取列数据耗时在800毫秒左右。后来把索引从单列(customer_id)改成复合索引(customer_id, order_no, order_amount)查询不再回表耗时降到了150毫秒。注意覆盖索引不是索引里塞的列越多越好。列多了索引体积膨胀写入和维护成本都会上升。实践中的做法是挑选高频查询里最常被SELECT的2~3列放进覆盖索引不要贪多。7.3 索引失效的典型场景速查我把YashanDB上实测容易踩的索引失效场景整理成一个表场景示例后果索引列参与函数运算WHERE TO_CHAR(create_time, YYYY-MM) 2024-06索引失效全表扫描隐式类型转换WHERE varchar_col 12345索引失效前导模糊匹配WHERE name LIKE %张索引失效联合索引未用前导列索引(a,b)条件只写b无法走联合索引范围条件后的等值列索引(a, b, c)a范围b等值c的定位能力受限最后一条值得多说一句联合索引中第一个范围条件之后的列不会再参与索引定位。比如(a, b, c)三列索引a是范围条件b是等值条件那么c只能做过滤不能做索引定位。设计联合索引时尽量把范围条件放到最后。8. 视图到底能不能加快查询速度一个总被误解的问题网络热词里有个问题很典型视图可以加快查询速度吗我的答案一直是普通视图本质上是SQL语句的封装不是物化存储因此它本身没有加速能力。你在YashanDB里建一个视图底层其实就是一个被命名的SELECT语句。查询视图时优化器会把视图定义展开跟外层查询合并再统一生成执行计划。视图不会自动建索引也不会预先把数据缓存好。所以如果你发现查询视图比直接写SQL慢多数情况不是视图的锅而是视图背后的基表缺少合适的索引或者视图写得太复杂关联了太多意义不大的中间结果集。那什么时候视图能看起来变快主要是这几种间接情况视图把复杂查询封装后你无意间让优化器获得了更准确的谓词下推空间执行计划优化得更好视图内部设计成了物化视图如果YashanDB版本支持提前计算并存储了汇总结果你在视图对应的基表上建了合适的索引访问路径变好了所以与其指望视图加速不如把精力放在视图底层SQL的写法、基表索引和统计信息上。视图给你的更多是逻辑复用和权限控制的价值而不是性能价值。9. 从诊断到优化的完整套路我的YashanDB提速清单最后分享一套我自己在项目里反复使用的提速排查清单遇到查询慢直接照着做能省掉不少走弯路的时间。排查顺序是这样的确认是不是真慢看慢查询日志或监控排除网络抖动、客户端渲染、中间件排队等问题拉执行计划确认走了什么访问路径有没有全表扫描、排序、笛卡尔积核对WHERE条件逐一检查索引列有没有函数包裹、隐式转换、前后缀模糊匹配看统计信息新鲜度最后收集时间距离现在多久数据量是否发生了量级变化看等待事件是CPU密集还是IO等待还是锁等待确认并发状况当前活跃会话数多少有没有长事务和临时表堆积设计或调整索引按等值条件先行、范围靠后的顺序设计联合索引评估覆盖索引收益改SQL写法深分页改游标分页大批量IN查询拆分OR改写成UNION ALL等这套流程看着简单但每一条背后都有实际的翻车案例支撑。我在YashanDB上真正体会最深的一件事是大部分慢查询的根本原因不是数据库不行而是SQL表达和索引设计与优化器的运行机制没有对齐。只要理解了优化器的心智模型YashanDB的性能完全能跟成熟商业数据库掰手腕。如果你自己也遇到了类似的迁移后变慢、时快时慢、加索引无效这类问题按上面的顺序排查一遍基本能定位到八成以上的根因。剩下的两成可能在等待事件里藏着也可能在统计信息收集的细节里。那些问题就需要针对具体业务再做更细致的分析了。