PostgreSQL正则表达式实战:操作符、函数与性能优化
很多人在 PostgreSQL 里写查询一碰到字符串处理就头疼。尤其是那种我就想从一段乱七八糟的文本里把想要的东西抠出来的需求比如从日志里抽时间戳、从 URL 里拆参数、从接口返回里剥掉 JSON 包裹。你会搜到一堆五花八门的答案有人让你用substring有人甩给你split_part还有人直接说你上正则吧。但真到了正则这一步问题又来了PostgreSQL 里的正则跟 Python 的、跟 shell 的、跟 Perl 的长得有点像用起来却不是一回事。~是什么~*又是什么regexp_matches和regexp_replace到底哪个该用更别说那个看起来人畜无害的SIMILAR TO其实是个大坑。这篇东西就把 PostgreSQL 正则这块给你掰开揉碎讲透。不聊那些网上一搜一大把的 API 罗列我直接按实战场景来拆什么时候该用哪个操作符、哪个函数索引怎么配合性能怎么保命还有我在生产环境里踩过的一堆坑。适合正在写 SQL 但被字符串处理折磨的人也适合那些刚接触 PostgreSQL 想说正则我就用 LIKE 凑合一下的朋友——别凑合了看完这篇你能省下不止一下午。1. 从 LIKE 到正则把控匹配的三种姿势1.1 LIKE 和 ILIKE 的边界你得心里有数很多人最早接触 PostgreSQL 字符串匹配都是从WHERE name LIKE %张三%这种写法开始的。LIKE 本身没有错它简单、快、有索引可用前缀匹配场景但它的问题在于表达能力太弱。你想表达字符串里包含一个数字LIKE 做不到你想表达以字母开头、后面跟三个数字结尾LIKE 也很难受。PostgreSQL 为这个提供了SIMILAR TO语法试图在 LIKE 和正则之间找一个中间态。但我要直接泼冷水SIMILAR TO在实际工作里能不用就不用。它的语法混合了 LIKE 的%和_又混进了正则的*、、|写出来既不如 LIKE 直观又不如标准正则强大。而且它在EXPLAIN里优化器基本没法给出特别聪明的计划性能表现也平平。很多时候你费劲巴拉写了一个SIMILAR TO表达式别的同事还得拿正则的书当场翻译。真正值得你花时间的是 PostgreSQL 里完整的正则支持。它默认使用的是 AREAdvanced Regular Expression引擎也就是从 PostgreSQL 7.4 开始内置的 Henry Spencer 那套正则库能力上接近 POSIX ERE 的增强版支持前瞻、后顾、非贪婪匹配这些常见的高级特性。这套引擎就是你在~、~*、regexp_matches、regexp_replace等操作符和函数背后真正干活的家伙。1.2 操作符家族~、~、!~、!~PostgreSQL 提供了四个最基础的正则操作符分别是匹配、不匹配、忽略大小写匹配、忽略大小写不匹配-- 匹配str 满足正则 pattern SELECT abc123 ~ abc; -- true SELECT abc123 ~ ^abc\d$; -- true -- 不匹配 SELECT abc123 !~ ^xyz; -- true -- 忽略大小写匹配 SELECT ABC123 ~* ^abc\d$; -- true -- 忽略大小写不匹配 SELECT ABC123 !~* ^xyz; -- true这四个操作符几乎覆盖了日常 90% 的判断某个字段是否满足某种格式的需求。它们的执行语义是返回布尔值所以最常出现在WHERE子句里做过滤。注意操作符的语义是部分匹配而不是整个字符串完全等于这个正则。什么意思abc123 ~ abc返回 true因为abc是abc123的一部分。如果你要表达整串必须完全匹配就得在正则两端加上^和$或者用\A和\Z但简单场景从^/$开始就够了。这里藏着一个容易被忽略的细节~底层的比较逻辑其实是该字符串中是否存在某个子串满足给定的正则。所以foobar ~ o*b是 true*在这里表示前面的字符重复零次或多次跟 LIKE 里的%完完全全是两码事。刚接触的人最容易在这里被绕进去把*当成通配符乱用结果查出来的数据莫名其妙。1.3 正则怎么写出能用的模式先过这三关第一关锚点。^匹配字符串开头$匹配字符串结尾。PostgreSQL 默认是逐行模式默认.不匹配换行符除非你用(?m)之类的标志开启多行模式所以^和$在有换行符的长文本里的行为需要想清楚。日常拿短字段做检查基本是安全的。第二关字符类和分组。[a-zA-Z0-9_]这类写法是正则基本功PostgreSQL 还额外支持\w、\d、\s这类简写也支持[[:alpha:]]这种 POSIX 字符类写法。括号()用来分组分组的另一个作用是捕获后面配合regexp_matches或regexp_replace的反向引用时会用上。第三关量词。*、、?、{m,n}这些跟标准正则一致。但要注意 PostgreSQL 正则默认是贪婪的也就是能多匹配就多匹配。比如对a123b456c用a.*b去匹配结果会是a123b456c中的a123b456c吗不是a123b加上后面的456c都不是。.*会一直吞到最后一个b所以匹配到的是a123b456b不对原串里只有一个b。我换一个更清晰的例子对a1b2b用a.*b匹配贪婪模式下它会在最后一个b处停下来结果整体是a1b2b。如果写a.*?b非贪婪模式会在第一个b处停结果是a1b。这个细节非常非常容易踩坑。尤其是你想从开头一直匹配到第一次出现某个关键字的时候忘了加?就会吞掉一大片不该吞的内容。我见过不止一个同事在提取日志内容时因为贪婪量词把两三条日志全烙在一行里排查了半小时才发现是正则的锅。2. 核心函数逐个拆matches、replace、split 一个都别放过2.1 regexp_matches别被多行返回坑了regexp_matches返回的是匹配到的部分并且按正则里的捕获组拆开。这个函数的签名是regexp_matches(string text, pattern text [, flags text]) - setof text[]注意返回类型是setof text[]。也就是说它在语义上是一个集合返回函数会为每个匹配位置返回一行每行是一个数组数组元素是每个捕获组对应的内容。如果你只写了零个捕获组也没关系它的数组里只有一个元素——整个匹配的文本。举个典型例子SELECT regexp_matches( 订单号A12345金额88.50元订单号B67890金额12.00元, 订单号([A-Z]\d)金额(\d\.\d)元, g );g标志表示全局匹配。不加g时它只返回第一个匹配位置加了之后返回所有。由于它返回的是集合所以不能直接在SELECT列表里妄图当成单值用虽然 PostgreSQL 允许集合函数出现在列表里但结果行数会膨胀很容易出现莫名其妙多出来几行的情况。更稳妥的做法是配LIMIT 1或作为一个子查询或者在FROM里对函数结果做连接。再强调一个容易炸的点如果正则里有捕获组返回的数组元素个数由捕获组个数决定不是由匹配到的整体决定的。你想拿整体就必须写成(?:...)非捕获组或者干脆不写括号。2.2 regexp_replace最刚需的字符串清洗利器regexp_replace的用法跟其他语言的正则替换很像签名如下regexp_replace(source text, pattern text, replacement text [, flags text])它最实用的地方在于你可以用\1、\2来引用正则里的捕获组。比如你想把手机号中间四位打码SELECT regexp_replace(13812345678, ^(\d{3})\d{4}(\d{4})$, \1****\2); -- 输出138****5678这个函数我还常用在清理脏数据上。比如某个字段里混入了不可见字符、连续空格、换行符一条 SQL 就能规整掉-- 把连续空白字符空格、制表符、换行替换成单个空格 SELECT regexp_replace(raw_text, \s, , g) FROM some_table;还有个力气活是去掉字符串里所有 HTML 标签。虽然我不建议让数据库干这种前端该干的活但偶尔数据修复场景可以顶一下SELECT regexp_replace(html_content, [^], , g);regexp_replace和regexp_matches有个性能上的差异regexp_replace是单趟扫描、逐处替换开销集中在正则引擎的匹配过程返回的是单行单列不会产生行数膨胀。所以能用regexp_replace搞定的清洗工作绝对不要先regexp_matches拆成行再string_agg拼回去那是绕远路。2.3 regexp_split_to_table 和 regexp_split_to_array小心分隔符陷阱这两个函数一个把字符串按正则拆成多行另一个拆成数组。-- 用逗号分隔但忽略括号里的逗号 SELECT regexp_split_to_table(a,b(c,d),e, ,(?![^()]*\)));这个例子就是典型的我要按分隔符拆但又不想在括号内的分隔符处拆。正则领域的标准解法是用负向前瞻后面不能跟着直到遇到右括号之前都没有左括号的情况。不过说实话这种复杂正则不是每次都能写得对写之前先考虑一下你的字符串格式到底稳不稳定如果连分隔符本身都不规范不如先做一步清洗再拆。regexp_split_to_array的用法类似只是返回数组SELECT regexp_split_to_array(apple,banana;cherry|date, [,;|]); -- 结果{apple,banana,cherry,date}这里有个非常常见的坑如果输入的字符串以分隔符开头或结尾regexp_split_to_*会返回一个空字符串元素。比如regexp_split_to_array(a,b,, ,)的结果是{a,b,}末尾的空字符串会被保留。而如果字符串本身就是空的可能返回空数组或者包含空字符串具体行为在不同版本上还有过变化——这类边缘 case最好在应用层先做空值判断别指望数据库替你兜底。2.4 别忽略好用的小工具regexp_substr、regexp_instr、regexp_count这几兄弟不是 PostgreSQL 原生的但 PostgreSQL 15 开始新引入的regexp_substr、regexp_instr、regexp_count真的是大大提升了日常效率。以前大家想取子串得regexp_matches配合下标或者substring配合正则现在可以写得更像 Oracle-- 从字符串中提取第一个数字序列 SELECT regexp_substr(订单号A12345总价88.5, \d(\.\d)?); -- 返回12345 -- 找到第一个数字出现的位置 SELECT regexp_instr(订单号A12345总价88.5, \d); -- 返回4这里是字符位置从1开始 -- 统计数字序列出现了几次 SELECT regexp_count(A1 B22 C333, \d); -- 返回3虽然这三个函数在纯函数式写法上比早期的 PostgreSQL 版本更友好但如果你还在用 PostgreSQL 14 或更早版本就得老老实实回到regexp_matches和strpos的老路上去。要注意函数的具体可用版本是硬约束我这篇内容默认以 PostgreSQL 15/16 为主参考老版本用户看到函数不存在别慌看自己服务器的version()先。2.5 substring 里的正则这是被忽视的第一入口substring(string from pattern)是 PostgreSQL 专门用来做提取第一个匹配片段的语法糖。日常提取 IP、时间戳、订单号它比regexp_matches更简洁因为它是标量返回不会给你搞出集合来SELECT substring(访问IP: 192.168.1.1 时间: 2024-05-01 12:00:00 FROM (\d{1,3}(\.\d{1,3}){3})); -- 返回192.168.1.1注意substring只返回第一个捕获组。如果你的正则里没有捕获组就返回整个匹配串。这种行为跟regexp_matches的数组元素逻辑类似但省去了集合提取的烦恼。基于这个特性它非常适合出现在SELECT列表里直接生成清洗后的列。3. 实战从一段真实日志里把关键字段抠出来3.1 场景定义与数据准备假设我们要处理一条线上服务日志格式长这样[2024-05-01 23:45:04.213] INFO order-service - order created user9527 amount199.00 item无线鼠标 ip10.12.34.56目标提取日志时间、日志级别、用户ID、金额、IP 地址。这种东西你当然可以用split_part一层一层剥但日志字段顺序固定、长度不定的情况下split_part写起来非常痛苦比如 item 里可能还有空格和引号直接按空格拆会全乱。正则在这里的优势是按模式定位而不是按位置拆分。建表模拟数据CREATE TABLE log_tmp (line text); INSERT INTO log_tmp VALUES ([2024-05-01 23:45:04.213] INFO order-service - order created user9527 amount199.00 item无线鼠标 ip10.12.34.56), ([2024-05-01 23:46:11.887] ERROR order-service - order failed user9527 amount199.00 item无线键盘 ip10.12.34.56);3.2 一条 SQL 完成多字段提取下面这段 SQL 是核心示例我建议你在自己的环境里跑一遍看效果SELECT substring(line FROM \[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\]) AS log_time, substring(line FROM \] (\w) ) AS log_level, substring(line FROM user(\d)) AS user_id, substring(line FROM amount(\d\.\d{2})) AS amount, substring(line FROM ip(\d{1,3}(\.\d{1,3}){3})) AS ip FROM log_tmp;重点讲几个细节。第一log_time里我用了\[和\]转义括号因为方括号在正则里有字符类语义不能直接裸写。第二log_level的\] (\w)我在右括号后放了空格然后匹配单词字符。日志里INFO和order-service之间的空格数可能不固定所以这个写法定死了必须紧跟一个空格——如果空格数变了就会漏。更稳妥的写法是\]\s(\w)\s。第三IP 的正则(\d{1,3}(\.\d{1,3}){3})这里外层的括号是捕获组用来提取整个 IP内层(\.\d{1,3}){3}是为了表达点加三位数字重复三次。但是请注意substring会优先返回第一个捕获组我们这里外层的(...)是第一个捕获组所以能正确返回完整 IP。如果你不小心把内层捕获组写到前面去了返回的就是最后一组三位数字了。3.3 提取结果为空先查正则的命中前提不少人写完后发现某个字段一直返回NULL第一反应是数据是不是不对。但更常见的原因是正则不够宽松。比如上面提取 IP 的正则我用了\d{1,3}(\.\d{1,3}){3}但如果日志里 IP 后面还跟着别的内容比如ip10.12.34.56,这个正则其实也能匹配因为{3}只约束了分组重复次数后面有没有逗号不影响匹配成功。问题往往出在锚点缺失导致匹配到了错误的子串。比如你提取时间是\[(\d{4})而一行日志里可能在 message 部分又出现了一个年份——那substring会提取出第一个匹配也就是时间戳里的年份这没问题但如果你实际想提取的是消息里埋的年份这个写法就废了。一条实用的排错思路先用不带捕获组的宽正则看能不能匹配到目标再用regexp_matches(..., g)把所有匹配结果拉出来看一遍确认你的第一个匹配到底命中在哪个位置。3.4 数据清洗实战一行命令去掉 JSON 转义和 HTML 标签再分享一个我在实际项目里高频使用的清洗场景修复从上游接口写入的脏数据。比如字段里混入了\n、\t这种可见转义反斜杠加字母而不是真正的换行符或者混进了p、br这类 HTML 残留。一条regexp_replace就能处理UPDATE articles SET body regexp_replace( regexp_replace(body, \\[ntr], , g), [^], , g ) WHERE body ~ \\[ntr]|[^];这里有个小细节我要匹配反斜杠本身所以在 SQL 字符串里要写\\[ntr]。如果用标准字符串写法\n会被 PostgreSQL 解析成真正的换行符那就完全不是这个意图了。很多新手第一次写正则替换都会被这个反斜杠转义搞崩溃。记住一句口诀SQL 字符串里的两个反斜杠在正则引擎眼里就是一个反斜杠如果你想匹配一个反斜杠加一个字母nSQL 里得写三个甚至四个反斜杠。实际上在 SQL 中匹配单个反斜杠最稳的写法是\\\\因为 PostgreSQL 标准字符串里\\变成\正则引擎收到\后如果后面不接特殊字符有时会报错或行为奇怪。所以我的习惯是能用[\\]就不要用\\。比如匹配 ASCII 转义序列我会写成-- 匹配一个反斜杠后面跟着 n/t/r regexp_replace(body, [\\][ntr], , g)这里[\\]在正则里是一个字符类表示字符集中的反斜杠干净利落不折腾。4. 性能优化别让正则变成慢查询元凶4.1 正则不用索引也不一定全对很多人一听正则就条件反射式地说无法走索引。这个说法过于绝对。确实~ abc.*这种写法在原理上很难利用普通 B-tree 索引因为优化器不知道匹配的字符串落在一个有序区间内的哪个范围。但如果你只要前缀匹配那么正则也能走索引。比如name ~ ^abc优化器可以将其转换成一个范围条件从abc开始到abd结束从而利用 B-tree 索引。不过这种转换并不是 PostgreSQL 的默认操作实践中你最好还是用name LIKE abc%这种写法优化器对这种写法有明确的索引支持。另外如果正则匹配的格式是前缀固定 后缀可变且这个前缀有一定的区分度可以考虑表达式索引。比如CREATE INDEX idx_log_user_prefix ON log_tmp ((substring(line FROM user(\d))));但这种索引只能配合完全一致的表达式才能命中为了这一小撮查询建索引性价比通常不高。更常见的方式是给待匹配的字段解构出独立列比如用户ID、订单号、状态码对这些离散列建普通 B-tree 索引查询时直接等值匹配彻底绕开正则。这也是能拆列就别正则的核心理由——正则不是万金油很多时候先做一次 ETL 把字段拆好后续查询性能轻松涨一个量级。4.2 大表 LIKE 和正则的性能对比实验我自己测过一张百万行级的表字段remark查询WHERE remark ~ 订单号A[0-9]{5}全表扫描耗时大概在 800ms 上下取决于平均字段长度。而同样的查询需求如果上游在写入时已经拆出了order_id列并建索引WHERE order_id LIKE A%或等值匹配耗时直接降到几十毫秒。所以结论很清晰对大数据量的在线查询正则过滤能避则避不能避也尽量把正则限定在全表扫描可控的小表上或者放到离线分析场景。当然也有场景是正则跑得比你想象中快的。比如你给一个text字段建了 GIN 索引配合pg_trgm模块这时候某些正则也能借助三元组索引加速。但这玩意儿配置起来麻烦且它只能加速部分模式比如模式里有固定字符串片段如果你的正则全是.*这种GIN 也救不了你。4.3 避免在 JOIN 条件或 GROUP BY 里无脑用正则这是我踩过最深的一个坑把正则写进 JOIN 的ON条件里做模糊匹配。比如SELECT * FROM a JOIN b ON a.code ~ (^ || b.pattern || $);这种写法的执行计划必然是先做嵌套循环然后对每一对组合跑一次正则。外层一百行、内层一万行就是一百万次正则编译加匹配查询直接卡死。现实里这么干的需求往往是想做规则表匹配但更好的做法是先判断你的规则能不能拆成离散字段不能的话至少把规则表加个前缀索引用LIKE把候选集缩小到几十行再对剩下的做正则精匹配。4.4 正则缓存和防呆写法PostgreSQL 的正则引擎会缓存最近使用的正则模式允许的缓存大小是regex_cache相关参数控制的。但如果你一条 SQL 里动态生成了成千上万个不同的正则比如上面 JOIN 场景缓存根本顶不住反而会因为频繁淘汰导致额外开销。所以最佳实践是同一个模式尽量全局复用不要拼接无谓的字符串。举个反面教材-- 不要这样 WHERE remark ~ (SELECT pattern FROM rule WHERE id 1)每行执行时都去查规则表再重新编译正则性能烂透。正确做法是先取出来变成参数或者拆成两步-- 先拿到 pattern再作为绑定变量传给查询 PREPARE check_remark(text) AS SELECT * FROM t WHERE remark ~ $1; EXECUTE check_remark(订单号A\d{5});PREPARE的好处是让 PostgreSQL 把正则模式作为参数缓存下来重复执行时不用每行都重新编译正则省下的开销在高频查询下非常可观。5. 常见问题与坑位速查5.1 不同版本正则函数有多大差异PostgreSQL 不同版本之间正则相关的能力差异还挺大的。版本变更重点PostgreSQL 10 及以前只有~系列操作符、substring、regexp_matches、regexp_replace、regexp_split_to_*PostgreSQL 11 / 12 / 13 / 14无大变化都是内引擎增强PostgreSQL 15新增regexp_substr、regexp_instr、regexp_countPostgreSQL 16对regexp_*系列函数的行为做了细节修正并优化了部分边界情况如果你在 PostgreSQL 14 上跑regexp_count会直接报function regexp_count(text, text) does not exist。这不是你的 SQL 写错了是版本不支持。所以遇到函数不存在先查current_setting(server_version)别折腾半天正则表达式最后发现是版本问题。5.2 flags 参数到底怎么写regexp_*系列函数的最后一个可选参数是 flags常用的有flag含义g全局匹配返回所有匹配位置i忽略大小写n让.也能匹配换行符新行m多行模式^和$匹配每行的开头和结尾s让空白字符按\s匹配非默认行为视正则有差异最容易被忽略的是n和m的区别。m是让^/$按行生效n是让.能跨行。如果你处理的是多行日志文本想在每一行里提取某个字段就得用m。比如SELECT regexp_matches( E第一行 error1\n第二行 error2\n第三行 error3, ^.*error(\d)$, gm );注意这里g和m连写在一起。如果没有m^只会匹配整个字符串的开头结果就只有一个1。5.3 正则里的括号、反斜杠、$ 符号的转义迷局这是几乎所有正则新手都会撞的墙。在 PostgreSQL 字符串里写正则遇到的转义层级是双重的先是 SQL 字符串层再是正则层。举例你要匹配字符串里的美元符号$在正则里$是锚点要匹配字面$需要转义成\$在 SQL 标准字符串里反斜杠需要写成\\所以你在 SQL 里要写\\$看起来是不是头大我这里给你一个治本的方法优先使用 PostgreSQL 的 dollar-quoted 字符串常量来写正则或者用E转义字符串。比如SELECT $100 ~ E\\$\\d; -- true SELECT $100 ~ $\\d; -- 这个可能不会按你预期工作实际上更稳的习惯是正则里尽量用字符类避免反斜杠转义。匹配$可以写成[$]匹配.可以写成[.]匹配*可以写成[*]。字符类内部的这些特殊字符多数不需要转义写出来也更安全。5.4 常见问题速查表我把平时工作里被问得最多的场景整理成一张速查表遇到可以直接抄需求推荐写法判断字符串是否以数字开头str ~ ^\d判断字符串是否只包含字母str ~ ^[a-zA-Z]$判断是否包含连续三个以上空格str ~ {3,}提取第一个时间戳标准格式substring(str FROM \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})提取所有邮箱regexp_matches(str, [a-zA-Z0-9._%-][a-zA-Z0-9.-]\.[a-zA-Z]{2,}, g)去掉字符串首尾空格regexp_replace(str, ^\s按逗号拆行但忽略括号内逗号regexp_split_to_table(str, ,(?![^()]*\)))手机号中间四位打码regexp_replace(phone, ^(\d{3})\d{4}(\d{4})$, \1****\2)需要特别提醒的是上面这些写法的性能只适合中小数据量百万行以内的单表过滤。上了千万行还这么查神仙也救不了。你得回到 4 节说的思路先拆列、再索引、最后才正则。5.5 定位问题的方法论从结果反推正则模式当你遇到结果字段为 NULL或者匹配的行比预期多不要急着反复修改正则。我的建议是分三步走第一步只跑正则不带业务条件。比如SELECT line, regexp_matches(line, 这里写你的模式, g) FROM log_tmp LIMIT 20;先直接看它匹配出了什么。第二步逐段拆解。如果你的正则写了 60 个字符把它拆成三个小片段分别测。比如先测\d{4}-\d{2}-\d{2}再测 \d{2}:\d{2}:\d{2}最后拼起来。这一步能快速定位是前半段错了还是后半段错了。第三步确认边界字符。看原始数据里是不是混入了不可见字符比如\r回车、零宽空格、全角空格等。用SELECT line, encode(line::bytea, escape) FROM log_tmp;可以直观看到隐藏字符。很多时候字段显示没问题一查编码发现里面藏着\r正则的$就匹配不上了。6. 经验之谈正则不是银弹但确实是瑞士军刀最后讲一点我在实战里的体会。正则表达式在 PostgreSQL 里是极度好用的瑞士军刀但它不是银弹。日常开发里我给自己定了三条军规。第一能拆列就不要绕正则。如果你的业务字段本身有结构比如订单号永远是前缀数字那就应该在建表时拆成order_prefix和order_no两列。维护成本其实很低但查询性能和可读性提升巨大。正则适合偶发清洗和复杂提取不适合放进高频查询链路里做核心过滤。第二正则写完一定要加注释。SQL 里的正则本来就难读没人愿意半个月后回来猜你这串(?![^()]*\))到底想干嘛。我会在字段边上留一行注释写明意图和输入样例。有些公司还会把这些常用正则收集成一张正则模板表新同事直接复用避免重复踩坑。第三上线前用真实数据量做一次性能验证。在小表上这条 SQL 跑了 50ms 不叫快千万行表上跑了 50ms 才是真快。正则引擎的开销跟字符串长度和复杂度密切相关不可控因素很多。我的习惯是先把线上数据捞一小部分回来建表造个近似量级的数据压测一下再放到生产环境跑。尤其是那种会自动拼正则的规则引擎上线前不验证等于埋雷。PostgreSQL 的正则能力比很多数据库都要完整从基础操作符到高级函数覆盖了从判断、提取、替换到拆分的所有场景。但它也有自己的语法边界、版本边界和性能边界。把这些边界摸透你才能在写不出来和跑不动之间找到那条优雅的平衡线。这篇内容里提到的每种写法、每个坑位都是我实际踩过或帮别人排查过的场景。你不需要一次性全部背下来但建议收藏起来等真遇到从一个长字符串里抠字段的需求时再来对照着用。