广告位 · 页头(后台-模块管理-新建模块后替换此区域)

MySQL通配符查询的性能影响有多大?LIKE语句索引利用分析

在 MySQL 中,LIKE 模糊查询是业务系统里出现频率极高的操作。无论是搜索用户昵称、商品标题还是日志关键词,几乎都会用到百分号 % 和下划线 _ 这两个通配符。表面上看,性能问题来自模糊匹配本身,但实际上真正的分水岭在于通配符出现的位置。经验丰富的 DBA 在看一条 LIKE 语句时,第一反应通常是先确认模式开头有没有通配符,因为这决定了查询是走索引范围扫描还是全表扫描。

MySQL通配符查询的性能影响有多大?LIKE语句索引利用分析

接下来从 B+ 树索引的匹配机制出发,结合 EXPLAIN 执行计划和具体优化手段,把通配符对性能的影响讲清楚。

一、通配符位置决定索引是否生效

MySQL 最常见的索引结构是 InnoDB 的 B+ 树。B+ 树的叶子节点按照索引列的值从小到大顺序排列,并且通过双向链表连接。这种有序性让范围查询和前缀匹配变得非常高效。当执行 name LIKE 'abc%' 时,优化器可以把这个条件转换成 name 大于等于 abc 且小于 abd 的范围条件。也就是说,数据库只需要在索引树中定位到第一个以 abc 开头的值,然后沿着链表向后扫描,直到遇到不以 abc 开头的值为止。整个过程中不需要检查每一行数据,因此索引可以被充分利用。

但如果写成 name LIKE '%abc',优化器无法确定匹配值的起始位置,因为字符串末尾的 abc 并不影响索引排序。在 B+ 树中,键值 abc、xabc、yabc 可能分散在完全不同的叶子节点中,B+ 树无法提供从后往前匹配的能力。此时 MySQL 只能从索引的最左端开始扫描所有记录,或者直接扫描聚簇索引的每一行数据。EXPLAIN 输出中的 type 字段通常会从 range 退化为 ALL,key 字段也可能变成 NULL。

下面用一个简单表结构做验证。假设 users 表的 name 列建立了普通索引,分别执行三种 LIKE 查询,对比执行计划。

CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(100) DEFAULT NULL,
    INDEX idx_name (name)
) ENGINE=InnoDB;
EXPLAIN SELECT * FROM users WHERE name LIKE 'abc%';
EXPLAIN SELECT * FROM users WHERE name LIKE '%abc';
EXPLAIN SELECT * FROM users WHERE name LIKE '%abc%';

在实际结果中,第一条语句的 type 是 range,key 是 idx_name,rows 通常较小;第二条和第三条语句的 type 大概率是 ALL,key 为 NULL,rows 接近全表行数。这个对比说明,优化器选择执行计划的核心依据不是是否使用了 LIKE,而是通配符是否破坏了索引的有序性利用。

二、前导通配符为什么会拖垮查询

前导通配符的代价不只是索引失效,更严重的是它会迫使 InnoDB 进行大量的无效磁盘读取。InnoDB 的聚簇索引以主键为顺序存储完整行数据,二级索引存储索引列和主键值。如果 name 列的二级索引无法缩小范围,执行器可能选择直接扫描聚簇索引。对于一张几百万行的表,这意味着需要从磁盘读取大量数据页到缓冲池,造成频繁的 I/O 竞争。

这里有一个容易混淆的地方:即使优化器选择扫描二级索引而不是聚簇索引,也未必能显著提升性能。对于 name LIKE '%abc%' 这类条件,二级索引同样需要遍历所有叶子节点,而且每匹配到一行还要回表取完整记录。如果匹配比例很高,回表的随机 I/O 可能比全表扫描更慢,因此优化器通常会直接选择全表扫描。这种情况下的性能瓶颈不是索引本身,而是 SQL 设计让数据库无法利用索引排序来减少搜索空间。

可以通过一个对比测试观察扫描行数的变化。同一个表分别使用前导通配符和后导通配符,EXPLAIN 的 rows 字段可以相差几个数量级。比如 100 万行数据中,后导通配符可能只定位到几十行,而前导通配符则需要扫描全部 100 万行。即使每次扫描都在内存中完成,CPU 和内存带宽的消耗也会随着数据量线性增长。并发场景下,这种查询很容易成为连接池和数据库 CPU 的瓶颈。

-- 使用后导通配符,扫描范围小
EXPLAIN SELECT id, name FROM users WHERE name LIKE 'mysql%';
-- 使用前导通配符,扫描范围大
EXPLAIN SELECT id, name FROM users WHERE name LIKE '%mysql%';

另外,下划线 _ 作为单个字符通配符,在模式开头出现时同样会导致索引失效。区别在于 _ 只匹配一个字符,但在 B+ 树中依然无法转换为范围条件。所以性能分析的方法与百分号完全一致。

三、优化模糊查询的几种实践方案

如果业务确实存在中间模糊匹配或后缀模糊匹配的需求,不能直接放弃索引优化。最直接的思路是调整查询方式,把模糊匹配尽量转化为前缀匹配。例如搜索商品名称时,如果用户输入的关键词通常出现在名称开头,就可以把 LIKE '%关键词%' 改成 LIKE '关键词%',并在业务层引导用户输入完整前缀。对于固定后缀的场景,可以考虑增加一个反向存储列。

反向生成列的做法是:新建一列 reverse_name,其值为原生列字符串的反转结果,然后对该列建立索引。需要查询后缀时,把搜索词也反过来,使用 reverse_name LIKE 'cba%',这样原本的后缀匹配就变成了前缀匹配。比如要查 name 以 mysql 结尾,可以写入 reverse_name 列值为 lqsym,查询 reverse_name LIKE 'lqsym%'。这种方案会占用额外存储空间,增加写入时的计算成本,但查询性能会大幅提升。

ALTER TABLE users ADD COLUMN reverse_name VARCHAR(100) GENERATED ALWAYS AS (REVERSE(name)) STORED;
ALTER TABLE users ADD INDEX idx_reverse_name (reverse_name);
-- 查询 name 以 mysql 结尾的记录
SELECT id, name FROM users WHERE reverse_name LIKE CONCAT(REVERSE('mysql'), '%');

对于真正的中间模糊搜索,MySQL 自带的 FULLTEXT 全文索引也是一种选择。InnoDB 支持在 CHAR、VARCHAR 和 TEXT 列上建立全文索引,通过 MATCH ... AGAINST 语法进行分词匹配。不过全文索引默认有最小词长和停用词限制,中文场景还需要配置 ngram 分词器,对短关键词的匹配效果可能不理想。适用于文章内容、评论等长文本的搜索,不适合用户昵称这种短字符串。

ALTER TABLE articles ADD FULLTEXT INDEX ft_title_content (title, content);
SELECT id, title FROM articles
WHERE MATCH(title, content) AGAINST('MySQL 性能优化' IN NATURAL LANGUAGE MODE);

如果数据量很大且搜索需求复杂,更合理的架构是将数据同步到 Elasticsearch 等专用搜索引擎。搜索引擎使用倒排索引,天然支持任意位置的词项匹配,查询延迟稳定。但这会引入数据同步延迟和额外的运维成本,需要根据业务预算和团队能力权衡。对大多数应用来说,优先优化 SQL 模式、使用前缀索引或覆盖索引,已经能解决大部分性能问题。

四、用覆盖索引降低回表成本

有时模糊匹配无法避免,但还是可以通过覆盖索引减少回表次数。覆盖索引的意思是查询需要的列全部包含在索引中,执行器扫描索引时不需要再回聚簇索引读完整行。比如查询只需要 id 和 name,而索引正好是 name 列加上主键 id 的组合,那么即使 LIKE 前导通配符导致索引全扫描,也只需要扫描更小的二级索引,而不是扫描包含所有列的主键索引。

来看一个例子。users 表中有二级索引 idx_name_only 只包含 name,但查询还需要 email 列,此时索引不全,必须回表。如果将索引改为 idx_name_email 包含 name 和 email,并且查询列只有 name 和 email,那么即使使用 %name% 模式,优化器也可能选择扫描这个二级索引。原因是二级索引比聚簇索引更紧凑,数据页更少,扫描代价更低。

-- 只能回表
EXPLAIN SELECT name, email FROM users WHERE name LIKE '%mysql%';
-- 创建覆盖索引后,可避免回表
ALTER TABLE users ADD INDEX idx_name_email (name, email);
EXPLAIN SELECT name, email FROM users WHERE name LIKE '%mysql%';

需要注意的是,覆盖索引并不能让前导通配符变成范围扫描,它只是把全表扫描替换成了更轻量的索引全扫描。在 EXPLAIN 输出中,type 可能是 index,表示 MySQL 扫描了整个索引,但通常比 ALL 快,因为索引占用的空间小于整行数据。如果查询还需要排序或分组,覆盖索引还可以减少临时表的使用。但如果匹配行数占比较高,性能提升仍然有限,根本办法还是减少搜索空间。

此外,在建索引时还要注意索引列的顺序。对于组合索引,只有查询条件符合最左前缀原则才能被优化器充分利用。例如索引 idx_name_email 中 name 列在最左,如果查询只按照 email 列模糊匹配,该索引依然无法生效。因此设计和评估索引时,必须结合具体查询模板,而不是简单地为所有搜索列创建单列索引。

五、执行计划检查与常见注意事项

优化模糊查询时,最可靠的方法是查看 EXPLAIN 输出,而不是仅凭经验判断索引是否生效。重点观察 type、key、rows 三个字段。type=range 说明走索引范围扫描;type=index 说明扫描了二级索引的全部叶子节点;type=ALL 说明全表扫描。key 字段为 NULL 表示没有使用索引。rows 是优化器估算的需要扫描的行数,数值越小越好。如果发现 rows 和表总行数接近,说明查询很可能没有缩小范围。

除了通配符位置,排序规则也会对 LIKE 匹配产生影响。在默认的 utf8mb4_general_ci 或 utf8mb4_0900_ai_ci 排序规则下,比较是不区分大小写的,这可能导致 MySQL 无法有效利用某些前缀索引。如果业务要求区分大小写,可以在定义列时使用相应的二进制排序规则。同时,LIKE 还支持 ESCAPE 子句自定义转义字符,例如 LIKE '%50\%' ESCAPE '\' 可以匹配包含 50% 的字符串。在这种写法中,反斜杠是转义符而不是路径分隔符,但与 SQL 模式匹配相关时要小心处理。

最后,对于已经上线的高频慢查询,不要急着推翻表结构。可以先收集慢查询日志,确认问题语句的实际执行频率和平均影响行数。针对低频率但高耗时的查询,可以调整缓存策略;针对高频率查询,优先优化索引或改写 SQL。通配符查询的性能影响是可控的,关键是根据数据分布和查询模式选择合适的技术手段。

广告位 · 文章底部(后台-模块管理替换此区域)