3045 字
约 10 分钟
19
S11-A12 · 慢查询治理的组织方式:谁来改、改到哪一步
2024-12-31
2026-11-09

慢查询治理最常见的失败形态不是"SQL 改不动",而是"没人认领"。日志每周导出一次,Top 20 贴进群里,下周同一批语句还在榜上;三次之后连导出都停了。技术手段都是现成的,真正决定能否收敛的是组织安排:谁改、什么时候改、改到什么程度算完。这篇只谈这三个问题的判据与代价。


1. 归属判定先于技术判定

一条慢 SQL 的修改方,等于持有该 SQL 代码的团队∩承担该接口 SLO 的团队。两者是同一个团队时,治理几乎自动发生;两者分离时,任何优化方案都会卡在"这不是我的排期"上。

现象 表面归因 真实原因
日志导出后无人跟进 大家都很忙 清单没有责任人字段,天然无人接
每次都说"下个版本改" 排期紧 慢查询没有进入需求池,永远排在功能后面
改完一周又出现在 Top 20 索引没加上 没有验证环节,改动是否生效无人确认
DBA 反复提同一张表 业务不配合 缺跨团队的成本共识,只有技术诉求

判据很简单:影响面跨团队时,先落 owner,再谈方案。没有 owner 的优化,做完也不会有人验收。


2. 三类慢查询,处置路径完全不同

把 Top 20 当同质问题处理,效率最低。按时间形态分三类,处置手段差别很大:

类型 识别特征 典型成因 首选处置 明确不适合
偶发抖动 均值正常、P99 尖刺,与时刻相关 批量任务、缓存集中失效、并发突增 定位触发源、限流、资源隔离 加索引(无稳定执行形态)
稳定偏慢 每次都慢,与数据量无关 缺索引、函数包裹、N+1、多余 JOIN 改写、补索引 扩容(不解决单语句代价)
随增长变慢 上线时快,按月变慢 表变大、深分页、统计信息陈旧 索引调整、改分页、归档/分区 只调参数

第三类最容易被误判为"机器不够"。判据:同一条 SQL 在历史数据量的备份上跑一次,耗时随行数近似线性上升即为增长型,扩容只能推迟。


3. 责任划分的三种模式与可持续性

模式 谁定优先级 优势 失效点 可持续的前提
DBA 主导 DBA 出清单,业务排期 全局视角、清单出得快 不懂业务语义,改法保守;业务侧无强制力 有固定排期通道 + DBA 对上线有否决权
业务主导 各团队自定 语义最准、改法最贴需求 看不到全局、重复投入、无外部压力就不做 慢查询进入发布门禁,而非靠自觉
平台托管 平台统一拦截/改写 覆盖面最大、可强制 改写风险集中、易误伤、责任模糊 白名单 + 可回滚 + 改写命中可观测

能长期跑下去的多是混合式:平台负责发现与拦截(不漏),业务负责语义判定(改对),DBA 负责索引与表变更(安全)。交接物不是会议,而是清单、责任字段、验证记录三张表。


4. 止损手段的代价阶梯

五种手段成本严格递增。先做便宜的,不是因为它更好,而是它能立刻止住影响面,为贵的手段争取时间。

顺序 手段 见效 主要成本 副作用 适用信号
1 限流/降级 分钟级 一个配置项 部分请求失败 抖动型、影响面大
2 加缓存 小时级 一致性设计 数据陈旧 读多写少、可容忍延迟
3 改写 SQL 天级 开发 + 回归 语义漂移 稳定偏慢
4 加索引 天级(含变更窗口) 写放大、变更风险 写入变慢、空间增长 选择性好的过滤条件
5 改表结构/归档 周级 迁移、双写、回滚 停机或长事务 增长型

限流的代价要提前说清:它把"慢"换成"错",必须配明确降级语义,否则数据库问题会变成体验问题。

# 止损第一层:按接口维度限流,而不是按 SQL 维度(SQL 维度拦不住并发来源)
rate_limit:
  endpoint: /api/order/list
  qps: 120                 # 按压测出的单实例安全水位打折
  burst: 40
  fallback: cache_stale    # 明确降级语义:返回陈旧缓存 + 标记
  observe: [latency_p99, db_qps, reject_ratio]

5. "加索引"的边界

索引最容易被滥用,因为它看起来零风险。三个真实约束:

写放大:每多一个二级索引,每次写入要多维护一棵 B+ 树。写多读少的表上,加索引可能净亏。判据是写读比,不是"这条查询快了多少"。

优化器可能不用:区分度低的索引,优化器评估后可能直接放弃,实际执行计划不变,白加一次变更。

区分度:count(distinct col)/count(*) 是粗判据,典型量级低于 0.1 的列单独建索引收益有限(按 MySQL 8.0 口径核对,版本间采样有差异)。

-- 区分度:低于 ~0.1 的列,单列索引通常被优化器跳过
SELECT COUNT(DISTINCT status) / COUNT(*) AS sel_status,
       COUNT(DISTINCT user_id) / COUNT(*) AS sel_user
FROM orders WHERE created_at >= '2026-01-01';

-- 冗余索引:前导列被别的索引覆盖时,这条是纯写放大
SELECT a.TABLE_NAME, a.INDEX_NAME, a.COLUMN_NAME, b.INDEX_NAME AS covered_by
FROM information_schema.STATISTICS a
JOIN information_schema.STATISTICS b
  ON a.TABLE_NAME = b.TABLE_NAME AND b.SEQ_IN_INDEX = 1
 AND a.SEQ_IN_INDEX > 1 AND a.COLUMN_NAME = b.COLUMN_NAME
WHERE a.TABLE_SCHEMA = DATABASE();

还有一点常被忽略:索引变更也是变更,大表加索引要走在线 DDL,有锁与主从延迟风险,需与发版一样有窗口和回滚。


6. 改写 SQL 的四类

类别 典型形态 收益来源 风险
拆查询 一次 JOIN 五张表、一条 SQL 拿全量 减少中间结果与排序 多次往返、一致性窗口
改 JOIN 驱动表选错、被驱动表无索引 改变驱动顺序 语义随数据分布变化
换分页 LIMIT 100000, 20 深分页 消除大偏移扫描 游标需稳定排序键
去函数包裹 WHERE date(created_at) = ? 恢复索引可用 时区与边界语义
-- 深分页:偏移扫描改为游标(排序键必须唯一且稳定)
SELECT id, amount FROM orders
WHERE (created_at, id) < (:last_created_at, :last_id)
ORDER BY created_at DESC, id DESC LIMIT 20;

-- 函数包裹:改为范围条件,让索引可用
-- 差:WHERE DATE(created_at) = '2026-03-01'
SELECT id FROM orders
WHERE created_at >= '2026-03-01 00:00:00'
  AND created_at <  '2026-03-02 00:00:00';

改写最贵的部分不是写新 SQL,而是证明语义没变:对同一批输入比对新旧结果集,行数与关键字段一致才算完成。


7. 阈值:响应时间 × 调用量

单一响应时间阈值("超过 1s 算慢")会把低频语句拉进清单,稀释真正的问题。改用二维判据:

调用量 \ 响应时间 < 200ms 200ms–1s > 1s
> 100 QPS 记录 P1:进入本周排期 P0:当日止损
10–100 QPS 忽略 P2:下个迭代 P1:本周排期
< 10 QPS 忽略 忽略 观察(多为报表/后台)
slow_query_policy:
  window: 7d
  dimensions: [query_digest, endpoint, team]
  priority:
    p0: {rps: ">=100", p99_ms: ">=1000"}
    p1: {rps: ">=100", p99_ms: ">=200"}
    p2: {rps: ">=10",  p99_ms: ">=1000"}
  exclude: [batch_nightly, admin_report]   # 显式排除,避免清单失真
  owner_required: true                     # 无 owner 的条目不计入闭环率

"改到哪一步算完"要写进策略,常见口径是 P0 的 P99 回到阈值内并稳定 7 天,而不是"加了个索引"。缺稳定期,改动是否生效无从区分。


8. 闭环:五个环节各有产物

环节 动作 必须留下的产物 常见断点
发现 日志聚合、按 digest 归并 清单 + 调用量 + 影响接口 按原文聚合会把参数拆散
归属 按 endpoint 映射到团队 owner 字段、SLO 归属 映射表缺失
修复 按阶梯选手段 变更记录 + 回滚方式 只记录"已优化"
验证 新旧结果比对 + 指标回落 对比结果、7 天曲线 看一眼截图就结案
防复发 评审、门禁、压测 规则、基线、告警 靠人记

防复发三个抓手,成本递增:

# 1) SQL 评审:在 CI 拦截已知模式,比人工 review 稳定
sql_review_rules:
  - no_function_on_indexed_column
  - no_select_star
  - join_table_limit: 3
  - require_limit_on_list_api
# 2) 压测门禁:新接口上生产前给出 P99 与 DB QPS 基线
# 3) 索引变更流程:走变更单 + 窗口 + 主从延迟观察

度量只留四个:慢查询语句数占比(digest 维度)、最长 P99、治理周期天数(发现到验证通过的中位数)、回潮率(修复后 30 天内重新入榜比例)。回潮率信息量最大,直接暴露"改法是否对症"。


9. 排错:治理推不动时的对照表

现象 判断 处理
清单每周一样 归属缺失 补 owner,无 owner 条目不进讨论
业务说"这不是我的 SQL" 代码与调用归属分离 以 endpoint 归属为准,落到调用方
改了索引但仍慢 优化器未选或区分度低 看执行计划确认,别只看 DDL 成功
优化后 P99 反升 写放大或索引维护 对比写读比,删冗余索引
治理一阵后停摆 靠人推动,无门禁 规则进 CI,阈值进发布检查

10. 边界:这些情况治理回报低于投入

场景 判断依据 更合适的做法
数据量小、低频 全表扫描也在毫秒级 不治理,保留观察
系统 6 个月内下线 投入回收不了 只做限流兜底
后台报表/离线 不影响在线 SLO 显式排除,免得稀释清单
慢因外部依赖 慢查询只是表象 先治下游,再回看 SQL

一句话选择:影响面跨团队、且随数据增长恶化,才值得建治理机制;其余用限流+缓存兜底。


11. 自测

  1. 一条慢 SQL 的代码归属与 SLO 归属分属两个团队,先解决什么?
  2. 偶发抖动型慢查询,为什么加索引通常是无效动作?
  3. 限流作为第一层止损,代价是什么,需要配合哪件东西才成立?
  4. 区分度低的列建了索引却没被使用,怎么确认是优化器放弃了它?
  5. "改到哪一步算完"应该写成什么样的验收口径?
  6. 回潮率高,说明治理链条上哪个环节最可能有问题?
S11-A12 · 慢查询治理的组织方式:谁来改、改到哪一步
https://www.clxhxhhr.top/posts/6228/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。