like 中的\_和%是通配符而非字面字符,需用escape子句或预处理转义,否则查询逻辑被篡改;推荐应用层统一转义后配合escape使用,避免反斜杠被多层框架吞掉。

LIKE 里的 _ 和 % 是通配符,不是字面字符
MySQL 的 LIKE 运算符在底层会把 _ 当作“匹配任意单个字符”,% 当作“匹配零个或多个任意字符”。这和正则表达式里的 .、.* 类似,是语义级的模式解释,不是字符串逐字比较。
所以当你查字段值为 "user_name" 或 "60%" 的记录时,如果写成:
SELECT * FROM users WHERE name LIKE '%user_name%';
实际效果是:匹配“以任意字符开头 + user + 任意单个字符 + name + 任意字符结尾”的字符串,比如 "userXname" 也会命中,而真正的 "user_name" 反倒可能被漏掉——因为下划线被当成了通配符,不是下划线本身。
不转义就等于查询逻辑被悄悄篡改
常见错误现象包括:
- 用户搜
"test_123",结果返回了"testA123"、"testZ123"等一堆无关数据 - 用户输入
"99%"想查折扣字段,却查出全部记录(%在开头+结尾导致'%99%%'等价于'%%') - MyBatis 中用
like '%${param}%'且未过滤,前端一输%就变全表扫描
根本原因不是 SQL 写错了,而是你把「要查的值」直接塞进了「模式表达式」里,没区分“数据内容”和“匹配语法”。
两种转义方式:反斜杠 \ vs ESCAPE 子句
MySQL 默认用 \ 作转义字符,但必须配合 ESCAPE 显式声明才生效;单独写 '50\%' 在某些版本或 SQL 模式下可能被忽略(尤其客户端做了预处理)。
推荐统一用 ESCAPE 方式,可控性更强:
SELECT * FROM products WHERE title LIKE '%50#% off%' ESCAPE '#';
说明:
-
#是你自定义的转义符,避开反斜杠在 Java/Python 字符串里容易被吃掉的问题 -
#%表示字面意义的%,前面的%仍是通配符 - 如果字段本身含
#,就得换别的转义符,比如@或! - 别用
\作转义符再加ESCAPE '\'—— Windows 路径、Java 字符串、JSON 都可能提前吞掉一个反斜杠,最终发到 MySQL 的只剩一个\%,而 MySQL 看不到ESCAPE声明就会报错或误匹配
应用层处理比 SQL 层更可靠
真正容易被忽略的是:转义动作不能只靠 SQL 写法兜底。用户输入不可信,SQL 拼接风险高,尤其涉及 MyBatis、JDBC PreparedStatement 或 ORM。
实操建议:
- 收到用户输入后,立刻做预处理:
input.replaceAll("%", "/%").replaceAll("_", "/_"),然后在 SQL 里固定写ESCAPE '/' - 避免用
concat('%', #{param}, '%')—— 如果#{param}已含转义符,concat会把它当普通字符拼进去,失去转义效果 - 如果用
PreparedStatement,不要试图在参数里塞转义符,而应在业务代码中对原始字符串转义后再 setParameter - 注意 MySQL 的
sql_mode:若启用了NO_BACKSLASH_ESCAPES,默认反斜杠转义会失效,必须显式用ESCAPE
最麻烦的点往往不在 SQL 本身,而在字符串经过 N 层框架、日志、中间件时被反复 decode/encode,导致转义符数量对不上。宁可多一步手动 replace,也不要依赖某一层自动转义。











