☰
SQL模糊查询中转义符与通配符冲突的完整避坑指南
2026/10/6 4:03:46 网站建设 项目流程

先交代一个背景:上周团队处理一批客户上传的商品数据时,发现一个诡异现象——用LIKE '%50%'去查折扣率刚好是 50% 的商品,结果把 5% 的、150% 的、甚至以“50”结尾的 SKU 全部捞了上来。更离谱的是,当数据里出现反斜杠时,同样的 SQL 在测试环境跑得好好的,一上生产就查不到东西。排查到半夜才发现,问题全出在“字段里的转义符”和LIKE通配符打架。这篇文章就把这类“字段中含有转义符涉及模糊匹配查询”的问题彻底讲透。

这类问题的核心,是数据库里的数据本身带有反斜杠、百分号、下划线这些特殊字符,而模糊匹配查询(LIKE)又恰好把这些字符当成了通配符。两边一冲突,查询结果就完全不可控。本文适合所有写过 SQL 的后端开发、数据分析师和 DBA,尤其是踩过“明明加了%却查不到”“没加条件却查出来一堆”这种坑的人。

1. 问题现象:一次返工到凌晨的模糊查询事故

1.1 事故现场复盘:查询结果为什么会“超纲”

先还原当时的具体场景。商品表product里有一个discount_rule字段,存储的是优惠规则的原始文本,比如:

满300减50 折扣率:50% 折扣率:5% 原价:150元

我用了一条最简单的模糊查询去统计所有“折扣率是 50%”的商品:

SELECT id, discount_rule FROM product WHERE discount_rule LIKE '%50%';

结果是 347 行。验数的时候发现里面有大量误伤:折扣率 5% 的商品被打包进来了,因为“5%”里的%是通配符,5%可以匹配“5”后面跟任意字符;折扣率 150% 的商品也进来了,因为“150”里包含了子串“50”。更隐蔽的问题是,如果字段里出现路径字符串(比如C:\temp\50%off),%off会被当成%匹配任意字符再加off,整个查询逻辑就乱成了筛子。

这就是“字段中含有转义符”和“模糊匹配”叠加时的典型症状:结果集要么“超纲”,要么“漏查”。而且很多情况下,同样的 SQL 在开发库跑得好好的,到了生产库因为sql_mode或者数据库版本差异,行为还会变。

1.2 转义符在查询链路里的三个身份

要理解这个问题,得先搞清楚一个字符在查询过程中会经历哪些层级的解释。我把它拆成三层:

  • 数据层的原始字面量:数据库里真实存储的字符,比如一个反斜杠\、一个百分号%、一个下划线_,在数据里就是普通的字符,没有特殊含义。
  • SQL 解析层的字符串转义:你写在 SQL 里的字符串常量,比如'50\%',解析器先处理反斜杠,把它当成转义标记。在 MySQL 默认模式下,'50\%'会被解析成一个“反斜杠 + 百分号”的字面量还是“百分号”,取决于上下文。
  • LIKE 运算符的通配符语义:进入LIKE匹配阶段后,%和_才会被赋予通配符含义。这时,前面的转义符才派上用场,用来告诉引擎“我后面的%和_是普通字符,不是通配符”。

大多数人踩坑,是因为只考虑了其中某一层。比如写LIKE '%50%%'想去匹配“折扣率:50%”,结果后面那个%又被当成通配符了;又比如写LIKE '%50\%',在 MySQL 里看着对了,代码一迁移到 PostgreSQL 又废了,因为不同数据库对字符串转义的处理逻辑不同。

1.3 通配符“泛滥”的业务场景比想象中多

别以为只有极端脏数据才会有这种问题。我梳理了下,至少这几类常见业务场景会踩雷:

  • 用户输入的搜索关键词:用户搜索50%、C++、100_200之类的关键词,直接把用户输入拼进LIKE,百分号和下划线就会变成通配符。
  • 存储路径和 URL:Windows 路径C:\Program Files\、URL 参数?rate=50%、对象存储 key,这些字符串天然带着反斜杠和百分号。
  • 折扣、百分比、占比类数值的格式化文本:优惠50%、完成率80%,百分号在业务文本里是合法字符,在LIKE里却是通配符。
  • 文件名和序列号:2024_report_001.xlsx、SKU_50_BLACK,下划线在_是单字符通配符,匹配别的字符会造成大量误报。

只要做的是搜索、筛选、数据清洗,就离不开通配符与转义符的正确处理。它不是偶发问题,而是高频雷区。

2. 根因拆解:LIKE 的通配符机制和转义规则的碰撞

2.1 %、_、\ 三兄弟在 LIKE 里的真实语义

先讲基础,把LIKE的通配符机制彻底讲清楚。在几乎所有关系型数据库里,LIKE支持两类通配符:

  • %(百分号):匹配任意数量的字符,包括零个字符。
  • _(下划线):匹配恰好一个字符。

这两条规则意味着:LIKE '%50%'的含义是“包含子串 50”的任何字符串,其中50前后可以跟任意内容;而LIKE '50_'则是精确匹配“以 50 开头、后面跟且只跟一个字符”的字符串,比如502、50A都命中,5010就不命中。

第三个角色是转义符。SQL 标准提供了一个ESCAPE子句,用来指定一个转义字符,让它后面的通配符被还原为普通字面量。比如:

SELECT * FROM product WHERE discount_rule LIKE '%50!%%' ESCAPE '!';

这里的!是转义符,!%表示字面量的百分号。这条 SQL 就能匹配“折扣率:50%”,因为第一个%(在50前)是通配符,!%被当成普通字符,最后一个%是通配符。

这里有个关键点:ESCAPE从句指定的转义符只作用于LIKE匹配阶段,不影响 SQL 字符串本身的解析。而很多人更熟悉的反斜杠转义,是另一个层面的东西。

2.2 反斜杠转义的“两层皮”:别把两件事混为一谈

反斜杠在 SQL 里有“两层皮”,这是我见过最多人犯迷糊的地方。

第一层,字符串转义。在 MySQL 的默认行为里,\是字符串字面量的转义符。也就是说:

SELECT '50\%';

实际上返回的是50\%还是50%?答案是50%,因为\%被字符串解析器当成一个普通的%字符输出了。这意味着你在 SQL 里写LIKE '%50\%%',走到 LIKE 运算时,已经变成LIKE '%50%%'——后面两个%全成了通配符,根本表达不了“查找含 50% 文本”的意图。

第二层,LIKE 通配符转义。MySQL 在默认模式下,LIKE的默认转义符也是反斜杠。所以:

SELECT * FROM product WHERE discount_rule LIKE '%50\%%';

能生效的前提,是 SQL 字符串里真的包含“反斜杠 + 百分号”这两个字符,让 LIKE 引擎把\%识别成“普通百分号”。但前面第一层的解析往往已经提前把\%合并成了%,于是两层逻辑互相干扰,最终行为变得极其脆弱。

这就是为什么我强烈不建议在模糊匹配里依赖反斜杠转义:不同数据库对字符串层的处理不一致,MySQL 的NO_BACKSLASH_ESCAPES模式一改,原先能跑的 SQL 立刻失效;PostgreSQL 里standard_conforming_strings的设置也会改变\的含义;Oracle 则干脆用ESCAPE子句来显式指定转义符,默认根本没有反斜杠转义。把逻辑押在一个“各数据库行为不统一”的字符上,迟早要出事。

2.3 为什么参数化查询救不了这个场

很多同学会说:我不是都用了预编译和参数化查询吗?为什么还是出了这种问题?

这个必须澄清:参数化查询解决的是 SQL 注入,以及 SQL 字符串拼接带来的语法错误。参数化之后,用户输入的50%确实会作为参数高效传递进去,不会破坏 SQL 语法结构。但数据库收到参数之后,执行到LIKE算子时,通配符语义依然生效——50%中的%不会因为是参数就变成普通字符。

试一个经典案例。用户搜索输入50%,你写了 Django ORM:

Product.objects.filter(discount_rule__contains=user_input)

你期待的是“包含字面量50%”,但实际 SQL 生成的是LIKE '%50%%',最终匹配了所有含 50 的字符串。参数化帮我挡住了 SQL 注入,却没有挡住通配符的语义问题。

所以,转义符处理是独立的一层工作,必须显式做:要么在传入参数前把%、_、\进行转义预处理,要么在 SQL 里用ESCAPE子句指定转义字符。两者结合,才能得到一个真正严谨的模糊查询。

3. 实操解法:各语言携带 ESCAPE 的规范化实现

3.1 MySQL 场景:显式 ESCAPE 才是跨模式的正解

MySQL 默认将反斜杠作为 LIKE 的转义符,但这个行为不靠谱,因为它是可配置的。最稳妥的手法,是使用一个业务侧的占位符作为 ESCAPE 字符,然后对所有需要按字面量匹配的特殊字符做统一处理。

先看标准语法:

SELECT * FROM product WHERE discount_rule LIKE '%50!%%' ESCAPE '!';

这里!就是自定义的 ESCAPE 字符,!%代表一个字面量百分号,最后的%是通配符。注意 ESCAPE 子句只能指定单个字符,不能是字符串。选!还是\或者别的字符,原则是“尽量选业务数据里极少出现的字符”,减少把正常业务字符误伤成转义符的概率。

写成生产环境可复用的完整场景,应该是这样。用户输入的搜索关键词,先做转义函数处理:

def escape_like(keyword: str) -> str: return ( keyword .replace('!', '!!') # 先转义转义符本身 .replace('%', '!%') # 再转义百分号 .replace('_', '!_') # 最后转义下划线 )

然后在 SQL 里:

SELECT * FROM product WHERE discount_rule LIKE CONCAT('%', #{escaped_keyword}, '%') ESCAPE '!';

这个函数里有严格的“先转义转义符本身”的顺序,原因是如果先转义%,再转义!,那么!%里的!会被二次处理,破坏原有转义结果。顺序错了,你处理完的关键词可能完全失效。

3.2 PostgreSQL 与 SQL Server:默认转义符并不相同

PostgreSQL 的默认行为跟 MySQL 差别很大。PostgreSQL 的 LIKE 默认没有转义符,所以LIKE '%50\%%'在这里不会按\%来理解,它会原封不动地把\%当成一个反斜杠跟一个百分号去匹配。这意味着,如果你要把 MySQL 的代码迁移到 PostgreSQL,之前的“默认转义”逻辑会全部失效。

PostgreSQL 里推荐的做法依然是显式 ESCAPE:

SELECT * FROM product WHERE discount_rule LIKE '%50!%%' ESCAPE '!';

同样要配escape_like函数做前置处理。不过 PostgreSQL 提供了更现代的替代方案,如果你不是非用 LIKE 不可,可以试试正则表达式和POSITION函数。比如:

SELECT * FROM product WHERE discount_rule LIKE '%' || escape_like('50%') || '%' ESCAPE '!';

或者用strpos做纯子串匹配,它不带通配符语义:

SELECT * FROM product WHERE strpos(discount_rule, '50%') > 0;

这样一点转义符都不用处理,其实是更省心的路子。

SQL Server 又有一套玩法。它支持ESCAPE子句,但特别的是,可以用方括号[]来包裹特殊字符,表示“匹配这个字面量字符本身”。比如:

SELECT * FROM product WHERE discount_rule LIKE '%50[%%]' ESCAPE '!';

这里[%]表示匹配字面量的百分号。当然,方括号这种语法是 SQL Server 的方言,不通用。如果目标是跨数据库兼容,优先选择标准的ESCAPE子句。

3.3 Django ORM、Java MyBatis 与 Node.js 中的落地实践

套用到具体框架,操作方法有两类:

第一类:尽量用“包含查询”代替 LIKE 通配查询,避免手动转义。

Django ORM 里:

from django.db.models.functions import StrPos # 最推荐的做法:只要是判断子串,就直接用 strpos rows = Product.objects.annotate( pos=StrPos('discount_rule', search_text) ).filter(pos__gt=0)

StrPos是 PostgreSQL 专有函数,底层就是strpos,完全绕开 LIKE。如果必须在 MySQL 里做等价操作,可以用LOCATE函数作为注解来过滤,但 Django 对 MySQL 的LOCATE封装并不统一,我更建议用原生态 SQL 处理。

第二类:预处理参数,然后用 ORM 的 contains 方法。

Django ORM 的__contains对应 SQL 的LIKE '%keyword%',不会自动帮你转义特殊字符,所以必须在传入之前处理:

def escape_like(keyword: str) -> str: for char in ['\\', '%', '_']: keyword = keyword.replace(char, f'\\{char}') return keyword keyword = escape_like(user_input) rows = Product.objects.filter(discount_rule__contains=keyword)

这里有一个细节:Django 在 MySQL 后端下,__contains生成的 SQL 默认会把反斜杠作为转义符;但如果你切换到了 SQLite 或者其他数据库,行为可能完全不同。所以最稳的方案,还是避开contains,用数据库提供的“纯子串函数”。

Java MyBatis 的场景也差不多。Mapper XML 里这种写法非常常见:

<select id="search" resultType="Product"> SELECT * FROM product WHERE discount_rule LIKE CONCAT('%', #{keyword}, '%') </select>

但#{keyword}传入的任何%和_都依然是通配符。正确做法是在 Java 服务层预先调用转义工具方法:

public static String escapeLike(String keyword) { return keyword .replace("!", "!!") .replace("%", "!%") .replace("_", "!_"); }

XML 里写成:

SELECT * FROM product WHERE discount_rule LIKE CONCAT('%', #{escapedKeyword}, '%') ESCAPE '!'

Node.js 配合 mysql2 或者 Knex 时同理,取到用户输入后先做字符串替换,再用参数化查询绑入,SQL 里同样加ESCAPE '!'。

3.4 Oracle 的 CLOB 字段特殊处理

如果字段类型是 Oracle 的CLOB,直接LIKE是没法用的。Oracle 的LIKE不能作用在 CLOB 上,会直接报ORA-00932: inconsistent datatypes。这时候要先用DBMS_LOB.INSTR或DBMS_LOB.SUBSTR做预处理。

转义符的坑在 CLOB 场景里同样存在,而且更隐蔽。因为 CLOB 不支持LIKE,所以很多同学会写:

WHERE DBMS_LOB.INSTR(discount_rule, '50%') > 0

INSTR是纯字面量查找,没有通配符语义,这样反而天然规避了 percent 通配符的问题。但如果商品文本里既有50%又有下降档位5%末尾加了个字符,实际业务仍需要模糊语义时,就得先把 CLOB 切片出来再匹配,例如:

WHERE DBMS_LOB.SUBSTR(discount_rule, 4000, 1) LIKE '%50!%%' ESCAPE '!'

这里SUBSTR会把 CLOB 转为 VARCHAR2 再处理,注意 CLOB 截断上限是 4000 字节,超长会被截断,匹配结果可能会有遗漏,所以要结合DBMS_LOB.INSTR做两段式判断。

4. 索引选择、数据清洗与查询正确性的通盘校验

4.1 别再天真地对 LIKE 前缀查询建索引

谈完转义语法,还得说一个常常被忽略的问题:模糊查询的性能。LIKE '%keyword%'这种双百分号的写法,本身就是索引杀手。就算你处理对了转义符,查询也对不了全表扫描的命运。

如果业务真的需要高频搜索文本字段,通常要考虑三类方案:

  • 方案一:利用覆盖索引 + 前缀匹配。只有LIKE 'keyword%'(右模糊)才能用上普通索引,左模糊和双模糊都走不了。这要求业务能接受“只按前缀匹配”。
  • 方案二:用全文索引/全文检索。MySQL 的FULLTEXT、PostgreSQL 的tsvector、Elasticsearch,都是更适合大文本搜索的载体。通配符和转义符的问题在这些系统里语义完全不同。
  • 方案三:数据清洗 + 精确匹配。把需要搜索的字段拆出来,单独存一个“标准化字段”,让查询走精确匹配或者前缀匹配,绕开%通配符。

如果你的查询模式注定是双模糊,那性能这块基本无解,得从架构上引入检索组件。

4.2 脏数据里的反斜杠和转义符怎么清洗

有时候问题出在数据写入端。比如 CSV 导入时,50%被导成了50\%;Windows 路径被存成C:\\Users\\name,字段里有两个反斜杠;跨系统同步时,JSON 转义符\"被原样存进了字段。这些数据如果不清理,转义符问题会反复出现。

清洗的思路分两步:

先做“字段体检”,找出哪些行包含可疑特殊字符:

-- 找出含有反斜杠、百分号、下划线的记录 SELECT id, discount_rule FROM product WHERE discount_rule LIKE '%\\%%' ESCAPE '\\' OR discount_rule LIKE '%\\_%' ESCAPE '\\' OR discount_rule LIKE '%\\\\%' ESCAPE '\\';

这里每一行的 ESCAPE 用法很讲究,比如第一个条件想找“含百分号的记录”,用%\\%%这个模式的意思是:首尾两个%是通配符,中间\\%是转义后的字面量百分号。

体检之后再做批量 UPDATE。清洗时要注意“一次性修复根因”,别只修表象。比如如果是导入程序写坏了,哪怕你手动 UPDATE 清一遍,下次导入还会再脏。我通常的做法是写个幂等清洗脚本,在数据写入的 pipeline 里统一执行:把\\%还原为%、把\\_还原为_、把双反斜杠还原为单反斜杠。

不过,清洗脚本本身要小心:如果业务里真的存在“反斜杠 + 百分号”这种合法连续字符,一刀切地替换会破坏业务数据。所以清洗前一定要先取样确认,最好把清洗规则放在测试环境跑一遍,核对变更后的数据字段语义是否仍然正确。

4.3 排查模糊查询问题的速查表

把实战中常见的症状、原因和修复方法整理成一张表,直接对照使用:

症状可能原因排查与修复
查%50%返回了5%相关数据用户输入中的%被当成通配符先用escape_like处理输入,再LIKE ... ESCAPE '!'
数据里是50%但怎么都查不到字符串层的\%被提前解析成了%,或 LIKE 层转义失败不要依赖反斜杠默认转义,改用显式ESCAPE子句
查询包含下划线,结果异常_被当成单字符通配符转义_或换用INSTR/strpos纯字面量匹配
迁移数据库后查询失效不同数据库对默认转义符行为不一致全面改用ESCAPE '!'显式语法
数据字段含\导致匹配错乱反斜杠在字符串层或 LIKE 层被特殊处理清洗字段,或者统一用自定义转义符
查询慢,全表扫描双百分号 LIKE 无法走索引改用全文检索或前缀匹配方案
CLOB 字段无法 LIKEOracle 不支持 CLOB 直接用 LIKE用DBMS_LOB.INSTR或SUBSTR预处理

这张表基本覆盖了我这些年见过的模糊查询转义类问题,适合直接贴到团队文档里当 FAQ。

4.4 一个典型修复案例的完整还原

最后用文章开头那个“折扣率 50%”的案例,完整展示修复过程。

原始查询:

SELECT id, discount_rule FROM product WHERE discount_rule LIKE '%50%';

修复后的查询:

user_input = "50%" escaped = escape_like(user_input) # 结果为 "50!%"
SELECT id, discount_rule FROM product WHERE discount_rule LIKE CONCAT('%', '50!%', '%') ESCAPE '!';

执行流程拆解:

  • 先查%通配符会把50!%前后的任意内容兜住;
  • 中间50是普通匹配;
  • !%被 ESCAPE 子句识别为普通百分号字面量;
  • 结果集只包含“折扣率:50%”“优惠50%封顶”这类真正含字面量50%的记录;
  • 而5%、150%这些不再误入。

为了交叉验证,我习惯再跑一条对照组:

SELECT id, discount_rule FROM product WHERE discount_rule LIKE '%50!%%' ESCAPE '!';

和修复后结果对比,如果两条 SQL 的结果集不一致,说明还有转义逻辑遗漏的行,需要逐行核对字段里的特殊字符分布。这步虽然有点笨,但确实能兜住边界情况。

5. 个人经验与最后的避坑建议

处理了这么多轮转义符和模糊查询的问题,我的核心体会是:别跟数据库的默认行为较劲,直接跟它把规则定死。不管是 MySQL、PostgreSQL、Oracle 还是 SQL Server,一律用显式的ESCAPE '!',在前置参数处理里把%、_、!按固定顺序转义。这套规则写成一个公共函数,所有项目复用。

再给大家三个实操层面的建议:

第一个建议:能不用 LIKE 就别用 LIKE。如果业务目标是判断“字段里包含某个字符串”,优先用INSTR、LOCATE、strpos这类纯子串函数。它们没有通配符语义,根本不涉及转义,代码更简单,语义更清晰。我自己后来很多搜索逻辑都改成了这种模式。

第二个建议:写一个统一的转义工具函数,配上完整单元测试。这个函数要针对%、_、!(或者其他自定义转义符)做处理,并测试边界值:空字符串、全是特殊字符的串、只有单个%、%_连续出现等情况。只要这个函数测试覆盖到位,上层所有查询都安全。

第三个建议:每次排查模糊查询问题,先查数据,再查 SQL。先用工具把字段中的不可见字符原样查出来,搞清楚数据里到底存的是什么,再决定转义策略。很多时候不是 SQL 写错了,而是数据本身就是脏的,带着肉眼看不出来的隐藏字符,查了半天查不出头绪。

转义符的问题,说到底是“字符的语义在不同上下文之间切换”的问题。只要数据可能含有通配符或转义符,模糊查询就不存在“写一次永久通用”的银弹。让全团队形成统一的转义函数、统一的 ESCAPE 约定、统一的排查清单,这类“凌晨事故”发生的概率就会小很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询