做数据库开发和运维这些年,我越来越觉得正则表达式是个典型的“平时用不上、一用就真香”的技能。SQL里的REGEXP正则表达式,就是把这套文本模式匹配能力搬进数据库查询中,让你不用把几百万行数据导到应用层,在SQL层面就能完成字符串筛选、格式校验、内容提取和批量清洗。不少朋友在热搜里搜“正则表达式语法”“REGEXP怎么用”“为什么用REGEXP查询特别慢”,说明大家对这个功能有兴趣,但又被各种版本差异和坑折磨过。这篇就把REGEXP的语法、函数、跨数据库差异、性能问题和排查经验一次讲透,按我实际踩过的坑来讲,不绕弯子。
先说说这篇适合谁。一是业务开发,经常要对用户输入、手机号、邮箱、地址做模糊匹配;二是数据分析师和数据治理的同学,面对一堆脏数据要清洗归类;三是DBA和运维,排查慢SQL时发现大表加上REGEXP后CPU直接飙红。我给的方法都能直接用,但更重要的是把每条规则背后的逻辑讲明白,你才知道什么时候该用,什么时候千万别用。
1. 从LIKE到REGEXP:模糊查询能走多远
1.1 LIKE的边界在哪
先说一个最常见的场景:用户表里有几十万条手机号,有的带空格,有的带横线,有的是纯数字。领导要求把格式不规范的记录挑出来。用LIKE怎么写?LIKE '%138%'会把规范和不规范的全捞出来,因为只要包含“138”这三个连续字符就命中。想表达“必须是11位数字”这种结构规则,LIKE直接没辙。
LIKE本质上只有三种语义:以什么开头(%放后面)、以什么结尾(%放前面)、包含什么(两边都有%)。在SQL Server的LIKE里还能用[0-9]这种字符集做单字符匹配,但MySQL的LIKE连这个都不支持。一旦需求变成“第4位到第7位必须是某个范围”“必须由纯数字组成”“允许两种格式同时存在”,LIKE就会逼着你写出一长串OR条件,又丑又容易漏。
1.2 REGEXP解决的是“结构匹配”问题
REGEXP进入SQL生态后,情况完全不同。它允许你用一套描述性的字符串规则,去匹配另一个字符串是否符合这个规则。打个比方:LIKE像你告诉快递员“收货人名字里带‘张’”,而正则像是给快递员一张“地址格式卡”——必须包含省市区、街道名不能超过多少位、门牌号必须是数字。快递员按规则校验,而不是碰运气找关键字。
在MySQL里,最简单的用法是这样:
sql复制SELECT * FROM user WHERE phone REGEXP '^1[3-9][0-9]{9}$';
这句话的规则是:以数字1开头,第二位是3到9之间的任意数字,后面跟9位0到9的数字,总共11位。这个规则用LIKE是写不出来的。你可以直接用这行SQL扫描整张表,把不合规的手机号一条不漏地揪出来。这就是REGEXP的第一个价值:把“模式判断”下沉到数据库,减少数据传输,也减少应用层写循环匹配的代码量。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基础语法:SQL里写正则的正确姿势
2.1 常用元字符速查
正则的元字符是通用的,但在SQL里写的时候要确认当前数据库支持到什么程度。我整理了一份常用元字符表,覆盖绝大多数业务场景:
| 元字符 | 含义 | 示例 |
|---|---|---|
. |
匹配任意单个字符(换行除外) | a.c 匹配 abc |
* |
前一个字符出现0次或多次 | ab*c 匹配 ac、abc、abbc |
+ |
前一个字符出现1次或多次 | ab+c 匹配 abc,不匹配 ac |
? |
前一个字符出现0次或1次 | ab?c 匹配 ac、abc |
^ |
匹配字符串开头 | ^1 表示以1开头 |
$ |
匹配字符串结尾 | 9$ 表示以9结尾 |
[] |
字符集合 | [0-9] 表示任意数字,[a-zA-Z] 表示任意字母 |
[^] |
取反集合 | [^0-9] 表示非数字 |
() |
分组 | (ab)+ 匹配 ab、abab |
| |
或 | a|b 匹配 a 或 b |
{n} |
重复n次 | [0-9]{3} 匹配3位数字 |
{n,m} |
重复n到m次 | [0-9]{2,4} 匹配2到4位数字 |
\d |
数字,等价于[0-9](部分数据库支持) |
MySQL 8.0+支持 |
\w |
字母、数字、下划线 | 部分数据库支持 |
\s |
空白字符 | 部分数据库支持 |
需要特别留意POSIX字符类,在旧版MySQL中非常常用:[[:digit:]]表示数字,[[:alpha:]]表示字母,[[:alnum:]]表示字母和数字,[[:space:]]表示空白。如果你维护的MySQL版本还没升到8.0,用它比\d更稳。
2.2 谓词与函数:REGEXP、REGEXP_LIKE及其配套
MySQL的用法比较典型。5.x时代主要用expr REGEXP 'pattern'这种谓词写法,返回0或1。8.0之后提供了更完整的函数族:
REGEXP_LIKE(expr, pat[, match_type]):返回是否匹配,效果和expr REGEXP pat一致。REGEXP_SUBSTR(expr, pat[, pos[, occurrence]]):从字符串中提取符合模式的子串。REGEXP_REPLACE(expr, pat, repl[, pos[, occurrence]]):替换符合模式的内容。REGEXP_INSTR(expr, pat[, pos[, occurrence]]):返回符合模式的子串位置。
match_type参数也很实用:'c'表示区分大小写,'i'表示不区分大小写,'m'表示多行匹配。如果你想在MySQL里区分大小写匹配,可以这样写:
sql复制SELECT REGEXP_LIKE('Hello', '^h', 'c'); -- 返回0,因为小写h没匹配上大写H
SELECT REGEXP_LIKE('Hello', '^h', 'i'); -- 返回1,不区分大小写
2.3 双重转义:SQL字符串与正则的叠加陷阱
这是SQL里写正则最隐蔽、也最容易翻车的地方。很多人在Java、Python里写正则写惯了,\d代表数字,于是直接在SQL里写:
sql复制SELECT REGEXP_LIKE('abc123', '\\d+'); -- 期望匹配数字,实际可能报错或返回空
为什么?因为SQL字符串里反斜杠本身就有转义语义。在MySQL默认配置下,'\d'会被解析成d这个普通字符,正则引擎看到的模式就成了d+,自然匹配不到数字。正确写法是:
sql复制SELECT REGEXP_LIKE('abc123', '\\\\d+');
这样SQL字符串转义后正则引擎拿到的是\\d+,它再解析一遍才是数字。如果你在PostgreSQL里写,情况又不一样:PG默认standard_conforming_strings开启,反斜杠不再是字符串转义符,正则模式可以直接写'\d+',不用双反斜杠。所以“同一个SQL在不同数据库里结果不同”真不是玄学,而是转义规则差异造成的。
我自己的习惯是:先在本地正则工具里验证逻辑,再用小数据集在测试库跑一遍,确认无误后再上生产。后文第6章还会继续展开这个坑。
3. 实战场景:格式校验、数据清洗与内容分类
3.1 格式校验:手机号、邮箱、身份证怎么匹配
格式校验是REGEXP最刚需的场景。以手机号为例,只匹配中国大陆11位手机号,模式是^1[3-9][0-9]{9}$。注意第二位用了[3-9]而非[0-9],这一步就把早年那种以12、10开头的无效号码排除掉了,这是业务规则沉淀出来的细节。
邮箱校验的简化版模式:
sql复制SELECT email,
REGEXP_LIKE(email, '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$') AS is_valid
FROM user;
这个正则只能保证“格式上长得像邮箱”,不能保证邮箱真实存在。热点里有人搜“正则表达式字母和数字的组合怎么写”,这在账号名、密码规则里很典型。比如要求账号必须同时包含字母和数字,且总长度6到12位。MySQL 8.0支持前瞻断言,可以写:
sql复制SELECT REGEXP_LIKE('abc123', '^(?=.*[A-Za-z])(?=.*[0-9])[A-Za-z0-9]{6,12}$');
如果库是5.7或更早,不支持前瞻断言,那就拆成多个条件叠加:
sql复制WHERE col REGEXP '[A-Za-z]'
AND col REGEXP '[0-9]'
AND col REGEXP '^[A-Za-z0-9]{6,12}$';
效果一样,只是多写两行。这个思路在跨版本兼容时很好用,值得记下来。
身份证号校验要慎重,正则只能校验基本结构和出生日期段格式,不能保证号码真实存在。模式大约长这样:
sql复制SELECT REGEXP_LIKE('110101199001011234', '^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[0-9Xx]$');
提醒一句:涉及身份证这类个人敏感信息,在开发和测试环境中尽量用脱敏后的数据,不要在日志中明文打印匹配结果。
3.2 内容提取:REGEXP_SUBSTR从日志里摘字段
假设有一张日志表,log_line字段存的内容形如:
text复制2025-01-15 10:23:45 [ERROR] 192.168.1.100 timeout after 5000ms
要批量提取IP地址,用REGEXP_SUBSTR一行搞定:
sql复制SELECT REGEXP_SUBSTR(log_line, '[0-9]{1,3}(\\.[0-9]{1,3}){3}') AS ip
FROM log_table;
注意两点:一是点号在正则里是“任意字符”,必须写成\\.转义,否则192x168x1x100也会被匹配到;二是IP地址的四段数字范围是0到255,上面这个模式会匹配999.999.999.999这类非法值。如果需要严格校验,就要用前文提到的IPv4专用长正则:
sql复制'^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])$'
这个模式把每段数字分成四类:250到255、200到249、100到199、0到99,合起来正好覆盖完整范围。热搜里有人问“用正则判断IP地址并计算单网段最大主机数”,前半段就是用这个正则做判断,后半段是子网掩码和主机位计算,两者是独立的问题,可以分开处理。
3.3 数据清洗与分类:REGEXP_REPLACE + CASE WHEN 组合拳
清洗脏数据时,REGEXP_REPLACE非常顺手。比如把用户上传的电话号码统一成纯数字格式,去掉空格、横线、括号:
sql复制UPDATE user
SET phone = REGEXP_REPLACE(phone, '[^0-9]', '')
WHERE phone REGEXP '[^0-9]';
这段逻辑是:先把非数字字符全部替换为空字符串,再更新到phone字段。WHERE条件用REGEXP '[^0-9]'过滤出真正含有非数字的记录,避免所有行都触发UPDATE造成不必要的写放大。这里有个执行力很强的建议:UPDATE之前一定先跑一遍SELECT,把受影响的行数和样例数据看清楚。我见过不止一次,正则写错导致整列数据被清空,比如[^0-9]写漏了^变成[0-9],那就把所有数字删光,剩下一堆符号,灾难现场。
文本分类也常用到。比如根据地址字段给用户打城市等级标签:
sql复制SELECT
CASE
WHEN address REGEXP '北京|上海|广州|深圳' THEN '一线城市'
WHEN address REGEXP '杭州|成都|武汉|南京' THEN '新一线城市'
ELSE '其他'
END AS city_level,
COUNT(*) AS cnt
FROM user
GROUP BY city_level;
这里|是正则的“或”操作符,一条SQL就把几万条地址归类统计完了。注意地址字段如果为NULL,正则函数通常返回NULL而不是false,COUNT时不会被统计进去,需要的话要提前用COALESCE处理。
4. 数据库差异:同一个正则,换个库可能就报错
4.1 MySQL与MariaDB:最常见的起点
MySQL应该是大家接触REGEXP最频繁的库。5.x时代以expr REGEXP pattern为主,8.0之后补全了REGEXP_LIKE、REGEXP_SUBSTR、REGEXP_REPLACE、REGEXP_INSTR,语法向Oracle靠拢。MariaDB的表现和MySQL基本一致,但两者的正则引擎内核不同,复杂模式在极端情况下行为会有细微差别。MySQL默认对大小写不敏感,这是由collation决定的;如果业务上要区分大小写,用REGEXP_LIKE(col, pattern, 'c'),或者对列做BINARY转换后再匹配。
MySQL另一个值得注意的点是字符集。在utf8mb4下匹配中文时,\d这类简写仍可用,但中文范围匹配要小心。很多人习惯写[\u4e00-\u9fa5],这是Java等语言的写法,MySQL不认识。要判断字段是否包含中文,最稳妥的办法是借助多字节字符的特性:
sql复制SELECT * FROM user WHERE LENGTH(name) <> CHAR_LENGTH(name);
LENGTH返回字节数,CHAR_LENGTH返回字符数,两者不等说明里面有多字节字符。这个方法不区分具体语言,只要是中文、日文、韩文等多字节字符都会被识别出来,而且速度比正则快得多。
4.2 PostgreSQL:~ 与 regexp_matches
PostgreSQL的正则表达式支持更贴近POSIX标准,写法也更丰富。匹配运算符是~(匹配)和~*(不区分大小写匹配),不匹配用!~和!~*。例如:
sql复制SELECT * FROM user WHERE phone ~ '^1[3-9][0-9]{9}$';
PG还提供了regexp_matches、regexp_replace、regexp_split_to_table等函数。其中regexp_split_to_table很有用,可以把一个字段按正则拆成多行,在ETL场景省掉不少Python脚本。注意PG的regexp_replace默认替换所有匹配,而MySQL 8.0的REGEXP_REPLACE默认只替换第一个匹配位置,这个差异会让同一段SQL在两个库上结果不同。
4.3 SQL Server:没有REGEXP怎么办
SQL Server原生T-SQL里没有REGEXP,这是很多从MySQL转过来的人最不适应的地方。T-SQL的LIKE倒是支持[0-9]、[^0-9]这类字符集,能解决一部分简单模式,但复杂逻辑就要靠其他手段:
- 用若干LIKE条件组合,能覆盖大部分业务规则,缺点是SQL会变得很长。
- 用CLR写自定义正则函数,功能强但部署复杂,运维负担大。
- 用应用层处理后再把结果集带回数据库,适合数据量可控的场景。
所以如果你在SQL Server上搜REGEXP搜不到东西,不是搜错了,是这个库原生就没这个功能。遇到类似需求,先评估数据量,再决定用LIKE组合还是走应用层,别硬用动态SQL拼正则,性能和安全风险都不小。
4.4 Oracle与SQLite:另外两种极端
Oracle的正则函数族非常成熟,REGEXP_LIKE、REGEXP_SUBSTR、REGEXP_REPLACE、REGEXP_INSTR、REGEXP_COUNT一应俱全,而且支持按参数控制大小写敏感性、多行模式等。写法和MySQL 8.0大体接近,但细节参数名略有不同。
SQLite则是另一个极端:默认根本不支持正则表达式。SQLite底层的LIKE只支持%和_,没有正则引擎。虽然可以加载扩展或通过自定义函数实现,但大多数场景下,SQLite更适合把数据取到应用层做正则处理,别在SQL层面折腾。
4.5 跨库正则能力速查表
| 数据库 | 匹配操作符/函数 | 默认大小写敏感 | 备注 |
|---|---|---|---|
| MySQL | REGEXP、REGEXP_LIKE等 |
否 | 8.0+支持更全的函数族 |
| MariaDB | REGEXP、REGEXP_REPLACE等 |
否 | 引擎与MySQL有差异 |
| PostgreSQL | ~、~*、regexp_matches等 |
是 | 功能最接近完整正则 |
| SQL Server | 无原生REGEXP | - | 用LIKE通配符或CLR |
| Oracle | REGEXP_LIKE、REGEXP_SUBSTR等 |
是 | 函数族强大 |
| SQLite | 默认不支持 | - | 需要扩展开启 |
这张表给我的最大启示是:写SQL正则前,先查当前数据库版本支持什么、默认行为是什么。把MySQL的写法直接搬进SQL Server,基本是白忙活。
5. 性能问题:REGEXP为什么会拖慢查询
5.1 为什么慢:索引失效与计算开销
REGEXP查询慢的根源在于:数据库基本没办法用索引来完成正则匹配。B+树索引依赖有序值,可以快速定位col = 'abc'或col LIKE 'abc%'这类前缀匹配,但正则可以表达任意模式,优化器不知道你的规则是前缀型还是中间型,只能老老实实全表扫描,逐行把正则引擎跑一遍。
正则匹配本身也是CPU密集型操作。像(a|b)*c这种带分支和回溯的模式,最坏情况下计算量会随着字符串长度呈指数级增长。在几百万行的大表上跑一个中等复杂度的正则,CPU使用率直接拉满,慢SQL日志瞬间被刷屏,这是我在生产环境真实遇到的问题。
5.2 优化手段:缩小范围、生成列与预计算
优化思路无非三条:减少扫描行数、减少正则计算次数、把正则结果提前算好。
第一招,先用普通条件缩小范围。比如要匹配以138开头的11位手机号,可以加一个等值或LIKE前缀条件:
sql复制SELECT * FROM user
WHERE phone LIKE '138%'
AND phone REGEXP '^138[0-9]{8}$';
优化器先用索引过滤掉绝大多数行,只对剩下的一小部分做正则匹配,开销小一个数量级。前提是手机号列建了索引,LIKE '138%'能走前缀索引。
第二招,用生成列把正则结果物化。MySQL 5.7开始支持generated column,可以把“是否符合规则”提前算成虚拟列,再在虚拟列上建索引:
sql复制ALTER TABLE user
ADD COLUMN phone_valid TINYINT GENERATED ALWAYS AS (phone REGEXP '^1[3-9][0-9]{9}$') VIRTUAL,
ADD INDEX idx_phone_valid (phone_valid);
之后查询直接过滤WHERE phone_valid = 0,走索引扫描而不是全表正则。这个方案适合“规则固定、存量数据大、高频查询”的场景。但要注意:一旦正则规则发生变化,生成列要重建,不能临时改。
第三招,如果数据量实在太大、正则又多又复杂,建议把清洗和匹配放到应用层或ETL流程中。数据库不是万能的,把几亿行数据里的脏文本清洗完再入库,比每次查询都在线跑正则要划算得多。热搜里大量关于慢SQL优化的问题,很多时候不是SQL写法的问题,而是架构上把不该数据库干的活压给了数据库。
5.3 排查流程:EXPLAIN先行
遇到REGEXP慢查询,先别急着优化SQL。第一步是EXPLAIN看执行计划,确认扫描行数;第二步看是不是有更前置的普通条件可以利用索引;第三步评估是否真的需要在线正则,还是离线清洗更合适。这个排查顺序我基本固定下来了,能避免很多无效优化。
6. 高频坑与排查实录
6.1 转义与特殊字符:写对了才有结果
转义问题我前面提过,这里是完整版。SQL里写正则,要经过两层解析:第一层是SQL字符串解析,第二层是正则引擎解析。两层都对,结果才对。最容易出错的是反斜杠和几个特殊字符。
举几个实际案例:
- 匹配点号
.,正则模式应该写\\.,SQL字符串里写成'\\.'。 - 匹配数字
\d,正则模式是\d,MySQL里SQL字符串写成'\\d'。 - 匹配字面量反斜杠
\,正则模式是\\,SQL字符串里要写'\\\\'。
查不出来问题,先怀疑转义。我的排查习惯是:把SQL字符串用SELECT打出来看一遍,确认正则引擎实际收到的模式是什么。
6.2 大小写、NULL与字符集
MySQL默认不区分大小写,这导致一个常见误解:REGEXP '^abc'能匹配ABC。如果业务上必须区分,就要用REGEXP_LIKE(col, '^abc', 'c')或BINARY col REGEXP '^abc'。
NULL值则是另一个隐形坑。正则函数对NULL入参通常返回NULL,所以WHERE col NOT REGEXP 'pattern'不会把NULL行查出来。如果你想找出“不符合规则或者干脆没填”的记录,必须显式加上OR col IS NULL:
sql复制SELECT * FROM user
WHERE phone NOT REGEXP '^1[3-9][0-9]{9}$'
OR phone IS NULL;
字符集方面,utf8mb4下中文匹配要谨慎。前面提到的LENGTH(name) <> CHAR_LENGTH(name)方法判断是否含多字节字符,是最简单可靠的技巧,推荐优先用。
6.3 贪婪匹配与非贪婪的差异
正则默认是贪婪的,会尽可能多地匹配字符。比如从文本中提取两个引号之间的内容:
sql复制SELECT REGEXP_SUBSTR('"abc" and "def"', '".*"');
你直觉上想要"abc",实际可能返回"abc" and "def",因为.*会贪婪地一直吞到最后一个引号。解决办法是用字符集合排除引号,写"[^"]*",这样就不会跳过中间的引号。优先用[^"]*排除法,而不是依赖.*?这类非贪婪写法——因为不少数据库的正则引擎对非贪婪量词支持并不稳定,MySQL老版本甚至直接不支持,排查起来很麻烦。
6.4 正则表达式在SQL里的“脾气”和Python不一样
很多人在Python、JavaScript里学正则,习惯了一些高级写法:前瞻断言(?=...)、反向引用\1、非贪婪.*?、命名分组(?P<name>...)。这些在SQL里不是全都能用。MySQL 8.0的ICU正则引擎支持前瞻断言,但5.7不支持;PostgreSQL支持反向引用,但MySQL的REGEXP不支持反向引用(至少官方文档里长期没有明确支持)。跨语言、跨数据库写正则,最稳妥的办法是:只用基础元字符,复杂逻辑拆成多个简单正则组合。
7. 正则的安全边界:不要把用户输入直接拼进模式
7.1 拼接式正则的注入风险
聊完性能,必须聊聊安全。关于正则和SQL注入,我见过一种误解:觉得数据库用了正则校验,输入就安全了。实际上正则只是格式校验,完全不能替代参数化查询。
最危险的写法是把用户输入直接拼进WHERE条件,外面套一层REGEXP:
sql复制SELECT * FROM user
WHERE username REGEXP CONCAT('^', user_input, '$');
如果user_input里混进了SQL片段,整个查询语义可能被改写。正则表达式本身不是注入漏洞的根源,但这种直接拼接的方式为注入大开方便之门,即使不是正则场景,只要拼接字符串进SQL,风险都一样。
7.2 三条防护原则
我自己定的三条原则,分享出来:
第一,所有用户输入一律走参数化查询,哪怕只是作为正则模式的一部分。不要图省事把用户输入拼进SQL字符串。
第二,正则只用来做“格式校验”和“分类筛选”,不要用来做“安全过滤”。防SQL注入靠的是参数化,不是正则。
第三,不要向业务用户开放“自定义正则”的入口。复杂正则模式可能引发灾难性的CPU消耗,类似拒绝服务攻击的原理。如果产品确实需要开放,至少要做模式长度限制、复杂度限制和频控。
最后说点实在的
写了这么多,回头看看这几年在项目里用SQL正则的经历,最大的感触是:正则表达式确实能解决LIKE解决不了的问题,但前提是你必须清楚自己正在用的数据库支持到什么程度。我最初把MySQL的REGEXP语法直接搬到SQL Server,结果白忙活半天,后来养成一个习惯——先在官方文档确认当前版本函数清单,再用小数据集验证,最后看执行计划再上线。这个“三步走”的流程帮我避开了很多线上事故。尤其是涉及UPDATE和DELETE之前,一定先用SELECT把影响范围查清楚,正则写错一列数据的代价,可比多写两行SQL高多了。
