REGEXP 在 SQLite 里不是内建函数,是一个「钩子」,得先注册实现。
正则的锚点比 LIKE 的通配符严格:少写一个 $,结果就悄悄多算几十行。
正则用不上索引,还要逐行回调,比前缀 LIKE 慢两个数量级。
9.8 讲 MySQL 的 REGEXP。沙箱里没有这个函数,这一篇先把它造出来再讲用法。
这一篇是第 9 章的收尾,后面 9.9 小结和 9.10 实践与练习并进本篇末尾。
一、REGEXP 在沙箱里是个空缺
照抄教材的 列 REGEXP '模式',沙箱直接报 no such function: REGEXP。
原因不是「缺个插件」,而是 SQLite 把 REGEXP 设计成了一个可注册的钩子。
库只认这个名字和它的调用约定:REGEXP 运算符等价于调用 regexp(模式, 值)。
具体怎么比,由使用者挂一个函数上去。没挂,就报找不到函数。
所以注册只要三行:定义一个两参数函数,用 create_function 挂到 regexp 这个名字上。
挂上之后,教材里的句子几乎能原样敲。
def regexp(pat, s):
return 1 if s is not None and re.search(pat, s) else 0
c.create_function(u"regexp", 2, regexp)
注意参数顺序是「模式在前、值在后」,和人的直觉相反。 写反了不会报错,只会一个都匹配不上——这种错最难查,因为语句看起来完全正常。
二、六个常用模式
正则的语法一天学不完,但 SQL 里常用的就那几个。
| 模式 | 命中行数 | 意思 |
|---|---|---|
^G0000[1-9]$ |
9 | G0000 后面一位 1 到 9 |
^G0001 |
10 | 以 G0001 开头 |
1$ |
500 | 以 1 结尾 |
^G[0-9]{5}$ |
5000 | G 加五位数字 |
^(数码|家电)商品1[0-9]$ |
2 | 两个前缀任选其一 |
商品(99|100)$ |
2 | 编号是 99 或 100 |
^ 是开头、$ 是结尾、[0-9] 是一位数字、{5} 是「前面那个重复五次」。
| 是「或者」,括号用来把「或者」的范围圈住。
第一行和第二行只差一个 $,命中数从 9 变成 10。
因为第二行没写结尾锚点,G00010 到 G00019 也都被算进去了。
所以写正则要养一个习惯:能加 ^ 和 $ 的地方都加上。
少一个锚点不会报错,只会悄悄多算几十行,这种错在统计数字里最难看。
三、星号和加号:匹配多个字符
教材这一节的小标题写的是「全角星号和加号」,其实应该是半角 * 和 +。
正则里的「全角」只是字符,没有通配含义,按半角记就对了。
| 符号 | 含义 | 例子 |
|---|---|---|
* |
前面那个重复 0 次或多次 | ab*c 匹配 ac、abc、abbc |
+ |
前面那个重复 1 次或多次 | ab+c 匹配 abc、abbc,不匹配 ac |
? |
前面那个出现 0 次或 1 次 | ab?c 匹配 ac、abc |
{n} |
前面那个恰好 n 次 | a{3} 匹配 aaa |
{n,m} |
前面那个 n 到 m 次 | a{2,4} 匹配 aa、aaa、aaaa |
* 和 + 的差别就在「零次行不行」,这是最容易写错的一处。
* 允许前面那个字符一次都不出现,所以它几乎总能匹配到「空」,用起来要小心。
LIKE 里没有这一层:它的 % 就是「零到多个」,_ 就是「恰好一个」。
所以能说清的需求,用 LIKE 更稳;说不清的(比如「数字至少三位」),才请正则上场。
四、正则能表达、LIKE 表达不了的
两件事 LIKE 根本做不到:一是「某一位必须是数字」,二是「多个前缀任选」。
LIKE 的 _ 只代表「任意一个字符」,分不出数字和字母。
| 要查什么 | LIKE 的写法 | REGEXP 的写法 |
|---|---|---|
| G00001~G00009 | 9 行 | 9 行 |
| 两个前缀、两位数的编号 | 2 行 | 2 行 |
两格的结果都对得上,说明「能不能查出来」这一层,两者打平。
差别在写起来的长度:第二格 LIKE 要两个 OR,正则一对括号就包住了。
每多一个前缀,LIKE 就多一条 OR,正则只多一个竖线。
前缀多到五六个的时候,正则就好读多了。
还有几件事 LIKE 完全做不到:
「最后一位是 1 或 3」,正则写 [13]$;「倒数第三位必须是数字」,正则写 [0-9]..$。
五、代价:正则用不上索引
同一个前缀条件,LIKE 是 54 微秒,REGEXP 是 9.92 毫秒,差了 183 倍。
两个原因:一是它必须每行回调一次函数,二是它用不上索引,只能全表扫。
前缀匹配是「范围查找」,正则是「逐行判定」。
这不是优化技巧的差别,是算法级的差别——一个只要定位一段,一个必须看完全表。
所以能写成 LIKE '前缀%' 的,就别用正则。
正则留给「LIKE 说不清」的那部分需求,比如「第几位必须是数字」这种。
MySQL 那边也一样的道理:REGEXP 不走索引,用之前先想清楚表有多大。
十万行的表能忍,千万行的表就该换个思路,比如把特征先抽成一列存起来。
六、教材上的写法与两处差异
教材的句子是 SELECT * FROM tb_goods WHERE code REGEXP '^G0000[1-9]$';,
在 MySQL 上直接能跑。
沙箱里得先挂函数才能跑,挂完语法一模一样。
这就是本课一直的做法:能等价执行的照原样敲,不能的就补一个替身。
差异在细节:MySQL 8.0 的正则走 ICU 库,支持 \d 这种写法;
SQLite 这边走 Python 的 re,也支持 \d,但两边对多字节字符的处理不完全一致。
所以正则写在 MySQL 上跑通,搬到别处还得再验一遍。
这一条没有捷径,只能现测。
七、动手 Demo:正则查询,与它在沙箱里的替身
这一段先照抄教材撞一次墙,再挂上替身函数,然后才讲用法和代价。 顺序故意这么排:先看见「跑不了」,才知道为什么要多那三行。
import re, sqlite3
c = sqlite3.connect(u"d043.db")
# ① 照抄会报错
try:
c.execute(u"SELECT COUNT(*) FROM tb_goods WHERE code REGEXP '^G0001'")
except sqlite3.OperationalError as e:
print(e) # no such function: REGEXP
# ② 挂一个替身:模式在前、值在后
def regexp(pat, s):
return 1 if s is not None and re.search(pat, s) else 0
c.create_function(u"regexp", 2, regexp)
print(c.execute(u"SELECT code FROM tb_goods "
u"WHERE code REGEXP '^G0000[1-9]$'").fetchall())
# ③ 代价:正则 vs 前缀 LIKE
c.execute(u"PRAGMA case_sensitive_like = ON")
c.execute(u"SELECT COUNT(*) FROM tb_goods WHERE code LIKE 'G0001%'").fetchall()
c.execute(u"SELECT COUNT(*) FROM tb_goods WHERE code REGEXP '^G0001'").fetchall()
实跑输出:
--------------------------------------------------------------------
第四十三课:正则查询,与它在沙箱里的替身
--------------------------------------------------------------------
**教材 9.8 讲用正则表达式查询。MySQL 的写法是 `列 REGEXP '模式'`。**
**沙箱的 SQLite 没有内建这个函数,得先给它装一个。**
一、照抄教材会直接报错
--------------------------------------------------------------------
**报错原文:`no such function: REGEXP`。**
**`REGEXP` 在 SQLite 里不是一个内建函数,它是一个「钩子」——**
**库只认这个名字,具体怎么比由使用者注册进去。没注册,就报找不到函数。**
**把函数挂上去,同一句就能跑了:**
code
------
G00001
G00002
G00003
G00004
G00005
G00006
G00007
G00008
G00009
9 rows in set
**注册只要三行:定义一个 `regexp(模式, 值)` 函数,再用 `create_function` 挂上去。**
**挂上之后,`列 REGEXP '模式'` 就等价于 `regexp('模式', 列)`。**
**注意参数的顺序是「模式在前、值在后」,写反了匹配就全错。**
二、六个常用模式
--------------------------------------------------------------------
模式 命中行数 意思
------------------------------------------------------------
`^G0000[1-9]$` 9 G0000 后面是一位 1 到 9
`^G0001` 10 以 G0001 开头
`1$` 500 以 1 结尾
`^G[0-9]{5}$` 5000 G 加五位数字,整串对齐
`^(数码|家电)商品1[0-9]$` 2 两个前缀任选其一
`商品(99|100)$` 2 编号是 99 或 100
**`^` 是开头、`$` 是结尾、`[0-9]` 是一位数字、`{5}` 是「前面那个重复五次」。**
**`|` 是「或者」,括号用来把「或者」的范围圈住。**
**第一行和第二行只差一个 `$`,命中数从 9 变成 10。**
**因为第二行没写结尾锚点,`G00010` 到 `G00019` 也都被算进去了。**
**所以写正则要养一个习惯:能加 `^` 和 `$` 的地方都加上。**
**少一个锚点不会报错,只会悄悄多算几十行,这种错在统计数字里最难看。**
三、REGEXP 能表达、LIKE 表达不了的
--------------------------------------------------------------------
要查什么 LIKE 的写法 REGEXP 的写法
--------------------------------------------------
G00001~G00009 9 行 9 行
两个前缀、两位数的编号 2 行 2 行
**两格的结果都对得上,说明「能不能查出来」这一层,两者打平。**
**差别在写起来的长度:第二格 `LIKE` 要两个 `OR`,正则一对括号就包住了。**
**每多一个前缀,`LIKE` 就多一条 `OR`,正则只多一个竖线。**
**前缀多到五六个的时候,正则就好读多了。**
**还有几件事 `LIKE` 根本做不到:**
**比如「最后一位是 1 或 3」,正则写 `[13]$`;**
**「倒数第三位必须是数字」,正则写 `[0-9]..$`。**
**`LIKE` 的 `_` 只代表「任意一个字符」,分不出数字和字母,所以这类需求它写不出来。**
四、代价:正则用不上索引
--------------------------------------------------------------------
写法 耗时
---------------------------------
`code LIKE 'G0001%'` 54 微秒
`code REGEXP '^G0001'` 9.21 毫秒
**同一个前缀条件,正则慢了 170 倍。**
**两个原因:一是它必须每行回调一次 Python 函数,二是它用不上索引,只能全表扫。**
**所以能写成 `LIKE '前缀%'` 的,就别用正则。**
**前缀匹配是「范围查找」,正则是「逐行判定」,这不是优化技巧的差别,是算法级的差别。**
五、教材上的 MySQL 写法
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| SELECT * FROM tb_goods WHERE code REGEXP '^G0000[1-9]$';
教材原码,本沙箱需先注册 regexp 函数才能跑
**MySQL 的 `REGEXP` 是内建的,不用注册,语法和这里一模一样。**
**差异在细节:MySQL 8.0 的正则用的是 ICU 库,支持 `\d` 这种写法;**
**SQLite 这边走的是 Python 的 `re`,也支持 `\d`,但两边对「多字节字符」的处理不完全一致。**
**所以正则写在 MySQL 上跑通,搬到别处还得再验一遍。**
**这一条没有捷径,只能现测。**
报错那一行是 no such function: REGEXP,注意它说的是「没有这个函数」,不是「语法错误」。
这一点很关键:REGEXP 是 SQL 里的运算符,但底层实现是一个同名函数。
所以注册的时候名字必须叫 regexp,大小写无所谓,但拼错就白搭。
挂上之后立刻给出 9 行:G00001 到 G00009。
这个 9 和「不带 $ 的 10」一比,就是锚点那一课最好的注解。
最后那段代价是 54 微秒对 9.92 毫秒,183 倍。
这个倍数不是稳定值,机器不同会变;但「两个数量级」这个量级是稳的。
30 秒实验
- 把模式改成
^G0000,看命中数怎么变,想想为什么不是 9。 - 把
regexp的两个参数写反,确认它一个都匹配不上却不报错。 - 把
LIKE 'G0001%'换成LIKE '%0001%',看它还走不走索引。
常见误区
- 以为
REGEXP是内置函数,报「找不到函数」就去怀疑安装包。 - 注册函数时把「模式」和「值」两个参数写反,语句正常但一个都匹配不上。
- 在千万行的大表上用正则筛,查一次就把库拖住。
自测三问(点开对答案)
1. 为什么沙箱里 `REGEXP` 要注册才能用? —— 因为 SQLite 把 `REGEXP` 设计成可注册的钩子,库只认名字,不提供实现。 不挂函数就报 `no such function`。 2. `*` 和 `+` 的差别在哪? —— `*` 允许前面那个字符出现零次,`+` 要求至少一次。所以 `ab*c` 能匹配 `ac`,`ab+c` 不能。 3. 为什么正则比前缀 `LIKE` 慢那么多? —— 两个原因:它必须逐行回调函数,而且用不上索引只能全表扫。 前缀 `LIKE` 是范围查找,能直接定位到一段。第 9 章到这里结束。下一篇进第 10 章:常用函数,从数学函数开始。
- ☐ (本篇完,下一篇 10.1 MySQL 函数)