一条统计接口变慢,排查发现应用层把三百万行明细全查了出来,在内存里按天分组求和。改成一条带 GROUP BY 的 SQL 后,耗时从十一秒降到八十毫秒,返回的数据量从三百兆降到几十行。
于是团队里出现一条新规矩:能下推到数据库算的,一律下推。三个月后另一条接口按这个规矩写完,数据库 CPU 打满——因为它下推的是一个需要调用外部服务才能确定参数的复杂表达式。
下推不是"更靠近数据就更快",它是一次资源置换:把应用的内存与网络换成了数据库的 CPU。哪一边更紧张,答案就不同。
1. 下推换掉了什么
| 资源 | 下推前 | 下推后 |
|---|---|---|
| 网络传输 | 大(全量明细) | 小(聚合结果) |
| 应用内存 | 大(装载全量) | 小 |
| 数据库 CPU | 小 | 大 |
| 数据库连接占用 | 短 | 可能变长(复杂查询) |
| 可扩展性 | 应用可水平扩 | 数据库难水平扩 |
最后一行是核心:应用层是无状态、可水平扩展的;数据库通常是单点或有上限的。把计算从可扩展的一侧搬到不可扩展的一侧,短期变快,长期把瓶颈集中了。
还有一笔隐性成本容易被忽略:下推之后,这段逻辑就离开了单元测试的覆盖范围。应用层的一段计算可以写断言、可以断点、可以在本地复现;一条 SQL 的正确性只能靠结果集比对,而且它依赖具体数据库版本的行为(空值排序、除法取整、日期函数)。逻辑越复杂,这笔成本越高。
这也是为什么"下推"在不同规模下答案不同:数据量小、应用内存紧张时,下推几乎总是对;数据库已经成为瓶颈时,下推是在往火上浇油。
2. 该下推的五类计算
| 类型 | 例子 | 为什么该下推 |
|---|---|---|
| 过滤 | WHERE 条件 |
减少传输量,且能用索引 |
| 聚合 | COUNT/SUM/GROUP BY |
结果集远小于输入 |
| 排序分页 | ORDER BY ... LIMIT |
避免全量取回再截断 |
| 连接 | 多表 JOIN |
避免应用层手工拼装与 N+1 |
| 去重 | DISTINCT / EXISTS |
数据库有专门优化 |
-- 同一条聚合下推,收益差别可以很大
-- 高收益:百万行 → 几十行
SELECT DATE(created_at) AS d, COUNT(*) FROM orders GROUP BY DATE(created_at);
-- 低收益:百万行 → 九十万行(按唯一 ID 分组,结果几乎不缩减)
SELECT order_id, COUNT(*) FROM order_items GROUP BY order_id;
-- 此时下推省的是序列化开销,不是数据量;若应用层还要再算,收益进一步抵消
第二项的关键在于结果集与输入的比例。聚合把百万行压成几十行,传输与内存成本几乎归零;而如果聚合后仍是一百万行(比如按唯一 ID 分组),下推的收益就只剩网络序列化的开销,收益有限。
第五项"去重"容易被低估:应用层用 Set 去重的成本是"先把重复行全部取回",而下推后数据库只需返回不重复的那部分。重复率越高,两者差别越大。
第三项常被忽略:在应用层"取回全部再截断到前二十条",等于每次都传输全量。分页必须下推,否则数据量一上来,第一页和最后一页的成本一样高。
3. 不该下推的四类计算
| 类型 | 为什么不该下推 | 替代 |
|---|---|---|
| 需要外部服务参与 | 数据库无法调用外部 RPC | 应用层算,或先物化参数 |
| 复杂的业务规则 | SQL 表达后无法测试、无法复用 | 应用层用代码表达 |
| 需要频繁变更的逻辑 | 每次改要发 SQL 变更,风险高 | 应用层部署即可 |
| 跨库/跨源聚合 | 需要联邦查询能力 | 应用层或数据平台 |
第三项的成本随时间累积:一段写在 SQL 里的规则,改动要走 DDL/视图变更,回滚也要走一遍同样的流程。而应用层的规则改动只是部署,回滚是切版本。变更成本的不对称,会让团队越来越不愿意改这段逻辑,最终它变成没人敢碰的黑盒。
第一项是上一节那个事故的成因:下推的表达式里有一个参数需要查另一个系统的接口才能确定,结果这条 SQL 变成了在数据库里做超长等待。数据库的计算能力再强,也调不了外部服务。
第二项的代价是长期的:一段用 SQL 表达的业务规则,测试要靠比对结果集,无法用单测覆盖;多人修改时冲突在 SQL 里,评审难度远高于代码。
4. 判断流程与五个问题
// 下推判断:五问,任一为"是"就停
1. 需要调用外部服务或读取非本库数据? → 不下推
2. 逻辑会频繁变更(月度级别)? → 不下推
3. 结果集与输入集比例 > 0.5? → 收益有限,优先考虑其它优化
4. 数据库当前 CPU 已接近瓶颈? → 不下推,改应用层并行
5. 过滤/聚合/排序/连接/去重之一? → 下推
第三问的阈值是经验值,它的含义是:下推的收益主要来自数据量缩减,缩减不明显时,下推只是换了执行位置。
第四问最容易被跳过。判断"数据库是不是瓶颈"有一个直接方法:看下推前后的数据库 CPU 与慢查询数,而不是看接口耗时。接口变快了但数据库 CPU 涨了 20%,这笔交易是否划算取决于数据库还有多少余量。
-- 下推前先确认:这条查询会消耗多少数据库资源
EXPLAIN ANALYZE
SELECT DATE(created_at) AS d, SUM(amount) AS s
FROM orders
WHERE created_at >= '2026-10-01'
GROUP BY DATE(created_at);
-- 关注三处:扫描行数、是否用临时表、是否用文件排序
-- 若出现 Using temporary; Using filesort 且行数大,下推代价可能高于应用层
5. 折中形态:部分下推
真实系统里最常用的不是两个极端,而是把一件事拆成两段。
| 拆法 | 做法 | 适用 |
|---|---|---|
| 库内过滤 + 应用内聚合 | WHERE 下推,聚合在应用做 |
聚合逻辑复杂且会变 |
| 库内聚合 + 应用内拼装 | GROUP BY 下推,多源结果在应用合并 |
多库数据 |
| 物化中间结果 | 定期把聚合结果写入汇总表 | 报表、看板 |
| 两阶段查询 | 先用库内查询拿 ID 集合,再按 ID 取明细 | 复杂条件分页 |
第三种在报表场景最实用:把计算从"每次请求"移到"定时任务"。代价是数据有延迟,收益是请求路径变成一次简单查询。它把"下推与否"这个问题变成了"实时性要求多高",而这个问题的答案通常更清楚。
// 报表类需求的三档选择,按实时性要求定
report_strategy:
realtime: # 要求秒级:下推聚合,接受数据库压力
mode: pushdown
cache_ttl: 0
near_realtime: # 分钟级:物化中间结果
mode: materialized
refresh: "*/5 * * * *" # 每 5 分钟刷新汇总表
batch: # 天级:离线任务
mode: offline
refresh: "0 2 * * *"
6. 排错:五种"下推之后更糟"
| 现象 | 成因 | 处理方向 |
|---|---|---|
| 接口变快但数据库 CPU 打满 | 计算搬到瓶颈侧 | 改应用层并行,或加物化 |
| 复杂 SQL 偶发超时 | 执行计划随数据分布变化 | 固定计划或拆成两段 |
| 统计口径与明细对不上 | 下推的 SQL 与应用逻辑不一致 | 统一口径,用同一段代码生成条件 |
| 报表查询拖慢在线业务 | 大查询占用连接与 IO | 只读副本、资源队列、限流 |
| 改一个规则要发 SQL 变更 | 业务逻辑写进了 SQL | 移回应用层 |
可以给下推后的查询设一条明确的上限,把它从"可能拖垮在线业务"变成"可控的慢查询":
-- 会话级护栏:给报表查询设超时与资源组,避免占满连接
SET SESSION max_execution_time = 20000; -- MySQL:20 秒硬上限
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 配合只读副本或资源组(如 MySQL 8.0 的 resource group),与在线查询隔离
第四项是生产上最常见的连带伤害:一条报表 SQL 跑二十分钟,把连接池里的连接占住,在线接口开始排队。这不是下推的错,是没有把报表查询与在线查询隔离。隔离手段(只读副本、独立连接池、查询超时)应该与下推决策一起考虑。
7. 边界:什么情况下推几乎总是对的
| 场景 | 为什么 | 说明 |
|---|---|---|
| 数据量远大于结果量 | 传输与内存成本占主导 | 聚合、去重、分页 |
| 应用内存受限(容器限额小) | 装载全量会 OOM | 强制下推 |
| 数据库余量充足 | 置换方向有利 | 可以放心下推 |
| 逻辑稳定且可测 | 无长期维护成本 | 下推不增加风险 |
还有一条实践中的次序建议:先优化索引与查询形状,再谈下推。大量"下推才有救"的查询,实际问题是缺少一个合适的复合索引,或者用了 SELECT * 取回了不需要的列。这两件事改动的成本远低于搬迁计算位置,而且不改变资源分配格局。
判断口径一句话:这次下推把成本从哪搬到哪,接收方还有多少余量。答不出接收方的余量,就不该下推。
8. 自测
- 下推本质上是哪两种资源的置换?为什么这个置换在规模变大后会反转?
- 聚合下推的收益主要来自什么?为什么"按唯一 ID 分组"收益有限?
- 为什么"需要外部服务参与的计算"不能下推?数据库能做什么、不能做什么?
- 判断流程第三问"结果集与输入集比例"为什么设阈值?它想拦住哪种下推?
- "物化中间结果"把哪个问题转换成了哪个问题?它适合什么实时性要求?
- 报表 SQL 拖慢在线业务,根因是下推决策错了吗?真正缺的是什么?