每次看到业务方拿一条带 LIKE 的 SQL 过来问"为什么这么慢""为什么数据不对"的时候,我基本都会先扫一眼语句里的通配符长什么样。MySQL 里的通配符看着就两个主角——% 和 _,外加一个容易被忽略的转义机制,但绝大多数线上事故和数据异常,追到最后都能在这几个字符上找到根因。这篇文章我不打算把官方文档复述一遍,而是从实际使用的角度把通配符的匹配规则、索引失效原理、转义陷阱、正则边界,以及我在真实项目里踩过的一次完整排查过程都捋一遍,顺便对照一下 Redis、Linux、Word 这些场景里长得差不多的通配符到底有什么区别。无论你是刚写完 SELECT * FROM user WHERE name LIKE '%张%' 的新手,还是正在准备面试、想搞清楚"为什么 % 放前面就用不上索引"的进阶选手,这篇应该都能给你点实在的东西。
1. %和_的边界感:先让结果匹配你的直觉
1.1 一个字符和任意字符的区别,比你想的更微妙
% 代表任意长度的任意字符,可以是 0 个字符,也可以是 100 个字符;_ 则只代表且必须代表 1 个字符。这个定义背下来很简单,但落到真实数据上,反直觉的地方特别多。
我建一张临时表来演示,这样比你干看文档直观得多:
sql复制CREATE TABLE t_user_tag (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50)
);
INSERT INTO t_user_tag(name) VALUES
('张三'), ('张三丰'), ('张伟三'), ('老张'), ('王老张'),
('张'), ('欧阳张'), ('张大妈'), ('张小张');
现在执行下面几条查询,猜猜结果分别是什么:
sql复制SELECT name FROM t_user_tag WHERE name LIKE '张%';
-- 结果:张三、张三丰、张伟三、张、张大妈、张小张
SELECT name FROM t_user_tag WHERE name LIKE '%张';
-- 结果:张三、老张、王老张、张、欧阳张、张大妈?张小张?先别急
SELECT name FROM t_user_tag WHERE name LIKE '_张';
-- 结果:老张、王张(如果有的话),但"张"不行,"欧阳张"也不行
SELECT name FROM t_user_tag WHERE name LIKE '%张%';
-- 包含"张"的所有行,除了NULL
第一句和第二句是最容易搞混的。'张%' 是以"张"开头,后面爱有没有;'%张' 是以"张"结尾,前面爱有没有。但注意 '%张' 会匹配 '张大妈' 吗?不会,因为 % 匹配的是任意字符,但 '张大妈' 结尾是"妈",不是"张"。反过来,'张%' 会匹配 '张小张',因为最前面的"张"开头满足了,后面 % 可以匹配 '小张' 这三个字符,而第二个"张"只是被 % 吞掉的普通字符,它不会被再次解析成 % 的语义。
_ 的坑在于很多人把它理解成"一个字母或一个汉字",这没错,但它严格限定是"一个字符"。所以 '_张' 能匹配 '老张',但匹配不了 '欧阳张'——欧阳张是三个字,_ 只占一个位置,后面 张 占一个位置,总共只能匹配两个字符的字符串。同理,'张_' 能匹配 '张三',匹配不了 '张',因为 _ 必须吃掉一个字符。
1.2 空字符串、NULL和%%这种"看起来不过滤"的写法
还有一个很容易被忽略的边界:LIKE '%%' 和 LIKE '%'。很多人写代码时为了表示"这里不过滤",会拼一个 LIKE '%%' 进去,觉得反正匹配所有。但 LIKE '%' 不会返回 NULL 行——NULL LIKE '%' 的结果是 NULL,而 NULL 会被当成假值过滤掉。如果你本意是"取全表非空数据",LIKE '%' 能碰巧实现;但如果你想表达"无条件"或"取全表含 NULL",它就不对。
sql复制-- 假设存在一条 name IS NULL 的记录
SELECT COUNT(*) FROM t_user_tag WHERE name LIKE '%%';
-- 不会统计到那条 NULL 记录,但全表扫描照样发生
SELECT COUNT(*) FROM t_user_tag WHERE name IS NOT NULL;
-- 语义清晰,且如果条件能利用索引,代价更可控
我更推荐的做法是:在应用层就避免拼出空通配符。如果搜索条件为空,干脆不拼 WHERE name LIKE 这段;如果一定要动态拼 SQL,也明确区分"无条件"和"非空过滤"两种场景,别让 %% 流入生产 SQL。
1.3 排序规则悄悄影响匹配结果
关于大小写,LIKE 的行为是由表的 collation(排序规则)决定的。utf8mb4_general_ci 和 utf8mb4_0900_ai_ci 这类 ci(case insensitive)规则下,WHERE name LIKE 'a%' 能匹配到 'Apple';但如果你把字段或表的排序规则设成 utf8mb4_bin,'a%' 就只匹配小写 a 开头的数据。
实际工作里,因为排序规则导致线上"查不到数据"的情况我见过不止一次。比如用户注册时存的是大写 ABC,你用 LIKE 'abc%' 去查,ci 规则下没问题,但一旦某天 DBA 把字段改成 utf8mb4_bin 或者数据库迁移到排序规则不同的环境,同样的 SQL 结果就变了。建议在排查"通配符查不到"问题时,先看一眼字段的 collation,别光盯着通配符本身。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 通配符把索引搞废的三个姿势:前缀匹配为何是唯一例外
2.1 B+树索引为什么只认前缀
很多人背过结论:LIKE 'abc%' 能走索引,LIKE '%abc' 和 LIKE '%abc%' 不能走索引。但面试官一追问"为什么",就卡住了。我用一句话解释:B+树的叶子节点是按索引列的值有序排列的,前缀确定后,优化器可以在有序结构里定位到一个连续区间;前缀不确定,就没法二分定位,只能把整棵树扫一遍。
举个生活化的例子:一本按姓氏拼音排好的电话簿,你要找所有姓"张"的人,直接翻到 Zhang 那一带就能找到连续几页;但如果你要找所有名字里带"张"的人,就没办法靠字母顺序定位了,只能从第一页翻到最后一页,每一页都眼睛扫一遍。
对应到 SQL 里:
WHERE name LIKE '张%':索引定位到"张"开头的区间,走range扫描。WHERE name LIKE '%张':索引无法定位,因为"以张结尾"这个条件和字母顺序没有对应关系。WHERE name LIKE '%张%':同理。
2.2 别以为查询列都在索引里就万事大吉
有一种误解是:我建了 (name, age) 联合索引,SELECT age FROM t WHERE name LIKE '%张%' 是不是就快了?这叫"覆盖索引"优化,优化器确实可能选择扫描整个索引树(Index Full Scan),因为索引树比表小,扫描代价低于全表。但注意,它依然是全索引扫描,不是区间定位,数据量大了照样扛不住。
我实测过一张 500 万行的用户表,name 字段建了普通索引:
sql复制-- 前缀匹配,name 有索引
EXPLAIN SELECT * FROM t_user WHERE name LIKE '张%';
-- type: range,rows 可能只有几千,毫秒级返回
-- 中间匹配,name 有索引但帮不上忙
EXPLAIN SELECT * FROM t_user WHERE name LIKE '%张%';
-- type: ALL,rows 500万,全表扫描,线上直接磨死 CPU
在机械硬盘时代这个差距是几十倍,SSD 时代虽然好一些,但 500 万行全表扫仍然要几百毫秒到秒级,如果这个查询被高频调用,数据库直接被打满。所以"覆盖索引能救 %...%"这种想法,小表无所谓,大表千万不要赌。
2.3 后缀查询的实用解法:生成列 + 函数索引
如果业务真的必须做"以某串结尾"或"包含某串"的模糊查询,又不是非用搜索引擎不可,MySQL 8.0 提供了函数索引,配合 REVERSE() 可以把后缀查询变成前缀查询。这个方案我在项目里实际落地过,效果很稳。
sql复制-- 假设业务要查手机号尾号 1380 的用户
-- 先加一列反转手机号,并建函数索引
ALTER TABLE t_user
ADD COLUMN phone_rev VARCHAR(20) GENERATED ALWAYS AS (REVERSE(phone)) STORED,
ADD INDEX idx_phone_rev (phone_rev);
-- 查询尾号 1380 的写法,反转后变成前缀匹配
SELECT * FROM t_user WHERE phone_rev LIKE '0831%';
-- 等价于 phone LIKE '%1380',但可以走索引
注意几点:生成列要选 STORED,如果选 VIRTUAL 配合函数索引也可以,但 STORED 在某些版本和备份工具下兼容性更好;REVERSE('1380') = '0831',别写反了;最后查询条件里要写反转后的前缀,不能直接写 LIKE '%1380',否则优化器不会把你这个条件转成生成列上的范围扫描。
这种方案适合"后缀固定"的场景,比如尾号查询、订单号后几位查询。如果是真正自由的中文包含搜索,比如文章内容里搜"通配符"三个字出现在任意位置,函数索引也救不了,得考虑全文索引或外部搜索,这个下面讲。
2.4 别把函数套在索引列上,8.0也不行
还有一个高频失误:明明列上有索引,但写成了 WHERE LEFT(name, 2) = '张'。在 MySQL 8.0 之前,一旦对索引列做了函数运算,索引就废了,这跟通配符无关,但很多人会误以为是通配符的问题。8.0 之后可以为这个表达式单独建函数索引才能走索引:
sql复制ALTER TABLE t_user ADD INDEX idx_left_name ((LEFT(name, 2)));
如果不建函数索引,LEFT(name, 2) = '张' 就是全表扫描。所以排查慢查询时,先看 WHERE 条件里有没有函数、有没有前导通配符,这两样都是索引杀手。
3. 想匹配通配符本身?ESCAPE转义里的那些坑
3.1 搜索包含下划线的数据,下划线被吞了
_ 和 % 在 LIKE 模式里是特殊字符,但真实业务数据里完全可能包含这两个符号。最常见的场景是手机号被格式化存储成 138_0000_1111,订单号里带横杠但下划线也会出现,或者描述文本里写了一句 50% off。如果你不加处理直接查,通配符会把它们当成匹配规则,而不是普通字符。
举一个我处理过的真实案例。业务方导入了第三方平台的用户备注,里面有大量 ID_10086 格式的编号,现在要精确查 ID_10086 这个编号有多少条记录。新手会写:
sql复制SELECT * FROM t_remark WHERE content LIKE '%ID_10086%';
这个 _ 会被当成"任意一个字符",所以 IDX10086、IDA10086 都会被匹配出来,你以为查出 10 条,实际应该只有 1 条。数据量小的时候你发现不了,一旦做聚合统计,报表直接偏掉。
3.2 反斜杠转义和ESCAPE,哪个更靠谱
MySQL 默认支持用反斜杠转义 LIKE 模式里的特殊字符:
sql复制SELECT * FROM t_remark WHERE content LIKE '%ID\_10086%';
但这里有一个非常隐蔽的坑:MySQL 的字符串解析本身也把反斜杠当成转义符。在大多数默认配置下,'\_' 在字符串解析阶段会被解析成 '_',再进入 LIKE 模式匹配,_ 依然是通配符。实际经验是,MySQL 客户端和多数驱动里,LIKE '%\_%' 的转义是生效的,因为 LIKE 解析器会先处理 \_。但如果你把 NO_BACKSLASH_ESCAPES 这个 SQL 模式打开了,反斜杠就变成普通字符,'\_' 就不会被转义了。
为了不让自己和团队在这个细节上反复踩坑,我强烈建议在代码里用 ESCAPE 明确指定转义符:
sql复制SELECT * FROM t_remark WHERE content LIKE '%ID$_10086%' ESCAPE '$';
这里我用 $ 充当转义符,$_ 表示一个字面意义的下划线。ESCAPE 子句的好处是,它显式告诉 MySQL 我自定义了转义规则,不依赖全局 SQL 模式配置,可读性也更强。注意 ESCAPE 后面只能跟一个字符,不能写 ESCAPE '$#' 这种多字符。
3.3 匹配百分号字面量的完整写法
跟下划线一样,% 本身也可能出现在数据里。比如要查所有折扣为 50% off 的商品描述:
sql复制SELECT * FROM t_product WHERE description LIKE '%50\%%';
这条 SQL 的意图是:50 后面跟一个字面意义的 %,然后 % 再匹配后面任意内容。写成 '%50\%%',前两个 % 是通配符,中间 \% 是字面量百分号。如果觉得反斜杠容易和字符串转义混淆,就改成:
sql复制SELECT * FROM t_product WHERE description LIKE '%50!%%' ESCAPE '!';
我个人的习惯是:只要模式里出现需要转义的特殊字符,一律用 ESCAPE,不用反斜杠。因为别人读代码的时候,ESCAPE '!' 一眼就能看出 !% 是字面量,而一堆 \% 很容易看错层数,尤其在 Java、Python 字符串里还要再考虑一层转义,写出来就是 "LIKE '%50\\%%'",看着就头疼。
4. LIKE和REGEXP的适用边界:正则不是通配符的升级版
4.1 REGEXP能做什么,LIKE做不到什么
很多新手觉得 REGEXP 是 LIKE 的加强版,遇到复杂匹配就直接上正则。确实,正则能写一些 LIKE 完全做不到的条件,比如:
sql复制-- 名字以"张"开头,第二个字是三、四、五其中之一,且最后一个字必须是字母或数字
SELECT * FROM t_user WHERE name REGEXP '^张[三四五][a-zA-Z0-9]$';
LIKE 想表达这种"字符集合"和"出现次数"的约束很费劲,基本要拆成好几个条件慢慢拼。另外,正则做数据质量校验很有用,比如查出所有手机号格式不对的记录:
sql复制SELECT * FROM t_user WHERE phone NOT REGEXP '^1[3-9][0-9]{9}$';
4.2 REGEXP的两个硬伤:索引和回溯
但 REGEXP 在生产环境有两个硬伤,大家务必重视。
第一,REGEXP 基本不会用索引,哪怕你写的是前缀正则。MySQL 的优化器没有把 REGEXP '^张' 转换成 LIKE '张%' 的能力(至少到 8.0 为止我没见过稳定的场景),所以 WHERE name REGEXP '^张' 在 500 万行表上就是全表扫,换成 LIKE '张%' 就能走索引。这个差异在数据量大的表上非常致命。
第二,复杂的正则表达式可能带来灾难性的计算开销,尤其是嵌套量词和回溯。比如 REGEXP '^([a-z]+)*$' 这类表达式在某些输入下会触发"灾难性回溯",直接把 CPU 拉满。我在一次日志分析里见过一个 200 字符的字符串导致 MySQL 单核 CPU 100% 持续几十秒的案例。所以正则适合做数据量可控的批量校验、一次性分析,不适合放在高频查询的 WHERE 条件里。
4.3 我自己的选型原则
接线上查询需求时,我的优先级是这样的:
- 能走索引的
LIKE '前缀%',永远优先,这是底线。 - 后缀匹配用
REVERSE()生成列 + 函数索引,也别上正则。 - 全文搜索需求(包含关系、相关性排序)用
FULLTEXT索引 +MATCH...AGAINST,不要用LIKE '%关键词%',更不要用REGEXP。 - 只有做数据清洗、一次性报表、数据校验这些跑批场景,才放开手脚用
REGEXP。
FULLTEXT 是很多人没注意到的选项。MySQL 的全文索引支持中文需要 ngram 解析器,建表时可以这样:
sql复制CREATE TABLE t_article (
id INT PRIMARY KEY,
title VARCHAR(200),
body TEXT,
FULLTEXT KEY ft_body (body) WITH PARSER ngram
) ENGINE=InnoDB;
SELECT * FROM t_article
WHERE MATCH(body) AGAINST('通配符' IN NATURAL LANGUAGE MODE);
在同样包含搜索场景下,MATCH...AGAINST 的性能和相关性排序都比 LIKE '%...%' 好太多。当然,如果数据量到千万级、并发要求很高,那就得考虑外部搜索引擎了,MySQL 的全文索引更适合中小规模业务。
5. 别把MySQL通配符和其他场景搞混:Redis、Linux、Word的一次对照
5.1 Redis KEYS命令的星号为什么不能当MySQL的百分号用
最近一段时间好几个同事问我 Redis 里 KEYS 命令能不能用 %,我说你这是在用 MySQL 的习惯套 Redis。Redis 的 KEYS 命令用的是 glob 风格通配符:* 匹配任意多个字符(对应 MySQL 的 %),? 匹配单个字符(对应 MySQL 的 _),[abc] 匹配字符集合。
比如有人问 KEYS ekyc_pic_* 是什么意思,这在 glob 语义里表示匹配 ekyc_pic_ 之后跟任意内容的所有 key,和 MySQL 的 LIKE 'ekyc_pic_%' 是同一个目标。但注意一个致命差别:Redis 生产环境禁止用 KEYS,因为它是 O(N) 遍历所有 key,会阻塞 Redis 单线程,导致整个实例卡顿。正确做法是用 SCAN 命令配合同样的 pattern 分批遍历:
bash复制SCAN 0 MATCH ekyc_pic_* COUNT 1000
这个差异非常典型:看起来一样的通配符语法,在不同的系统里性能和风险完全不是一个量级。MySQL 里 LIKE 虽然也可能全表扫,但至少不会把整个数据库实例阻塞到不可服务;Redis 的 KEYS 是真的会。
5.2 Linux find 和 Word 通配符:形似而神不似
Linux 的 find -name '*.log' 用的是 shell glob 通配符,* 匹配任意多个字符,? 匹配单个字符,[ 匹配集合。但 shell glob 有一个重要特性:* 开头的匹配不会匹配隐藏文件(以 . 开头),除非显式写 .*。这和 MySQL 完全不同,MySQL 里没有"隐藏字符"这个概念。
Word 的查找替换一旦勾选"使用通配符",规则也很独特:* 匹配任意字符串,? 匹配任意单个字符,[!x] 表示非 x 的任意字符,{n,} 表示至少 n 个重复。看起来和 SQL 很像,但 Word 的正则语法和 SQL 的 REGEXP 又不完全一致,替换时可以用 \1 引用分组,这又是从脚本语言里借来的概念。
我为什么专门写这一节?因为在多语言、多系统协作的项目里,很多人会把 MySQL 的 % 带到 Redis 或者 Linux 命令里,或者反过来把 shell 的 * 直接写进 SQL。每次跨系统对接时,先问一句"这个通配符是哪个系统的语义",能省掉很多排查时间。
下表是我整理的常用对照,方便你收藏:
| 场景 | 匹配任意字符串 | 匹配单个字符 | 字符集合 | 转义方式 | 典型风险 |
|---|---|---|---|---|---|
| MySQL LIKE | % |
_ |
不支持 | \ 或 ESCAPE |
前导通配符导致全表扫描 |
| MySQL REGEXP | .* |
. |
[abc] |
\ |
无法走索引,灾难性回溯 |
| Redis KEYS/SCAN | * |
? |
[abc] |
\ |
KEYS 阻塞实例 |
| Linux find/glob | * |
? |
[abc] |
\ |
隐藏文件不匹配 |
| Word 查找替换 | * |
? |
[abc]、[!x] |
无统一转义 | 替换语法易混 |
5.3 记住语义比记住符号更重要
符号只是表象,本质是每种工具的匹配引擎不同。MySQL 的 % 和 _ 是在 SQL 解析器的 LIKE 模式里定义的;Redis 的 * 和 ? 是在 glob 匹配器里定义的;正则里的 .* 是在正则引擎里定义的。它们看起相似,但实现、性能、边界规则都不一样。我最怕的一句话就是"这个在那套系统里能查到,为什么在 MySQL 里查不到"——答案往往就是"因为那不是同一个通配符体系"。
6. 一次线上慢查询的完整排查链路:从现象到根因
6.1 现象:一个普通的搜索框,让数据库CPU飙到80%
之前负责一个电商后台系统,运营人员维护了几百万条商品数据。某天监控告警,数据库 CPU 长期在 80% 以上,主库负载异常。我先查了 SHOW FULL PROCESSLIST,发现大量同一条 SQL 在反复执行:
sql复制SELECT *
FROM t_product
WHERE product_name LIKE '%蓝牙%'
ORDER BY sold_count DESC
LIMIT 0, 20;
运营在后台商品搜索框里输入"蓝牙",这个搜索没有要求前缀,代码里就直接拼了 %蓝牙%。商品表 500 万行,product_name 有索引,但 LIKE '%蓝牙%' 是中间匹配,索引用不上,每次搜索都全表扫,再加上 ORDER BY sold_count 排序,就是雪上加霜。
6.2 定位:EXPLAIN一眼看出了type=ALL
我让负责的同事执行 EXPLAIN:
sql复制EXPLAIN SELECT *
FROM t_product
WHERE product_name LIKE '%蓝牙%'
ORDER BY sold_count DESC
LIMIT 0, 20;
结果里 type 是 ALL,rows 估算 500 万,Extra 里有 Using where; Using filesort。ALL 意味着全表扫描,filesort 意味着为了排序还要额外把 500 万行涉及的数据放到排序缓冲区。这两个叠加在一起,这条 SQL 的代价就是灾难级的。
对比一下,如果把条件改成前缀匹配:
sql复制EXPLAIN SELECT *
FROM t_product
WHERE product_name LIKE '蓝牙%'
ORDER BY sold_count DESC
LIMIT 0, 20;
type 会变成 range,rows 大幅下降,ORDER BY 在走索引时通常不再需要 filesort,因为索引本身就是有序的。
6.3 解决:按场景拆需求,而不是一味加机器
排查到这里,根因已经清楚:中间匹配通配符 + 无索引排序 + 大表 = 慢查询。但解决方式不是把 %蓝牙% 改成 蓝牙% 就完事,因为运营的搜索需求确实希望"包含蓝牙"的商品都能搜出来,而不只是以"蓝牙"开头的。
我的处理分了三步:
第一步,先保住前缀匹配的主路径。后台搜索默认走 product_name LIKE '蓝牙%',这能覆盖相当一部分需求,性能也够。
第二步,对必须做包含搜索的场景,用全文索引接管。给 product_name 加了 FULLTEXT 索引,搜索接口改成 MATCH(product_name) AGAINST('蓝牙' IN NATURAL LANGUAGE MODE),实测从全表扫描的 1.8 秒降到 50 毫秒左右。这里注意,全文索引有自己的相关性排序,和原来的 ORDER BY sold_count 语义不完全一致,所以产品侧需要接受"搜索排序按相关性优先"这个调整,如果业务上必须按销量排序,那就只能拆成两步:先全文索引筛出候选集,再对候选集按销量排序。
第三步,加了一道代码规范检查。规定后台列表查询中,WHERE 条件禁止以 % 开头,除非有 DBA 单独审批。这道卡点在 CI 阶段就能拦截大部分同类问题。
6.4 复盘:如果从一开始就按前缀存储
这个案例最后还有个延伸。如果我们当初在建表时,就把商品名称额外存一份倒序字段 product_name_rev,并建立函数索引,那么"以某词结尾"的搜索也能走索引,%蓝牙 这种后缀匹配就不再是全表扫。但对"包含"搜索,函数索引也无能为力,最终还是得靠全文索引或搜索引擎。
我觉得这个案例最值得记录的不是某个具体的 SQL 写法,而是排查思路:先看 PROCESSLIST 找到罪魁 SQL,再用 EXPLAIN 确认是不是索引问题,然后按业务需求拆解法。不要一上来就加缓存、加机器,那样只会掩盖问题,数据库迟早被拖垮。
7. 面试题背后的通配符原理:这么答才有区分度
7.1 高频问题整理
MySQL 通配符在面试里出现频率不低,我总结几个常见的,附带我建议的回答要点:
问:LIKE '%张%' 和 LIKE '张%' 在索引使用上有什么区别?
答:通配符在开头的时候索引用不上,张% 可以走索引,因为 B+ 树按前缀有序,前缀确定能定位到连续区间;%张% 无法定位,只能全表扫或全索引扫。如果面试官再追问一句"为什么前缀确定就能走索引",就把"电话簿按姓氏拼音排"这个类比用上。
问:如果要匹配数据里真实存在的百分号或下划线怎么办?
答:两种方式,一是用反斜杠转义,比如 LIKE '%50\%%';更推荐用 ESCAPE 指定转义符,比如 LIKE '%50!%%' ESCAPE '!',避免依赖 SQL 模式的差异。
问:LIKE 和 REGEXP 怎么选?
答:能走索引的前缀 LIKE 永远优先;REGEXP 灵活但基本不走索引,且复杂正则可能导致 CPU 回溯膨胀,适合数据校验、跑批等离线场景,不适合高频在线查询。
问:数据量很大的表要做包含搜索,怎么优化?
答:按搜索需求分级。前缀搜索走普通索引;后缀搜索用 REVERSE() 生成列 + 函数索引;真正自由的包含搜索考虑全文索引或外部搜索系统(如 Elasticsearch),不要在 MySQL 里硬扛 %关键词%。
7.2 面试官到底在考什么
说实话,面试官问通配符,真不是考你背没背下 % 和 _ 的定义,而是考察两个底层能力:
第一,对索引机制的理解是否透彻。LIKE 能不能走索引,本质是 B+ 树有序性决定的,你能不能用一句话讲清楚"为什么前缀能走、中间不能走",这是区分背答案和真懂的分水岭。
第二,对边界条件的敏感度。能不能想到 NULL 不匹配 LIKE、能不能想到排序规则影响大小写、能不能想到转义冲突。这些细节平时不写 SQL 的人根本碰不到,只有真踩过坑的人才会在回答时自然而然地带出来。
7.3 一个补充细节
最后分享一个面试里很少被问到、但线上很实用的点:LIKE 'abc%' 虽然在优化器眼里能走索引,但它和 col >= 'abc' AND col < 'abd' 的范围查询并不完全等价。LIKE 的索引利用程度取决于字符集和排序规则,utf8mb4_0900_ai_ci 这类感知重音的排序规则下,某些特殊字符的边界处理会略微超出直觉。所以做极致的索引优化时,别只满足于 EXPLAIN 显示 range,还要实际验证结果集是否符合预期。我个人的习惯是,写完一条带通配符的 SQL,一定顺手看一眼 EXPLAIN 里的 type 和 rows,再跑一次真实数据抽样确认结果,这个习惯帮我挡掉了不少"看起来走了索引,实际结果不对"的隐形事故。
