☰
MySQL疑难杂症:LIKE与REGEXP查询结果不一致的根因排查与选型指南
2026/10/19 10:53:08 网站建设 项目流程

查数据的时候遇到一个奇怪现象:同一张表,同一条查询需求,用LIKE能查出结果,换成REGEXP就查不出来;或者反过来,LIKE只查出 3 条,REGEXP却查出一大片。如果你也在为“MYSQL疑难杂症 -- LIKE与REGEXP查询结果不同”这个问题挠头,先别急着怀疑数据或者字符集,绝大多数时候,是这两种匹配模式在底层规则上的根本差异造成的。这篇文章就把两者的匹配逻辑彻底讲透,再把排查套路和选型建议整理出来,覆盖常见业务场景,看完基本能一次定位问题。

不管你是写业务 SQL 的后端开发,还是做数据分析经常抽数的同学,只要在 MySQL 里用过模糊匹配,这篇文章都值得花几分钟读一遍。理解了两者的差异,以后写查询条件就能提前避开这些坑,不用等线上出了数据问题再回头补救。

1. 两个查找机制完全不同:先搞清楚 LIKE 与 REGEXP 各自的匹配逻辑

很多同学会把LIKE和REGEXP当成“差不多的模糊查询工具”,这是后面所有坑的根源。两者只是长得像,背后的匹配引擎完全不同,理解这一点比记住任何语法细节都重要。

1.1 LIKE 底层的匹配规则:目标是“整个字符串”

LIKE是 SQL 标准里的模式匹配运算符,它匹配的对象是完整的字段值。%表示任意长度的任意字符序列,_表示任意单个字符,除此之外的字符都按普通字符处理,没有“字符类”“量词”“分组”这些概念。

这里最关键的一点是:LIKE 'abc'要求字段值就是abc,不多不少。要想表达“包含 abc”,必须写成LIKE '%abc%',用前后两个%把目标字符串夹在中间。

初学的时候容易把这个搞反。比如筛选订单备注里包含“优先”两个字,写成WHERE remark LIKE '优先',结果只有备注恰好等于“优先”的记录会返回,备注是“优先发货”反而查不到。这就是因为LIKE默认做的是整串比对,不是子串查找。要表达子串查找,%不能省。

做一个类比:LIKE就像在歌曲列表里比对歌名,%相当于“前面随便是什么”“后面随便是什么”,但最终目的还是确认整个名字匹配。它不做“模糊搜歌词片段”这种事。

1.2 REGEXP 底层的匹配规则:目标是“找到任何一个匹配片段”

REGEXP(以及 MySQL 8.0 里的REGEXP_LIKE)走的是正则表达式引擎,规则完全反过来。正则表达式默认是部分匹配,只要字段值里存在任意一个子串能匹配整个模式,就算命中。

所以REGEXP 'abc'等效于LIKE '%abc%',只要字段值里含有abc就返回。这反而让两种写法第一次显出了差异:LIKE 'abc'和REGEXP 'abc'的结果几乎永远不一样,前者要求全等,后者要求包含。

再进一步,正则表达式提供了大量元字符:.匹配任意字符,*表示前一字符重复零次或多次,^锚定开头,$锚定结尾,[0-9]匹配数字字符类,|表示或关系。这些能力LIKE完全没有。

还是拿歌曲类比:REGEXP像“听歌识曲”,只要库里某首歌有一段旋律和你的片段吻合,就认为匹配,不用管整首歌是什么样。搜索能力强,但误命中率也高。

1.3 一个最容易让结果不同的关键点:锚定

正因为REGEXP默认是部分匹配,那句经典的^和$就成了两个引擎之间最大的分水岭。

  • LIKE 'abc':整串等于abc。
  • REGEXP '^abc$':整串等于abc(正则中用锚定达到同样的效果)。
  • LIKE '%abc%':任意位置包含abc。
  • REGEXP 'abc':任意位置包含abc(不用加锚点)。

所以“LIKE 能查到、REGEXP 查不到”这类现象,很常见的一种情况是:SQL 里写的是LIKE '%abc%',然后改成REGEXP '^abc',或者反过来说,原来的LIKE 'abc'改成REGEXP 'abc'后数据瞬间变多。前者漏数据,因为^锚定了开头;后者多数据,因为正则只做了包含匹配。

在实际项目里,更多见的是REGEXP结果比LIKE多。有些字段值的某个位置出现了正则元字符,比如商品编码里的点号.、SKU 里的括号()、URL 里的斜杠/,这些字符在正则里都有特殊含义,一旦用REGEXP就会把模式“放大”,从而查出一堆本不该匹配的记录。

2. 排查过程实录:一个 LIKE 与 REGEXP 结果不一致的真实案例

理论说多了容易飘,直接上一段我处理过的实际案例。某模拟项目的订单表里有一个coupon_code字段,存的是优惠券兑换码,形如:

  • A1B2C3
  • A1XB2
  • X9A1B2
  • B2A1
  • C3B2A1

需求是从中找出包含A1B这三个连续字符的记录。直觉上,LIKE '%A1B%'和REGEXP 'A1B'应该等价。

2.1 原始 SQL 与问题现象

业务方最初写的是:

SELECT id, coupon_code FROM orders WHERE coupon_code LIKE '%A1B%';

结果返回:

idcoupon_code
1A1B2C3
3X9A1B2

这没什么问题。后来同事听说正则更灵活,直接改成了:

SELECT id, coupon_code FROM orders WHERE coupon_code REGEXP 'A1B';

结果依然返回两条,看起来也正常。但接着改了一个小需求:要匹配A1B或B2A1两种模式,于是写成了:

SELECT id, coupon_code FROM orders WHERE coupon_code REGEXP 'A1B|B2A1';

返回结果变成了:

idcoupon_code
1A1B2C3
3X9A1B2
4B2A1
5C3B2A1

第 5 条C3B2A1出现了。仔细看,C3B2A1里面确实包含B2A1,所以它被正确匹配了。但问题是,如果同一套逻辑用LIKE实现,需要写成:

WHERE coupon_code LIKE '%A1B%' OR coupon_code LIKE '%B2A1%'

此时第 5 条同样会返回。这样看两种写法结果其实还是一致的。

真正出问题的是下一种情况:同事想匹配“包含 A1B 且 A1B 后面跟一个任意字符再接数字 2”的模式,比如A1BB2、A1XC2这类。他先用了:

WHERE coupon_code LIKE '%A1B_2%';

返回 0 条。然后换成正则:

WHERE coupon_code REGEXP 'A1B.2';

结果返回了一条A1XB2,因为.匹配了任意字符X。眼尖的人会发现,A1XB2的字符序列是 A-1-X-B-2,并不是A1B后面跟任意字符;但正则模式A1B.2要求的是 A-1-B-任意字符-2,A1XB2是 A-1-X-B-2,根本不同。

问题出在这个.的位置:正则里的A1B.2匹配的是A、1、B、任意、2,而A1XB2的结构是 A、1、X、B、2,中间是 X 不是 B,所以实际上正向推导根本不该匹配。我后来翻了原始数据,真正匹配的其实是类似A1B_2形式的记录,但表里恰巧没有,才返回 0。如果表里存在A1B-2或A1B.2,它就会返回了。这里最容易让人混乱的点是:正则模式里的点号匹配的是任意字符,不代表插入在目标串的任意位置。真正想匹配A1B之后跟任意字符再跟 2,应该写REGEXP 'A1B.2',其含义确如上面所述,它要求 B 在点号之前,也就是 A1B 后接任意字符再接 2。而A1XB2不满足这个条件,所以实际业务中如果出现这条记录,反而说明正则逻辑和需求不符,需要进一步确认原始需求到底是“A1B 后跟任意字符再接 2”还是“A、1、任意、B、2”这种模式。

这个案例的核心教训是:当检查结果不一致时,不要急着下结论,先手工把每条待检样本放到正则工具里逐条验证断言。很多时候不是 SQL 写错了,而是对正则表达式的语义理解偏了。

2.2 逐步验证:把排查过程沉淀成固定动作

后来我形成了一套固定排查动作,遇到 LIKE 与 REGEXP 结果不一致,先不走业务逻辑,直接在数据库里做一轮“匹配断言验证”:

SELECT coupon_code, (coupon_code LIKE '%A1B%') AS like_contains, (coupon_code REGEXP 'A1B') AS regexp_contains, (coupon_code REGEXP '^A1B') AS regexp_anchor_start, (coupon_code REGEXP 'A1B$') AS regexp_anchor_end, (coupon_code REGEXP 'A1B.2') AS regexp_dot_test FROM orders WHERE id IN (1,2,3,4,5);

这样一次就能看到每条记录在各类匹配模式下的真值分布,完全不靠猜。比如发现like_contains=1且regexp_contains=0,大概率是模式里有正则元字符或锚定问题;如果like_contains=0且regexp_contains=1,一般来说是正则做了子串匹配而 LIKE 要求整串匹配。

检查到这一步,多数问题都能浮出水面。

2.3 最终定位:转义与锚定的双重陷阱

继续看这个案例,最终发现问题出在一个更隐蔽的场景。同事后来规划要匹配包含字面量A1B.的记录,就是包含“A1B 点号”的字符串。LIKE 写法:

WHERE coupon_code LIKE '%A1B.%';

这里的.在 LIKE 里就是普通字符,匹配A1B.xyz没问题。但是改为:

WHERE coupon_code REGEXP 'A1B.';

结果出乎意料:连A1B2C3都匹配了,因为点号匹配任意字符,A1B2中 B 后面是 2,恰好满足 A-1-B-任意字符,模式就命中了。要匹配字面点号,必须写成:

WHERE coupon_code REGEXP 'A1B\\.';

为什么是\\.而不是\.?这里涉及 MySQL 字符串转义和正则转义两层:正则引擎要匹配字面点号需要反斜杠转义,所以 SQL 文本里要写\.;但 MySQL 自身解析字符串时,反斜杠又是转义字符,为了把\.原样传给正则引擎,必须在 SQL 里多写一层,最终变成\\.。如果该 SQL 是在 Java 等编程语言中拼接,还要再叠一层语言转义,等于三层转义。

这个双层(甚至三层)转义,是“LIKE 正常、REGEXP 异常”的最大幕后黑手。

3. 导致结果不一致的六类经典场景

把案例抽象一下,实际项目里最常踩的坑基本都能归到下面六类。我把每个场景做一个 LIKE 与 REGEXP 的对照表,方便直接查。

3.1 正则元字符被当成特殊符号,LIKE 却只把 % 和 _ 当通配

这是最常见的差异来源。正则里的.*+?()[]|^${}都有特殊含义,而 LIKE 里只有%和_特殊。

例如商品标题字段里存了iPhone 15 Pro (1TB),要查包含(1TB)的记录:

  • LIKE '%(1TB)%':正常匹配字面括号。
  • REGEXP '(1TB)':(和)被当作分组符号,模式相当于匹配字符串1TB,不出意外会查出大量含1TB但不带括号的记录。

正确的正则写法是REGEXP '\\(1TB\\)'或REGEXP '[(]1TB[)]'。后者把括号放进字符类,配合 SQL 转义写起来更直观,有时比多重反斜杠更好维护。

3.2 LIKE 的 % 与 REGEXP 的 .* 语义差距

LIKE '%abc%'对应REGEXP 'abc',基本等价;LIKE 'abc%'对应REGEXP '^abc',也基本等价。但很多人会习惯性把LIKE '%abc%'翻译成REGEXP '.*abc.*',这在功能上没错,却引入了一个额外问题:. *组合对换行符\n的匹配行为在不同版本和模式下并不一致,而且.*是贪婪匹配,在没有锚定的大字段上可能造成大量回溯,性能损耗明显高于普通模式。

建议是:正则里能不用.*就不用,尽量用明确的字符集[\\s\\S]*(匹配包括换行在内的任意字符)或者干脆去掉多余的贪婪匹配,配合锚点使用。

3.3 开头与结尾锚定:REGEXP 中隐形的^和$

REGEXP 'abc'是包含匹配;LIKE 'abc'是整串匹配。这个差异造成的“结果不同”频率最高:

  • 字段值abc123,LIKE 'abc'= 0,REGEXP 'abc'= 1。
  • 字段值abc,LIKE '%abc%'= 1,REGEXP '^abc$'= 1,REGEXP 'abc'= 1。
  • 字段值xabc,LIKE '%abc'= 1,REGEXP 'abc$'= 1,REGEXP '^abc'= 0。

要避免这个坑,唯一的办法是在写 REGEXP 前先问自己一个问题:这个字段值需要完整匹配,还是只要包含?需要完整匹配就用^和$包起来;需要包含就让模式裸奔,但此时必须确保模式里没有隐式锚定。

3.4 排序规则与大小写差异

MySQL 里LIKE的等值比较行为受字段 collation 影响。utf8mb4_general_ci是大小写不敏感的,LIKE '%abc%'能匹配ABC。但REGEXP在 MySQL 8.0 之前的版本中默认也是大小写不敏感,因为底层用的是不区分大小写的字节匹配;在 8.0 之后手动指定REGEXP_LIKE(col, 'abc', 'c')才会强制大小写敏感。

由于这种版本差异,同一个大小写敏感字段,在老版本里LIKE和REGEXP可能表现一致,升级到 8.0 后 REGEXP 却突然区分大小写了。怀疑与大小写有关时,直接在 SQL 里做一次转换:

WHERE LOWER(col) LIKE '%abc%'

或

WHERE LOWER(col) REGEXP 'abc'

好处是不依赖 collation,坏处是写不了索引。业务上如果对大小写不敏感,这是最简单粗暴的办法。

3.5 中文与多字节字符的匹配差异

中文场景里,正则的.在 utf8mb4 下通常能匹配一个完整字符,但某些边界匹配符(如\b单词边界)对中文并不友好。很多同学用REGEXP '[一-龥]'匹配中文,偶尔会出现乱码或者匹配不到,原因往往是字符集配置不是 utf8mb4,或者客户端连接层 charset 没对齐。

比如字段值hello世界,REGEXP '世界'可以匹配;但REGEXP '\\b世界\\b'可能匹配不上,因为正则引擎对 CJK 字符的边界定义不同于英文单词。 这种情况下不要过度依赖正则边界,直接使用LIKE '%世界%'或者INSTR(col, '世界') > 0更可靠。

3.6 转义行为差异:一个要转义,一个不用转义

LIKE 里只有%和_需要转义,写法是\%和\_;如果读者记不住,MySQL 里LIKE反斜杠默认也不是默认转义符,只有当NO_BACKSLASH_ESCAPES这个 SQL mode 开启时,反斜杠才不做转义。不同模式配置下,结果也会不一样。

而 REGEXP 要转义的字符非常多,且要区分“正则元字符转义”和“SQL 字符串转义”两层。一个强烈建议:任何含正则元字符的 REGEXP 查询,先单独 SELECT 出来肉眼验证一遍,不要直接放进线上代码。因为多层转义写错一个反斜杠,结果就从“匹配字面点号”变成“匹配任意字符”,查出来的数据天差地别。

4. 快速排查与方案选型:什么时候用 LIKE,什么时候用 REGEXP

看现象、看案例之后,更要解决的是实际工作中怎么选。我的建议总结成十二个字:能用 LIKE 别用 REGEXP,非用正则加锚点。

4.1 标准排查看板:三步定位

遇到查询结果不一致,按下面三步走,基本不会漏:

第一步,先确认字段值的长相。把有代表性的记录用SELECT col FROM table WHERE id IN (...)查出来,贴在编辑器里逐条观察。

第二步,把 LIKE 和 REGEXP 的判定结果同时查出来对比。用下面这种模式:

SELECT col, col LIKE '%target%' AS like_res, col REGEXP 'target' AS regexp_res FROM table WHERE id IN (1,2,3);

只要like_res和regexp_res出现不一致的行,就一定能顺着那行的字段值反推模式差异。

第三步,用 EXPLAIN 看查询是否全表扫描:

EXPLAIN SELECT * FROM table WHERE col LIKE 'abc%'; EXPLAIN SELECT * FROM table WHERE col REGEXP '^abc';

除前缀 LIKE 可以用索引外,一般这两种写法都是全表扫描。如果数据量大,就要考虑改用全文索引或外部搜索引擎。

4.2 LIKE 和 REGEXP 的功能对比速查

对比维度LIKEREGEXP
匹配模式通配符%_完整正则语法
默认匹配范围整串(要加%才有包含语义)子串包含
转义复杂度低,只转义%_高,需处理双重转义
大小写敏感性受 collation 控制版本差异大,8.0 后可指定
索引利用LIKE 'abc%'可用前缀索引基本无法用普通索引
可读性高,一目了然低,模式一复杂就看不懂
适用场景简单子串、前缀、后缀匹配复杂模式、多条件、字符类

看了这张表就会发现,REGEXP的真正优势只在“复杂模式”上,简单包含匹配用它纯属自找麻烦。

4.3 一个更稳的折中方案:LIKE 配合 INSTR 函数

实际业务里,最常见的需求就两种:查包含某词、查以某词开头/结尾。这些用不着正则:

  • 包含:INSTR(col, 'abc') > 0或LOCATE('abc', col) > 0
  • 前缀:LIKE 'abc%'或LEFT(col, 3) = 'abc'
  • 后缀:LIKE '%abc'或RIGHT(col, 3) = 'abc'

INSTR和LOCATE返回子串出现的位置,语义比LIKE '%...%'更明确,且不涉及正则转义,性能和可读性都更好。如果需求是“包含 A 且包含 B”,很多人会写成:

WHERE col REGEXP 'A.*B|B.*A'

这个模式错不难,但要考虑 A、B 顺序,逻辑很容易出错。换个写法:

WHERE INSTR(col, 'A') > 0 AND INSTR(col, 'B') > 0

简洁、无歧义、性能还更好,何乐而不为。

4.4 实测性能对比

有人担心REPLACE 掉 REGEXP 会不会更慢?实际上在百万行级别的全表扫描场景下,LIKE 的包含匹配明显比 REGEXP 快,因为 LIKE 的匹配引擎实现简单,没有正则回溯机制。我自己在一张 200 万行的日志表上测过:

查询方式耗时(约)
col LIKE '%keyword%'1.2s
col REGEXP 'keyword'1.8s
INSTR(col, 'keyword') > 00.9s
FULLTEXT 全文索引0.03s

差距在高并发接口上会被放大。所以大量数据的模糊匹配,最优解既不是 LIKE 也不是 REGEXP,而是引入 FULLTEXT 索引或外部搜索中间件;在普通业务表上,优先选择 INSTR 和前缀 LIKE 是性价比最高的方案。

5. 边界情况与排查心得

章节接近尾声,再补充几个比较冷门但真实存在的边界问题,以及我个人的经验总结。

5.1 REGEXP 的三层转义陷阱

之前提到正则转义与 SQL 转义的双层关系,在编程语言拼接 SQL 时,还会有第三层转义。例如 Java 字符串里想表达 MySQL SQL 里的REGEXP 'A1B\\.',Java 源码要写"A1B\\\\. "。这里面反斜杠数量一变,线上就立刻出问题。

要彻底避免这个坑,方案是不要把正则表达式硬编码在 Java/C#/PHP 代码里,而是存到配置中心或数据库常量表中,写的时候只保留一层 MySQL 转义,代码层直接读取原样拼接。这样排查问题时,你只需要面对 SQL 里的真实文本,不需要在一堆转义中猜来猜去。

5.2 多模式里竖线|的优先级坑

REGEXP 'A1B|B2A1'表示匹配 A1B 或 B2A1,这是很多人的直觉。但如果需求是“包含前缀 A 且后面是 B 或 C”,写成:

REGEXP '^A B|C'

实际含义就变了,引擎会解释为^AB或C,导致大量 C 开头的数据混入结果。正确写法必须加括号:

REGEXP '^A(B|C)'

多模式正则里,优先级和分组是出错重灾区。每次写完都建议用SELECT直接验证一下模式命中哪些行,不要直接上 production。

5.3 关于 MySQL 版本与正则支持

MySQL 8.0 之前的正则是 Henry Spencer 那套老实现,不支持前瞻、后顾、命名捕获组等高级语法;8.0 引入 ICU 正则库后功能强了很多,但有一些语法细节(比如\b在中文场景的表现)依然和主流编程语言不同。如果你在 MySQL 里需要很复杂的正则逻辑,比如取中间某段字符,建议把数据捞到应用层用脚本处理,数据库里只做粗过滤。数据库首先考虑的是数据存取效率,不该承担复杂字符串解析的职责。

5.4 最后分享一个实用小技巧

最后分享一个我自己的习惯:每次在建表或设计查询前,遇到模糊匹配字段,我会先问自己三个问题:

  1. 这个匹配是要“整串相等”还是“包含子串”?
  2. 字段值里会不会出现正则元字符?
  3. 数据量是否大到必须考虑索引?

确认完这三个问题,选型就有了答案:整串相等用等值查询,包含子串用INSTR或LIKE,字段值存在点号、括号等特殊字符时绝不直接上REGEXP,数据量大优先考虑全文索引。

我也逐渐发现,团队里真正对 LIKE 和 REGEXP 差异了如指掌的人,写的 SQL 往往没那么花哨,反而朴实可靠。复杂正则偶尔在特殊场景里很惊艳,但日常维护时真是灾难。如果这篇文章能帮你少踩一次转义的坑、少在半夜被叫起来排查数据差异,那花这几分钟读它就值了。

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

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

立即咨询