MySQL 8.0索引新特性实战:不可见、降序、函数索引与Skip Scan MySQL 的索引是个老生常谈的话题但说实话大部分人对索引的认知还停留在主键索引、普通索引、联合索引这些“老三样”上。我自己也是踩过不少坑之后才发现MySQL 8.0 之后在索引上的更新远比想象中多不可见索引、降序索引、函数索引、索引跳跃扫描每一个都在解决过去让人头疼的实际问题。这篇就来聊聊我在实际使用中验证过的几个 MySQL 索引新特性它们到底新在哪里、该怎么用、有哪些坑。适合正在学 MySQL、准备面试、或者日常写 SQL 优化慢查询的同学尤其是那些已经知道 B 树和聚簇索引、但一直没时间系统了解 8.0 新特性的朋友。1. 索引到底是怎么帮我们加速的先温习一个基本问题在看新特性之前我建议先花几分钟把索引的原理在脑子里过一遍。原因是后面讲的每个新特性本质上都在围绕索引的底层存储结构做文章你只有先理解了“为什么索引能快”才能真正明白“这些新特性在解决什么”。1.1 B树索引加速的底层逻辑InnoDB 的索引结构是 B 树。你可以把 B 树想象成一本带目录和页码的书非叶子节点相当于章节目录叶子节点是实际内容而且叶子节点之间按顺序连成一个链表。查询一条数据时从根节点出发每层比较一次几次就能定位到叶子节点。树的高度一般只有 3 到 4 层所以哪怕一张表有几千万行定位一条记录也只需要几次磁盘 IO。这个结构有两个关键点。第一数据是有序存储的所以索引不仅能加速等值查询还能加速范围查询、排序和分组这也是很多优化策略能成立的基础。第二叶子节点上的双向链表让 MySQL 可以按索引顺序快速扫描这也解释了为什么 ORDER BY 走索引时不需要额外排序。理解了这两点你再看新特性就会容易很多。比如降序索引解决的就是“如何让索引内部的顺序更加贴合查询需求”函数索引解决的是“如何让一个函数表达式的结果也能按字典序存储在 B 树里”。这些都是基于 B 树的特性做出来的扩展。1.2 为什么新特性总是围着索引转早期 MySQL 的索引功能其实比较朴素后来为了和 Oracle、PostgreSQL 竞争才逐步引入了一波新能力。这些新特性主要解决两类问题。第一类是让优化器有更多执行路径可选。比如不可见索引以前想知道某个索引能不能删只能 DROP 之后观察风险很大现在可以直接让优化器“看不见”它验证完再决定去留。第二类是减少人工 SQL 改写。比如查询里对列使用了函数导致索引失效以前的常见做法是改写 SQL 为范围查询或者维护冗余列。MySQL 8.0 之后可以直接创建函数索引让优化器原生支持这类场景你在 SQL 层基本不用动业务代码。我建议在看后面的内容之前先确认你本机 MySQL 版本是 8.0 及以上因为下面聊的大部分新特性在 5.7 及以下版本里是用不了的。可以用一条命令确认SELECT VERSION();如果版本低于 8.0很多语法虽然不报错但实际会被优化器忽略排查问题时会很困惑。2. 不可见索引让优化器“看不见”却能随时恢复的索引不可见索引是我第一个想安利给所有人的新特性因为它几乎是线上环境调整索引风险最低的手段。它的核心思想很简单创建一个索引但数据库优化器在生成执行计划时会忽略它就像这个索引不存在一样。2.1 不可见索引的语法与用法创建不可见索引的语法非常直观CREATE INDEX idx_email ON users(email) INVISIBLE;也可以在创建表的时候直接指定或者用 ALTER TABLE 追加ALTER TABLE users ADD INDEX idx_email (email) INVISIBLE;这里的关键是最后的INVISIBLE关键字。创建完成后用 SHOW INDEX 查看会发现多了一列Visible或Visibility值为 NO 就代表当前不可见。如果你已经有一个可见索引想把它临时改成不可见不需要 DROP 再重建直接ALTER TABLE users ALTER INDEX idx_email INVISIBLE;想恢复就改成 VISIBLEALTER TABLE users ALTER INDEX idx_email VISIBLE;另外优化器开关里有一个参数控制是否使用不可见索引SET SESSION optimizer_switch use_invisible_indexeson;默认是 off也就是优化器无视不可见索引。如果你在某个会话里把这个参数打开优化器就会把不可见索引当作普通索引来考虑这在调试某个 SQL 是否有必要用这个索引时非常有用。2.2 不可见索引的最佳实践场景我实际使用中最常见的一个场景就是“判断一个索引能不能删”。有一次线上有个订单表历史原因建了一个三列联合索引同事怀疑已经没有业务在使用它但没人敢直接删因为删了如果发现性能下降重建大索引耗时可能非常久而且会导致锁表风险。这种情况下不可见索引就非常合适。操作流程就是先把这个索引设为不可见观察线上慢查询、错误日志、业务反馈观察周期看业务规模一般建议至少一个完整的业务周期比如一周。如果没有任何异常就可以放心 DROP如果发现问题一条 SQL 就能恢复可见整个过程几乎不产生不可逆影响。第二个场景是应对“短期内写了大量数据索引维护成本过高”的问题。比如你有一个报表表平时有索引支撑查询但某个跑批任务要瞬间更新大量行每个索引都要同步更新写放大特别严重。如果批量任务期间不依赖这个索引可以先把它设为不可见减少 DML 开销跑批结束后再恢复。注意这里说的是减少一部分开销因为索引维护只是写入开销的组成部分并不是全部。第三个场景是验证“新查询到底要不要建索引”。业务上线前SQL 执行比较慢你推测可能是缺索引但又不想贸然加索引影响线上。可以先创建一个不可见索引然后在会话里打开use_invisible_indexes用 EXPLAIN 看这个 SQL 是否能用上它。确认有效后再把索引改成可见避免“建了没用”的索引继续占用资源和增加写负担。2.3 使用不可见索引的坑不可见索引看着好用但有几个细节特别容易踩坑。第一主键索引不能设置为不可见。如果你尝试对主键执行 ALTER INDEX INVISIBLEMySQL 会直接报错。原因也不难理解InnoDB 是聚簇索引组织表主键就是数据的物理存储顺序数据文件依赖它表结构本身不允许它“消失”。第二唯一索引即使设成不可见唯一性约束依然会生效。也就是说写入时 MySQL 仍然会检查这个唯一索引是否冲突但查询时优化器不会用它。如果你想通过把唯一索引设为不可见来减少写入开销那这个想法是错的因为唯一性校验不会因此减少。第三不可见不代表索引文件被删除了索引占用的磁盘空间、统计信息、更新维护都照常进行。它只是在优化器选择路径时被排除物理文件还在DML 时的维护成本也还在。第四线上环境千万不要全局打开use_invisible_indexeson否则不可见索引就失去了“不可见”的意义所有不可见索引都会被优化器考虑。这个参数更多是我做问题定位时用建议只在会话级别开启。我在实际中观察过不可见索引最舒服的使用节奏是先设置为不可见配合慢查询监控观察几天确认没有问题再 DROP不要一上来就删除也不要设置完就忘了最后留下一堆不可见索引占空间。3. 降序索引告别文件排序的实用优化降序索引在 MySQL 8.0 之前一直没被真正支持。过去的版本里你虽然可以在 CREATE INDEX 语法里写 DESC但 MySQL 只会记住这个定义实际存储和扫描依然是升序导致一个非常经典的性能问题多列排序方向不一致时索引很难直接帮忙排序只能额外走 Using filesort。3.1 为什么降序排序用不上升序索引我们举个例子。假设有张订单表CREATE TABLE orders ( id INT PRIMARY KEY, user_id INT, created_at DATETIME );业务查询经常是SELECT * FROM orders WHERE user_id 123 ORDER BY created_at DESC;如果建的是普通升序联合索引(user_id, created_at)那么索引内部的顺序是 user_id 升序、created_at 升序。由于 user_id 是等值条件在 user_id123 这个范围内created_at 的索引数据已经是有序的正向扫描就能直接拿到升序结果反向扫描就能拿到降序结果所以这种情况下普通升序索引能同时满足升序和降序不需要额外排序。问题出在多个列的排序方向不一致。看这个例子SELECT * FROM orders ORDER BY user_id ASC, created_at DESC;联合索引存储顺序是 user_id 升序、created_at 升序。正向扫描的结果是 user_id 升序、created_at 升序不符合“created_at 降序”的要求。反向扫描结果变成 user_id 降序、created_at 降序user_id 的方向又反了。两头都不对优化器就只能把查到的主键回表取出整行数据再做一次内存或磁盘排序也就是 Using filesort。数据量一大这个排序开销非常可观。3.2 创建降序索引的语法与验证MySQL 8.0 之后可以在索引定义中单独指定每个列的方向CREATE INDEX idx_user_created ON orders(user_id ASC, created_at DESC);很多人的习惯是只写 ORDER BY 语句里需要的 DESC但实际上创建索引时可以分别指定。这样索引内部的存储顺序就和查询需要的排序顺序完全一致了优化器直接按索引顺序扫描返回结果Extra 里不会再出现 Using filesort。怎么验证降序索引真的生效了两个办法。一个是用 SHOW CREATE TABLE 查看建表语句你会看到索引定义里明确显示(user_id, created_at DESC)。另一个是用 EXPLAIN 查看执行计划。如果优化器用了这个索引并且不再需要额外排序Extra列里就不会有 Using filesort。比如| id | select_type | table | type | possible_keys | key | rows | Extra | | 1 | SIMPLE | orders | ALL | idx_user_created | idx_user_created | 100 | Using where |如果还是出现 Using filesort说明查询的排序方向和索引定义没有对上或者优化器认为全表扫描排序更快。3.3 降序索引的适用场景与限制降序索引最有价值的场景是“多列排序方向组合”。单列索引加 DESC 意义不大因为单列索引本来就可以反向扫描升序索引拿到降序结果毫无压力。真正需要降序索引的是组合列比如上面说的ORDER BY col1 ASC, col2 DESC这种一个升序一个降序的情况。另外前面提到WHERE user_id 123 ORDER BY created_at DESC不需要降序索引但如果查询变成WHERE user_id IN (1,2,3) ORDER BY created_at DESC问题就复杂一些了。user_id 是范围条件而不是单等值时索引只能保证每个 user_id 内部 created_at 有序从多个 user_id 合并结果时可能又需要排序。这种情况下建立一个(created_at DESC, user_id)的索引反而可能是更好的方案具体看业务查询条件分布用 EXPLAIN 验证最靠谱。降序索引有几个限制要记住。第一MySQL 5.7 及以下版本虽然能执行建索引语句但 DESC 被忽略索引仍然是升序存储。第二对于 InnoDB 来说降序索引的叶子节点内部是倒序链表MySQL 8.0 的扫描逻辑已经完整支持但如果你用旧版本驱动或者旧版 pt-online-schema-change 之类的工具操作可能出现兼容性问题。第三降序索引同样会增加写入和存储开销尤其是 DML 频繁的大表上不是所有排序慢的地方都适合硬加索引。我在一个报表系统里实测过一单。某查询在 2000 万行数据上做ORDER BY customer_id ASC, order_time DESC原来 Using filesort 开销大概 1 秒多加上分页之后更慢。加了一个降序索引后查询耗时降到 100 毫秒以内提升非常明显。但前提是这个查询是这个表的核心路径如果是低频查询为了它增加一个索引的写入成本就要掂量一下了。4. 函数索引让条件查询不再被迫“失效”函数索引是我个人认为日常开发价值最高的一个特性因为它直接对治了最经典的“索引失效”问题——对索引列使用函数。8.0.13 之前MySQL 并不支持直接在索引定义中使用函数表达式8.0.13 起原生支持底层相当于是虚拟列索引。4.1 索引失效的经典场景对索引列使用函数先看个最常见的例子SELECT * FROM orders WHERE DATE(created_at) 2024-06-01;如果 created_at 上有普通索引这个查询能用上吗不能。原因是索引中存储的是 created_at 的原始值比如2024-06-01 10:23:45而现在查询条件是DATE(created_at)的结果2024-06-01这两个东西在 B 树里没有直接对应关系。优化器没法利用索引的有序性直接定位到目标数据只能全表扫或者使用其他条件过滤。类似的问题还有SELECT * FROM users WHERE LOWER(email) testexample.com; SELECT * FROM product WHERE price * 0.8 100; SELECT * FROM user_phone WHERE phone 13800001111; -- phone 是 varchar最后一个其实是隐式类型转换导致索引失效本质一样查询条件对列做了类型转换索引列本身的值和查询值类型不匹配B 树的比较逻辑无法直接应用。以前遇到这种情况大家的方案一般是两条路。第一改写 SQL把函数从列上拿下来比如created_at 2024-06-01 AND created_at 2024-06-02这是我最推荐的也是不依赖新特性的最佳实践。第二维护冗余列比如单独存一个created_date字段由应用层或触发器写入缺点是需要改代码、容易漏写维护成本比较高。函数索引出现后就有了第三种选择让数据库把表达式结果直接存进索引查询时只要条件表达式和索引定义匹配就能走索引。4.2 函数索引的创建语法与原理函数索引的创建语法是在索引定义中直接写表达式注意要加一层括号CREATE INDEX idx_orders_created_date ON orders((DATE(created_at))); CREATE INDEX idx_users_lower_email ON users((LOWER(email)));核心原理是InnoDB 内部会为这个表达式生成一个隐藏的虚拟列在插入或更新数据时MySQL 会计算表达式并把结果存到索引的 B 树里。所以查询时只要 WHERE 条件里的表达式和索引定义里的表达式完全一致优化器就能通过索引直接定位到结果而不需要全表扫描。这里的关键是“完全一致”。比如SELECT * FROM orders WHERE YEAR(created_at) 2024;可以匹配索引((YEAR(created_at)))。但如果查询写的是SELECT * FROM orders WHERE created_at 2024-01-01 AND created_at 2025-01-01;那优化器不会用这个函数索引因为表达式的形态不一样。此时如果能改成范围查询直接利用 created_at 上的普通索引效果通常更好。所以函数索引不是万能钥匙能用范围查询改写仍然推荐优先改写。4.3 函数索引的注意事项函数索引在用的时候有几个很关键的点。第一表达式必须是确定性的也就是说同样的输入永远得到同样的输出。像 NOW()、RAND()、UUID() 这种函数不能用于函数索引因为每次执行结果都不同数据库没法提前计算并存储索引值。第二函数索引会增加写入开销。每次 INSERT 或 UPDATE 都需要额外计算表达式并更新这个隐藏列对应的索引。写入量很大的表添加函数索引前一定要压测不然会放大写放大问题。第三函数索引会占用额外磁盘空间。表达式的计算结果需要持久化虽然单条记录占用不大但千万级大表积累起来也很可观。可以偶尔用 information_schema 查看索引大小做好监控。第四对 JSON 字段的查询函数索引非常有用。比如你有一列 JSON经常根据某个 key 过滤CREATE INDEX idx_json_attr ON event_log((CAST(extra-$.user_id AS UNSIGNED)));这样WHERE CAST(extra-$.user_id AS UNSIGNED) 123就能稳定走索引极大改善 JSON 字段过滤的全表扫问题。我自己的一次实践是给登录日志表加了一个LOWER(email)的函数索引。当时线上有个查询WHERE LOWER(email) ?数据量 500 万查询要扫全表耗时接近 3 秒。加上函数索引后执行时间降到十几毫秒效果立竿见影。不过我也在项目里给一个高写入表加过函数索引结果写入 QPS 掉了将近 15%后来又去掉了。所以我的经验是函数索引适合读多写少的场景写入密集的表要谨慎。5. 索引跳跃扫描联合索引前缀列选择性不高时的优化聊完函数索引再来看一个 MySQL 8.0 的优化特性Skip Scan索引跳跃扫描。这个特性不像前面几个那么常用但理解了它对优化器的决策很有帮助面试也经常被问到。5.1 Skip Scan 的原理先回忆联合索引的最左前缀原则。假设有联合索引(gender, age)查询WHERE age 20因为查询条件没有用到联合索引的第一个列 genderMySQL 通常无法使用这个索引只能全表扫描。Skip Scan 的思路是如果联合索引第一个列 gender 的基数很低也就是去重值很少比如只有男、女两个值那么优化器可以“拆解”这个查询。它相当于自动把上面的查询改写成SELECT * FROM users WHERE gender M AND age 20 UNION ALL SELECT * FROM users WHERE gender F AND age 20;这样每个分支都能利用联合索引(gender, age)找到对应范围的 age再取并集。从执行效果上看优化器就好像“跳过”了 gender 这个列直接去扫每个 gender 值对应的 B 树索引子区间。所以叫跳跃扫描。这个机制依赖一个前提被跳过的前缀列去重值必须很少。如果 gender 有几百个、上千个去重值那优化器就需要跳到几百个索引区间分别查找累计代价可能比全表扫描还高优化器就会放弃这个策略。5.2 触发条件与限制官方对 Skip Scan 的触发条件有一些要求。第一必须存在联合索引而且跳过的列是组合索引中靠前的列。第二跳过列的重复度要足够高也就是去重值数量要少一般我们说的低选择性列比如状态、类型、性别、渠道。第三查询条件里要带上跳跃列之外的列条件通常是非前缀列的等值或范围条件这样每个子扫描区间才有明确的过滤效果。第四优化器会根据统计信息和代价模型自动决定是否使用 Skip Scan不需要也不能强制使用。怎么确认走了 Skip Scan用 EXPLAIN在 Extra 列里看到 Using index for skip scan 就是命中了。限制方面首先对于前缀列基数比较高的联合索引比如前缀列是订单号、用户ID这种高基数字段Skip Scan 不仅没用还会让优化器计算很复杂遇到这种情况不如直接为查询列单独建索引。其次MySQL 8.0 对跳过多个前缀列的支持还比较有限如果联合索引有 a、b、c 三列查询只给 c 条件通常很难触发 Skip Scan。最后Skip Scan 对查询条件的形态也有限制比如范围条件、IN 列表等场景支持不完全我建议遇到“只用联合索引后面某列”的查询先别指望 Skip Scan直接建一个独立的索引更稳定。我实际遇到过一次优化器自动走 Skip Scan。当时线上有个索引(status, user_id, created_at)status 只有几个值某个后台查询只传 user_id 和 created_at 条件。EXPLAIN 显示优化器选择了 Skip Scan效果也不错。后来业务改了status 也要当作查询条件索引策略才一并调整。这个案例给我的启发是当你的联合索引前缀列本身就是低基数字段时不要急着把它拆掉或者额外建索引可能 MySQL 8.0 的优化器已经能帮你应对一部分场景。6. 常见问题与排查技巧实录最后这部分我想把日常排查索引问题时的思路整理一下。尤其结合刚才聊的几个新特性形成一个“遇到索引失效怎么办”的速查体系。6.1 索引失效的典型场景与新特性对照先整理一个对照表方便你后面遇到问题直接对号入座。场景现象传统解决方案新特性方案WHERE 对索引列使用函数索引失效全表扫描改写 SQL 为范围查询、冗余列函数索引隐式类型转换varchar 列和数字比较索引失效手动 CAST 改写 SQLCAST 函数索引多列排序方向不一致Using filesort接受排序或改写 SQL降序索引查询未包含联合索引前缀列联合索引无法使用新建独立索引、UNION ALLSkip Scan索引不确定是否还有用删除风险高、修改影响大直接 DROP 后观察不可见索引6.2 索引没生效的排查思路我调优索引的时候习惯按下面这个顺序排查。第一步先用 EXPLAIN 看执行计划。重点看type、key、rows、Extra四列。type 如果是 ALL说明全表扫描索引没被用上key 如果是空说明优化器没有选中任何索引Extra 里出现 Using filesort 或 Using temporary说明排序或分组没走索引。第二步确认版本。很多“新特性没生效”的问题根因是 MySQL 版本太老。比如函数索引要求 8.0.13 以上降序索引和不可见索引要求 8.0。先 SELECT VERSION()再决定是不是该升级版本。第三步确认索引定义。用 SHOW INDEX FROM 表名 查看索引的列顺序、可见性、基数。尤其注意函数索引MySQL 对表达式匹配要求非常严格DATE(created_at)和DATE(CREATED_AT)大小写不同可能都无法匹配。第四步查优化器开关。不可见索引默认不走需要确认use_invisible_indexes参数Skip Scan 相关参数是optimizer_switch里的skip_scan。如果这些开关被关闭新特性自然不生效。第五步统计信息要新。索引走不走优化器要参考统计信息。数据量剧增或索引刚重建后不妨执行ANALYZE TABLE 表名;更新统计信息。第六步考虑数据量大小。如果一张表只有几百行数据优化器认为全表扫描比走索引更快这也是正常现象不一定要强行走索引。你可以用 FORCE INDEX 强制测试但生产环境别这么干让优化器自己选。6.3 一个综合实测案例最后分享一个我自己的综合排查过程。某业务有一个订单明细表大概 3000 万行查询痛点集中在三个地方第一按创建日期查某一天的订单SQL 写的WHERE DATE(created_at) 2024-05-01导致全表扫描。我加了函数索引((DATE(created_at)))之后查询降到了毫秒级。但同时我提醒业务方这种写法只是让现有 SQL 能用上索引如果后续需要按日期范围查询还是建议改成范围条件。第二管理后台需要ORDER BY user_id ASC, created_at DESC分页原来有 Using filesort我建了降序索引(user_id, created_at DESC)解决。第三表上原本有一个三列联合索引我怀疑已经没有查询使用了。为了安全我先把它设为 INVISIBLE观察了整整两天线上慢查询日志确认没有新增慢 SQL 之后才 DROP。这个案例里三个新特性各用了一次流程走下来之后整张表的写入开销没有明显变化但核心查询从 2 秒 3 秒下降到几十毫秒效果非常可观。最后说两句我做了这么多年数据库优化最大的体会是索引新特性再多核心还是围绕 B 树在打转无非是让树的组织方式更贴近查询需要让优化器有更多选择。但特性归特性生产环境里的每个决策最后都要用 EXPLAIN 说话用监控指标说话。如果你刚开始接触这些新特性我建议先拿不可见索引入手。它风险最低操作最简单却能在线上环境解决一个很实际的问题删索引前再也不用提心吊胆了。接下来再试函数索引它能帮你解决不少长期依赖 SQL 改写的场景。至于降序索引和 Skip Scan理解了原理在合适的时候用上效果会很明显但千万别为了用而用为一张高写入表盲目加新索引代价可能比收益更大。最后再分享一个小技巧每次调整索引前记录一下当前 SQL 的执行时间、扫描行数、Extra 信息调整后再对比一次这样优化效果才有客观依据。索引优化这件事经验再丰富也不能靠直觉拍板。