
前阵子帮一个客户做 YashanDB 的性能应急现场情况很典型数据库响应突然变慢值班同事凭经验把内存类参数调大了一倍重启之后不仅没变快连业务超时告警都变多了。后来坐下来一查发现瓶颈根本不在内存而是一条关键 SQL 的执行计划走了低效路径——8 秒的查询被拖成了 30 秒。这件事让我一直想写一篇总结数据库性能优化十有八九不是靠调参堆出来的而是靠看清问题在哪一步步理出来的。这篇文章围绕 YashanDB 整理了我最常用的 5 种性能优化思路分别是 SQL 执行计划、索引设计、参数配置、并发锁策略、数据模型与存储布局。别看方向挺多顺序很重要动手之前必须先做第 0 步——把性能基线和量化指标立起来否则后面每一步都是拍脑袋。无论你是 DBA、后端开发还是架构师只要系统里跑着 YashanDB这 5 种思路都能直接落到你的日常排障里去。1. 先立靶子没有量化指标的调优都是碰运气1.1 为什么大多数性能优化最后变成了玄学很多团队遇到性能问题第一反应永远是加内存把并发调大重启试试。这些操作不是不能做而是缺少一个前提你到底想让什么指标变好现在的吞吐量是多少目标是多少改动前后差了多少性能优化的本质其实是定位瓶颈→提出假设→验证假设→量化结果的循环。没有基线你连方向对不对都无法判断。我在现场见过太多案例有人觉得慢查询日志里的某条 SQL 用得最多就埋头去优化它结果优化完后整体响应时间几乎没变化——因为那条 SQL 虽然执行次数多但单次只要 5 毫秒真正拖垮系统的是每天凌晨跑一次、要跑 40 分钟的批处理。这就是典型的不先量化就动手。在 YashanDB 上做性能优化第一步永远不是优化而是记录。你要先回答这几个问题业务高峰期平均响应时间是多少p95 和 p99 是多少慢查询 Top 10 是哪几条锁等待发生的频率和等待时长是多少数据库 CPU、IO、内存的使用率有没有明显异常这里特别提一下 p95/p99因为平均值真的会骗人。100 个请求里 99 个只要 100 毫秒1 个要 5 秒平均下来是 149 毫秒看起来好像不算慢但那个 5 秒的请求可能直接让用户超时重试进而产生更多压力。所以建立基线时重点关注长尾指标而不是只盯着平均值。1.2 YashanDB 里快速采集基线的几个入口很多朋友问那这些数据从哪里拿我自己的习惯是三个入口配合使用不用全上按现场条件取舍。第一个入口是 YashanDB 的动态性能视图也就是 v$ 打头的那一批。它们和 Oracle 系的视图体系很接近能看当前会话、SQL 执行统计、等待事件、系统级累计统计等。最常用的场景是业务卡住时先看当前活动会话在等什么资源再顺着 SQL_ID 去翻对应 SQL 的执行统计基本能把问题会话定位出来。第二个入口是慢查询日志和审计日志。慢查询日志能直接给你哪些 SQL 超过了预设阈值这是最粗颗粒度但也最直观的筛选方式。建议把阈值设置得比业务容忍线更低一些宁可多捞一些候选也别漏掉问题 SQL。第三个入口是 YashanDB 自带的图形化管控平台如果部署环境里有的话。它能直接展示趋势图、会话列表和资源曲线比手敲 SQL 查视图要省事得多。我通常会用图形平台看整体趋势再用动态性能视图下钻定位到具体 SQL。拿到这些数据之后把它们汇总成一张基线表。表不用很复杂但至少要有下面这些字段类别指标说明吞吐TPS / QPS每秒事务数/查询数判断整体压力延迟平均响应时间 / p95 / p99尤其关注 p99 的长尾延迟SQL慢查询 Top 10次数×耗时的乘积排序而非只看单次耗时资源CPU / IO / 内存使用率用于判断瓶颈在计算、存储还是内存缓存数据页缓存命中率命中率长期过低说明内存配置和访问模式不匹配并发活跃会话数 / 锁等待次数判断是否存在大量阻塞和锁竞争1.3 建立基线后先排序再动手基线数据拿到手后先别急着优化用次数×耗时给慢 SQL 排个序。一个每天执行 10 万次、每次 200 毫秒的查询和一个每天执行 10 次、每次 30 秒的报表谁对系统的伤害更大显然是前者因为它累计要占掉 5.5 个小时的数据库执行时间而后者只有 5 分钟。我曾经接手过一个 YashanDB 项目当时线上系统每到下午就开始大面积卡顿。原始数据显示平均响应时间从 80ms 涨到 500ms大家一直在查哪条 SQL 最慢结果发现最慢的那条单次要 8 秒但每天只跑十几次根本不是元凶。后来按次数×耗时重排才发现是一条订单状态查询——单次只有 150ms但每秒要执行 200 多次因为少建了一个索引每次都要全表扫。这就是先排序再动手的价值你优化的对象应该是影响面最大的那部分而不是单次耗时最吓人的那部分。2. 思路一从执行计划入手向 SQL 要性能2.1 为什么 SQL 优化是性价比最高的一步SQL 是数据库最直接的工作指令。你告诉数据库要什么数据库通过优化器决定怎么拿。同一个需求不同的写法、不同的访问路径性能可能差出一个数量级。而在所有优化手段里SQL 层面的调整通常是最快见效、最不需要重启服务、风险也相对可控的。关键在于通过 EXPLAIN 拿到执行计划看数据库实际是怎么执行这条 SQL 的。我经常打一个比方SQL 是你要去的目的地执行计划是司机选的路线。司机绕路了你光催油门没用先让他换条路线才是正解。在 YashanDB 上查看执行计划的入口很直接像主流数据库一样用EXPLAIN关键字就能把执行路径打出来。拿到执行计划后重点看三个东西表的访问方式全表扫描还是索引扫描、表之间的连接方式嵌套循环、哈希连接还是排序合并、以及连接顺序谁是驱动表谁是被驱动表。这三件事决定了这条 SQL 90% 的代价。2.2 执行计划里的危险信号下面这几个执行计划特征是我在排查 YashanDB 性能问题时最常遇到的看到基本可以判定 SQL 需要动手术大表全表扫描几百万行的表没有走索引每次查询都把所有数据页读一遍。如果业务对响应时间有要求这几乎永远是第一嫌疑人。嵌套循环连接方向倒挂理想的嵌套循环是小表驱动大表即外层结果集小、内层通过索引点查。如果反过来了外层是大结果集内层每次都全表扫这个 SQL 基本就废了。不必要的排序看到执行计划里出现大的排序操作要确认是否真的需要排序。很多时候是DISTINCT或ORDER BY不够严谨或者连接过程产生了大量中间结果需要去重。索引条件失效SQL 里对索引列做了函数运算或隐式类型转换优化器会放弃使用该索引。这类问题特别隐蔽因为 SQL 看起来完全正常。过滤条件下推不彻底先关联后过滤把大量无关行数提前放大导致中间结果集膨胀后续每一步都更慢。我把这些信号称为执行计划的体检项。你不用一次看全先找代价最大的节点然后顺着它往上游看基本能定位到慢的根因。2.3 一个三表关联从 8 秒到 0.5 秒的真实改造说一个实际案例。业务需求是统计 2025 年第一季度各客户的订单总金额涉及三张表订单表 orders、客户表 customers、订单明细表 order_items。最初的 SQL 长这样SELECT c.customer_name, SUM(oi.amount) AS total_amount FROM orders o JOIN customers c ON o.customer_id c.id JOIN order_items oi ON o.id oi.order_id WHERE o.order_time BETWEEN DATE 2025-01-01 AND DATE 2025-03-31 GROUP BY c.customer_name;这条 SQL 单次执行要 8 秒左右。看执行计划发现orders 表在时间过滤后仍然有几十万行而且优化器用这几十万行作为驱动表先去关联 customers再去关联 order_items关联过程中中间结果被放大到几百万行最后才做分组聚合。优化的核心思路是先缩小参与连接的数据量再做连接。对于这个需求正确做法是先按订单维度把金额聚合好再用小结果集去关联客户表避免明细行数被中间过程无限放大。改写后的 SQL 如下SELECT c.customer_name, t.total_amount FROM ( SELECT o.customer_id, SUM(oi.amount) AS total_amount FROM orders o JOIN order_items oi ON o.id oi.order_id WHERE o.order_time BETWEEN DATE 2025-01-01 AND DATE 2025-03-31 GROUP BY o.customer_id ) t JOIN customers c ON t.customer_id c.id;改写后执行时间从 8 秒降到了 0.5 秒。为什么差别这么大关键在于子查询先把每个客户消费了多少算完结果集只有几千行再去关联客户表时数据库只需要做极少量数据的连接操作。而原来的写法是先关联再聚合中间结果被订单明细表放大聚合压力全部压在最后一步。这个案例也说明了一个常见误区并不是SQL 越短越好也不是表连接越少越好而是中间结果集越小越好。你写的只是最终需求数据库执行的却是整个数据流水线流水线里每一步的数据体积决定了这条 SQL 的生死。3. 思路二索引设计——用最少的索引覆盖最多的查询3.1 从访问路径倒推索引结构如果说 SQL 改写在帮数据库少干活那索引就是在帮数据库抄近路。很多人对索引的理解停留在查询慢就加索引但索引到底怎么建、建在哪几列、列的顺序怎么排这里面有不少讲究。核心原则是根据你高频查询的过滤条件倒推索引结构。比如一个订单查询条件是这样SELECT * FROM orders WHERE status PAID AND create_time DATE 2025-01-01 AND create_time DATE 2025-02-01;这种等值加范围的查询模式很典型。建复合索引时等值条件的列放前面范围条件的列放后面。所以这里推荐(status, create_time)而不是(create_time, status)。原因在于 B 树的特性等值列放在前面可以在索引内先精确缩小到一个区间然后在这个区间内按时间范围扫描反过来放create_time 的范围条件先消耗了索引最左前缀status 就无法参与索引内的进一步过滤只能在范围内逐个比对。再补一条原则选择性高的列尽量放前面。所谓选择性就是某一列不同取值的占比。比如 status 只有PAID和UNPAID两个值选择性就很弱customer_id 如果分布广选择性就强。高选择性的列放前面能更快把索引范围收窄。3.2 覆盖索引与回表的取舍覆盖索引是性价比很高但又容易被忽视的技巧。普通索引扫描到目标行之后还需要拿着主键回到数据表里取其他列这一步叫回表。回表次数多了性能损耗也不小。如果索引本身包含了查询所需的所有列数据库只需要扫索引完全不用回表这种索引就叫覆盖索引。举个例子业务上经常要查已支付订单的 ID 和金额SELECT id, amount FROM orders WHERE status PAID AND create_time DATE 2025-01-01;如果只建(status, create_time)索引那么查询满足条件时索引里能拿到 id主键会附带在索引上和 create_time但 amount 还得回表取。如果建一个(status, create_time, amount)的覆盖索引amount 也能直接从索引里取一次索引扫描解决问题。但覆盖索引不是没有代价。索引列越多索引体积越大占用的内存和磁盘空间也就越多写入时需要维护的索引结构也越重。我的建议是覆盖索引只针对高频的、业务核心的查询做不要试图让一个索引覆盖所有查询否则索引膨胀带来的写入开销会抵消查询收益。3.3 索引维护的隐性成本与统计信息索引的真正代价往往要在写入时才会体现。每次 INSERT、UPDATE、DELETE数据库都要同步维护所有相关索引。一张表如果有 5 个索引写一次数据等于同时写 5 份目录索引太多批量写性能会肉眼可见地下降。所以不要抱着多建几个索引总有好处的心态索引是给查询用的不是给表撑场面的。这里要特别提醒一个隐蔽的坑索引建了不代表优化器会用。统计信息过期时优化器对数据分布的判断会严重失真明明有索引它可能觉得全表扫描更快结果就是不选。这类问题在 YashanDB 上也遇到过。常规的做法是周期性收集统计信息尤其是在大量增删改之后。你可以在业务低峰期手动触发统计信息收集也可以配置自动维护策略确保优化器手里的数据分布情报不过期。另外日常排查时可以留意一下执行计划里有没有出现索引失效的隐形操作对索引列做函数计算、类型隐式转换、前导通配符的 LIKE 查询这些都会让索引直接失效。这类 SQL 往往要回改代码把函数运算移到等号另一端让索引列保持干净。4. 思路三参数调优——不是照抄最佳实践就能躺赢4.1 参数影响的是资源分配不是速度很多人对参数调优寄予厚望总觉得哪里有个隐藏开关打开之后数据库就飞快了。实际情况是参数调优确实有用但它优化的是资源分配方式而不是凭空产生更多算力。数据库的总资源就摆在那里你要做的是让内存、CPU、IO 更匹配当前负载的特征。YashanDB 的参数体系大致可以分成几类控制数据页缓存大小的内存类参数、控制最大连接和并行度的并发类参数、控制日志落盘策略的 IO/日志类参数、影响优化器行为的优化器类参数。不同版本的具体参数名可能有差异我在现场通常先按类别去定位再去翻对应版本的官方文档确认精确名称。我在评估参数合理性时最关心的是缓存命中率、活动会话数、并行度使用率和日志落盘频率这四类指标。记住参数值不是越大越好也不是越小越好而是和你的业务负载匹配才是最好。4.2 OLTP 与 OLAP 负载的参数取向差异参数调优最核心的一点是搞清楚你的系统是 OLTP 类型还是 OLAP 类型因为两者的参数取向几乎是反的。负载类型典型场景参数取向OLTP订单、交易、登录、支付小事务多、高并发、低延迟重缓存低并行连接数控制严格OLAP报表、数仓、聚合分析大查询多、吞吐优先可接受较高延迟适合增大并行度和排序内存如果一套配置同时跑两类负载往往要找到一个折中点或者通过资源组、会话级参数把两类负载隔离。曾经有个客户把 OLTP 和报表跑在同一套 YashanDB 集群上报表任务一到订单支付的响应时间就飙到 1 秒以上典型的资源抢占问题。后来把报表会话的并行度限制到低档位同时在高峰期错峰调度订单支付才恢复正常。4.3 单变量调参的完整流程我见过太多人改参数是一次改五六个重启完看天吃饭。这种做法最致命的地方在于如果效果变好了你根本不知道是哪个参数起的作用如果变差了你也不知道该回滚哪个。我的调参流程是固定的四步备份当前参数配置记录优化前的基线指标只修改一个参数观察一段时间或跑一轮压测再对比基线决定保留还是回滚。等这个参数确认稳定了再去动下一个。虽然看起来慢但每一步都有据可查出了问题也能精准回滚。在观察周期上我建议参数生效后至少压测 15 分钟以上或者在业务低峰期观察一个完整波峰周期。不要改完看两分钟没异常就宣布成功——数据库的很多资源问题是压力持续累积后才暴露的。4.4 一个把并行度调爆的翻车案例说一个我踩过的坑。有一回给一个 OLTP 系统做调优客户抱怨报表查询慢我把并行度参数直接拉高心想并行查询总比串行快。重启后白天高峰期系统 CPU 利用率从平时的 30% 直接飙到 95%报表确实变快了一点但订单支付这条核心链路的平均响应时间翻了快三倍最后只能紧急回滚。事后分析原因其实不复杂并行查询确实能加速单个大查询但它会同时启动多个并行 worker每个 worker 都要占用 CPU 和内存。在 OLTP 混合负载下大量小事务本来就对 CPU 非常敏感你把 CPU 抢占走了大家当然一起慢。这个案例给我的教训很深刻参数调优必须考虑全局负载而不是只看某一个慢查询。所有最佳实践值都只能作为起点不能作为终点。5. 思路四并发与锁策略——把争抢变成流水线5.1 锁等待CPU 不高但系统就是慢有一个非常典型的假象数据库 CPU 使用率只有 20%但所有业务的响应时间都在持续走高。很多人盯着 CPU 和 IO 看半天找不到原因其实问题出在锁竞争。想象一个柜台只有一个人能同时办理业务其他人都得排队等。数据库的行锁就是这样一个事务持有了某行的锁其他事务要修改或锁定同一行只能等待。如果这个事务迟迟不提交队列就越排越长。表现到外部就是系统忙而不动——CPU 不高活动会话却大量阻塞日志里全是锁等待事件。在 YashanDB 里排查这类问题通常先看活动会话的状态和等待事件如果发现大量会话卡在锁等待上再顺着当前锁的持有者找长事务。很多时候锁的源头就是一条忘了提交的事务或者一个半夜跑批的报表任务把大量数据行都锁住了。5.2 缩短持锁时间分批更新的实战操作锁竞争的本质是持锁时间太长所以优化的核心方向不是消灭锁而是把持锁窗口缩短。举个例子业务需要把 2024 年以前的订单全部标记为已归档。直觉写法是UPDATE orders SET status ARCHIVED WHERE create_time DATE 2024-01-01;这条 SQL 在数据量大时会发生什么它会在事务内更新所有满足条件的行持续持有大量行锁直到整个更新结束才提交。执行期间任何要修改这些订单或相关表数据的业务请求都会被堵住。优化思路是分批更新每一批只锁一小部分行提交后再处理下一批for start in range(0, max_order_id, 1000): execute( UPDATE orders SET status ARCHIVED WHERE id BETWEEN ? AND ? AND status ARCHIVED , start, start 999) commit()每批只动 1000 行锁窗口从十几分钟缩短到毫秒级其他事务可以在批次之间正常执行。实测中这个改动能让整个批处理的任务从业务不可用变成业务无感知。但要注意分批提交会破坏单事务的原子性——如果中间某批失败需要记录断点做续跑不能无脑重头执行。5.3 隔离级别与锁粒度选择隔离级别是很多人容易忽略的并发调优点。可重复读RR模式下事务需要维护更复杂的快照信息锁的使用也更严格读已提交RC模式下事务只读取已提交的数据锁的持有范围通常更小。对于大部分业务系统RC 级别已经能满足需求而且并发性能更好。如果业务没有明确要求可重复读语义可以考虑把隔离级别从 RR 调整为 RC。还有一个常见坑是SELECT ... FOR UPDATE。有些开发图省事查询时顺手把主键范围锁住结果锁住的不是单行而是几千行。正确做法是尽量让 WHERE 条件精确命中需要修改的那几行或者干脆用版本号做乐观锁让冲突检测在更新时通过条件判断完成而不是提前把所有可能涉及的行都锁死。5.4 热点行拆分的流水线思路有些场景下即使事务短小也会因为某一行被高频读写而产生排队。最典型的场景是库存扣减大家都在抢同一个 SKU 的库存数字本质上是把高并发请求全部压到一行更新上。这时候可以把单行计数拆成多个库存桶每个桶独立扣减比如预分配 10 个桶每个桶初始库存 100 件扣减时随机选一个还有余量的桶最后查询时再聚合各桶之和。这样单个热点行的锁竞争就变成了多行间的并发处理性能会有非常明显的提升。这就是我标题里说的把争抢变成流水线当多个事务不再争抢同一把锁而是各自处理自己所在的桶并发能力自然就上去了。6. 思路五数据模型与存储布局——性能的上限在建模阶段就决定了6.1 分区表让查询只走该走的那段路SQL 和索引优化是在已有的表结构上做文章而数据模型优化是在更靠前的位置决定性能上限。很多性能问题从建表那一刻起就注定了。大表最常用的优化手段是分区。比如订单表按时间范围做分区CREATE TABLE orders ( order_id INT, customer_id INT, order_time DATE, ... ) PARTITION BY RANGE (order_time);查询时如果带上时间条件数据库可以通过分区裁剪直接跳过无关分区只扫描必要的那几段数据。这就好比一本 1000 页的书你查第一季度只需要翻前 100 页而不是把整本书从第一页翻到最后一页。对于大表而言分区带来的收益往往是数量级的。选择分区策略时要看业务查询的过滤维度有明显时间维度的用范围分区按地域或类型过滤多的用列表分区数据分布不均匀、想让数据更均衡分散的可以用哈希分区。分区不是越多越好分区数量过多反而会让元数据管理和 SQL 解析的开销变大通常单表几十到几百个分区都是合理区间。6.2 行存与列存/压缩的取舍YashanDB 这类支持混合存储的数据库允许不同表按访问模式选择行存还是列存。行存适合频繁的等高值点查和小范围更新因为一条记录的所有字段都集中在一起取出来就完事列存适合大范围聚合分析因为只需要读取参与计算的少数几个列扫描时能大幅减少 IO。选择存储模型时别只看单张表要看这张表的访问模式。订单流水是典型的行存场景报表汇总用的聚合明细表则更适合列存。另外还要考虑压缩策略压缩能显著减少存储和 IO但压缩和解压本身要消耗 CPU。数据很少更新、只读查询多的表大胆开压缩高频插入更新的表压缩收益有限CPU 开销却很实际。6.3 冷热分离与生命周期管理删数据也是优化很多性能问题不是查询写得差而是数据堆得太多了。一张表 1 亿行其中 95% 是超过一年的历史数据但这些数据几乎不会被业务访问。它们躺在表里不仅占用空间还会让索引越来越大、缓存命中率越来越低、统计信息越来越失真。我在 YashanDB 项目里比较常用的方案是把数据按时间维度分层。近 30 天的热数据保留在高性能在线存储30 天到 90 天的温数据放到普通存储分区超过一年的数据归档到独立的只读分区或离线表。需要查历史时定向去归档分区查平时业务查询只需要扫热分区整体性能会有质的提升。这个方案的关键在于建模阶段就预留分区和归档路径。如果在建表时没做设计后来再改分区迁移成本会很高。所以我一直强调架构师在画表结构的那一刻就已经决定了未来性能优化的天花板。SQL 优化是在这个天花板下面腾空间而数据模型优化是直接把天花板抬起来。做了几年 YashanDB 性能优化我的体会是这 5 种思路不是并列选项而是有先后顺序的。先把第 1 章的立靶子做扎实用基线数据明确目标和影响面然后优先从 SQL 和索引下手因为大多数性能问题都出在这一层参数和并发锁更像是放大器——根因没找对盲目放大只会让问题更严重最后才是数据模型层的调整因为它牵涉面最广、改动成本最高。最后再分享一个小习惯我会在每次调优后把「日期、SQL/参数、基线指标、改动项、结果」记到一张表格里。这看起来笨但三个月后你会发现它比任何监控面板都好用——因为你记录的不是数字而是你当时做判断的依据。希望这 5 种思路能帮你在 YashanDB 上少踩几个我当年踩过的坑。