喜迎
春节

SQL LIKE 中的下划线陷阱:如何正确匹配带下划线的字符串


在日常的数据库查询中,我们经常需要使用 LIKE 操作符进行模糊匹配。当表名或字段名本身包含下划线(_)时,一个隐蔽而常见的错误就可能悄悄出现——下划线在 SQL 的 LIKE 模式中是一个通配符,代表任意一个字符。如果忘记转义,就会匹配出意料之外的结果。

本文将以一个真实案例为引,深入讲解下划线通配符的原理、错误原因,并提供多种解决方案。


一、一个让人困惑的查询

某天,你想查询数据库中所有以 z_log_ 开头的表(例如 z_log_2023z_log_archive)。于是写下如下 SQL:

1
2
3
4
5
SELECT TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME LIKE 'z_log_%'
AND TABLE_SCHEMA = 'your_db'
ORDER BY TABLE_NAME;

你期望的结果只有 z_log_xxx 这样的表。然而,查询结果中却出现了 z_login_recordsz_logistics 等表。

为什么会这样? 因为下划线 _LIKE 模式中是单字符通配符,它匹配任意一个字符(字母、数字、下划线本身除外,后面会讲)。所以 'z_log_%' 的实际含义是:

  • z - 字母 z
  • _ - 任意一个字符(例如 loi 等)
  • log - 字母 l, o, g
  • _ - 又是一个任意字符
  • % - 任意多个字符

因此,z_log_% 会匹配:

  • z_log_2023 ✅(第一个下划线匹配 _ 本身?等一下,仔细分析:z_log_2023 中:z,然后第一个通配符 _ 匹配 _(下划线字符本身也是“任意一个字符”),接着 log 匹配 log,然后第二个通配符 _ 匹配 _?不,实际上 z_log_2023 中,在 z 之后紧接着是 _(下划线字符),这个下划线字符正好被第一个通配符 _ 匹配了;然后 log 匹配 log;第二个通配符 _ 匹配 _;最后 % 匹配 2023。所以 z_log_2023 匹配成功。)
  • z_log_archive
  • z_login ✅(z + _(通配符匹配 l) + log + in? 等等 z_loginz,通配符1匹配 _?不,z_loginz 后是 _?不对,是 z 后直接 l,因为 z_login 没有下划线。让我们重新分析:z_login 字符串:位置1 z,位置2 l,位置3 o,位置4 g,位置5 i,位置6 n。模式 z_log_%z -> 匹配 z;第一个通配符 _ -> 匹配任意一个字符,它匹配了 l;然后 log -> 匹配 og?但模式中 log 是三个连续字母,而字符串在 l 之后是 og,正好匹配 log;接着第二个通配符 _ -> 匹配 i;最后 % 匹配 n。所以 z_login 也被匹配了!这就是问题根源。)

简单总结:z_log_% 会匹配任何 以 z + 任意字符 + log + 任意字符 开头 的表名。


二、SQL LIKE 通配符速查

通配符 含义 示例
% 任意多个字符(包括零个) 'a%' 匹配以 a 开头的任意字符串
_ 恰好一个任意字符 'a_' 匹配 aba1,但不匹配 aabc
[ ] 指定范围内的一个字符(仅 SQL Server 等支持) 不通用,此处不展开
[^] 不在范围内的一个字符 同上

由此可见,下划线 _ 是一个极易被忽视的通配符。


三、正确匹配含下划线的字符串

要匹配 字面意义上的下划线,必须对 _ 进行转义。SQL 标准使用 ESCAPE 子句定义转义字符,但不同数据库的默认转义行为略有差异。

方法一:使用反斜杠转义(MySQL、MariaDB)

在 MySQL 中,默认的转义字符就是反斜杠 \。因此可以直接写:

1
WHERE TABLE_NAME LIKE 'z\_log\_%'

注意:在普通字符串中,反斜杠也可能被解释为转义,但 MySQL 中 LIKE 模式会识别 \_ 为字面下划线。你也可以显式指定 ESCAPE 字符,提高可读性:

1
WHERE TABLE_NAME LIKE 'z$_log$_%' ESCAPE '$'

这里 $ 被定义为转义字符,$_ 表示字面下划线。

方法二:使用方括号转义(SQL Server、Sybase)

SQL Server 支持用方括号将通配符括起来表示字面值:

1
WHERE TABLE_NAME LIKE 'z[_]log[_]%'

这种方法不需要 ESCAPE 子句,非常直观。

方法三:使用 ESCAPE 子句(SQL 标准,所有数据库均支持)

这是最通用、最推荐的做法:

1
2
3
4
SELECT TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME LIKE 'z$_log$_%' ESCAPE '$'
AND TABLE_SCHEMA = 'your_db';

ESCAPE 后面跟的字符(这里用 $,也可以用任何未在模式中出现的字符,如 #@ 等)将指示紧随其后的下划线被当作普通字符处理。


四、不同数据库的转义差异对比

数据库 默认转义字符 推荐方法 示例
MySQL \ 使用 \_ESCAPE LIKE 'z\_log\_%'LIKE 'z#_log#_%' ESCAPE '#'
PostgreSQL 无默认 必须用 ESCAPE LIKE 'z$_log$_%' ESCAPE '$'
SQL Server 无默认 方括号 [_]ESCAPE LIKE 'z[_]log[_]%'
Oracle 无默认 ESCAPE LIKE 'z$_log$_%' ESCAPE '$'
SQLite 无默认 ESCAPE 同 PostgreSQL

最佳实践:为了代码的可移植性,始终使用 ESCAPE 子句显式指定转义字符。


五、更优雅的替代方案

如果你的数据库支持正则表达式(如 MySQL 的 REGEXP、PostgreSQL 的 SIMILAR TO~),可以用正则表达式来避免通配符歧义。

MySQL 示例:

1
WHERE TABLE_NAME REGEXP '^z_log_'

正则表达式中的下划线没有特殊含义,无需转义。但要注意正则表达式的性能通常低于 LIKE

PostgreSQL 示例:

1
WHERE TABLE_NAME ~ '^z_log_'

六、完整示例:查询以 z_log_ 开头的表

结合上述知识,正确且健壮的查询语句应为:

1
2
3
4
5
6
-- 使用 ESCAPE 子句(所有数据库通用)
SELECT TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME LIKE 'z$_log$_%' ESCAPE '$'
AND TABLE_SCHEMA = 'your_database_name'
ORDER BY TABLE_NAME;

如果只需要 MySQL,也可以简写为:

1
2
3
4
5
SELECT TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME LIKE 'z\_log\_%'
AND TABLE_SCHEMA = 'your_database_name'
ORDER BY TABLE_NAME;

七、常见误区与注意事项

  1. 下划线本身也是“任意一个字符”LIKE '_' 会匹配任何长度为1的字符串,包括单个下划线字符。
  2. 转义字符本身也需要转义:如果要匹配 %_ 并且你的转义字符是 \,那么 \\ 表示字面反斜杠,\% 表示字面百分号。
  3. ESCAPE 子句的转义字符只对紧随其后的一个字符生效:例如 ESCAPE '$' 时,$% 表示字面百分号,$_ 表示字面下划线。
  4. INFORMATION_SCHEMA 中的表名大小写敏感:不同文件系统会影响大小写匹配,建议使用 LIKE 时注意大小写。

八、总结

下划线 _ 在 SQL 的 LIKE 模式中是单字符通配符,这是很多人踩过的“坑”。当你要匹配包含下划线的字符串时,切记转义。推荐使用 ESCAPE 子句显式指定转义字符,这样既清晰又兼容所有主流数据库。

需求 错误写法 正确写法
匹配 z_log_ 开头的表 'z_log_%' 'z$_log$_%' ESCAPE '$'
匹配包含下划线的任意字符串 '%_%' '%$_%' ESCAPE '$'
匹配名为 a_b 的列 WHERE col = 'a_b' 等号没问题,LIKE 才需转义

记住这个小知识,你将避免一次令人困惑的 debug 经历。下次再遇到诡异的 LIKE 结果,请首先检查:你的下划线,转义了吗?


文章作者: Crazy Boy
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Crazy Boy !
评 论
 上一篇
Laravel 中的进度条(ProgressBar)使用指南
Laravel 中的进度条(ProgressBar)使用指南
Laravel Artisan 命令底层集成了 Symfony Console 组件,可轻松实现控制台可视化进度条。 一、基础用法1234567891011121314151617181920212223242526272829303
2026-06-01
下一篇 
当心 `strtotime('+n days', 0)` 的陷阱:时间戳计算中的时区与语义之争
当心 `strtotime('+n days', 0)` 的陷阱:时间戳计算中的时区与语义之争
一次看似“语义化”的代码优化,却险些引入生产事故。从 3 * 86400 到 strtotime('+3 days'),这中间究竟藏着哪些不为人知的坑? 一、一个来自代码审核的意外发现某天,你在优化一段判断用户操作是否超过三天时限的代码
2026-05-07
  目录
hexo