有一回做投诉工单报表,要从一张几万行的表里筛出“产品名称中带 5% 字样”的记录。我顺手写了WHERE product_name LIKE '%5%%',执行完当场愣住——全表几乎全中。第一反应是数据脏,排查了大半天,最后才发现锅根本不在数据,而是这条 SQL 里的%太“自作聪明”了。像“字段中含有转义符”这类坑,凡是写过模糊匹配查询的人基本都会踩一次,区别只是有人半小时就绕出来了,有人会为此怀疑人生。
这篇就专门聊透这个事:LIKE 里的%和_到底在干嘛、转义符是怎么回事、不同数据库怎么处理、工程代码里怎么防御,以及这类查询的性能和索引陷阱。内容以 MySQL 为主线,兼顾 Oracle、SQL Server、PostgreSQL,最后附上踩坑记录和排查速查表。适合正在做数据查询、报表统计、接口开发的朋友,尤其是被模糊查询结果弄到一头雾水的那几位。
1. 模糊匹配为什么会“栽”在转义符上
1.1 LIKE 里 % 和 _ 的真实身份
先明确一个基本概念。LIKE做模糊匹配时,模式串里有两个特殊字符是通配符:
%:匹配任意长度的任意字符,包括空字符串。_:匹配且仅匹配任意单个字符。
生活化类比:%相当于你搜索文件时输入的*,只要文件名里有一段对得上,整串都能匹配;_则更像下棋时的“一个空格”,必须刚好有一个字符占住这个位置,多一个少一个都不行。
问题在于:如果业务字段的数据本身包含%或_,比如“洗面奶 100% 正品”,或者编码规范里常见的KPI_2024_考核表,那么这些字符在 LIKE 模式里会被当成通配符来解释,查出来的结果自然不是字面含义。
这是“字段中含有转义符”的核心矛盾:数据里的特殊字符和查询语法的特殊字符撞车了。解决思路只有一个——告诉数据库:这里出现的%或_不是通配符,是普通字符。这个动作就叫“转义”。
1.2 一个典型的误匹配现场
用数据说话。假设有一张投诉记录表,结构很简单:
CREATE TABLE complaint_log ( id INT PRIMARY KEY, product_name VARCHAR(100), deal_rate VARCHAR(20) ); INSERT INTO complaint_log VALUES (1, '洗面奶促销装100%正品', '15%'), (2, '洗发水500ml', '5%'), (3, 'KPI_2024_考核表', NULL), (4, 'KPX2024考核表', NULL);业务需求:查出所有商品名称里带KPI_2024的记录。
直觉写法:
SELECT * FROM complaint_log WHERE product_name LIKE '%KPI_2024%';这条 SQL 不会报错,但结果里第 3 条和第 4 条都会出来。第 4 条KPX2024考核表并没有_,但因为_被当成“任意单个字符”,KPI_2024中的_恰好匹配了X,整条就命中了。
再看%的例子。需求是查“包含 5% 这个字面值”的记录:
SELECT * FROM complaint_log WHERE product_name LIKE '%5%%';这个写法更离谱:第二个%是通配符,等价于%5%,于是只要字段里出现过数字 5,就会全部命中。表里第 1、2 条都会查出来,甚至某条记录里带个“5 元优惠券”也会被捞上来。
这两段代码看起来人畜无害,实际跑起来全是坑。根本原因就是没做转义。
1.3 不同数据库的默认转义行为
这里有个特别重要的点:不同数据库对 LIKE 中转义符的默认处理是不一样的。我整理了一个对照表,方便你排查时对号入座。
| 数据库 | 默认转义字符 | 需要显式 ESCAPE 吗 | 补充说明 |
|---|---|---|---|
| MySQL | \是默认转义符 | 可以不写,但受NO_BACKSLASH_ESCAPES模式影响 | 模式不统一时容易出隐性 bug |
| SQL Server | 无默认转义符 | 必须写ESCAPE,否则%和_永远是被通配 | 还支持[]做单字符匹配,容易混 |
| Oracle | 无默认转义符 | 必须写ESCAPE | 字符串字面量里的\处理也容易混乱 |
| PostgreSQL | \是默认转义符 | 建议显式写ESCAPE | 受standard_conforming_strings影响,字面量写法易混淆 |
所以“为什么我在 MySQL 里写了%\_%有效,在 SQL Server 里却什么都不匹配”这类问题,答案往往就在这个表里。SQL Server 不认反斜杠转义,你必须显式给出ESCAPE,比如:
WHERE product_name LIKE '%KPI\_2024\_%' ESCAPE '\';MySQL 里默认能直接用反斜杠,但如果数据库处于NO_BACKSLASH_ESCAPES模式,反斜杠也会失效。因此最稳妥的做法,是不要依赖数据库的默认行为,每条 LIKE 都显式声明转义符。
2. 正确姿势:ESCAPE 子句与等价替代方案
2.1 ESCAPE 子句的完整写法
SQL 标准提供了一套通用的解决方案:在 LIKE 模式末尾加ESCAPE子句,自定义一个转义字符。
语法格式:
WHERE 字段 LIKE '模式' ESCAPE '转义字符';转义字符的作用是:当它出现在%或_前面时,后面的通配符就被“降级”为普通字符。
用前面的5%场景举例:
SELECT * FROM complaint_log WHERE product_name LIKE '%5/%%' ESCAPE '/';拆开看这段模式:第一个%是通配符,中间的/是自定义转义符,/%表示“字面意义上的百分号”,最后的%又是通配符。整条表达的意思是:字段里只要包含字符串5%,无论前后有没有其他内容,都算命中。
匹配下划线的写法同理:
SELECT * FROM complaint_log WHERE product_name LIKE '%KPI/_2024/_%' ESCAPE '/';这里要注意三点。
第一,转义符与后面的%或_之间不能有空格,必须连写。因为空格本身是普通字符,一旦插入空格,转义逻辑就断了。
第二,转义符只对它紧跟着的一个字符生效。如果要匹配两个连续的特殊字符,比如字符串100%%,模式就要写成%100/%%/%,一个字符一个转义位。
第三,如果字段本身包含反斜杠,比如 Windows 路径C:\Users\test,推荐用/这类非歧义字符做转义符,避免和字符串字面量级别的反斜杠转义叠加,否则写两层转义很容易数错斜杠。
2.2 用字符串函数代替 LIKE,绕开通配符问题
如果不需要用%做真正的模糊匹配,只需要“判断字段中是否包含某个固定文本”,那更干净的做法是用字符串定位函数。这些函数搜索的是普通字符串,不存在通配符语义,也就不需要转义。
四个主流数据库的写法:
-- MySQL SELECT * FROM complaint_log WHERE LOCATE('5%', product_name) > 0; -- Oracle SELECT * FROM complaint_log WHERE INSTR(product_name, '5%') > 0; -- SQL Server SELECT * FROM complaint_log WHERE CHARINDEX('5%', product_name) > 0; -- PostgreSQL SELECT * FROM complaint_log WHERE POSITION('5%' IN product_name) > 0;这套方案的优点非常直观:不用管%、_、\,搜索什么就是什么。适合“完全匹配子串但不关心前后缀”的场景,比如在黑名单词表里查包含违禁%的数据。
缺点也有:不像LIKE那样能表达复杂的模糊模式,比如“以 A 开头、中间包含 B、以 C 结尾”这种组合,用函数就不好写,还得回到 LIKE 加正则那套。
2.3 正则表达式方案,适合复杂场景的兜底
既然 LIKE 的通配符和转义符容易出错,另一个思路是换成正则表达式。在正则的世界里,%和_是普通字符,没有通配符的语义,所以不需要对它们转义。
以 MySQL 8 和 Oracle 的REGEXP_LIKE为例,想查包含字面100%正品的记录:
SELECT * FROM complaint_log WHERE REGEXP_LIKE(product_name, '100%正品');这里%在正则里就是百分号本身,不需要像 LIKE 那样加转义。
但别高兴得太早,正则有另一套自成体系的元字符:.、*、[]、()、|等,它们才是需要转义的对象。比如要搜KPI_2024_考核表,这个需求用正则是正常查,但若要搜KPI.2024中的字面点号,正则里就得写成KPI\.2024。
简单总结:
- 只搜固定子串,优先用字面函数(
LOCATE/INSTR/CHARINDEX)。 - 需要复杂模式、且字段里
%、_出现频繁时,用正则表达式反而省事。 - LIKE 加
ESCAPE则适合需求简单但就是想用 LIKE、团队习惯统一的场景。
3. 工程代码里如何稳妥处理“转义符”查询
3.1 参数化查询不等于自动转义
很多开发者在代码里用参数化查询,以为把用户输入原样塞进%...%就万事大吉。这是个很致命的误解。
参数化查询解决的是 SQL 注入问题,它确保用户输入被当成“值”而不是“SQL 片段”来解析。但在LIKE内部,%和_依然会被数据库解释为通配符。举个 Python 操作 MySQL 的例子:
keyword = "5%" sql = "SELECT * FROM complaint_log WHERE product_name LIKE %s" params = (f"%{keyword}%",) # 错误的做法,% 还是通配符这样做执行后,5%中的%依然通配一切,结果还是会把所有含 5 的记录全部捞出来。
正确做法是在拼接 LIKE 模式之前,先对关键词做一次转义处理:
def escape_like(keyword: str) -> str: # 转义顺序有讲究:先反斜杠,再 %,再 _ return keyword.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") keyword = "5%" escaped = escape_like(keyword) # 得到 5\% sql = "SELECT * FROM complaint_log WHERE product_name LIKE %s ESCAPE '\\\\'" params = (f"%{escaped}%",)这里ESCAPE '\\\\'对应数据库里的反斜杠转义符,代码里写多少个斜杠经常把人绕晕。所以我在工程里更推荐自定义一个不容易出错的转义符:
def escape_like(keyword: str) -> str: return keyword.replace("/", "//").replace("%", "/%").replace("_", "/_") sql = "SELECT * FROM complaint_log WHERE product_name LIKE %s ESCAPE '/'" params = (f"%{escape_like(keyword)}%",)这个方案一眼就能看明白:用户输入里原来的/被写成//,%写成/%,_写成/_。查询时使用ESCAPE '/',可读性比数反斜杠好太多。
3.2 ORM 框架里同样要预先处理
ORM 并不负责帮你转义 LIKE 的特殊字符。比如 Django 的 ORM:
# 看起来没问题,实际生成 SQL 是 LIKE '%5%%',照样全表捞 ComplaintLog.objects.filter(product_name__contains="5%")__contains只是帮你拼了个%...%,内部的%该通配还通配。正确的做法是用extra或RawSQL,把转义逻辑显式写进 SQL 片段:
from django.db.models.expressions import RawSQL escaped = escape_like("5%") qs = ComplaintLog.objects.extra( where=["product_name LIKE %s ESCAPE '/'"], params=[f"%{escaped}%"], )MyBatis 场景要区分#{}和${}。#{}是参数绑定,安全但不能自动处理 LIKE 通配符;${}是文本拼接,有注入风险,不适合直接拼用户输入。稳妥的写法是在 XML 中用CONCAT,并且提前在 Java 服务层完成转义:
<select id="search" resultType="..."> SELECT * FROM complaint_log WHERE product_name LIKE CONCAT('%', #{escapedKeyword}, '%') ESCAPE '/' </select>escapedKeyword在 Service 层已经调用escape_like()处理过。这么做,既避免注入,又防住了通配符穿透。
JPA 的@Query同理,LIKE :keyword里的keyword必须携带转移后的模式。
3.3 动态拼 SQL 时的防守底线
如果团队里还有人习惯用字符串拼接的方式生成查询 SQL,一定要立几条规矩:
- 所有进入 LIKE 模式的用户输入,必须先走统一的转义函数。
- 转义函数里必须包含对转义符本身、
%、_三者的处理,缺一不可。 - 拼接时
LIKE必须显式带ESCAPE,不允许依赖数据库默认行为。 - 尽量限制输入长度,比如 50 个字符以内,防止有人把超长正则或通配符变体塞进来。
另外一个时常被忽略的坑是字段名本身带特殊字符或数据库保留字。比如热词里提到的“mysql 表中字段为关键字”,如果字段命名是order、group、desc这类保留字,查询时必须用反引号包裹:
SELECT * FROM complaint_log WHERE `order` = '1';这类问题和转义符属于同一大类的“特殊字符处理”,在开发规范里值得专门加一条:字段命名尽量避开保留字和特殊符号,实在避不开,要统一标识符的引用方式。
4. 含有转义符的模糊查询,性能和索引怎么兼顾
4.1 前导通配符会让索引失效
聊完正确性,必须说说性能。很多人在排查转义符问题时,往往忽略了 LIKE 查询本身就存在索引陷阱。
WHERE product_name LIKE '%xxx%'这种前后都带通配符的写法,数据库无法利用普通 B+ 树索引,只能走全表扫描。原因很简单:优化器没法确定匹配的起点,不知道应该从索引树的哪个位置开始扫描。
即使你加了ESCAPE,把特殊字符转义成了字面量,这个性能问题也不会消失。ESCAPE解决的是匹配结果的正确性,索引失效的根源是前导%本身。
如果你的查询模式是固定的前缀,比如LIKE 'abc%',那普通索引还能用上。但“字段中间含有特殊字符”这类需求,往往逃不掉全表扫描。这时就需要靠下面两种手段兜底。
4.2 用生成列或函数索引优化
MySQL 8.0 以上支持生成列。可以提前把字段中的特殊字符剥离出来,存成独立列,再对这个列建索引,查询时直接走索引:
ALTER TABLE complaint_log ADD COLUMN product_name_clean VARCHAR(100) GENERATED ALWAYS AS (REPLACE(REPLACE(product_name, '%', ''), '_', '')) STORED; ALTER TABLE complaint_log ADD INDEX idx_clean (product_name_clean);这样业务代码里查“包含 5% 的记录”时,可以先按干净列过滤,或者直接对干净列做常规查询,效率和正确性都得到了保障,缺点是占存储空间。
Oracle 支持函数索引,可以针对INSTR这类函数建索引:
CREATE INDEX idx_instr_rate ON complaint_log (INSTR(deal_rate, '%'));查询时用WHERE INSTR(deal_rate, '%') > 0,优化器就有机会走函数索引。
PostgreSQL 的表达式索引类似,直接在查询列上建立表达式:
CREATE INDEX idx_instr ON complaint_log ((POSITION('%' IN deal_rate)));4.3 视图解决不了性能问题
热词里有个高频问题:“视图可以加快查询速度吗?”这个误解在模糊查询场景里特别常见。有人把WHERE product_name LIKE '%5%%'封装成一个视图,指望查询视图能变快。
可以直接给你结论:视图只是把一段 SQL 保存成了一个命名对象,它不存储数据,也没有自己的索引。查询视图等价于执行视图中嵌套的那条 SQL,扫描成本和直接写 LIKE 没有任何区别。
真正能提速的是:合理的前缀匹配、覆盖索引、全文索引,或者干脆把特殊字符剥离后建生成列。想靠视图解决模糊查询性能问题,方向就错了。
5. 常见问题与实战排查小抄
5.1 快速对照表
为了让你遇到问题时能快速定位,我整理了下面这张表。
| 问题现象 | 可能原因 | 处理办法 |
|---|---|---|
| 查询结果明显过多 | 字段里的%被当成通配符 | 使用ESCAPE转义为字面量 |
明明字段含_,条件就是匹配不上 | _被当成单字符占位符 | 对_转义,或改用LOCATE等函数 |
MySQL 里写了\%无效 | 数据库处于NO_BACKSLASH_ESCAPES模式 | 改用显式ESCAPE '/' |
SQL Server 里\%无效 | SQL Server 无默认转义符 | 必须写ESCAPE '\'或用[] |
| 反斜杠路径字段查询不到 | 字符串字面量转义与 LIKE 转义叠加 | 换用非\的转义符,或参数绑定 |
用户输入%导致全表匹配 | 参数化查询未处理 LIKE 通配符 | 入参前统一调用escape_like |
| 模糊查询很慢 | 前导%导致索引失效 | 生成列/函数索引/全文索引 |
| 把模糊查询封装成视图想提速 | 视图不存储数据,不能加速 | 改为对索引列做前缀查询 |
5.2 三个真实踩坑现场
第一个坑:业务编码里大量使用下划线。商品编码规范是SZ_2024_001这类格式,需求是“查询 2024 年所有深圳商品”。同事写的是LIKE '%SZ_2024%',结果把SZ02024、SZ12024都捞出来了,因为这些_都可以被当成任意单字符。排查时如果不先用一个小数据集验证 LIKE 语义,很容易误判为数据质量问题。
第二个坑:JSON 字段里做模糊匹配。有人把整个 JSON 字符串存到字段里,查询时直接LIKE '%"type":"A"%'。虽然 JSON 里的双引号和冒号不是通配符,但如果 JSON 数据里恰好含%或_,一样中招。更麻烦的是 JSON 字符串中的反斜杠转义会叠加。这种情况不要用 LIKE,直接用数据库的 JSON 函数,比如 MySQL 的JSON_EXTRACT,既安全又高效。
第三个坑:SQL Server 的方括号。SQL Server 的 LIKE 支持[]做字符集匹配,比如LIKE 'sales[_]2024'可以匹配sales_2024。但如果你想匹配字面方括号,就又得引入一层转义。类似这种“一个数据库一套规则”的细节,跨数据库移植时最容易翻车。建议所有涉及LIKE的 SQL 都写清楚ESCAPE,不要裸奔。
5.3 特殊字段名和 IN 查询的补充注意
虽然本文主角是转义符,但“特殊字段处理”还有两个经常一起出现的坑,值得多说两句。
一是热词里提到的字段名是数据库保留字。MySQL 用反引号,Oracle 和 PostgreSQL 用双引号,SQL Server 用方括号。各数据库的标识符引用规则不一样,迁移 SQL 时很容易忽略。最省心的办法是建表时就避免使用保留字命名。
二是IN查询报错或结果异常。常见原因包括:列表里混入了 NULL,导致结果缺少记录;字段类型和列表元素类型不一致;列表过长超过数据库限制。排查顺序应该是:先打印实际执行的 SQL,确认列表内容有没有特殊字符;再用COALESCE或IFNULL把 NULL 处理掉;最后检查字符集和排序规则是否统一。
最后说点实际的
我在实际项目里被这种问题折腾过好几回之后,总结出一条经验:凡是写 LIKE 相关的需求,第一件事就是问清楚字段内容里会不会出现%、_、\这些特殊字符。会,就统一在代码里做一次escape_like(),数据库层一律显式ESCAPE '/',绝不依赖默认行为。这个习惯养成了,后面能少踩很多坑。
再分享一个调试小技巧:遇到“模糊查询结果不对”时,别急着改 SQL 来回试。先用一条SELECT把转义后的模式串打出来看一眼,往往能立刻发现%和_的位置错了。比如你原本以为模式是%5/%%,打印出来发现成了%5%%/%,那问题就一目了然了。这种问题最耗时间的环节从来不是写修复语句,而是确认数据里的特殊字符到底是什么。把排查顺序理顺,效率能提高不少。