上个月帮一个客户排查从Oracle迁移到YashanDB后的性能问题,他们的业务量不算夸张,单表几千万行,但一个订单汇总查询跑了十几秒,页面超时告警不断。越排查越发现,很多性能问题不是YashanDB本身慢,而是迁移时把旧库的“使用习惯”也带过来了——索引建法不合理、SQL还是逐行写、统计信息没人管。整理了一下,我这边实际验证有效的技巧一共5个,从索引、批量操作、内存参数、分区表到慢SQL排查都有,给正在用或准备用YashanDB的同仁做个参考。
1. 索引设计:从执行计划反推索引真正生效的条件
1.1 先学会看YashanDB的执行计划
很多人建索引全凭感觉,哪个字段用得多久建哪个,结果查询还是慢。我先说一个原则:所有索引调整都以执行计划为准。YashanDB的优化器思路和Oracle比较接近,你可以用EXPLAIN PLAN查看SQL的执行路径,核心关注两点:语句最终是走了索引范围扫描还是全表扫描,以及每一步返回的数据量估算是否合理。
那次排查就发现一个典型问题。业务SQL写的是:
select * from order_info where to_char(create_time, 'yyyy-mm-dd') = '2024-01-15';create_time上有索引,但执行计划显示走了全表扫描。原因很简单:对索引列做了函数转换,索引就失效了。to_char把create_time变成了字符串,优化器无法直接拿这列去匹配索引范围扫描。改成下面这种写法,索引立即可用:
select * from order_info where create_time >= to_date('2024-01-15 00:00:00', 'yyyy-mm-dd hh24:mi:ss') and create_time < to_date('2024-01-16 00:00:00', 'yyyy-mm-dd hh24:mi:ss');我见过不少从Oracle迁过来的开发同事,都喜欢用trunc、to_char这类函数包着索引列。YashanDB的优化器虽然做一些改写尝试,但最稳妥的做法还是直接在SQL层面消除函数包裹。
1.2 组合索引的列顺序:等值条件在前,范围条件靠后
再聊一个容易被忽视的问题:组合索引列顺序。很多人的习惯是“哪个字段用得多就放前面”,这在混合查询场景里并不对。
我建议的排序规则是:先放等值查询字段,再放范围查询字段,最后放排序或分组字段。举个例子,订单表经常这样查:
select * from order_info where status = 1 and create_time >= sysdate - 7 order by pay_time;如果索引建的是(create_time, status),那么status的等值过滤就没法在最前面缩小范围。更合理的索引是(status, create_time, pay_time)。这种情况下,status直接锁定目标数据子集,create_time再收缩范围,pay_time已经预排序,连排序操作都能省掉。
有时候需要同时跑两批SQL,一批按时间查,一批按状态查。你可以建两个索引来分别支撑最核心的两条路径,而不是试图用一个索引包打天下。组合索引超过3个字段之后收益就明显下降,维护成本还高。
1.3 一个实战案例:从7秒到0.2秒
客户有一个对账查询,单条SQL如下:
select account_id, sum(amount) from transaction_flow where biz_date = '2024-04-10' and channel_code in ('01', '02', '03') group by account_id;原始表上只有主键索引,biz_date和channel_code完全没有索引,执行计划必然是全表扫描。我加了组合索引(biz_date, channel_code),业务侧同时把biz_date的字符串条件改成date类型比较。改造后查询从7秒直接掉到0.2秒,执行计划里出现了索引范围扫描。
这里有个经验:创建索引后一定要去核实执行计划是否真的变了。有时候因为统计信息没更新,优化器宁可全表扫描也不用新索引。所以建完索引顺手收集一下统计信息,不是多余动作。
2. 批量操作:用集合思维改写逐行循环
2.1 逐行处理为什么是性能杀手
很多存储过程迁移到YashanDB后性能反而变差了,最常见的原因是逐行处理逻辑。比如在PL/SQL风格的过程里写一个循环,一条一条update,一条一条insert。这种写法在数据量小时没感觉,一旦涉及几万、几十万行,SQL解析次数、网络交互次数、日志写入量都会成倍放大。
我打个比方:逐行处理相当于搬家时一件一件搬,比如一次性把当天所有的订单状态从“待支付”改成“已支付”,就应该用一条update搞定,而不是循环几千次。
2.2 三种批量改写模式
第一种,批量插入。原来可能是一行一行insert,改成一次多行插入:
insert into order_batch (order_id, product_id, amount) values (1, 101, 12.50), (2, 102, 18.00), (3, 103, 25.00);第二种,批量更新,尤其是需要根据另一张表做匹配更新时,用MERGE INTO可以避免循环加判断:
merge into order_info t using settle_result s on (t.order_id = s.order_id) when matched then update set t.status = s.status;第三种,分批提交。一次性处理一百万行会导致回滚段过大和锁竞争,我的习惯是每5000行左右提交一次,既减少单次事务的日志压力,也让锁持有时间更短。这个批量值没有绝对标准,如果单行数据较大,就改成1000行一批。
2.3 事务粒度与一致性取舍
批量操作必须想清楚事务边界:是全部成功才算成功,还是可以分批成功。如果要求强一致,就不能拆成多个提交;如果只需要最终一致,拆批没问题。这个取舍直接影响回滚日志量和锁等待。
我在YashanDB上还踩过一个坑:批量update超大批量数据时,直接一条update一旦中间失败,整个事务回滚的时间非常长。后来改成按主键范围分批update,比如每次更新10万条主键范围内的数据。这样即便某批失败,也只回滚那一批。
提示:批量操作在高峰期执行要特别注意锁等待。别在业务最忙的时间跑大批量update,很容易把其他会话的查询都堵住。如果必须跑,可以放到夜间窗口。
3. 内存与并发参数调优:先看指标再动参数
3.1 YashanDB内存结构的基本认知
YashanDB的内存模型和Oracle有相似之处,也分为共享区、会话区和日志缓冲区几大部分。共享区存放SQL解析结果、执行计划、数据字典缓存;会话区存放排序、哈希连接运算等中间结果;日志缓冲区负责redo日志的暂存。理解这些,才能知道参数调大后到底缓解了哪部分压力。
我遇到很多同行上来就问“内存参数配多大合适”。说实话,没有固定答案,必须看监控指标。盲目把各类内存上限调得很高,结果是内存浪费,甚至物理内存不够引发交换,性能反而崩。
3.2 三个关键指标的判断方法
我先看三个指标:
- SQL解析命中率。如果同一个SQL反复被硬解析,说明共享区可能偏小,或者系统里大量SQL没有使用绑定变量。
- 缓冲区命中率。这个指标反映需要访问的数据块有多少能从内存中直接拿到。命中率长期低于90%,才需要考虑扩大数据缓冲区。
- 排序落盘比例。如果排序操作大量落到磁盘,说明会话区的排序内存不够,典型表现是涉及order by或group by的大查询变慢。
判断原则是:命中率只是参考,不是越高越好。命中率99%也可能是因为SQL全走小结果集,命中率85%也不代表配置有问题,要看趋势和业务类型。
3.3 并发锁等待的排查与缓解
还有一个容易被忽略的环节是锁等待。业务高峰期偶尔出现几条SQL卡住不动,多数不是CPU瓶颈,而是会话之间互相阻塞。我处理过的一个case:两个事务分别更新了记录A和B,然后又互相申请对方持有的记录,结果形成死锁等待,等了30秒才超时。
排查方式是查当前锁定等待的会话,找到持锁的会话id和对应SQL,然后让持锁方提交或回滚。治本的方法是缩短持锁时间:小事务快速提交、批量操作分批执行、避免一个事务里做太多非必要查询。YashanDB的死锁检测机制会自动回滚掉代价较小的事务,但业务侧如果能在代码里统一加锁顺序,就不会走到死锁那一步。
4. 分区表与并行查询:千万级数据量的加速引擎
4.1 分区键怎么选
YashanDB支持范围分区、列表分区、哈希分区以及组合分区。分区键选得好,查询会自动裁剪掉不需要访问的分区,效果接近“换了张小一点的表”。
我的选型经验按业务类型分三类:
| 业务类型 | 推荐分区方式 | 分区键示例 |
|---|---|---|
| 流水/日志数据 | 范围分区 | create_time(按月/按天) |
| 业务状态/区域数据 | 列表分区 | channel_code、province |
| 无明显范围但数据量大 | 哈希分区 | order_id |
流水类数据最常用按月范围分区。查询某一天的数据时,优化器会自动确定只扫对应分区,数据量直接从几千万降到几百万。
4.2 分区裁剪的效果验证与生命周期管理
分区建好之后,要通过执行计划确认是否真的发生了分区裁剪。如果执行计划里显示扫描了全部分区,说明查询条件里对分区键的处理有问题,比如对分区键做了运算或隐式类型转换,又会导致失效。
分区表还有一个额外收益:数据生命周期管理特别方便。比如只需要保留最近6个月流水,每个月过了时间就直接:
alter table transaction_flow drop partition p_202401;这比delete from千万行快几个数量级,而且释放的空间立即可用。做这类操作前最好备份分区数据,防止误删需要追溯。
4.3 并行查询的适用场景与避坑
并行查询不是银弹,它通过多线程协作把一个大任务拆开。在YashanDB里最常见的用法是给大表全扫、大聚合查询加并行提示:
select /*+ parallel(t, 4) */ count(*), sum(amount) from transaction_flow t where biz_date between to_date('2024-04-01', 'yyyy-mm-dd') and to_date('2024-04-30', 'yyyy-mm-dd');不过我之前在实际运营中就遇到过使用并行查询后系统CPU突然飙升的案例。原因是高并发OLTP系统里一条普通的小查询也被加了并行提示,本来一次执行几毫秒的任务,现在要调度多个并行进程,开销反而更大。并行度设置原则是:大任务才可以并行,小查询不要碰。我的经验是单次处理数据量低于100万行时,并行查询收益通常不明显;高于这个量级,并行度设置为2到4比较安全,后续再根据CPU负载调整。
5. 慢SQL排查:建立一套可复用的优化流程
5.1 统计信息过期会让执行计划“跑偏”
YashanDB的优化器依赖统计信息来估算行数和成本。如果表数据量在急剧变化,统计信息没跟上,优化器就可能选错执行计划,比如明明有索引却走全表扫描。
我自己的巡检习惯是:每次表数据量变化超过20%,就重新收集统计信息。尤其是批量导入、定时清理这类操作之后,一定要补一下:
analyze table transaction_flow compute statistics;如果你不想每次手动执行,也可以开启自动收集任务,在低峰期定期跑。但自动收集的调度频率要合理,太频繁消耗资源,太稀疏又跟不上数据变化。
5.2 绑定变量是OLTP系统的硬解析克星
排查慢SQL时有一条非常重要的线索:同一个业务接口每次请求都生成不同的SQL文本,那场景就麻烦了。例如:
select * from order_info where order_id = 100001; select * from order_info where order_id = 100002;这两个SQL文本不同,优化器会当成两条全新SQL处理,每次都要做硬解析,共享区就被大量无意义的解析信息占满,最终影响整体并发能力。正确的写法是使用绑定变量:
select * from order_info where order_id = :order_id;绑定变量让不同入参共用同一个执行计划,YashanDB只需要解析一次,后续都走软解析,能省下大量CPU时间。我发现这个坑在从Oracle迁移过来的系统里特别常见,因为很多旧代码是拼接SQL生成的,迁移时顺手把习惯也带过来了。
5.3 SQL改写优先,Hint最后再用
面对一条慢SQL,我有固定的处理顺序:先看执行计划,再确认统计信息是否最新,然后考虑SQL改写,最后才轮到Hint介入。
Hint是给优化器“下命令”,比如强制走某个索引、强制用并行。但Hint是一把双刃剑:短期内能压住慢查询,长期看只要数据分布变化,就会越来越不合理。我记得有次为了快速止血,给一条SQL强行指定了索引,结果一个月后数据量涨了不少,那个索引的选择率变差,反而比全表扫描更慢了。
所以在YashanDB上我的建议是:能改SQL就不加Hint,能更新统计信息就不动优化器决策。如果实在要加Hint,记得做备注说明:这条SQL为什么加,什么时候加的,便于后人重新评估。
最后分享一个我常年坚持的工作习惯:每个月底对数据库做一次例行体检,把慢SQL清单拉出来跑一遍,看执行计划,看统计信息新鲜度,看索引使用频率。很多问题都是提前发现的,而不是等到业务告警才排查。YashanDB的生态还在快速完善,但它的优化器思路和SQL标准兼容性做得不错,只要遵循这些基础优化原则,处理千万级数据完全没有问题。如果你在实际项目里碰到YashanDB性能相关的问题,欢迎多交流,大家一起填坑。