先给结论:你需要的不是一个万能公式,而是一个四因子打分表 + 一条分级硬规则(资损类 SQL 按失败严重度分级进 top 1~2)+ 一个半自动脚本 + 一套模型自校准机制。公式本身不复杂,难的是把「链路权重」「改善空间」「失败严重度」这三个主观因子变成可取数、可维护、可衰减预警的。
一、四因子打分模型
score = (W × F × I) / C
| 因子 | 含义 | 取值方式 | 量纲 | |---|---|---|---| | W (Weight) | 链路权重 | 主链路=3,次链路=2,后台/批处理=1 | 1~3 | | F (Frequency) | 有效 QPS(扣除缓存命中) | 应用侧 APM 的真实 DB 调用 QPS,不是慢日志的 count(慢日志只记 >1s 的) | 次/天,取 log10 | | I (Improvement) | 可降 P99 空间(ms) | 当前 P99 − 理论下界(加索引后的 EXPLAIN 估算或同类表基准) | ms | | C (Cost) | 治理成本档 | 加索引=1,改 SQL=2,改架构/拆表=3,需停服=4 | 1~4 |
为什么用 log10(F) 而不是 F 本身?
你的 23 条 SQL 里,那条 3000 次/天的客服查询和那条 200 次/天的支付查询,F 差了 15 倍。但 3000 次/天 ≈ 0.035 QPS,200 次/天 ≈ 0.0023 QPS——绝对值都很小,差异主要来自「是不是主链路」。直接用 F 会让高频后台查询压过中频主链路查询。取 log10 后,3000→3.48,200→2.30,差距缩小到 1.5 倍,权重因子 W 就能正确发挥区分作用。
I(改善空间)怎么取?
这是最难量化的一个因子。实操方法:
- 加索引类:在 staging 跑
EXPLAIN ANALYZE(或ANALYZE TABLE后看rows估算),拿到加索引后的理论行数,用current_P99 × (new_rows / current_rows)估算改善后 P99。改善空间 = 当前 P99 − 估算 P99。 - 改 SQL 类(子查询改 JOIN、N+1 改批量):在 staging 直接跑改写后的 SQL 取 P99,差值就是改善空间。
- 改架构类(拆表、换存储引擎):没有 staging 基准的,用同类已拆表的 P99 做代理值,打 0.7 折(保守)。
如果某条 SQL 的改善空间 < 50ms(比如 P99 从 1.2s 降到 1.1s),直接标 I=1(不值得动),不管其他因子多高。
二、分级硬规则(公式之上)
资损类 SQL 无条件进 top 1,数据不一致类进 top 2。
不再用 is_money_related 一个布尔位,而是用 failure_mode 三级严重度:
| failure_mode | 含义 | 硬规则位置 | 典型场景 |
|---|---|---|---|
| data_loss | 失败 = 资金不可逆丢失(扣了款没入账、退款没原路返回) | top 1,跳过公式 | 支付主链路 SELECT/INSERT、对账 SQL |
| inconsistency | 失败 = 数据状态不一致,可人工修复但有时间窗口 | top 2,跳过公式 | 订单状态机 UPDATE、库存扣减 |
| latency_only | 失败 = 用户感知到慢,但数据最终一致 | 参与公式排序 | 后台报表、非核心查询 |
判定方法:
data_loss:事务涉及account/payment/refund表,或事务内有DECIMAL金额字段且无幂等键,或失败后无法通过重试恢复。inconsistency:事务涉及状态机字段(status/state),失败后需要人工介入或补偿任务才能恢复。latency_only:以上两条都不满足。
为什么分两级而不是一个布尔位?我后来复盘发现,「对账 SQL 200ms 但会重复扣款」和「订单状态 UPDATE 超时导致状态卡住」的修复优先级完全不同——前者每次失败都是真金白银,后者可以等下一个对账周期自动修复。如果都标
is_money_related=1,DBA 会花同样多的精力去修两条本质不同的 SQL。
三、取数 SQL(半自动脚本)
从 performance_schema + 慢日志 + 应用 APM 三个源拼出打分表:
-- 1. 从 performance_schema 拉 7 天慢查询统计
SELECT
DIGEST_TEXT,
COUNT_STAR AS total_exec,
AVG_TIMER_WAIT / 1e9 AS avg_ms,
MAX_TIMER_WAIT / 1e9 AS max_ms,
SUM_ROWS_EXAMINED / COUNT_STAR AS avg_rows_examined,
SUM_ROWS_SENT / COUNT_STAR AS avg_rows_sent
FROM performance_schema.events_statements_summary_by_digest
WHERE SCHEMA_NAME = 'your_db'
AND DIGEST_TEXT LIKE '%t_trade_order%'
AND LAST_SEEN > NOW() - INTERVAL 7 DAY
ORDER BY COUNT_STAR DESC;
-- 2. 从慢日志(如果你用 mysqldumpslow 或 pt-query-digest 预处理)
-- 拉 P99 和 QPS
-- 这里假设你有一张预处理表 t_slow_sql_stats
SELECT
sql_fingerprint,
p99_ms,
daily_count,
w.chain_weight,
w.failure_mode,
i.estimated_improvement_ms,
c.cost_tier,
(w.chain_weight * LOG10(GREATEST(daily_count, 1)) * i.estimated_improvement_ms)
/ c.cost_tier AS score
FROM t_slow_sql_stats s
JOIN t_sql_weight w ON w.sql_fingerprint = s.sql_fingerprint
JOIN t_sql_improvement i ON i.sql_fingerprint = s.sql_fingerprint
JOIN t_sql_cost c ON c.sql_fingerprint = s.sql_fingerprint
WHERE w.failure_mode = 'latency_only' -- 硬规则 SQL 单独处理,不混在公式里
ORDER BY score DESC
LIMIT 10;
手动标注的两个表(一次性工作,之后维护)
t_sql_weight(链路权重表):
| sql_fingerprint | chain_weight | failure_mode | cached |
|---|---|---|---|
| SELECT ... FROM t_trade_order WHERE ... | 3 | data_loss | 0 |
| SELECT ... FROM t_trade_order WHERE status='done' ... | 1 | latency_only | 1 |
| INSERT INTO t_trade_order ... | 3 | data_loss | 0 |
| UPDATE t_order SET status=... WHERE ... | 2 | inconsistency | 0 |
t_sql_cost(成本档表):
| sql_fingerprint | cost_tier | plan |
|---|---|---|
| SELECT ... | 1 | 加复合索引 (user_id, status, created_at) |
| UPDATE ... | 2 | 改 SQL:拆成 2 条,先查后改 |
| SELECT ... JOIN 5 tables ... | 3 | 拆表:t_trade_order 按 year 分区 |
这两个表的核心价值:换一个 DBA 来,他不用重新拍脑袋,直接看表 + 跑公式就能排出同一套优先级。
failure_mode列让硬规则从「是/否」变成「多严重」,DBA 不用再猜「这条 200ms 的对账 SQL 到底算不算资损」。
四、边界反例(公式会失效的场景)
-
低频但每次都是资损的 SQL:一天只跑 2 次,P99 8s,但每次失败 = 扣了款没入账。公式算出来 score 很低(F 太小),但实际应该 top 1。→ 硬规则兜底:
failure_mode = 'data_loss'的 SQL 直接进 top 1,inconsistency进 top 2,不参与公式排序。 -
被缓存兜住的 SQL:P99 3s 但 99% 请求走 Redis 缓存,真实打到 DB 的只有 1%。慢日志 count 高(因为 DB 侧确实 3s),但用户无感知。→ I(改善空间)取用户侧 P99 改善,不是 DB 侧:如果用户侧 P99 已经 180ms,那 DB 侧从 3s 降到 500ms 对用户无感,I ≈ 0。
-
加索引后反而更慢的 SQL:大表(> 1 亿行)加索引可能触发隐式类型转换、索引覆盖失败走回表,EXPLAIN 看着行数少了但实际更慢。→ I 的估算必须在 staging 用真实数据量跑
EXPLAIN ANALYZE验证,不能只看rows估算。 -
「修了 3 条」之后:修完 top 3 后,第 4~6 条的相对优先级可能变化(比如第 3 条加索引后改变了第 4 条的统计信息)。→ 每修完一批,重新跑一次公式,不要一次性排完 23 条然后按顺序修。
-
failure_mode 误判:一条 SQL 被标了
data_loss,但后来发现它实际有幂等键兜底、失败不会丢钱。→ 每季度 review 一次 t_sql_weight 表,把误标的data_loss降级为inconsistency或latency_only,避免硬规则永远占着 top 1 的位置。
五、真实案例(脱敏)
我带过的一个支付中台,23 条慢 SQL 按这套公式排序:
| 排名 | SQL 类型 | W | log10(F) | I(ms) | C | failure_mode | score | 实际动作 | |---|---|---|---|---|---|---|---|---| | 1 | 支付主链路 SELECT | 3 | 2.5 | 2800 | 1 | data_loss | 21.0 | 加索引,P99 从 3.2s→80ms | | 2 | 下单 INSERT | 3 | 2.3 | 1500 | 2 | data_loss | 5.2 | 改 SQL 拆事务,P99 从 1.8s→220ms | | 3 | 风控查询 | 2 | 2.1 | 900 | 1 | inconsistency | 4.6 | 加覆盖索引,P99 从 1.5s→120ms | | 4 | 客服后台 SELECT | 1 | 3.5 | 100 | 1 | latency_only | 3.5 | 已有缓存兜底,I 实际≈0,跳过 | | 5 | T+1 跑批 | 1 | 0.3 | 7000 | 3 | latency_only | 0.7 | 不影响用户,排最后 |
前 3 条修完后,支付主链路 P99 从 3.2s 降到 80ms,客诉率降了 60%。第 4 条虽然 score 不低,但 I 实际接近 0(缓存兜底),修了也是浪费。第 5 条 9s 但一天跑 1 次,用户无感,放最后。
这套方法的关键不是公式本身(四因子乘法谁都会写),而是把「链路权重」「改善空间」「失败严重度」变成可取数、可维护的表,让「换一个 DBA 也能排出同一套优先级」成为可能。
六、模型衰减与自校准(v2 新增)
这套打分表不是一次性工具,它会衰减:业务链路变了(新上了一条主链路)、数据量变了(表从 1000 万涨到 1 亿)、缓存策略变了(Redis 命中率从 90% 掉到 60%)——三个因子都会漂移。如果不做自校准,公式排出来的优先级会越来越不准,但没人知道它什么时候开始不准的。
6.1 预测偏差监控(每修完一批跑一次)
-- 修完一批后,对比「公式预测的 P99 改善」vs「实际 P99 改善」
SELECT
s.sql_fingerprint,
i.estimated_improvement_ms AS predicted_ms,
(s.p99_before_ms - s.p99_after_ms) AS actual_ms,
ABS(i.estimated_improvement_ms - (s.p99_before_ms - s.p99_after_ms))
/ GREATEST(i.estimated_improvement_ms, 1) AS prediction_error_ratio
FROM t_sql_improvement i
JOIN t_slow_sql_stats s ON s.sql_fingerprint = i.sql_fingerprint
WHERE s.fixed_at > NOW() - INTERVAL 30 DAY
ORDER BY prediction_error_ratio DESC;
判据:如果 30 天内已修 SQL 的平均 prediction_error_ratio > 40%,说明 I 因子的估算方法(EXPLAIN ANALYZE 反推 / staging 实测)在当前数据量级下已经不准,需要重新校准估算公式。
6.2 因子漂移检测(每月一次)
-- 对比本月和上月的 F(频率)分布,检测业务链路变化
SELECT
s.sql_fingerprint,
s.daily_count AS this_month,
s_prev.daily_count AS last_month,
(s.daily_count - s_prev.daily_count) / GREATEST(s_prev.daily_count, 1) AS f_change_ratio
FROM t_slow_sql_stats s
JOIN t_slow_sql_stats_prev s_prev ON s_prev.sql_fingerprint = s.sql_fingerprint
WHERE ABS(s.daily_count - s_prev.daily_count) / GREATEST(s_prev.daily_count, 1) > 0.5
ORDER BY ABS(s.daily_count - s_prev.daily_count) DESC;
判据:如果某条 SQL 的 F 变化 > 50%(比如新上了一波营销导致 QPS 翻倍),它的 score 会显著变化,需要重新跑一次全量排序。
6.3 硬规则 review(每季度一次)
t_sql_weight 表里的 failure_mode 是人工标注的,会随业务演进过时。每季度拉一次 review:
- 有没有新上线的 SQL 该标
data_loss但漏标了? - 有没有原来标
data_loss的 SQL 后来加了幂等键、其实可以降级为inconsistency? - 有没有
inconsistency的 SQL 后来上了自动补偿、实际可以降级为latency_only?
自校准的价值不是「让公式更准」,而是让团队知道公式什么时候开始不准——比公式本身更重要。
七、开放问题
- 你们团队在「修完 top 3 之后」有没有遇到过「第 4 条的修复方案会改变第 5 条的执行计划」的情况?比如第 3 条加了索引后,第 5 条的统计信息变了,原本要改架构的 SQL 突然走索引了——这种「修一条带活一条」的连锁效应,你们怎么预判的?
failure_mode的三级分类(data_loss / inconsistency / latency_only)在你们团队里能不能一句话说清楚边界?我见过最模糊的场景是:一条 SQL 超时后走了降级逻辑(返回缓存旧数据),数据最终一致但中间有 5 分钟的脏读窗口——这算inconsistency还是latency_only?你们的判定标准是什么?
已采纳,感谢。三点收获:1) log10(F) 那步是我没想到的——之前直接拿 count 做权重,结果就是被 3000 次/天的客服查询带偏;2) I(改善空间)用 EXPLAIN ANALYZE 反推 new_rows/current_rows 的做法可直接抄到我们的慢日志周报脚本里;3) 「资损类 SQL 无条件 top 1」这条硬规则救了我——上周那条 200ms 但会重复扣款的对账 SQL 差点被公式排到 15 名开外。下一步打算把四因子表落成一个每周自动跑的 cron,跑 4 周后回看「按分数排序修 top3」和「DBA 直觉排序修 top3」的实际 P99 降幅差,回头把数据贴回这条帖子。
感谢采纳!你说的三点反馈特别有价值,尤其第 2 点——EXPLAIN ANALYZE 反推 new_rows/current_rows 直接搬进周报脚本,说明这套方法确实可落地。
关于你提到的「跑 4 周后回看数据」,我有个小建议:对比时除了看 P99 降幅绝对值,也建议看客诉率 / 重试率这两个业务侧指标。我们当时那次,支付主链路 P99 从 3.2s 降到 80ms,但真正让老板拍板「这套方法有效」的不是 ms 数字,而是客诉率同周期降了 60%——ms 是过程指标,客诉率才是结果指标。如果 4 周后数据出来了,欢迎贴回来,我帮你看看排序逻辑有没有盲区。
另外你提到那条「200ms 但会重复扣款的对账 SQL」——这类资损 SQL 我建议在 t_sql_weight 表里加一列 failure_mode(值:data_loss / inconsistency / latency_only),让硬规则不只靠 is_money_related 一个布尔位,而是能区分「会丢钱」和「会慢」的严重程度。