几年前接手一个老系统,需求说起来很简单:把用户表里格式不规范的手机号捞出来。我第一反应是写 LIKE,写到第三个条件就卡住了——"13 到 19 开头、第二位不能是 0/1/2、后面必须是 9 位纯数字",这种带位置和范围约束的规则,LIKE 根本表达不出来,只能靠 SUBSTR 一位一位截取比对,SQL 长得像一堵墙。那天下午我翻出了 Oracle 里的 REGEXP_LIKE,从那以后它就在我的工具箱里没离开过。这篇东西就是想把这几年用 REGEXP_LIKE 攒下来的经验完整讲一遍:它到底解决哪类问题、模式串怎么写、第三个参数为什么总被忽略、塞进真实 SQL 之后有哪些性能和坑,以及从其他语言搬正则过来时最容易翻车的几个地方。SQL 写得不多、被模糊匹配折磨过的朋友,或者写惯了 PCRE 想直接在 Oracle 里复用的人,都可以顺着往下看。
1. 先搞清楚 REGEXP_LIKE 在 Oracle 里的位置:它是个判断器,不是提取器
1.1 三个参数,只有第三个常年被忽略
函数签名很短:
REGEXP_LIKE(source_string, pattern [, match_parameter])第一个参数是要检查的字符串,可以是列、变量、字面量,也可以是REGEXP_REPLACE之类函数的返回值。第二个参数是模式串。第三个参数是可选的匹配开关,字符串形式,比如'i'、'im'、'cin'。
它的语义是"在 source_string 中是否存在与 pattern 匹配的子串",返回真或假。注意这句话里的关键词是"存在",不是"完全等于",这两者的区别是新手翻车最多的地方,后面会专门讲。
第三个参数之所以被忽略,是因为绝大多数示例只写两个参数,读者就默认它不重要。实际上大小写、换行、锚点行为这三件事全压在它身上。尤其是处理从外部导入的多行文本时,不指定这个参数,写出来的正则行为和预期完全是两回事。
1.2 它和 LIKE、INSTR 的分工边界
我自己的判断标准很粗暴:能用 LIKE 说清楚的需求就别上正则。
LIKE只支持两个通配符:%匹配任意长度(含零长度),_匹配单个字符。带固定前缀或固定后缀的模糊查询,LIKE 是最优解,而且还有机会走索引。INSTR/SUBSTR适合定位和截取固定位置的子串,位置已知或能通过简单运算推导出来的场景。REGEXP_LIKE处理的是"有结构约束"的匹配:数字位数、字符范围、可选分支、重复次数、分组组合。像"以 1 开头、第二位在 3 到 9 之间、后面 9 位数字"这种规则,正则一行就能表达清楚。
反过来说,如果你只是想找"包含 abc 的记录",却写了REGEXP_LIKE(col, 'abc'),那纯属给自己找麻烦——性能更差,可读性还下降了。
1.3 一个容易被忽略的事实:NULL 永远不匹配
这是我在生产环境里被坑过的地方。看下面这段:
SELECT CASE WHEN REGEXP_LIKE(NULL, '.*') THEN 'MATCH' ELSE 'NO' END AS r1, CASE WHEN REGEXP_LIKE('', '.*') THEN 'MATCH' ELSE 'NO' END AS r2 FROM dual;两列的结果都是NO。原因不是正则写错了,而是REGEXP_LIKE(NULL, ...)返回的是 NULL,不是 FALSE,而CASE WHEN NULL THEN ...会走到 ELSE 分支。同理,Oracle 里空字符串''就等价于 NULL,所以REGEXP_LIKE('', '.*')也是 NULL。
这个特性在 WHERE 里表现为"NULL 行永远不会被选中",这个还算符合直觉;但在 CASE 表达式和 CHECK 约束里就会出人意料。CHECK 约束那条尤其要注意:一个 NULL 值不会被 CHECK 约束拦下来,因为约束只在结果为 FALSE 时拒绝。如果你的字段允许为空,又想让非空值合规,写约束时最好显式加IS NULL OR,把意图写在脸上,别让后来改代码的人靠推理去猜。
2. 模式串怎么写:先把 PCRE 的习惯丢在一边
2.1 字符类与预定义类:\d、\w、\s 的真实边界
Oracle 的正则实现接近 POSIX 扩展正则(ERE),同时支持一部分 Perl 风格的简写。常用的几组:
| 简写 | 等价写法 | 含义 |
|---|---|---|
\d | [[:digit:]] | 0-9 |
\D | [^[:digit:]] | 非数字 |
\w | [[:alnum:]_] | 字母、数字、下划线 |
\W | [^[:alnum:]_] | 非上述字符 |
\s | [[:space:]] | 空白字符 |
\S | [^[:space:]] | 非空白 |
POSIX 字符类在方括号里用,写法是[[:alpha:]]这种双层方括号,别写成单层。支持的类别有[:alnum:]、[:alpha:]、[:blank:]、[:cntrl:]、[:digit:]、[:graph:]、[:lower:]、[:print:]、[:punct:]、[:space:]、[:upper:]、[:xdigit:]。
这里有个实践上的提醒:\w和[[:alpha:]]我实测下来基本都只认 ASCII 范围的字母数字,中文不会被它们匹配。所以别指望用\w+去匹配中文姓名,那个结果会让你怀疑人生。
另外几个 PCRE 里常见但 Oracle 里没有的东西,直接从其他项目抄正则过来时会报"模式不合法"或者静默匹配不到:
- 环视断言(lookahead / lookbehind),也就是
(?=...)、(?<=...),Oracle 不支持。 - 非捕获分组
(?:...),同样不在支持范围内。 - 命名分组、条件分支
(?(1)...),也没有。 - 单词边界
\b,官方文档里没有这一项,我试过几次效果不稳定,建议不要依赖,用[[:space:][:punct:]]或者显式空格来近似。
2.2 量词、贪婪与非贪婪:Oracle 支持 *? 和 +?
量词这一块是兼容的:*(0 到多次)、+(1 到多次)、?(0 或 1 次)、{n}(恰好 n 次)、{n,}(至少 n 次)、{n,m}(n 到 m 次)。
默认全是贪婪匹配,也就是能吞多长吞多长。Oracle 也支持非贪婪修饰符,在量词后面加?:*?、+?、??、{n,m}?。这个特性在提取而不是判断的场景下很有用。举个例子,用REGEXP_SUBSTR从<a><b>里取第一个尖括号:
SELECT REGEXP_SUBSTR('<a><b>', '<.*>') AS greedy, -- <a><b> REGEXP_SUBSTR('<a><b>', '<.*?>') AS non_greedy -- <a> FROM dual;贪婪版本一口气吃到最后一个>,非贪婪版本遇到第一个>就收手。虽然REGEXP_LIKE只关心"有没有",但量词的贪婪性会直接影响回溯量和执行时间,写复杂模式时值得留意。
2.3 锚点 ^ $ 与"整串匹配"这个最常见的误解
REGEXP_LIKE('abc123', '\d+')的结果是真。很多人以为它是在判断"这个字符串是不是纯数字",其实只要串里存在一段连续数字就算命中。
要判断整串,必须把头尾锚死:
SELECT REGEXP_LIKE('abc123', '^\d+$') AS r1, -- FALSE REGEXP_LIKE('123', '^\d+$') AS r2 -- TRUE FROM dual;^匹配源串开头,$匹配源串结尾(默认情况下不考虑中间的换行)。这两个锚点是数据校验类需求的生命线,写校验规则时永远别忘了加。
还有一个细节:^和$只在模式的特定位置才是锚点。放在字符类里就是字面意义上的字符,[^a-z]里的^表示取反,[a-z^]里的^就是普通的脱字符。
2.4 分组、分支与反斜杠:Java 里写 \d,Oracle 里写 \d
(...)是捕获分组,|是分支,两者可以嵌套组合。写邮箱、IP 这类模式时基本离不开它们。
反斜杠这块值得单独说,因为它是跨语言迁移时最烦人的一层。Oracle 的 SQL 字符串字面量里,反斜杠没有转义含义,所以:
-- Oracle SQL 里直接写一个反斜杠 SELECT REGEXP_LIKE('123', '^\d+$') FROM dual; -- 如果你真的想匹配一个反斜杠字符本身,需要写两个 SELECT REGEXP_LIKE('a\b', '\\') FROM dual;但在 Java 里,字符串"\\d"编译期会变成\d再传给数据库,所以 Java 里写正则永远是双反斜杠起步;Python 里用原始字符串r'\d'或者写'\\d';XML 配置里(比如 MyBatis 的 mapper)还可能再叠一层 XML 转义。三层叠加的时候,出问题基本都得靠打印实际字符串来定位。
如果模式本身带单引号,用 q 引用语法能省不少事:
SELECT REGEXP_LIKE('it''s ok', q'[^']+'] ) FROM dual;这样就不用把单引号写成两个了。
3. match_parameter:i、c、n、m、x 五个开关的实战含义
3.1 i 与 c:大小写到底谁说了算
'i'表示忽略大小写,'c'表示区分大小写,而且'c'是默认值。也就是说REGEXP_LIKE('ABC', 'abc')默认返回假,必须写成REGEXP_LIKE('ABC', 'abc', 'i')。
这一条和其他数据库的默认行为不一样。比如 MySQL 的REGEXP_LIKE是否区分大小写,取决于列的排序规则(collation),很多默认排序规则是大小写不敏感的。从 MySQL 迁到 Oracle 时,原本能匹配上的查询会突然查不出数据,八成就是这个原因。
我的做法是:只要涉及字母匹配,无论要不要忽略大小写,都把参数显式写上'i'或'c'。这样代码的意图不用靠"默认值是什么"来推断,换库或者换人维护都不容易出错。
3.2 n 与 m:点和锚点在多行文本里的行为差异
这两个参数是给多行文本准备的,不处理换行的话很容易踩雷。
默认情况下,点号.不匹配换行符。如果你在解析一段包含换行的备注、日志或报文,想让.跨过换行,就得加'n'。
'm'管的是锚点。默认情况下^只匹配整个字符串的最开头,$只匹配最末尾。加上'm'之后,源串被当作多行处理,^会匹配每个换行之后的位置,$匹配每个换行之前的位置。要从一段多行文本里判断"是否存在某一行以 ERROR 开头",'m'就是必需的。
SELECT REGEXP_LIKE('ok line' || CHR(10) || 'ERROR: disk full', '^ERROR', 'm') AS has_err_line FROM dual; -- TRUE不加'm'的话,^ERROR要求 ERROR 出现在整个串的第一个字符位置,这里显然不满足。
3.3 x 的妙用:把长正则写成可读的多行模式
'x'的作用是在匹配时忽略模式串里的空白字符。别看它不起眼,写复杂校验模式时它能把一坨天书拆成结构清晰的分段:
SELECT REGEXP_LIKE('13800138000', '^ 1 [3-9] \d{9} $', 'x') AS ok FROM dual;空白被忽略,所以这段和写成一行的^1[3-9]\d{9}$是等价的,但可读性完全不同。需要注意的是,官方对这个参数的说明就是"忽略空白字符",并没有承诺支持#开头的注释语法,我试过在一些版本上注释会直接变成模式的一部分,所以别用它写注释,只用来做缩进排版。另外,空白被忽略之后,如果你确实需要匹配一个空格,得写[ ]或者在字符类里放空格。
3.4 参数组合与书写顺序
多个参数直接拼在同一个字符串里,顺序不影响语义:
REGEXP_LIKE(col, pattern, 'im') REGEXP_LIKE(col, pattern, 'mi') -- 与上面等价'c'和'i'同时出现时,后面的那个生效,所以我一般只写一个,不给自己留歧义。剩下的'n'、'm'、'x'可以自由组合。我维护的一个日志检索模块用了'im'的组合:'i'保证error和ERROR都能命中,'m'保证按行匹配锚点。
4. 把正则塞进真实 SQL:WHERE、CASE、CHECK 与函数索引
4.1 WHERE 里的写法与"全表扫"的代价
最常见的用法就是过滤:
SELECT cust_id, mobile FROM t_cust WHERE REGEXP_LIKE(mobile, '^1[3-9]\d{9}$');这段 SQL 能跑,但有一个必须接受的现实:正则谓词基本无法利用 B 树索引,执行计划上通常表现为对 mobile 列所在表做全表扫描或全索引扫描。数据量小的时候无所谓,几百万行以上就要考虑怎么减负。
我常用的手法是用一个便宜的谓词先在前面粗筛:
SELECT cust_id, mobile FROM t_cust WHERE mobile LIKE '1%' -- 便宜,有机会走索引 AND REGEXP_LIKE(mobile, '^1[3-9]\d{9}$'); -- 精确LIKE 前缀条件能把候选行数压下去,正则只在这个小集合上跑。这个组合看起来啰嗦,但在千万级表上差别很明显。
还有一个更重要的前提:合理使用不等于滥用。判断格式是否合规、做数据清洗、做临时的样本筛选,这些都是正则的舒适区;把它放进高频 OLTP 查询的核心路径里,早晚会出问题。
4.2 CHECK 约束做字段格式兜底
表设计阶段用 CHECK 约束把格式卡住,能省掉后面无数次数据清洗:
CREATE TABLE t_cust ( cust_id NUMBER PRIMARY KEY, mobile VARCHAR2(20), email VARCHAR2(120), CONSTRAINT ck_cust_mobile CHECK ( mobile IS NULL OR REGEXP_LIKE(mobile, '^1[3-9]\d{9}$') ), CONSTRAINT ck_cust_email CHECK ( email IS NULL OR REGEXP_LIKE(email, '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$') ) );这里有两点经验。第一,约束里显式写IS NULL OR,虽然 NULL 本来就不会触发拒绝,但写出来意图更清楚。第二,约束是有执行成本的,每次 INSERT / UPDATE 都要跑一遍正则。批量导入几十万行的时候,这两个约束带来的额外耗时是能明显感觉到的。如果是一次性灌历史数据,可以考虑先禁用约束、灌完、清洗、再启用,别一上来就让正则陪着你逐行算。
4.3 用 CASE WHEN 把布尔转成 0/1
SQL 层没有布尔类型可以往外查,直接写SELECT REGEXP_LIKE(...) FROM dual在 11g、12c、19c 上都是不行的。要做成标记列,得包一层:
SELECT cust_id, CASE WHEN REGEXP_LIKE(mobile, '^1[3-9]\d{9}$') THEN 1 ELSE 0 END AS is_valid FROM t_cust;这个写法在做数据体检报表的时候特别好用,一眼就能看出每张表里有多少条不规范数据。需要注意的是 NULL 的坑又出现了:mobile 为 NULL 时结果是 0,看起来像是"格式不对"。如果你的报表要区分"空值"和"格式错误",得再加一层CASE WHEN mobile IS NULL THEN -1。
顺带提一句,REGEXP_LIKE在 PL/SQL 里是真正返回 BOOLEAN 的,可以直接放进 IF 判断:
IF REGEXP_LIKE(v_input, '^\d+$') THEN v_num := TO_NUMBER(v_input); END IF;这个用法在存储过程里做入参校验非常顺手。
4.4 想在正则上建索引,只能绕道
一个经常被问到的问题:能不能给用了 REGEXP_LIKE 的查询建索引?答案是——正则谓词本身建不了,能建的是把正则的提取结果固化下来的函数索引。
-- 前提是提取逻辑固定,比如只取订单号里的纯数字部分 CREATE INDEX idx_order_no_digits ON t_order (REGEXP_SUBSTR(order_no, '\d+'));这样查询条件也必须写成完全一样的表达式,才能命中这个函数索引:
SELECT * FROM t_order WHERE REGEXP_SUBSTR(order_no, '\d+') = '20240101';约束很硬:表达式必须一字不差,模式串改了索引就废了。所以这条路的适用面很窄,只有那种"提取规则极其稳定"的字段才值得这么做。真正需要模糊检索大量文本的话,更合适的工具是文本索引那套方案,而不是在 B 树索引上硬凑。
5. 高频校验场景的正则清单,可以直接抄
5.1 手机号、邮箱、IP、日期、金额
下面这张表是我这几年攒下来的常用模式,都是实测跑过的写法。注意每一行的模式都带了首尾锚点,因为校验场景必须整串匹配:
| 场景 | 模式 | 说明 |
|---|---|---|
| 中国大陆手机号 | ^1[3-9]\d{9}$ | 1 开头,第二位 3-9,共 11 位 |
| 宽松邮箱 | ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ | 只要域名后缀多于 2 个字母即可 |
| 邮政编码 | ^\d{6}$ | 六位纯数字 |
| 日期格式 | ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$ | 只校验格式,不校验闰年 |
| 金额 | ^\d+(\.\d{1,2})?$ | 整数或最多两位小数 |
| 四位验证码 | ^\d{4}$ | 固定长度 |
| 工号 | ^[A-Z]{2}\d{6}$ | 两个大写字母加六位数字 |
日期那条要多说一句:正则只能保证"月是 01-12、日是 01-31",它拦不住 2 月 30 日这种逻辑上不存在的日期。要严格校验,还得加上TO_DATE转换,用异常捕获兜底。我一般把正则当第一道粗筛,把转换当第二道精筛,两道一起用才是稳的。
IPv4 的模式很多人会写成一大坨范围判断:
^((25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)$这段确实能正确校验 0-255,但分支多、回溯量大,数据量一大就慢。我的做法是拆开:先用一个宽松模式确认整体形状是"四段数字加三个点",再单独对每段做数值范围判断。形状校验用正则,范围校验用TO_NUMBER比较,各司其职,比硬堆一个巨型正则稳得多。
5.2 按逗号拆分前的合法性过滤
标签、分类这种用逗号拼接存储的字段,拆分的时候经常需要先过滤掉脏数据,否则会拆出一堆空串和奇怪的分段。拆分的经典写法是这样:
SELECT a.id, TRIM(REGEXP_SUBSTR(a.tags, '[^,]+', 1, lv)) AS tag FROM t_article a CROSS JOIN (SELECT LEVEL lv FROM dual CONNECT BY LEVEL <= 30) n WHERE lv <= REGEXP_COUNT(a.tags, ',') + 1 AND REGEXP_LIKE(a.tags, '^[^,]+(,[^,]+)*$');最后那行REGEXP_LIKE是过滤器,只放行"由非空分段通过逗号连接而成"的字符串。这样a,,b、,a、a,这种脏格式会被直接排除,避免拆出空标签。这个模式比事后在应用层去重来得干净,而且REGEXP_COUNT用来算分段数也非常合适。
5.3 中文与非 ASCII 字符:先测环境再下结论
这块我不给死结论,因为结果跟字符集和排序规则绑得太紧。能确定的是:\w和[[:alpha:]]通常只覆盖 ASCII。想匹配中文,常见的两种做法是在模式里直接用汉字范围,或者用UNISTR拼出范围端点:
-- 写法一:直接写汉字区间,是否生效取决于会话的字符集环境 SELECT REGEXP_LIKE('张三', '[一-龥]+') FROM dual; -- 写法二:用 UNISTR 构造端点,可读性差但意图明确 SELECT REGEXP_LIKE('张三', '[' || UNISTR('\4E00') || '-' || UNISTR('\9FA5') || ']+') FROM dual;我的习惯是在正式使用前,先在目标库上跑一次验证 SQL,把几个典型值(纯中文、中英混合、带空格)都过一遍,确认结果符合预期再写进业务代码。别在自己机器上测通了就直接上线,不同环境的表现真的会不一样。
如果只是要挑出"含有非 ASCII 字符"的记录,有个更省事的反选思路:用字符类排除掉所有 ASCII 可见字符,剩下的就是非 ASCII。这个写法比猜汉字区间更稳。
5.4 日志文本里定位关键字
排查线上问题时,用正则直接在文本字段里捞错误行,比把日志导出来再 grep 快得多:
SELECT id, log_time, SUBSTR(log_text, 1, 200) AS snippet FROM t_app_log WHERE log_time >= SYSDATE - 1 AND REGEXP_LIKE(log_text, '(ERROR|FATAL|ORA-\d{5})', 'i');配合REGEXP_INSTR拿到错误码出现的位置,再配合REGEXP_SUBSTR把上下文截出来,基本就是一个轻量的日志检索器。
SELECT REGEXP_SUBSTR(log_text, 'ORA-\d{5}', 1, 1) AS err_code, REGEXP_INSTR(log_text, 'ORA-\d{5}', 1, 1) AS err_pos FROM t_app_log WHERE REGEXP_LIKE(log_text, 'ORA-\d{5}');这四个正则函数是一套的,分工很清楚:REGEXP_LIKE判断有没有,REGEXP_INSTR告诉你位置,REGEXP_SUBSTR取出内容,REGEXP_REPLACE做清洗替换,加上 11g 之后的REGEXP_COUNT负责计数。搞清楚这个分工,写起来就不会到处乱用。
6. 性能与踩坑记录
6.1 回溯爆炸:什么样的模式能让数据库 CPU 打满
正则引擎的基本工作方式是回溯。模式里出现"可选的、可重复的分组"或者"多个分支覆盖同一个字符集"时,回溯量会呈指数增长,这就是所谓的回溯爆炸。
典型的危险模式长这样:
'^(a+)+$' -- 嵌套量词,最经典的陷阱 '^(\d+)*$' -- 星号套加号,同样危险 '(\w+\s?)*' -- 可选空白配重复,长串上会卡在 Oracle 里,这类模式跑在几万行的长文本列上,效果就是 CPU 直接拉满,查询长时间不返回。我遇到过最难受的一次是某个清洗脚本用了嵌套量词,单条 SQL 跑了十几分钟还没出结果,最后只能 kill session。
有几条实用的防御手段:
- 能用字符类合并分支就别用
|。[abc]比(a|b|c)简单得多,回溯也少得多。 - 给重复加上明确上限。
\d{1,20}比\d+安全,因为引擎不需要在无限长度上试探。 - 无条件加上锚点。
^...$能让引擎在开头失配时就快速放弃,而不是每个位置都试一遍。 - 最重要的一条:先缩小数据集。把正则跑在已经过滤过的子集上,风险就降到了可控范围。
Oracle 不支持原子分组和占有量词(就是 PCRE 里(?>...)和a*+那套),所以没法从语法层面彻底关掉回溯,只能靠上面这些手法控制影响范围。
6.2 转义层级:SQL 字符串、宿主语言、正则本身三层叠加
这是我踩得最多的坑。一条正则从应用代码传到数据库,中间可能经过三层处理:
| 层级 | 举例 | 反斜杠要写几个 |
|---|---|---|
| Oracle SQL 字面量 | REGEXP_LIKE(col, '^\d+$') | 1 个 |
| Java 字符串 | "^\\d+$" | 2 个 |
| Java 里的 XML(MyBatis mapper) | 视转义方式而定 | 可能 2 个或更多 |
| Python 原始字符串 | r'^\d+$' | 1 个 |
规律是:每经过一层会解释反斜杠的语言(Java、C#、部分配置解析器),就要多一倍。定位这类问题的唯一可靠办法是在代码里把字符串打印出来,看看到底传了什么,别靠脑补数反斜杠。用 JDBC 的 PreparedStatement 直接绑字符串参数时不存在额外转义,写一个反斜杠就是一个。
6.3 空串、NULL 与 NLS 带来的意外结果
除了前面说过的 NULL 和空串问题,还有一个不太容易注意到的因素:会话的语言和排序设置会影响字符类的判定。同一个模式,在不同 NLS_SORT 或 NLS_COMP 设置的会话里,对带重音符号的字母、全角半角字符的匹配结果可能出现差异。
我的建议是:涉及非纯 ASCII 的匹配逻辑,别写死在代码里,把验证结果记下来,或者干脆在应用层做字符规范化之后再入库。数据库层面只做兜底的粗校验,这样跨环境迁移时风险小得多。
另外提醒一个跟数据展示有关的小事:纯数字的长串(比如长订单号、长卡号)从数据库导出到表格工具后,经常会被表格软件自作主张地按科学计数法显示,看着像数据被改了,其实数据没变,是单元格格式的问题。处理方式是导出时强制转成文本类型,或者在 SQL 里拼一个前导字符,比如'' || col`,让表格软件知道这列该当文本处理。
6.4 几个我常用的降级替代方案
最后分享几个"不一定用正则"的替代思路,用得对能省不少事:
- 校验纯数字并且长度固定,用
LENGTH加TRANSLATE往往更快:LENGTH(TRANSLATE(col, '0123456789', ' ')) = 0判断是否全数字。 - 12.2 之后有
VALIDATE_CONVERSION,做数值和日期的合法性判断比正则加异常捕获干净得多。 - 判断固定前缀用
LIKE,判断固定长度用LENGTH,判断枚举值范围用IN。 - 大文本检索用文本索引方案,别用正则硬扫。
我自己的判断顺位是:能用LENGTH/TRANSLATE/LIKE/IN表达的,优先用它们;只有出现"位置约束 + 字符范围 + 重复次数"这种组合时,才请出REGEXP_LIKE。
7. 版本差异与跨库迁移
7.1 各版本的支持范围
这一套正则函数的引入有个时间线,查资料或者接手老项目时容易对不上:
| 函数 | 引入版本 | 备注 |
|---|---|---|
REGEXP_LIKE | 10g | 最早的一批正则函数 |
REGEXP_INSTR | 10g | 返回匹配位置 |
REGEXP_SUBSTR | 10g | 提取匹配子串 |
REGEXP_REPLACE | 10g | 替换,支持反向引用 |
REGEXP_COUNT | 11g | 统计匹配次数 |
也就是说,如果在 9i 的库上写REGEXP_LIKE,会直接报函数不存在的错误。虽然现在生产环境上跑的大多是 11g、12c 或 19c,但维护老系统的时候还是会碰到。
另外,11g 之后这些函数的行为基本稳定,没出现过大的语义变更。23ai 开始在 SQL 层支持布尔类型,REGEXP_LIKE的返回值可以更自由地出现在 SELECT 列表里,不用再包CASE WHEN。不过公司环境里跑的多半还是老版本,按条件位置使用的写法最保险,跨版本都能用。
7.2 Oracle、MySQL、SQL Server 正则写法对照
跨库迁移的时候,这部分差异最容易踩:
| 对比项 | Oracle | MySQL 8 | SQL Server |
|---|---|---|---|
| 函数名 | REGEXP_LIKE(str, pat, param) | REGEXP_LIKE(expr, pat, match_type) | 无内置正则 |
| 默认大小写 | 敏感 | 取决于列的排序规则 | 不适用 |
| 忽略大小写 | 参数写'i' | 参数写'i' | 不适用 |
| 多行锚点 | 参数写'm' | 参数写'm' | 不适用 |
| 简化模式 | 无 | 无 | PATINDEX/LIKE |
| 字符串里反斜杠 | 1 个 | 通常要 2 个 | 不适用 |
REGEXP_LIKE这个函数名在 Oracle 和 MySQL 8 里刚好完全相同,很容易让人以为可以无脑搬。实际上两个库的默认大小写行为、转义规则、支持的元字符集合都有差异,跨库写的时候一定要在目标库上重新验证一遍,尤其是涉及反斜杠和大小写的模式。
我之前做过一次从 MySQL 迁到 Oracle 的改造,最典型的问题就出在大小写上:原库里REGEXP_LIKE(code, '^ab')因为排序规则的关系能匹配到AB开头的记录,迁过来之后一条都匹配不到。最后是在模式里显式补了'i',同时在业务侧把数据统一成大写入库,双保险才彻底解决。
SQL Server 那边没有原生正则,只有PATINDEX这种简化模式(支持%、_、[],功能和 LIKE 接近)。真要迁移复杂正则,只能在应用层处理,或者写 CLR 扩展函数,工作量比改 SQL 大得多,评估改造量的时候要把这块单独拎出来算。