1. 先搞清楚索引到底在解决什么问题很多开发者写SQL够用但一到数据量上来就卡壳。你说他没写过SQL吧增删改查溜得很你说他懂吧一条SELECT能把数据库跑死。问题出在哪绝大多数情况是对索引的理解停留在这东西能加速查询的层面上根本不知道它加速的原理、适用的场景和踩坑的边界。索引这个东西本质上是数据库为了减少磁盘IO和数据扫描量而设计的一种附属结构。你可以把它理解成一本书的目录没有目录你要找某个知识点得从第一页翻到最后一页有目录你直接翻到对应页码几秒钟搞定。数据库里的全表扫描就是从第一页翻到最后一页索引就是那个目录。但目录也有讲究。不是随便在书最后塞几页纸就能叫目录数据库里的索引结构、字段选择、创建方式都存在大量细节。我见过太多人CREATE INDEX一把梭结果查询没快多少写入倒慢得感人最后还得删掉重建。这篇内容我就围绕两条线展开一条线是SQL本身的进阶写法另一条线是索引的设计与优化。因为这两件事是绑在一起的SQL写得好不好直接决定索引用得上用不上索引设计得合不合理直接决定SQL能不能跑得快。先搞懂原理再看实操最后讲排查一层层往下拆。2. 索引的数据结构为什么B树能成为数据库的默认选择2.1 B树与其他结构对比很多初学者第一次接触索引时会以为索引就是一颗二叉树。这个理解不算全错但离真相还差很远。数据库里最常用的索引结构是B树而不是普通的二叉搜索树也不是红黑树更不是哈希表。先对比几个结构二叉树的问题在于树的高度会随着数据量增长而变得非常高。假设有1000万条数据二叉树的高度大概在20多层每一层可能对应一次磁盘IO查一条数据要读20多次磁盘这个开销是无法接受的。红黑树虽然保证了平衡但本质还是二叉树高度同样下不来。B树神奇的地方在于它是一棵多叉的平衡树一个节点能存成百上千个key。同样是1000万条数据B树的高度通常只有3到4层。也就是说绝大多数情况下你查询一条数据只需要3到4次磁盘IO这比二叉树的20多次快了一个数量级。那哈希索引呢哈希索引的查询速度理论上是O(1)比B树还快。但哈希索引的致命弱点是不支持范围查询不支持排序也不支持前缀匹配。你写一个WHERE age 18 AND age 30哈希索引直接歇菜。实际业务中范围查询太常见了所以哈希索引只能作为补充不可能成为主流。2.2 为什么非叶子节点不存数据B树有一个非常关键的设计非叶子节点只存索引键值不存数据本身所有数据都挂在叶子节点上并且叶子节点之间通过链表相连。这个设计有两层深意。第一层每个节点能容纳的key数量大大增加。因为key通常比整行数据小得多同样的节点空间能塞下更多key树就变得更矮IO次数就更少。第二层叶子节点之间的链表让范围查询变成了一个顺序遍历的操作。比如你查age BETWEEN 18 AND 30B树先定位到18然后顺着叶子链表往后扫直到超过30为止整个过程基本是顺序IO性能非常稳定。我举个例子帮你理解。假设一棵树只有两层根节点像个路牌告诉你小于50的去左边大于50的去右边真正的货物数据都在下一层。路牌本身极小一次磁盘IO就能读进来下一层根据路牌指示径直到对应区块取货。这个过程又快又省资源和现实中在大型仓库里靠分区编号找货的原理一模一样。2.3 聚集索引与非聚集索引的区别索引结构说完紧接着要搞清楚的是一组非常重要的概念聚集索引和非聚集索引。聚集索引的叶子节点直接存的是整行数据。也就是说数据本身的物理顺序和索引顺序是一致的。InnoDB里每张表都必须有一个聚集索引默认是主键。你按主键查询时一次IO直接拿到全部数据效率极高因为不需要回表。非聚集索引也叫二级索引的叶子节点存的是索引键值加主键值。你通过非聚集索引查询时先在索引树里找到对应的主键然后再拿着主键去聚集索引里查整行数据这个动作叫回表。这里有个非常重要的性能判断标准回表次数越少查询越快。如果你能在非聚集索引的叶子节点里直接拿到想要的字段连回表都省了这就是覆盖索引。比如你有idx_user_age这个索引SQL只查SELECT age FROM user WHERE age 25索引里就有age不用回表直接返回效率拉满。举一个实际的例子。某业务表有几百万行数据经常要按用户状态统计数量。一开始直接SELECT COUNT(*) FROM orders WHERE status PAID每次查询都要扫全表耗时两三秒。后来我在status上建了非聚集索引由于InnoDB做COUNT时优先选最小的二级索引扫描这回查询直接降到几十毫秒原因就是扫描索引树比扫描聚簇索引的叶子节点要小得多。这个优化思路在报表场景里非常实用。3. SQL进阶写法从能用到用好的分水岭3.1 WHERE条件匹配索引生效的黄金法则SQL进阶的第一件事不是学什么高级语法而是搞清楚你写的WHERE条件能不能命中索引。这一点没搞明白后面全白搭。最核心的几条法则先列出来对索引列做计算、函数操作会导致索引失效。比如WHERE DATE(create_time) 2024-01-01索引在create_time上建了也没用因为数据库得先对每一行做DATE计算才能比较。正确写法是WHERE create_time 2024-01-01 00:00:00 AND create_time 2024-01-02 00:00:00。隐式类型转换也会让你吃大亏。比如索引列是varchar类型你写WHERE phone 13800138000数字数据库会把字符串列转成数字再比较索引直接失效老老实实用字符串形式写才能命中。LIKE查询的前缀模糊无法使用索引。WHERE name LIKE %张%和WHERE name LIKE %张都不会走索引只有WHERE name LIKE 张%才能利用索引的范围扫描能力。在我处理过的慢查询案例里隐式类型转换是个重灾区。有一次排查一条本应毫秒级返回的接口实际耗时将近1秒翻出SQL一看字段是varchar类型查询条件传了整型数据库只能把字段全部CAST一遍再做比较。改成字符串传参后查询直接走了索引耗时从1秒降到20毫秒。就改了一个参数类型天壤之别。这些细节看着小但在大表上会直接拉开几十倍的性能差距。SQL进阶的第一步不是学窗口函数是先学会让你的WHERE条件命中索引。3.2 JOIN的驱动表选择小表驱动大表多表关联查询是SQL进阶绕不开的点。JOIN本身不难写难的是写出性能可接受的JOIN。MySQL里JOIN的执行逻辑一般是先选一张驱动表逐行去匹配另一张被驱动表。驱动表的每一行都要在被驱动表里找匹配项。这时候被驱动表上有无索引直接决定匹配效率。这就是经典的小表驱动大表原则拿小表当驱动表大表当被驱动表且大表的关联字段必须建索引。举个例子。订单表有100万行用户表有1万行你要查订单中每个用户的信息。正确姿势是拿用户表当驱动表订单表当被驱动表订单表的user_id上建索引。这样只用遍历1万个用户每次去订单表用索引查一下关联记录。反过来的结果是你遍历100万行订单去匹配用户表就算用户表有索引开销也大得多。另外想提醒一句LEFT JOIN并不是强制左边的表当驱动表优化器有自己的判断逻辑。你可以用EXPLAIN看第一行是谁如果不符合预期必要时用STRAIGHT_JOIN强制指定驱动顺序。我一般在优化器犯傻的时候才用这个平时尽量让它自己选。3.3 窗口函数分组TopN的正确姿势讲到SQL进阶窗口函数必须重点说。窗口函数Window Function是在不合并行的前提下对每一行进行一个窗口范围内的计算。比如ROW_NUMBER() OVER (PARTITION BY category ORDER BY sale_amount DESC)可以在每个分类内部给数据排个名同时保留每一行的原始信息。做一个简单的对比你就知道窗口函数的价值了老式写法取每个分类销量前三的商品很多人会写成这样SELECT category, product_id, sale_amount FROM products p WHERE sale_amount ( SELECT MAX(sale_amount) FROM products WHERE category p.category AND product_id ! p.product_id -- 实际上这写法并不对这里只是示例 );这种子查询方案代码丑陋性能差逻辑还容易写错。窗口函数一行搞定SELECT category, product_id, sale_amount FROM ( SELECT category, product_id, sale_amount, ROW_NUMBER() OVER (PARTITION BY category ORDER BY sale_amount DESC) AS rn FROM products ) t WHERE rn 3;这段SQL的核心逻辑是先按category分组组内按sale_amount从高到低排个序号然后外面过滤掉序号大于3的行最终留下每个分类前三名。整个过程只需要扫描一次表效率非常高。窗口函数除了ROW_NUMBER()常用的还有RANK()排名可并列有跳号、DENSE_RANK()排名可并列不跳号、SUM() OVER(...)累计求和、LAG()/LEAD()取前后行等。面试题里最常见的分组TopN连续登录天数同比环比计算基本都是窗口函数的应用场景。3.4 分页深翻页优化LIMIT的隐藏陷阱分页查询大概是所有业务系统里最常用也最容易被忽视性能问题的SQL。LIMIT 100000, 20这样的写法在数据量小的时候感觉不到问题但一旦表里数据到了百万级你就会发现翻页越来越慢。原因是什么数据库为了拿到第100001条到第100020条数据会把前面10万条数据全部读出来一条一条数到偏移量再丢掉前10万条返回最后20条。这个数前面的10万条的过程浪费了大量IO。推荐的做法是用覆盖索引先定位偏移位置再关联回原表取数据。延迟关联是业内常用方案SELECT t.* FROM orders t INNER JOIN ( SELECT id FROM orders WHERE status PAID ORDER BY create_time LIMIT 100000, 20 ) tmp ON t.id tmp.id;这个写法的巧妙之处在于子查询只查主键id走覆盖索引速度极快找出20个id之后再回原表取完整数据。整个过程中真正回表的数据只有20条而不是10万条性能提升非常明显。如果你的业务有上一页/下一页这种场景还有一种更激进的方案记住上一页最后一条记录的排序字段值用WHERE create_time 上一页的最后时间 ORDER BY create_time DESC LIMIT 20来取下一页可以完全避开OFFSET。不过这个方案有一个限制就是不允许用户随意跳页。很多C端产品采用只允许上一页下一页的策略正是为了利用这个优化。4. 索引设计实战建索引不是顺手的事4.1 最左前缀原则是怎么起作用的在讲索引设计之前你先得把最左前缀原则搞懂因为这是联合索引一切规则的地基。联合索引(a, b, c)本质上按a先排序a相同再按b排序b相同再按c排序。所以这个索引能高效支持的条件组合是a、a, b、a, b, c。如果你直接查b 1对不起索引用不上因为树的第一层只按a组织b的排序只发生在a相等的前提之下你让数据库怎么在整棵树里快速找b这个原理很像手机通讯录。姓是首字母名是次级排序。你要找一个张伟先翻到Z区再在Z区里找伟如果你只告诉我名字里带伟的人我没法快速定位只能把整本通讯录抄一遍。这就叫中间条件断了后面条件全废。经常有人问我那把最常用的条件放最前面不就行了这话对但不全对。你得结合所有高频SQL来看选出能被最多查询复用的字段组合。比如你有两个高频查询一个是WHERE a ? AND b ?另一个是WHERE b ?。那(a, b)联合索引最多只能服务第一个查询而a单列索引也服务不了第二个。这时候没有完美解只能评估频率优先保高频。4.2 联合索引字段顺序的实战考量说到字段顺序我的建议是遵循一套优先级查询频率 区分度 业务稳定性。举个例子用户订单表里有user_id、status、create_time三个字段经常一起出现在查询条件里。怎么排先看频率user_id几乎每次都出现查某用户的历史订单把它放第一位。 再看区分度status只有几个枚举值区分度低放中间没问题。 最后create_time放最后还能顺带做排序优化。有些文章说区分度最高的字段放最前面这个说法在单条件查询下是对的但在联合索引中要结合查询频率来权衡。频率是哪些查询能用上这个索引的决定因素区分度只是索引内部扫描效率的影响因素。频率上的差距往往比区分度上的差距重要得多。4.3 覆盖索引与回表之间如何取舍我在前面提到过覆盖索引的概念查询的字段全部包含在索引中无需回表。这个技术在高频查询里极其有价值而且实现方式很灵活。比如业务上有一个高频查询根据用户ID查最近一笔订单的订单号和金额。你建一个联合索引(user_id, order_no, amount)那么SQLSELECT order_no, amount FROM orders WHERE user_id 128056 ORDER BY create_time DESC LIMIT 1;只要查询字段都在索引里MySQL就直接从索引树里拿数据不用回表。索引比数据行小得多同样的IO能读更多记录自然快。但覆盖索引不是没有代价的。你给索引塞的字段越多索引文件越大写操作维护成本越高缓冲池的压力也越大。所以覆盖索引的选用标准是高频查询、字段量少、内容稳定。低频报表类的需求不建议用覆盖索引去梭哈太浪费空间了。4.4 冗余字段与反范式用空间换时间的思路在索引优化之外SQL进阶还有一个非常实用的思路在表结构层面做些反范式设计从根源上减少复杂的JOIN和查询。举个例子订单列表页要展示用户名。一开始的做法是订单表只存user_id查询时JOIN用户表拿用户名。但订单量一大每次列表页都要JOIN性能就很紧张。这时候一个常见做法是在订单表里冗余一个user_name字段下单时从用户表带过来查询时就不需要JOIN了。这就是典型的反范式设计牺牲一定的数据冗余和一致性维护成本换取查询性能和SQL的简单性。这个方案当然有它的麻烦比如用户名改了历史订单里的名字不会自动变。有些业务能接受订单快照的语义名字以下单时为准有些业务不能接受。所以反范式不是什么场景都能用的要想清楚业务语义到底是最新状态还是历史快照。但从SQL进阶的角度讲懂得用空间换时间、用冗余换性能是一个成熟的开发者必备的思维。5. 执行计划让数据库亲口告诉你SQL该怎么调5.1 EXPLAIN输出里哪些信息最重要写一堆索引理论和SQL技巧最终还得看数据库买不买账。EXPLAIN就是你和数据库之间的翻译器它把SQL的执行计划摊开给你看。实际执行时重点看这几列type访问类型。性能从好到差依次是system、const、eq_ref、ref、range、index、ALL。如果你的SQL出现了ALL全表扫描那基本就是性能瓶颈所在。key实际用到的索引名。如果显示NULL说明没走任何索引这就是要排查的头号目标。rows预估扫描行数。这个数值越小越好。我通常先看这个如果预估扫描行数接近全表数量说明索引选择性不够好或SQL写法有问题。Extra这里内容最丰富。Using filesort表示需要额外的排序操作Using temporary表示用了临时表Using index表示覆盖索引Using where表示在存储引擎层之后再做过滤。有一次我排查一条报表SQLEXPLAIN出来Extra里赫然写着Using filesort。这个排序动作在百万行数据上会导致性能下降得厉害后来我把排序字段加入索引让ORDER BY直接走索引的有序性这次filesort从执行计划里消失了查询耗时直接降了几个量级。记住一条口诀ORDER BY的字段如果能安排进联合索引的末尾十有八九能干掉filesort。5.2 一个完整慢查询的排查过程说一个真实的排查过程方便你把上面的知识串起来。线上有一个接口某天开始持续变慢从原本的200ms涨到了2秒。翻出慢查询日志定位到一条SQLSELECT order_id, amount, status FROM orders WHERE user_id 123456 AND status PAID ORDER BY create_time DESC LIMIT 10;先执行EXPLAIN结果type ALL全表扫描key NULL没有索引可用rows 200万直接把整张订单表扫了一遍。再一看表结构只有主键索引和user_id的单列索引。为什么user_id的索引没被用上因为优化器评估后觉得哪怕走了user_id索引还得回表过滤status再filesort排序索性不走了直接全表全扫。优化方案分两步走。第一步创建联合索引(user_id, status, create_time)。这个索引同时覆盖等值条件user_id和status和排序条件create_time理论上查询可以直接走索引范围扫描且排序也能免去。第二步再考虑覆盖索引。查询还要取order_id, amount, status而order_id是主键二级索引叶子节点自带主键status在联合索引里只有amount不在。所以把索引扩成(user_id, status, create_time, amount)就能实现完全覆盖连回表都省掉。最终实测同一个查询从2秒降到了30毫秒提升了近70倍。整个过程没有改一行业务代码纯粹是索引设计的问题。5.3 预估行数与统计信息的关联还有一个容易忽略的细节优化器做判断时依赖的是统计信息。InnoDB的统计信息不是实时的有时候你建了索引但优化器不选就可能是统计信息陈旧导致的。遇到这种情况可以执行ANALYZE TABLE刷新统计信息让优化器能更准确地估算行数。我见过一个案例表里数据做了大批量清理之后统计信息还停留在几百万行的水平优化器错误地认为走索引比全表扫描更慢结果每次都做全表扫描。执行ANALYZE TABLE之后优化器才恢复正常判断。不过要提醒你ANALYZE TABLE在超大表上也会锁表不要在业务高峰期随手执行要挑低峰期操作或者干脆让系统自动维护统计信息的周期覆盖这种场景。6. 索引失效与常见问题的排查技巧6.1 惯性踩坑场景速查做了这么久的SQL优化有些坑是反复出现的我直接整理成一个列表你排查的时候可以对照查看。对索引列使用函数或表达式计算索引失效。隐式类型转换导致索引失效。LIKE前缀模糊查询导致索引失效。OR条件中有一个字段没索引整个查询可能转成全表扫描。NOT IN、NOT EXISTS等负向查询通常无法走索引。联合索引没有遵循最左前缀原则。ORDER BY字段不在索引中出现filesort。LIMIT深分页导致大量无效回表。优化器统计信息不准确导致错误地放弃索引。表数据量过小优化器认为全表扫描更快就不走索引了。这最后一个坑特别有意思。有时候你建了索引EXPLAIN一看type ALL就以为索引没建上。其实是因为表里就几千行数据全表扫描比走索引更快优化器做了正确选择。数据量上来之后它自然会走索引。所以排查问题时一定要结合数据量来看执行计划别一看全表扫描就乱了阵脚。6.2 计算列带来的隐性问题很多人会在索引列上做计算、做函数操作、做字符串拼接比如WHERE YEAR(create_time) 2023、WHERE price * quantity 100、WHERE CONCAT(first_name, last_name) 张三。这些写法有一个共性对索引列做了加工加工的结果是数据库必须在每一行上先计算一遍才能参与比较索引自然就用不上了。遇到函数操作的场景MySQL 5.7以上的版本有一种方案在表里增加一个计算列然后在计算列上建索引。比如ALTER TABLE user ADD COLUMN year_created YEAR AS (YEAR(create_time)) STORED; CREATE INDEX idx_year_created ON user(year_created);之后查询改成WHERE year_created 2023就能命中索引。虽然索引本身占空间但比起每次全表扫描优势依然是决定性的。MySQL 8.0也支持函数索引可以直接CREATE INDEX idx_year ON user ((YEAR(create_time)))写法更简洁。6.3 多个单列索引的误区索引合并我还发现很多人有个直觉字段多就每个字段建一个单独的索引。这种思路看似合理实际隐藏问题。当一条SQL的WHERE条件里同时出现多个单列索引字段时MySQL确实可能做一个叫索引合并的操作把多处扫描结果合并但这往往是补救性质的行为性能远不如直接建一个联合索引。打个比方你要查住在某小区的姓张的男生有两张单独的名单一张按小区分类、一张按姓氏分类。最笨但常见的做法是先在名单A里找出所有该小区的人再去名单B里找所有姓张的最后比对合并两份名单。如果有一份按小区姓氏性别交叉归档的总表直接一页翻到目标位置效率显然高得多。所以能用联合索引尽量用联合索引避免散装多个单列索引。单个索引并不是建得越多越好索引过多还会拖慢写入性能、占用磁盘空间、增大优化器的决策成本。7. 一个完整的SQL优化实战复盘7.1 业务场景还原与问题定义这里我想完整复盘一个我印象很深的优化案例把前面所有知识串起来。某业务场景有一张订单流水表trade_log经过长时间增长已有接近2000万行数据每天都有大量新写入。某天的报表查询开始频繁超时慢查询日志里堆了几条明显有问题的SQL大致汇总如下按用户ID查最近20笔订单SELECT * FROM trade_log WHERE user_id ? ORDER BY create_time DESC LIMIT 20按时间段统计每日订单总量和金额SELECT DATE(create_time) AS day, COUNT(*), SUM(amount) FROM trade_log WHERE create_time BETWEEN ? AND ? GROUP BY day后台列表分页SELECT * FROM trade_log ORDER BY create_time DESC LIMIT 100000, 20我拿到这些信息之后第一反应是先用EXPLAIN把每条SQL的执行计划拉出来看一眼而不是急着建索引。只有看到实际执行计划才知道哪些索引缺了、哪些索引建了也没用。7.2 逐条SQL的优化过程针对第一条按用户查最近订单建联合索引(user_id, create_time)把等值条件和排序条件一起放进索引。这还不够查询要返回全字段意味着不可避免要回表。考虑高频在索引里加上常用字段order_no, amount变成(user_id, create_time, order_no, amount)让高频查询实现覆盖索引。实测后该类查询从原来的几十毫秒到几百毫秒不等稳定在个位数毫秒。针对第二条按日统计的SQL前面提过DATE(create_time)会导致索引失效。这里改写成范围条件SELECT DATE(create_time) AS day, COUNT(*), SUM(amount) FROM trade_log WHERE create_time 2024-01-01 00:00:00 AND create_time 2024-01-31 00:00:00 GROUP BY day;然后在create_time上建普通二级索引。范围条件可以走索引扫描了但GROUP BY day则需要先把符合范围的数据拿出来再做聚合数据量大的时候还是有压力。所以更进一步的方案是引入汇总表每隔一段时间把明细聚合成每日统计查询直接查汇总表。这个思路叫预聚合在大数据报表场景里极其常见。针对第三条深分页SQL采用延迟关联优化子查询先按create_time索引取出主键ID再关联主键取全字段。改造之后翻到十万页也只回表20条性能提升非常明显。7.3 写入性能与索引数量的平衡这个案例的最后还面临一个取舍问题为了查询优化一口气加了几个索引结果发现写入变慢了。因为这表本身日写入量就大每一个索引在写入时都要同步更新索引越多写入放大越严重。我最后做了一件事重新梳理业务上真正的高频查询把低频需求从索引设计里拿掉将原来的7个索引精简到3个联合索引保证每个索引都能覆盖到多种查询场景。再次测试写入性能恢复到了可接受的水平查询性能也保持优秀。这值得每个做SQL优化的人记住索引不是多多益善而是按需设计。每多一个索引都是在用写入性能和存储空间换取查询性能。你要做的是在这两者之间找到平衡点而不是一味地堆索引。8. 索引维护与长期健康管理8.1 索引碎片化表长期增删改之后索引页会逐渐产生碎片。碎片意味着索引的逻辑顺序和物理存储顺序不一致扫描效率会下降。就像你把一本书的章节顺序打乱页面还在但你要按顺序读就得来回翻页。MySQL里OPTIMIZE TABLE可以重建表和索引整理碎片。要注意的是这个操作在表数据量大的时候耗时很长并且会锁表生产环境务必安排在维护窗口执行。因此我的建议是在业务低峰期定期执行OPTIMIZE TABLE或者提前规划表分区、归档旧数据防止单表无限膨胀关注表行数增长趋势建立容量预警机制。说句实在话实际业务中很多查询突然变慢的案例并不是SQL变了而是表数据量涨到了某个临界点或者碎片化让原本高效的索引退化成了低效的扫描。8.2 无用索引的识别与清理业务迭代过程中表结构会不断变。你可能会发现某些索引已经很久没有出现在任何一条SQL的执行计划里它们就是所谓的僵尸索引。怎么识别两个思路。一是开启performance_schema或者查询慢日志分析工具看看最近一段时间内各索引的访问次数。索引长期不被使用就应该考虑移除。二是定期做一次全量SQL梳理对比每个索引与当前业务SQL的匹配情况。好处不只是发现无用索引还能发现索引设计的优化空间比如多个单列索引可以合并成一个联合索引。清理无用索引对写入性能的提升立竿见影因为每次INSERT、UPDATE、DELETE操作都不再需要维护这些多余的结构了。8.3 生产环境的索引变更流程最后补一个上线流程的建议因为这个环节踩坑的代价很高。索引变更虽然比表结构变更简单但也不是说建就建的。在超过千万行的大表上直接执行CREATE INDEX虽然MySQL 5.6以上的版本支持在线DDL多数情况下不会锁表但仍然会消耗大量IO和CPU资源可能影响线上业务。我的习惯流程是先在测试库上执行验证索引对查询的实际效果在预发环境用EXPLAIN确认执行计划符合预期非高峰期在线上执行优先使用ALGORITHMINPLACE和LOCKNONE选项让索引创建过程对业务的影响降到最低上线后持续观察慢查询指标和写入延迟指标确认没有副作用。另外索引变更前记得备份当前表结构信息需要回滚的时候至少知道原来的索引长什么样。这些看起来琐碎但真出问题的时候每一环都能救命。9. 复盘这段时间在SQL进阶与索引上踩过的最值的几个坑文章最后分享几个印象最深的实操体会希望能帮你少走弯路。第一个体会优化SQL之前先把表和索引的实际情况看清楚。我见过太多人一上来就猜是不是索引没建最后发现是慢在排序或者回表上。EXPLAIN这条必杀技一定要养成习惯分析任何SQL都先看执行计划没有调查就没有发言权。第二个体会别为了学到某个高级功能而去用复杂写法。SQL进阶的真正意义是让你知道有哪些更优的写法可以替代旧的、慢的写法。能用普通JOIN解决就不要用奇怪的子查询能用简单索引解决就不要引入冗余表。代码越简单后续维护成本越低。第三个体会索引设计不是一次性的工作而是随着数据量和业务演变需要持续调整的。我见过设计方案时认为很完美的索引在半年的数据增长之后逐渐失效的情况。定期复盘线上的慢查询日志观察索引使用情况应当成为数据库日常运维的一部分。最后一个我很深的感受SQL优化的成就感不在于你用了多高级的语法而在于你用最简单的手段解决了最大的问题。一条本来要扫2000万行才能出结果的SQL通过一个联合索引把扫描范围缩到几百行那种秒杀的感觉是实实在在的。希望这篇内容也能帮你找到这种感觉。单看理论可能记得住但真真正正要形成直觉还得靠大量案例积累。建议你从现在开始每遇到一条慢SQL都习惯性地做三件事看执行计划、看表结构、看索引使用情况。重复三个月你会发现自己对SQL和索引的理解已经和以前不在一个层次上了。