大家好,我是数据库小学妹👋 我踩过的坑,你别再踩。
有段时间我迷上了让 AI 看慢查询、提索引建议。它返回得又快又规整,DDL 写得整整齐齐,我一度觉得这东西又全又对。直到我把一批建议放到生产验证,才发现自己太天真。它漏了最左前缀,还给我加了两条重复索引,等于白花写入代价。
那次之后我改了个习惯。AI 给的每条索引建议,我都不直接上,先拿执行计划过一遍。后来我摸清了它常犯的错,也摸清了它真正能帮上忙的地方。今天把这段历程拆给你。
(后面出现的表名和数字,是我为了讲清楚搭的示意场景,不是真实压测数据。换到你的表,以实测为准。)
它为什么容易被信
AI 提索引建议,逻辑很顺。它看到一条带 WHERE 和 ORDER BY 的 SQL,就照着字段往里塞。语句通顺,DDL 合法,看着没毛病。
问题在于,它看不到三样东西。你表上已经有哪些索引,它不知道。这个查询要扫多少行、哪列的选择性高,它不知道。最关键的,这张表是写入多还是读取多,它更不知道。索引是双刃剑,加错了是要还的。它只看得见你喂给它的那条 SQL,看不见这张表的脾气。
它常犯的三类错
第一类,漏最左前缀。来看一个复现出来的例子。一条查某用户最近订单的语句。
SELECT id, amount, create_time
FROM t_order
WHERE user_id = 88482
ORDER BY create_time DESC
LIMIT 20;
AI 给的是一条以 status 领头的索引。
-- AI 的建议
ALTER TABLE t_order ADD INDEX idx_status_time (status, create_time);
看着合理,其实没用。查询里根本没给 status,索引第一列就落空,user_id 又不在索引里。这条索引对不上这个查询,退化成扫描加排序。它把 ORDER BY 的列记住了,却忽略了领头的等值条件。正确的列序是等值列在前、排序列在后。
-- 正确的列序:等值 user_id 在前,排序列 create_time 在后
ALTER TABLE t_order ADD INDEX idx_user_time (user_id, create_time);
前后差别,用 EXPLAIN 一照就清楚。
| 方案 | key | rows | Extra |
|---|---|---|---|
| AI 建议(status, create_time) | NULL | 全表量级 | Using where; Using filesort |
| 改为(user_id, create_time) | idx_user_time | 该用户的订单量级 | Using where |
再看一眼 key_len。它反映 WHERE 条件实际吃到了联合索引的前几列。AI 那条 key 是 NULL,key_len 也是 NULL,压根没进去。换成 (user_id, create_time),key_len 能对上 user_id 的长度,说明领头列吃上了。排序有没有走索引,key_len 看不出来,得看 Extra 里有没有 Using filesort。可空列和变长列会多算几个字节,对照时按字段定义来,别按裸长度硬套。
还有个细节容易记反。查询里有多个等值条件时,等值列之间谁前谁后都能用上,但都得排在排序列前面。比如 WHERE user_id = ? AND status = ? ORDER BY create_time,(user_id, status, create_time) 和 (status, user_id, create_time) 都能同时吃到过滤和排序。换成 (user_id, create_time, status),排序照样能走索引,但 status 只能在扫描时逐行过滤,范围没被提前收窄。
第二类,重复冗余索引。AI 给一张表提建议时,常常忘了你已经有什么。它可能给你一条 (user_id),可你表上早有 (user_id, create_time)。前者是后者的前缀,白白多占一份写入。也可能一口气提两条,(user_id, status) 和 (user_id),直接冗余。
第三类,对写多的表乱加,制造写放大。每加一条二级索引,INSERT、UPDATE、DELETE 都要多维护一棵树。插入时,索引要先按键值找到插入点。页写满了,还会分裂。更新索引列,等于删一条再插一条。唯一索引还得逐条做唯一性校验。索引一多,buffer pool 被分走,redo 也跟着涨。一张交易流水表,本来就扛着高并发写,AI 为了几条报表查询给你塞四个索引,报表是快了,写入全慢了。高并发下写入延迟开始抖,先数一数索引,别急着怪数据量。这笔账,它从来不替你算。
先量基准,再谈改
我以前犯的错,是拿到建议就想着怎么加。现在我反过来,先把"改之前"的样子量出来。没有基线,你连它有没有用都判断不了。
-- 先清零,后面的计数才算这条查询的
FLUSH STATUS;
-- 8.0.18+:给出真实行数与耗时,不只看估算
EXPLAIN ANALYZE SELECT id, amount, create_time
FROM t_order WHERE user_id = 88482
ORDER BY create_time DESC LIMIT 20;
-- 看这段读写实际碰了多少行
SHOW SESSION STATUS LIKE 'Handler_read%';
EXPLAIN 看的是优化器的估算,EXPLAIN ANALYZE 跑的是真实执行。两个一起看,才知道这条查询现在到底卡在哪。是缺索引,还是索引用错了列序,还是根本该改 SQL。
Handler_read% 别只看总量,得分清楚谁是谁。Handler_read_key 是按索引键定位的次数,说明走了索引。Handler_read_next 是按索引顺序往下读,范围扫描和排序常见。Handler_read_rnd_next 高,基本就是全表扫。三个数摆到一起,是缺索引、列序不对,还是本来就在扫全表,心里就有数了。
逐条验证的四步
验证一条建议,我边走这四步。
先查已有索引,别撞车。
SELECT INDEX_NAME, SEQ_IN_INDEX, COLUMN_NAME, CARDINALITY
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA='app' AND TABLE_NAME='t_order'
ORDER BY INDEX_NAME, SEQ_IN_INDEX;
CARDINALITY 是估算的去重值,不是精确数。拿它估选择率可以,别拿来当判决。大批量导入或删除之后,统计会过期,EXPLAIN 的 rows 也跟着偏。判断前先 ANALYZE TABLE t_order,让优化器拿新统计说话。大表上它会采样索引页,仍有 IO 开销,挑低峰做。
再让 sys 库替你找冗余和没被用过的索引。
SELECT * FROM sys.schema_redundant_indexes WHERE table_schema='app';
SELECT * FROM sys.schema_unused_indexes WHERE object_schema='app';
第三步,把建议的索引放到 EXPLAIN 里对照,看它到底会不会被选中,key 是不是它,rows 降没降。第四步,评估写入代价,看这张表的读写比,必要的用压测比一下加索引前后的写入吞吐。
-- 8.0:看各索引的读写代价
SELECT * FROM sys.schema_index_statistics WHERE table_schema='app';
一套流程:AI 提,人来判
走到这里,我把用法固定成一条流水线。
AI 提建议,人来判。判断就看三样:已有索引里有没有冗余,这张表的读写比撑不撑得住,查询模式是不是高频。判过之后,再小流量验证,拿真实执行计划确认它真被用上。最后才上线。
一句话,AI 负责"想得到",人负责"该不该"。 它能帮你发现一个你没想到的候选索引,但加不加,是另一回事。
什么场景它能帮上忙
也不是说它一无是处。它的强项在低写入、结构清晰的慢查。
| 场景 | 能不能信 | 为什么 |
|---|---|---|
| 大宽表慢扫描,给过滤列加索引 | 高 | 写得少,收益直接 |
| 热点写表(交易、流水) | 低 | 写放大它算不到 |
| 联合索引的列序设计 | 低 | 依赖查询模式与最左前缀,它常排错 |
| 覆盖索引、减少回表 | 中 | 要看 SELECT 列与真实执行计划 |
| 其实该改 SQL,不是加索引 | —— | 它默认只会"加索引"这一个动作 |
判断力
AI 能帮你发现可能的索引,但决定加不加,永远要回到两个问题:这张表的读写比是多少,这条查询的真实代价有多高。它给你的是候选,不是结论。把候选当结论,代价会记在你的写入上。
读写比不用拍脑袋。performance_schema 里就有现成的计数,按表拆开看,比凭感觉准得多。
-- 这张表到底是读多还是写多
SELECT OBJECT_NAME, COUNT_READ, COUNT_WRITE
FROM performance_schema.table_io_waits_summary_by_table
WHERE OBJECT_SCHEMA = 'app' AND OBJECT_NAME = 't_order';
读的次数远超写入,加索引这笔账好算。写入比读取还多,就得先答一个问题:这条查询值不值得让每次写入替它买单。AI 答不了这个。它连这张表是流水还是字典都不知道。
真实代价也别只看 EXPLAIN 的估算。去 digest 表里翻这条查询,看它调了多少次、扫了多少行、占总耗时多少。前提是 performance_schema 开着。
SELECT DIGEST_TEXT, COUNT_STAR, SUM_ROWS_EXAMINED,
ROUND(SUM_TIMER_WAIT/1e9, 0) AS total_ms
FROM performance_schema.events_statements_summary_by_digest
WHERE DIGEST_TEXT LIKE '%t_order%'
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 5;
一条每天只跑三次的报表查询,扫描行数再大也没必要加索引。为它加,只会拖慢所有写入。反过来,一条每秒几百次、每次扫几万行的查询,收益明摆着。收益是省下的扫描行数乘以调用频次,成本是每次写入多维护一棵树。两边摆到一张纸上,答案不难选。
避坑清单
AI 不知道你的写入压力,索引是双刃剑。写多读少的表,加索引可能赔本,别只看它把查询变快了。每多一条二级索引,插入、更新、删除就多一次维护。这种表加索引,最好压测前后比一比写入吞吐,或者小流量跑几天,盯住写入延迟。加完之后写入变慢,先回头看索引,别急着怪数据库。
重复和冗余的索引会拖慢写入,上线前必查。sys.schema_redundant_indexes 和 schema_unused_indexes 两个视图,能帮你挡掉一大半。但这两个视图只统计 performance_schema 打开之后的访问,新实例别急着拿它当判决书。至少观察一个完整业务周期,比如熬过一次月末报表,再决定删不删。还有个容易漏的细节,(user_id) 被更长的联合索引收着,也算冗余,视图有时未必点全。
最后一条是我自己踩的。我一开始看 AI 生成的 DDL 那么规整,就觉得稳了,差点直接上。被一句话点醒:它没看过你的执行计划,DDL 漂亮不代表索引会被用上。现在每条建议我都先跑 EXPLAIN 对照,再补一次 EXPLAIN ANALYZE 看真实耗时。key 用上了也可能不划算,得看实际扫了多少行、省了多少时间。我给自己定了条规矩:没跑过执行计划的索引建议,不进变更单。
写在最后
AI 提索引建议,最大的坑不是它答错,是我们太信。它看得见语法,看不见你的数据分布、已有索引和写入压力。所以它只能当前排助手,当不了拍板的人。我现在换了用法,先把建表语句、现有索引和这张表的读写情况一起喂给它,再让它提候选。料给足了,它犯的错会少很多,剩下的交给 EXPLAIN 和真实写入数据。加索引是笔权衡的账,AI 擅长把可能性铺开,人得负责把账算平。它写的 DDL 再规整,也只是草稿。
你们会让 AI 帮忙提索引或优化建议吗?提完之后,又是怎么验证的?评论区聊聊。
我是数据库小学妹,帮你少走弯路少踩坑,咱们下篇见👋