面对 MySQL 慢查询,如何让 AI 帮你分析执行计划、生成索引建议和 SQL 改写方案,并验证效果。本文记录完整流程、提示词模板、踩坑复盘,以及与其他优化方式的真实对比。

背景:凌晨 3 点触发的慢查询告警
事情发生在上周三凌晨。我被手机告警震醒,打开一看:某支付系统的对账接口响应时间飙到 8 秒,数据库 CPU 直接打满。
定位到是一条看起来人畜无害的 SQL:
sql
SELECT t.order_id, t.tx_id, t.amount, t.status, t.created_at,
o.merchant_id, o.user_id,
r.refund_amount, r.refund_reason
FROM transactions t
LEFT JOIN orders o ON t.order_id = o.id
LEFT JOIN refunds r ON t.order_id = r.order_id
WHERE t.status IN ('SUCCESS','FAILED','PROCESSING')
AND t.created_at >= '2026-04-01'
AND t.created_at < '2026-05-01'
ORDER BY t.created_at DESC
LIMIT 50;
EXPLAIN 结果让我倒吸一口气:
| 表 | type | rows | Extra |
|---|---|---|---|
| transactions | ALL | 14,832,451 | Using where; Using filesort |
| orders | eq_ref | 1 | |
| refunds | ALL | 6,241,783 | Using where |
transactions 表 1400 万行全表扫描,refunds 表 600 万行也没走索引。难怪跑不动。
搁在以前,我会手动翻文档、看索引、改 SQL、测试、上线,一套下来至少半天。这次我决定试试,能不能让 AI 帮着加速这个流程。
我的工作流:三步让 AI 参与进来
我用的工具组合:
- ChatGPT (GPT-5.5):分析 EXPLAIN 结果,生成优化建议。
- Cursor (Claude Sonnet 4 模型):在项目代码中改写 SQL,并生成验证脚本。
- 终端 + MySQL 客户端:执行测试,验证性能。
整体节奏是:我分析 → 我提问 → AI 给方案 → 我验证 → 上线,AI 负责中间的"加速部分",决策还是我。
第一步:把诊断信息喂给 AI,让它看懂执行计划
执行计划看着简单,但到底缺什么索引,需要结合查询条件、数据分布来判断。我直接把 EXPLAIN 输出和表结构贴给了 ChatGPT,问了三个问题:
提示词原文:
diff
你是一位资深 MySQL DBA。下面是一条慢查询的 EXPLAIN 结果和表结构。
transactions 表约 1400 万行,orders 约 50 万行,refunds 约 600 万行。
## SQL
[上面的 SQL]
## EXPLAIN
[粘贴 EXPLAIN 输出]
## 表结构
transactions:
- id (PRIMARY)
- order_id (FK)
- tx_id (UNIQUE)
- status (ENUM 'SUCCESS','FAILED','PROCESSING','PENDING')
- amount
- created_at
- INDEX idx_created_at (created_at)
- INDEX idx_status (status)
orders:
- id (PRIMARY)
- ...
refunds:
- id (PRIMARY)
- order_id (FK)
- amount
- status
- reason
- INDEX idx_order_id (order_id)
请分析:
1. 为什么两个表走了全表扫描?
2. 建议创建哪些索引(给出精确的 DDL)?
3. SQL 本身是否可以改写以提升性能?
它给出的回复中,核心诊断让我有些意外,但也非常精准:
原因分析: transactions 表上虽然已有 idx_status 和 idx_created_at 两个单列索引,但查询中同时有
status IN(...)和created_at范围条件,优化器只能二选一。由于 status 过滤后仍有大量数据,最终选择全表扫描。refunds 表使用了 idx_order_id,但该索引不包含查询需要的
refund_amount和refund_reason,导致回表开销极大,优化器认为还不如全表扫描。
这个分析让我省去了自己推演的时间,直接确认了根因:联合索引缺失。
第二步:让 AI 给出精确的索引 DDL,但要自己验证
AI 接着给出了几条索引建议,其中最关键的是:
sql
-- 建议 1:transactions 表的覆盖索引
ALTER TABLE transactions ADD INDEX idx_status_created (status, created_at, order_id, tx_id, amount);
-- 建议 2:refunds 表的覆盖索引
ALTER TABLE refunds ADD INDEX idx_order_id_covering (order_id, refund_amount, refund_reason);
我注意到它把 amount 和 tx_id 也放进索引了,这是为了让索引"覆盖"查询所需的所有列,避免回表。
但这里我必须做的一件事:检查索引的大小和维护成本。AI 不会告诉你 1400 万行的表加一个 5 列索引需要锁多久、占用多少磁盘。我查了一下:
- 索引大小预估:约
14M * (4+4+8+8+8) ≈ 450MB,可接受。 - ONLINE DDL 时间:测试环境跑了一下,大约 3 分钟,生产环境是 PolarDB,可以用
ALGORITHM=INSTANT秒级完成。
于是我略作调整,加上了 ALGORITHM=INSTANT(MySQL 8.0 支持):
sql
ALTER TABLE transactions
ADD INDEX idx_status_created_cover (status, created_at, order_id, tx_id, amount),
ALGORITHM=INSTANT;
AI 给了起点,但最终决策需要结合环境常识。这是我的第一条原则:AI 出方案,人做安全校验。
第三步:让 AI 改写 SQL,顺便检查业务逻辑
加了索引后,EXPLAIN 显示 rows 降到 28 万,但仍有些慢。AI 在第一步分析时还提到一点:
SQL 可改写:先利用覆盖索引快速拿到 50 个 order_id,再用这些 ID 去关联 orders 和 refunds,避免大表 JOIN 后排序。
这启发了我,我让 Cursor 帮我改写:
sql
-- 优化后的写法:子查询 + 延迟关联
SELECT t.order_id, t.tx_id, t.amount, t.status, t.created_at,
o.merchant_id, o.user_id,
r.refund_amount, r.refund_reason
FROM (
SELECT order_id, tx_id, amount, status, created_at
FROM transactions
WHERE status IN ('SUCCESS','FAILED','PROCESSING')
AND created_at >= '2026-04-01'
AND created_at < '2026-05-01'
ORDER BY created_at DESC
LIMIT 50
) t
LEFT JOIN orders o ON t.order_id = o.id
LEFT JOIN refunds r ON t.order_id = r.order_id
ORDER BY t.created_at DESC;
子查询里只取 transactions 表本身的列,可以利用覆盖索引直接完成排序和 LIMIT,不用回表。外层的 JOIN 只关联 50 行,refunds 表 600 万行也只扫描 50 行,成本极低。
新执行计划:
| 表 | type | rows | Extra |
|---|---|---|---|
| transactions | range | 286,512 | Using where; Using index |
| orders | eq_ref | 1 | |
| refunds | ref | 3 | Using where |
refunds 从全表扫描变成 ref,rows 从 600 万降到了 3。整个查询耗时从 8 秒降到了 120 毫秒。
这个过程中我学到的 :SQL 改写不是炫技,而是根据数据分布和业务需求做取舍。AI 在这一点上给出了一个教科书式的"延迟关联"优化,但它并不知道我们业务中 refunds 是否会有重复。我确认了 order_id 在 refunds 表里是唯一的,这才放心用。
额外收获:AI 教了我怎么写"验证脚本"
优化完成后,我需要一个能对比优化前后性能的脚本,并确认结果集完全相同。我向 Cursor 描述需求,它生成了以下 Python 脚本:
python
import pymysql
import time
def run_and_measure(cursor, sql, label):
start = time.time()
cursor.execute(sql)
rows = cursor.fetchall()
elapsed = time.time() - start
print(f"[{label}] 返回 {len(rows)} 行,耗时 {elapsed:.4f} 秒")
return rows, elapsed
conn = pymysql.connect(host='localhost', user='test', password='xxx', db='pay')
cur = conn.cursor()
# 原 SQL
old_sql = "..." # 省略
# 优化后 SQL
new_sql = "..." # 省略
old_rows, old_time = run_and_measure(cur, old_sql, "原SQL")
new_rows, new_time = run_and_measure(cur, new_sql, "优化后SQL")
# 验证结果集一致性(只比关键列,因为 LEFT JOIN 可能引入 NULL 差异)
old_set = {row[0] for row in old_rows} # 只比 order_id
new_set = {row[0] for row in new_rows}
if old_set == new_set:
print("✅ 结果集一致")
else:
print("❌ 结果集不一致")
print(f"原SQL独有的: {old_set - new_set}")
print(f"优化后独有的: {new_set - old_set}")
print(f"加速比: {old_time / new_time:.1f}x")
conn.close()
跑完的结果:
css
[原SQL] 返回 50 行,耗时 7.8921 秒
[优化后SQL] 返回 50 行,耗时 0.1187 秒
✅ 结果集一致
加速比: 66.5x
实话说,这个验证脚本比我自己写的好看且严谨。它甚至考虑到了结果集差异的可能------如果 refunds 表有多条记录,LEFT JOIN 可能会多出几行,AI 在注释里提醒了我这一点。
最终修复上线和长期效果
索引和 SQL 上线后,对账接口的 P99 延迟从 8 秒降到了 150 毫秒以内。持续观察了一周,数据库 CPU 使用率下降了约 15%。
更重要的是,我在这个过程中总结出了一套"AI + DBA"的协作模板,后面遇到其他慢查询,也能很快复用。
我沉淀的"慢查询优化协作模板"
现在每次遇到慢查询,我会按这个流程走:
1. 准备上下文(5 分钟)
收集三样东西喂给 AI:
- 慢 SQL 原文
- EXPLAIN 输出(最好是 JSON 格式
EXPLAIN FORMAT=JSON) - 表结构(
SHOW CREATE TABLE或包含索引的部分)
2. 提问三连(让 AI 先分析)
使用固定的提示词模板:
markdown
你是 MySQL 优化专家。以下是慢查询的诊断信息。
请严格按顺序回答:
1. 指出当前执行计划的瓶颈(哪个步骤最耗时、为什么)。
2. 给出索引优化建议(精确的 DDL),说明每个索引的设计理由。
3. 给出 SQL 改写建议(如有必要),写出优化后的 SQL。
4. 预估优化效果(为什么你觉得这样会更快)。
注意:要求 AI 按顺序回答,这样它的思考链是连贯的,不会跳步。
3. 人工验证(最关键的一步)
AI 输出后,我必须做三件事:
- 检查索引维护成本:索引大小、加索引的 DDL 耗时、是否影响写入。
- 检查 SQL 语义等价:LEFT JOIN 改写是否改变了行数、过滤条件是否等价。
- 在测试环境实际跑一遍:用 AI 生成的验证脚本,对比结果和耗时。
4. 灰度上线
小流量观察,确认无误后再全量。
和其他优化方式的对比
过去我试过的优化手段:
| 方式 | 耗时 | 质量 | 风险 |
|---|---|---|---|
| 纯人工分析 | 2~4 小时 | 依赖个人经验,有时漏掉好方案 | 中 |
| 数据库自带 Advisor | 1 秒 | 通用建议,不考虑业务特殊性,经常给错误索引 | 高 |
| AI 辅助(本次) | 30 分钟 | 类似高级 DBA 水平,但需要人把关 | 低 |
AI 不是替代人,而是把人从"查文档、猜方案"的循环里解放出来,把精力花在更重要的决策上。
三个重要的提醒
1. 不要把表结构和生产数据直接发给 AI
涉及数据安全,尽量只发送表结构、索引和 EXPLAIN 输出,不要发送真实数据行。必要时用脱敏数据。
2. 永远验证 AI 的输出
AI 给出的索引可能会"过度设计",比如建议一个 8 列的覆盖索引------它能加速查询,但会严重拖慢写入并占用大量内存。需要结合业务读写比例判断。
3. 学会"套娃":让 AI 校验 AI 的结果
如果你对 AI 给出的 SQL 改写不放心,可以再开一个对话,把原始 SQL 和改写后的 SQL 一起发给 AI,要求它"对比两个 SQL 是否逻辑等价,指出所有可能的语义差异"。有时候两个 AI 对话会给你交叉验证的惊喜。
参考来源
文中使用的各模型 API Key 均可从 gpt108.com 获取(该渠道提供 ChatGPT Plus、Claude Pro、Gemini Advanced、Cursor Pro 及 API 充值服务)。笔者团队生产环境已稳定运行 4 个月,仅作技术方案记录。
你在做数据库优化时用过 AI 吗?遇到过什么坑或者意外收获?欢迎评论区聊聊。