数据库存:产品技术团队必看清单:用性能优化推动提升查询性能
数据库查询变慢,最先被感知的通常不是数据库监控上的一条慢 SQL,而是用户打开列表后多等了两秒、销售在高峰期无法查询客户、运营导出报表时页面持续转圈。很多团队的第一反应是“加一个索引”,但我在多次性能排查中发现,真正决定查询优化是否有效的,往往不是某一条 SQL 改得多漂亮,而是团队能否把业务场景、查询路径、数据分布、资源约束和上线验证串成一个闭环。
这也是《数据库存:产品技术团队必看清单:用性能优化推动提升查询性能》最值得重新理解的地方:性能优化不是数据库工程师的单点修复,而是产品技术团队围绕业务结果进行的一次系统治理。本文不把“加索引、少用函数、避免全表扫描”当作结论,而是进一步说明什么时候该优化 SQL,什么时候应调整分页方式,什么时候问题根本不在数据库,以及如何用数据证明一次优化没有把风险转移到写入、锁等待或连接池。
用户点击“查询订单”之后,系统通常要经过网关、应用服务、连接池、数据库、缓存、下游接口和序列化等多个环节。数据库执行耗时只是接口总耗时的一部分。如果接口总耗时从 800 毫秒上升到 3 秒,不能直接断言“数据库慢了”;也可能是连接池排队、应用层循环查询、锁等待或下游服务超时。
我通常会先把一次请求拆成几个时间段:请求进入应用的时间、等待数据库连接的时间、SQL 真正执行的时间、结果集传输时间、应用组装数据的时间,以及响应返回时间。只有把这些阶段分开,团队才能判断优化方向,而不是在没有证据的情况下反复改 SQL。
如果数据库只占接口耗时的 20%,即使把 SQL 优化到原来的十分之一,接口整体也不一定有明显改善。相反,如果数据库执行占比达到 80%,那么索引、分页、执行计划和数据访问方式才可能成为主战场。

数据库团队习惯看平均耗时、慢查询数量和 CPU 使用率,但产品和用户更在意页面加载时间、查询成功率、超时率和高峰期是否可用。因此,查询性能治理至少要同时观察三组指标。
平均耗时尤其容易误导判断。假设 99% 的查询耗时为 100 毫秒,1% 的查询耗时为 20 秒,平均值可能仍然看起来可以接受,但这 1% 的请求很可能集中发生在大客户、复杂筛选或月末报表场景,直接影响关键用户。
我在做性能复盘时,通常先问三个问题:最慢的 1% 请求发生在哪些业务场景?它们是否集中在某个时间段?这些请求的数据库耗时,还是应用排队耗时更高?这三个问题比“最近有没有慢 SQL”更容易找到真正的影响面。
没有优化前基线,就无法证明优化后的结果来自方案本身。尤其是数据库性能会受到缓存状态、数据量、并发度、统计信息、网络和机器负载影响,一次偶然的低延迟不能作为上线依据。
一份最低可用的性能基线,至少应包含以下内容:
性能优化的完成标准不是“执行计划看起来更好”,而是业务接口在真实负载下更稳定,并且没有把问题转移给写入链路。
许多系统在测试环境中只有几万条订单、几千个客户或几个月的明细数据,查询即使走了不理想的路径,也可能在几十毫秒内完成。上线后,数据量扩大到数千万甚至更高,原本被掩盖的全表扫描、回表、排序和大偏移分页才会暴露出来。
这类问题的危险之处在于,SQL 本身没有变化,产品功能也没有变化,但数据规模和分布发生了变化。研发人员容易认为“这条 SQL 以前一直没问题”,DBA 则可能发现执行计划已切换,或者优化器估算行数与实际行数出现较大偏差。
因此,性能测试不能只验证功能正确性,还要验证数据规模和数据分布。测试库中每个状态的记录比例、时间字段的冷热分布、客户等级的集中程度,都可能影响执行计划。只复制表结构、不复制真实分布的数据,通常无法验证线上查询风险。
“支持全部时间范围查询”“允许导出全部结果”“列表可以跳到任意页”“同时展示十几个关联字段”,这些需求从产品角度都很合理,但组合起来会形成明显的数据库压力。
例如,一个销售订单列表同时支持客户名称模糊搜索、订单状态筛选、创建时间排序、金额区间筛选、分页和导出。如果没有限制时间范围,用户可以查询三年的全部订单;如果分页使用大偏移量,数据库就可能先扫描前面大量记录再丢弃;如果每一行还要额外查询客户和商品信息,就可能出现 N+1 查询。
我更倾向于在需求评审阶段就把性能约束写进交互规则,而不是等上线后再由技术团队“优化”。例如,默认查询最近 30 天,单次最多返回 100 条,导出超过一定数量后转为异步任务,搜索条件不足时不允许直接发起全量查询。这些并不是限制用户,而是在用产品设计保护系统容量。
以九数云这类面向业务分析和数据可视化的平台为例,用户可能同时进行多维筛选、交叉分析、趋势对比和报表导出。这类场景的查询通常具有字段多、聚合重、时间范围可变和临时分析需求强等特点,和订单写入、支付扣款等事务型查询的访问模式完全不同。
在这种场景中,技术团队不能只问“这条查询能不能加索引”,还要判断数据是否应该经过汇总、分层、预计算或进入更适合分析的存储结构。把高频明细查询、实时事务写入和复杂报表聚合全部压在同一个数据库实例上,往往不是 SQL 技巧能够彻底解决的问题。
下面的案例数据是情景模拟,用于展示判断过程,不代表九数云官方性能承诺,也不是某个公开客户案例。它模拟一个同时包含业务列表和经营分析报表的系统,重点观察“单次查询耗时”和“整体资源占用”之间的关系。

索引确实是常见且有效的优化手段,但它不是慢查询的通用答案。一个查询是否适合加索引,取决于过滤条件的选择性、排序字段、联合索引顺序、返回字段、数据分布和写入频率。
例如,状态字段只有“启用”和“停用”两个值,单独为它建立索引,未必能显著缩小扫描范围。如果绝大部分记录都是“启用”,优化器可能认为全表扫描更便宜。相反,将状态与租户、时间范围或业务主键组合起来,在真实查询模式下可能更有价值。
索引还会产生持续成本。每次插入、更新或删除,都可能需要维护一个或多个索引;索引过多会增加存储、备份、变更和优化器决策成本。一个只改善低频查询、却让高频写入增加 30% 成本的索引,不能简单称为优化成功。
“使用索引”只是访问路径的一种描述,不代表扫描的数据量足够小,也不代表后续排序、回表和聚合成本可接受。某些查询虽然通过索引定位了入口,但随后仍要回表读取大量记录,最终耗时并没有明显下降。
在分析执行计划时,我会重点看“扫描了多少”和“最终返回多少”的比例。如果扫描 500 万行只返回 200 行,说明过滤条件或索引设计仍有较大改进空间;如果扫描 1 万行返回 9000 行,那么即使加更多索引,收益也可能很有限,因为查询本身就需要处理大量结果。
还要关注估算行数和实际行数的差异。两者偏差较大时,可能是统计信息过期、数据分布高度倾斜、条件相关性未被准确估计,或者查询参数导致计划对某些场景不适用。此时,继续堆索引往往不如先刷新统计信息、调整查询结构或拆分不同访问场景。
一条平均耗时 2 秒、每天执行 10 次的 SQL,和一条平均耗时 80 毫秒、每秒执行 500 次的 SQL,对数据库的影响完全不同。前者可能影响少数报表用户,后者却可能持续消耗 CPU 和连接资源。
我建议使用一个简单的优先级模型:优化优先级 = 单次资源成本 × 调用频率 × 业务影响系数。这个公式不是数据库内置算法,而是用于团队排序问题的工作方法。它能够避免大家只盯着“最慢的一条 SQL”,忽略了高频、低延迟查询对整体资源的长期消耗。
业务影响系数可以根据实际情况设置。例如,支付、下单、登录等核心链路可以设为高权重;后台低频报表设为中等权重;临时运营查询则可以安排在异步化或资源隔离方案中。

缓存可以降低重复读取,读写分离可以分散部分读压力,分库分表可以突破单体存储容量,但这些方案都会引入新的复杂度。缓存要处理失效、一致性和热点穿透;读写分离要处理复制延迟和读到旧数据;分库分表要处理路由、跨分片查询、扩容和数据迁移。
如果问题只是分页方式错误、返回字段过多、连接池配置不合理,直接引入复杂架构可能是在用更大的系统成本掩盖一个可以快速修复的基础问题。我的判断顺序通常是:先确认链路和资源,再检查 SQL 与执行计划,然后处理数据访问模式,最后才评估架构升级。
测试环境可以验证方案方向,但不能完全代替生产观察。线上数据具有真实的租户规模、参数分布、并发波峰、冷热数据比例和异常请求模式,这些因素经常让测试结果失真。
尤其是索引变更和 SQL 改写,应该在接近生产的数据量和分布上压测,并在上线后观察至少一个完整业务高峰周期。若是订单、营销、工资或月度报表等有明显周期性的系统,观察窗口还应覆盖相应的高峰时段。
我会先要求应用团队提供一条完整请求的链路数据,而不是只发来一句“数据库很慢”。最少要知道接口总耗时、等待连接耗时、SQL 执行耗时、返回数据量和调用下游的时间。
如果接口总耗时 5 秒,但 SQL 执行只有 200 毫秒,那么继续优化 SQL 的收益很小。此时应检查连接池是否耗尽、应用线程是否阻塞、是否存在串行 RPC、序列化对象是否过大,以及是否有重试机制放大了请求。
如果 SQL 执行占比很高,再进入数据库内部分析。这样的分层排查能把问题快速分为“应用链路问题”“数据库执行问题”和“容量或并发问题”,也能避免不同团队之间互相甩锅。
单次成本高的查询通常表现为复杂聚合、大范围排序、全量导出、跨多表关联或大时间范围统计;调用频率高的问题则常见于首页接口、列表刷新、权限校验、循环查询和重复读取。
两者的解决方式不同。单次成本高的查询更适合缩小范围、异步执行、预计算、分批处理或转移到分析型存储;调用频率高的问题更适合减少重复调用、批量查询、缓存稳定结果、调整刷新策略或合并请求。
| 问题表现 | 优先观察 | 常见优化方向 | 主要风险 |
|---|---|---|---|
| 单次查询耗时很高 | 执行计划、扫描范围、排序和聚合 | 改写 SQL、缩小范围、索引、预计算、异步化 | 结果口径变化、任务延迟、资源转移 |
| 单次耗时不高但调用量巨大 | 调用次数、重复请求、缓存和接口刷新频率 | 批量查询、缓存、请求合并、降频 | 数据时效性下降、缓存一致性问题 |
| 高峰期突然变慢 | 连接池、锁等待、CPU、IO和并发模型 | 限流、连接池调整、缩短事务、资源隔离 | 吞吐下降、请求排队、业务降级 |
| 数据量增长后变慢 | 数据分布、统计信息、分页、归档和执行计划变化 | 重新设计索引、冷热分层、游标分页、归档 | 历史查询受限、维护复杂度上升 |
一条查询可以被拆成四个关键环节:从哪里扫描数据,经过哪些过滤条件,是否需要额外排序或聚合,最后返回多少数据。这个拆解比单纯阅读 SQL 文本更容易发现成本来源。
例如,列表查询条件包含租户、状态和创建时间,并按照创建时间倒序返回 50 条记录。此时需要检查索引是否能够同时支持租户过滤、状态过滤和时间排序。如果数据库先通过一个低选择性索引找到大量记录,再进行额外排序,那么“用了索引”也无法保证查询足够快。
另一方面,如果查询返回 50 条记录,却带回了几十个大字段,数据库、网络和应用序列化都会产生额外成本。字段裁剪经常是低风险、容易验证的优化动作,但在实际项目中却容易被索引讨论掩盖。
数据库是共享资源。一次读查询优化可能增加索引维护成本,一次批量更新优化可能扩大锁范围,一次缓存改造可能让数据一致性变得复杂。因此,任何优化方案都要同时回答两个问题:它改善了什么?它让什么变得更昂贵?
我会把方案影响分为四类:查询延迟、写入吞吐、存储成本和运维复杂度。只有查询延迟改善而其他三项没有明显恶化,才适合直接上线;如果某项成本增加,则需要明确业务是否接受,以及是否有补偿措施。

任何列表、搜索和报表查询,都应明确最大时间范围、最大返回条数和最大并发。没有边界的查询,迟早会在数据增长或用户误操作时变成资源消耗源。
对于实时列表,我通常建议默认带有时间范围或业务范围,并限制单页返回数量。对于历史数据查询,可以引导用户先选择组织、客户、状态或时间条件;对于导出,则应转为异步任务,并将任务放入独立队列,避免用户点击一次就占满数据库连接。
“返回全部字段”也应谨慎。详情页可能需要较多字段,但列表页通常只需要展示字段。将大文本、JSON、图片地址集合和扩展属性从列表查询中剥离,往往比继续增加索引更直接。
过滤列上使用函数、表达式或隐式类型转换,可能使数据库无法直接利用索引。例如,对时间列进行日期格式化后再过滤,或者把数值列与字符串参数比较,都可能导致额外计算或访问路径变化。
更稳妥的做法是把条件改写为范围表达式,让数据库可以直接使用原始列的有序性。下面是一个通用示意,实际语法仍需根据数据库类型和字段类型验证。
-- 不建议:对时间字段进行函数计算 SELECT id, customer_id, amount FROM orders WHERE DATE(created_at) = '2026-09-16'; -- 更适合验证索引利用情况的写法 SELECT id, customer_id, amount FROM orders WHERE created_at >= '2026-09-16 00:00:00' AND created_at < '2026-09-17 00:00:00';
这并不意味着所有函数都必然导致性能问题。某些数据库支持函数索引、表达式索引或生成列,关键在于确认执行计划和真实耗时。优化建议必须与数据库版本、字段类型和索引能力结合,不能把一条经验规则当作绝对定律。
联合索引不是把所有查询字段简单拼接在一起。设计时至少要考虑过滤条件、排序条件、连接条件、字段选择性和查询覆盖范围。
假设系统最常见的查询是“按租户查询最近一段时间的已完成订单”,那么索引候选字段可能包含租户、状态和时间。但字段顺序不能只凭口诀决定,还要观察租户规模、状态分布和时间范围是否稳定。如果某个租户占据绝大多数数据,索引策略可能需要结合分区、数据隔离或其他访问方式重新评估。
索引设计还要考虑写入业务。订单表、日志表、流水表通常写入频繁,索引每增加一个,就可能增加插入和更新成本。对这类表,我会要求索引变更同时提供写入吞吐、锁等待和存储空间的对照数据。
传统分页通常使用页码和偏移量。前几页数据量较小时,这种方式简单直观;但当用户跳到很大的页码,数据库可能需要先定位并跳过大量记录,再返回目标页。
如果业务允许,基于有序键的游标分页通常更适合长列表。例如按照递增主键或稳定的时间加唯一键组合进行查询,下一页携带上一页最后一条记录的位置,而不是让数据库重复跳过前面所有数据。
-- 偏移量分页:页码越大,跳过的数据可能越多 SELECT id, created_at, title FROM messages ORDER BY id DESC LIMIT 50 OFFSET 500000; -- 游标式分页:从上一页的最后一个 id 继续读取 SELECT id, created_at, title FROM messages WHERE id < 987654 ORDER BY id DESC LIMIT 50;
游标分页也有边界。它不适合所有“任意跳页”产品交互,排序字段必须稳定,数据新增和删除还可能影响用户对结果连续性的理解。因此,产品应在交互上接受“下一页”或“加载更多”,而不是要求技术团队用高成本方案支持无限跳页。

N+1 查询经常隐藏在 ORM、模板渲染或服务层循环中。主查询先返回 N 条订单,应用再逐条查询客户、商品或权限信息,最终一次页面请求可能触发数百甚至数千次数据库访问。
这类问题的特点是每条 SQL 单独看都不算慢,但总执行次数很高。优化方式可能是批量查询、合理关联、一次性预加载、缓存稳定信息,或者重新设计接口返回结构。
我不会仅凭“SQL 数量减少了”就认定优化成功。批量关联查询可能返回大量重复数据,导致网络传输和应用内存上升。因此,仍要同时观察请求总耗时、数据库总耗时、返回字节数、应用内存和数据库扫描行数。
报表查询通常具有计算量大、时间跨度长、结果集多和执行频率不稳定的特点。如果它与在线交易查询共用同一资源池,用户偶尔点击一次复杂报表,就可能影响订单、库存和客户查询。
更合理的方案包括异步生成、分批读取、预计算汇总表、设置查询超时、限制并发、安排低峰期运行,或者将分析查询迁移到更适合聚合的数据处理链路。以九数云这类数据分析和可视化平台的使用场景为例,团队可以先区分“临时探索分析”和“高频固定看板”:前者需要控制资源边界,后者则更适合通过数据准备、结果复用和定时刷新降低实时计算压力。
这类优化的核心不是让每次报表都即时返回,而是让实时业务和分析业务都达到各自合理的服务目标。实时性不是所有查询的唯一目标,稳定性、数据新鲜度和成本同样需要被纳入设计。
当数据库变慢时,应用可能因为请求超时而自动重试。重试又会增加数据库并发,进一步造成连接池耗尽和更多超时,最终形成放大回路。此时,单纯扩大连接池并不一定有效,甚至可能让数据库同时执行更多查询,导致 CPU 和锁竞争更加严重。
排查连接池时,应同时记录连接池最大连接数、活跃连接数、等待连接数、平均等待时间、SQL 执行时间和请求重试次数。若等待连接时间明显高于 SQL 执行时间,优先处理连接生命周期、事务范围和并发控制,而不是立刻改写 SQL。

下面采用一个脱敏后的情景案例。假设某业务系统有一个订单列表接口,支持租户、订单状态、客户名称、创建时间筛选,并按创建时间倒序返回数据。系统早期只有约 300 万条订单,接口 P95 为 420 毫秒;一年后订单增长到 4800 万条,高峰期 P95 上升到 3.8 秒,超时率达到 4.6%。
产品团队认为是“订单数据变多了”,开发团队建议给状态字段增加索引,DBA 则发现慢查询主要集中在大时间范围、模糊客户名称和深分页场景。三种判断都部分正确,但任何一个单独方案都不足以解决整体问题。
第一轮排查发现,接口默认查询范围为一年,单页返回 50 条,但允许用户跳转到 5000 页以后。第二轮排查发现,客户名称搜索采用前缀不固定的模糊匹配,普通 B-tree 索引无法覆盖所有场景。第三轮排查发现,列表返回了多个大字段,且应用层还会针对每条订单查询一次客户标签。
这里的数据为样本推演,用于说明一套完整的分析方式。真实项目应以 APM、慢查询日志、执行计划和数据库监控数据为准。
| 观测项目 | 优化前结果 | 说明 |
|---|---|---|
| 接口平均耗时 | 1.9 秒 | 平均值已经偏高,但不能完整反映长尾情况 |
| P95 接口耗时 | 3.8 秒 | 高峰期多数用户开始感知明显卡顿 |
| P99 接口耗时 | 8.7 秒 | 极端筛选和深分页请求严重拖慢长尾 |
| 超时率 | 4.6% | 已经影响业务操作成功率 |
| 单次请求 SQL 数量 | 平均 74 条 | 存在应用层重复查询和 N+1 风险 |
| 数据库扫描行数 | 平均 320 万行 | 返回 50 条记录,但扫描范围过大 |
| 高峰期数据库 CPU | 82% | 资源余量不足,容易受突发请求影响 |
从这组数据可以看出,问题并不是某一个索引缺失。SQL 数量过多、扫描行数过大、时间范围过宽、深分页和返回字段过多,共同造成了接口长尾。
产品将订单列表默认时间范围从一年调整为最近 30 天,并在用户未填写关键筛选条件时提示选择时间范围或租户范围。这个动作没有修改数据库,却直接降低了单次查询可能处理的数据量。
有人担心这会影响用户查历史订单。解决方式不是取消限制,而是将历史查询设计成明确的操作:用户主动选择时间范围后发起查询,超过一定跨度时提示预计耗时,批量历史导出则转为异步任务。这样既保留业务能力,也避免普通列表请求承担不可控的数据范围。
订单列表不再支持无限跳页,交互改为“加载更多”和“下一页”。排序使用创建时间加订单唯一标识,确保同一时间戳下仍有稳定顺序。接口返回下一页游标,数据库从上一页末尾继续查询。
这种改造需要产品、前端和后端一起完成,不能只在数据库层“偷偷改掉”。如果用户仍然需要看到“第几页”,游标分页可能不符合交互预期;如果用户经常跳到任意页,技术团队需要在可用性和数据库成本之间做明确取舍。
团队根据真实查询模板重新设计索引,而不是为每个字段单独增加索引。测试中分别验证租户筛选、状态筛选、时间排序和分页条件,比较优化前后实际扫描行数、排序操作和 P95。
同时,团队删除了一个长期未使用的重复索引,并观察写入吞吐、索引空间和锁等待。这个步骤很重要,因为新增索引带来的查询收益如果没有和写入成本一起评估,后续可能在订单写入高峰期暴露新的问题。
原接口对每条订单逐条查询客户标签,平均 74 条 SQL 中有相当一部分属于重复访问。后端改为先批量收集客户标识,再一次性查询标签,并通过映射关系组装结果。
列表接口同时删除详情页才需要的大字段,将扩展属性和操作日志改为详情接口按需加载。结果集字节数和应用层对象数量下降后,数据库执行时间和接口序列化时间都得到改善。
在相近数据量、相近高峰并发和相同核心筛选条件下,样本推演结果如下。这里仍需强调,数据用于展示评估框架,不构成任何平台或客户的公开性能承诺。
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| 接口平均耗时 | 1.9 秒 | 0.62 秒 | 下降约 67% |
| P95 接口耗时 | 3.8 秒 | 1.1 秒 | 下降约 71% |
| P99 接口耗时 | 8.7 秒 | 2.4 秒 | 下降约 72% |
| 超时率 | 4.6% | 0.8% | 下降 3.8 个百分点 |
| 单次请求 SQL 数量 | 74 条 | 9 条 | 减少约 88% |
| 平均扫描行数 | 320 万行 | 4800 行 | 下降约 99.8% |
| 高峰期数据库 CPU | 82% | 56% | 下降 26 个百分点 |

这个案例最重要的结果不是 P95 从 3.8 秒下降到 1.1 秒,而是团队最终确认:产品约束、查询结构、数据库索引和应用访问方式共同决定性能。如果只新增状态索引,扫描范围和 N+1 查询仍然存在,接口长尾很可能无法下降到可接受水平。
另一个结论是,优化后的剩余问题仍然需要持续观察。用户可能开始使用更复杂的筛选组合,数据量还会继续增长,新的报表功能也可能复用订单表。性能治理不是一次上线任务,而是跟随数据和业务变化不断复盘的过程。
线上突发问题优先保证服务稳定,不要在高峰期直接进行大范围索引变更或复杂 SQL 重写。第一步应确认影响范围:是全部接口变慢,还是某个查询模板、某个租户、某个数据库节点异常。
如果数据库 CPU 已接近上限,第一目标是降低瞬时负载;如果锁等待严重,应优先缩短事务、终止异常长事务或调整批处理;如果连接池等待明显,则要检查连接泄漏、事务未提交和重试策略。
对于稳定复现的慢 SQL,可以按照“参数,执行计划,数据量,资源”的顺序排查。先确认实际参数和线上数据规模,再获取真实执行计划,之后对照扫描行数、返回行数、排序、临时结果和锁等待。
不要只用一个参数验证。某些 SQL 可能在小租户上很快,在大租户上很慢;在最近一天的数据上很快,在跨年查询上很慢。至少要覆盖常见参数、极端参数和高峰期参数。
如果估算行数与实际行数偏差很大,应先处理统计信息和数据分布问题;如果扫描行数与返回行数差距极大,应检查索引和过滤条件;如果返回行数本身就很大,应回到产品层讨论是否需要分页、异步或汇总。
高频查询适合从调用模式入手。检查前端是否重复刷新、应用是否重复读取、多个服务是否分别查询同一份数据、缓存是否因为键设计不合理而无法命中。
缓存不是“加一层就会变快”。需要定义缓存命中率目标、失效规则、回源保护、热点键保护和一致性接受范围。如果业务要求强一致,就不能用普通缓存方案简单替代数据库读取。
首先区分查询结果是否需要实时。实时看板、临时探索、月度结算和历史导出,通常不应使用同一套执行策略。
对于固定口径的高频报表,可以建立汇总表或预计算结果,并设置定时刷新;对于临时分析,应控制数据范围、字段数量和并发;对于大批量导出,应使用异步任务、分批读取和可恢复进度;对于复杂多维分析,应评估更适合聚合查询的数据存储或分析平台。
如果团队使用九数云这类分析平台,建议在接入前先梳理数据刷新频率、数据源查询压力和看板访问峰值。技术团队不应只关注图表是否能够展示,还应确认数据准备过程是否重复拉取明细、是否有不必要的全量刷新,以及分析访问是否会影响核心事务库。
数据增长型问题不能只靠临时索引。应提前评估归档、冷热分层、分区、分片、历史数据查询和容量扩展策略。
热数据通常服务于高频在线操作,冷数据更多用于审计、历史分析和低频查询。将两者混在同一张超大表中,既会增加在线查询扫描范围,也会让备份、维护和索引变更变得更加昂贵。
在做归档前必须确认业务保留期限、合规要求、历史查询入口和恢复方式。不能为了降低在线表数据量,把历史数据简单搬走,却让客服、财务或审计失去查询能力。

这是最应该先做的基础治理,因为改动通常集中在查询逻辑、索引和数据访问层,成本相对可控,也便于通过执行计划和压测验证。
它适合访问路径不合理、扫描范围过大、排序成本高、重复查询明显和返回字段过多的场景。但如果查询本身需要处理大量明细数据,或者数据库已经接近容量上限,单纯优化 SQL 的收益会逐渐变小。
它的主要风险是索引增加写入开销,SQL 改写改变结果顺序或边界,优化器在不同参数下选择不同计划。因此,必须保留原始结果集对照、执行计划和回滚方案。
缓存最适合数据变化不频繁、读取量大、短时间内允许旧数据存在的场景,例如配置、字典、部分权限信息和热点详情。
它不适合直接覆盖强一致交易状态,尤其是库存、余额、支付结果和实时风控判断。即使使用缓存,也要明确缓存失效、主动更新、回源限流和异常降级机制。
缓存命中率不是越高越好。如果为了提升命中率而把大量低频数据长期放入缓存,可能增加内存成本和失效管理复杂度。应结合命中率、回源成本、数据新鲜度和故障影响评估。
读写分离可以把部分查询流量转移到只读节点,但它无法自动解决慢 SQL。如果查询本身需要扫描大量数据,只是从主库换到从库,问题可能从 CPU 压力变成复制延迟和只读节点拥塞。
它适合读请求量大、读写边界清晰、业务能够接受短暂延迟的系统。对于刚写入后必须立即读到最新结果的链路,需要提供强制读主、延迟感知或一致性兜底。
汇总表可以把查询时的计算成本前移到数据处理阶段。对于固定时间粒度、固定维度和固定指标的报表,这种方式通常比每次实时扫描明细更稳定。
代价是数据新鲜度和口径维护。指标定义变化后,需要同步调整计算任务和历史数据;刷新失败时,还要让用户知道数据更新时间和完整性状态。
因此,预计算不是“不要实时查询”,而是把实时查询保留给真正需要实时的数据,把稳定、重复、可预测的计算交给离线或准实时流程。
分库分表能够改善单库容量、写入压力或热点集中问题,但会让查询路由、分页、排序、事务和运维复杂化。跨分片聚合、全局唯一标识、历史数据迁移和故障恢复,都需要额外设计。
如果系统还没有建立慢查询采集、SQL 审核和容量监控,过早进行分库分表,往往会让问题更难定位。只有当单库在容量、写入吞吐或资源隔离上确实达到边界,并且团队具备相应运维能力时,才应认真评估。
| 方案 | 适合解决的问题 | 不适合解决的问题 | 上线前必须确认 |
|---|---|---|---|
| SQL 与索引优化 | 访问路径不合理、扫描过大、排序低效 | 数据总量和并发已超过单库容量 | 执行计划、压测、写入影响、回滚 |
| 缓存 | 热点数据重复读取、低频变化数据 | 强一致交易状态、复杂组合查询 | 失效策略、命中率、回源保护、一致性 |
| 读写分离 | 读压力明显高于写压力 | 单条 SQL 本身扫描量过大 | 复制延迟、读主策略、故障切换 |
| 汇总与预计算 | 固定口径、高频报表和聚合分析 | 条件高度随机、必须实时计算的探索 | 刷新频率、口径版本、失败重算 |
| 分库分表 | 容量、写入吞吐和热点已突破单库边界 | 普通慢 SQL、基础索引缺失 | 路由、扩容、跨片查询、恢复演练 |

产品经理不需要分析执行计划,但需要知道哪些交互会显著增加数据库成本。无限时间范围、任意跳页、全量导出、复杂多条件组合和实时刷新,都会影响技术方案。
在需求评审中,可以增加几个固定问题:这个查询默认覆盖多长时间?用户是否需要看到全部结果?结果是否必须实时?单个用户是否可能连续刷新?导出是否允许异步?如果这些问题没有答案,技术团队很难制定可靠的性能目标。
代码评审不应只看 SQL 是否能够返回正确结果,还要检查 SQL 的执行次数、返回字段、分页方式、事务范围、异常重试和参数边界。
对于 ORM 框架,还要查看实际生成的 SQL。开发者写的是一个对象查询,数据库执行的可能是多条关联查询;代码看起来简洁,不代表数据库访问成本低。
DBA 的价值不只是“帮忙加索引”,还包括评估索引对写入的影响、执行计划稳定性、统计信息、锁风险、存储增长、备份窗口和高峰期资源余量。
对于高风险 SQL 变更,应保留变更前后的计划和关键指标;对于索引变更,应确认是否存在重复索引、是否有低峰期执行窗口,以及是否能快速删除或回退;对于参数变更,应明确影响的实例范围和观察周期。
性能测试的关键不是把数据机械地扩大到几千万行,而是尽量模拟真实分布。例如,某个大客户可能占据 40% 的订单,某个状态可能占据 90% 的记录,最近七天的数据可能是历史数据的主要访问热点。
测试还应覆盖边界条件:空条件、极大时间范围、深分页、热门租户、冷数据、复杂排序和并发导出。很多查询在平均条件下很快,却在这些边界条件下突然退化。
慢查询日志只是起点。团队还应能够按照接口、SQL 模板、调用次数、租户、时间段和数据库实例进行聚合分析,并观察优化上线后是否出现新的资源转移。
告警也不应只设置一个固定的“超过 1 秒”。不同接口的性能目标不同,实时交易、后台列表、报表和异步任务应使用不同阈值。更重要的是,告警需要关联业务影响,例如超时率上升、订单提交失败或看板刷新失败,而不是只展示数据库 CPU。

数据库查询性能优化最容易陷入两个极端:一边是把所有问题归因于 SQL 和索引,另一边是还没完成基础治理就开始讨论缓存、读写分离和分库分表。前者容易忽略产品和应用链路,后者则可能用更高的复杂度掩盖原本简单的问题。
我的判断顺序始终是:先确认用户真正感受到什么,再拆分接口链路;先建立 P95、P99、超时率和资源基线,再分析执行计划;先减少不必要的数据范围、字段和调用次数,再决定是否需要新增索引或升级架构。
查询性能的核心不是“数据库能不能跑得更快”,而是系统是否只让数据库做真正有价值的工作。一个没有时间范围的查询、一条重复执行几百次的 SQL、一个支持无限跳页的列表、一个和交易库共享资源的重型报表,都可能比某个具体语法问题更值得优先处理。
如果团队准备从今天开始治理,建议先完成三件事:选出一个用户影响最大的慢接口,补齐优化前基线;拉出最近 7 天累计数据库耗时最高的 SQL 模板,区分高频和高成本问题;在产品、开发、DBA、测试和运维之间建立一次联合复盘。不要先追求全面改造,先用一个真实场景跑通“发现问题,定位根因,实施优化,验证结果,观察回归”的完整闭环。
当这套闭环能够稳定运行,性能优化就不再是某位工程师临时加班救火,而会成为产品技术团队面对数据增长、业务复杂化和高峰流量时的一项基础能力。
我们团队以前遇到过列表页偶发超时,开发第一反应是检查 SQL,结果单次 SQL 平均耗时并不高。后来我才发现,真正拖垮体验的是高峰期 P99 和连接池等待,所以想知道性能排查到底应该从哪些指标开始。
不要先从“哪条 SQL 最慢”开始,而要先判断用户感知到的延迟发生在哪一层。一次接口请求可能同时包含连接池排队、SQL 执行、锁等待、网络传输和应用层组装,数据库耗时只占其中一部分。建议先建立一份按接口和 SQL 模板拆分的性能基线。平均耗时适合观察整体趋势,但不能代表用户体验;
列表、搜索、订单等实时场景,更应该优先关注 P95、P99、超时率和高峰期调用量。
指标它回答的问题容易误判的地方 平均耗时整体执行是否变快会掩盖少量极慢请求 P95/P99长尾用户是否等待过久需要按接口和时间段拆分 扫描行数/返回行数数据库是否做了大量无效工作不同数据库采集方式可能不同 连接池等待请求是否卡在获取连接不应误归因于 SQL 本身 超时率性能问题是否已经影响业务要结合重试和流量变化判断 我通常会把问题分成两组:一组是“慢但不频繁”的重型查询,另一组是“单次不算慢但调用量巨大”的普通查询。
后者经常更值得优先治理,因为总耗时可以近似理解为单次耗时乘以调用次数,优化一条高频查询往往比优化一条偶发报表 SQL 更能改善整体资源占用。如果一次接口的 P99 为 4.2 秒,但 SQL P99 只有 1.1 秒,就不应该继续盲目改索引,而应检查连接池、应用循环查询、下游 RPC 和序列化耗时。
我的判断标准是:先找到总链路中占比最高且可控的环节,再决定是否进入 SQL 优化。
我曾经处理过一个查询,执行计划里明确显示使用了联合索引,但数据量上升后耗时还是从几十毫秒涨到两秒多。团队一开始认为是数据库没有命中索引,后来才发现索引虽然被使用,却扫描了大量低选择性数据。
“使用索引”只说明数据库选择了某种访问路径,不等于这条路径足够高效。判断索引是否真正有价值,要看扫描范围、过滤后的行数、回表数量、排序代价以及最终返回的数据量。例如一张订单表有 5000 万行,状态字段只有“待支付、已支付、已取消”三种值。
即使状态字段上的索引被使用,查询“已支付订单”仍可能扫描数千万条索引记录,再回表读取详情,这种索引命中对性能帮助非常有限。
现象可能原因优先验证方式 使用索引但扫描行数很大字段选择性低或条件范围过宽查看实际扫描与返回行数 过滤快但排序慢排序字段无法复用索引顺序检查执行计划中的排序节点 索引范围不大但总耗时高回表次数多或返回字段过宽对比覆盖索引与非覆盖索引 估算行数与实际差距大统计信息过期或数据分布倾斜刷新统计信息后重新验证 我在审查联合索引时,不会只套用“最左匹配”这类口诀,而会把真实查询拆成过滤、连接、排序和返回四个动作。
索引字段顺序应服务于最常见的查询组合;如果业务总是按租户、状态和创建时间筛选,就应使用真实 SQL 和数据分布验证,而不是凭字段基数简单拍板。索引优化还要看副作用。新增一个索引可能让读查询变快,却让写入、更新、备份和存储成本上升。
更稳妥的做法是保留优化前执行计划,用接近生产规模的数据压测,至少同时观察查询 P99、写入延迟、CPU、磁盘 IO 和索引空间占用。
我们测试列表接口时,第一页只有几十毫秒,但翻到几千页后响应明显变慢,开发曾尝试把每页数量从 20 改成 100,结果数据库扫描量反而更大。我想知道什么情况下应该继续优化偏移分页,什么情况下必须改成游标分页。
传统偏移分页通常需要数据库先找到并跳过前面的大量记录,再返回目标页数据。页码越大,跳过的数据越多,即使最终只返回 20 行,数据库也可能已经扫描、排序或回表了大量无用记录。可以把两种方式理解为“从队列第 N 个位置数人”和“从上一次看到的人继续往后找”。
前者适合页码较小、数据量有限且用户确实需要跳页的后台场景;后者更适合时间线、订单流和移动端无限滚动等连续浏览场景。
方案适合场景主要代价 LIMIT + OFFSET小数据量、需要跳转页码深分页扫描成本随页码增长 基于唯一键的游标分页连续加载、按主键或时间顺序读取不适合任意跳页 基于时间和唯一键的复合游标时间排序且存在同一时间戳记录需要稳定排序规则 汇总表或搜索索引复杂筛选、报表和多字段检索增加同步与一致性维护成本 游标分页最容易踩的坑是排序不稳定。
只按 created_at 排序时,如果多条记录时间相同,翻页可能出现重复或漏数据;更可靠的做法是使用“created_at + 唯一 ID”作为稳定排序和游标条件,并确保索引顺序与查询条件一致。不要只看第一页压测结果。
我会至少测试第一页、中间页和深分页,并记录扫描行数、P95、P99及数据库 IO。如果产品要求支持跳到任意页,可以限制最大页码、缩小可查询时间范围,或把深分页请求改造成异步任务,而不是让实时数据库查询承担无限范围的数据访问。
我们以前的性能优化往往发生在事故之后:接口报警、开发改 SQL、上线后看几眼监控,过几周问题又复发。现在我更关心的是,产品、开发、测试、数据库和运维应该如何分工,才能把一次优化沉淀成长期机制。
数据库性能治理不能依赖某个熟悉执行计划的工程师临时救火。真正有效的闭环应包含发现问题、建立基线、定位根因、提出方案、压测验证、灰度发布、持续观察和复盘归档八个步骤。团队分工时,产品不应只提出“页面要快”,而应说明关键用户路径、可接受等待时间和高峰业务优先级;开发负责 SQL、事务和数据访问逻辑;
数据库专家负责执行计划、索引、锁和资源判断;测试负责真实数据分布与压力模型;运维负责监控、发布和回滚。
阶段必须留下的结果常见失败方式 发现接口、SQL 模板和业务影响范围只凭用户投诉定位 基线P95/P99、调用量、扫描量和资源指标只记录平均耗时 定位执行计划、锁等待和链路耗时拆分把所有问题归因于 SQL 验证优化前后对照数据与压测结果只在小数据量环境测试 发布灰度范围、观察窗口和回滚方案上线后立即关闭任务 复盘性能台账、责任边界和后续动作问题解决后没有沉淀 我建议每周维护一次 Top SQL 清单,但排名不能只按单次耗时。
更实用的排序方式是同时看总耗时、调用次数、长尾比例、资源消耗和业务影响;一条耗时 80 毫秒但每秒调用数很高的查询,可能比一条偶发耗时 5 秒的报表查询更值得优先优化。每次变更至少记录五项内容:问题现象、根因判断、具体改动、优化前后指标和回滚方式。
若优化后 P99 降低了,却导致写入延迟、锁等待或存储成本上升,就不能简单判定为成功。性能优化的最终目标不是让某条 SQL 看起来更漂亮,而是在可接受成本下改善真实业务链路。


读者评论
文章没有把查询变慢简单归因于索引,而是把连接池、应用组装、锁等待等环节纳入分析,这种按链路拆解问题的方法比较实用。
对产品团队来说,建议把查询范围、分页上限和大批量导出规则前置到需求评审中,这一点能减少很多上线后的性能争议。
文中强调关注P95、P99和超时率,而不只看平均耗时,尤其适合分析高峰期或少数复杂查询带来的用户体验问题。
关于索引收益与写入成本的平衡讲得比较客观。实际落地时还需要结合数据库类型、数据分布和执行计划进行压测,不能直接套用结论。
文章中的数据和图表属于情景模拟,适合帮助理解趋势,但如果用于技术决策,仍应补充真实业务流量、数据规模和基线测试结果。