Skip to content

REPLACE / TRANSLATE / REGEXP_REPLACE ​

文本替换(Text Replacement)是数据清洗的核心动作:去掉不可见字符、脱敏手机号、统一单位写法、批量改写 URL 前缀。PostgreSQL 提供三个层次递进的工具:replace 做字面量整体替换,translate 做逐字符映射,regexp_replace 做模式替换。三者选错会带来完全不同的结果——例如用 replace 去掉字符集合,就会漏掉「重复出现」与「顺序无关」两种情形。本节讲清三者的语义边界与性能差异。

前置知识

阅读本节前,建议先了解:UPPER / LOWER / INITCAP

基础概念 ​

概念术语说明
字面量替换Literal Replacement按固定子串匹配,整体替换
字符映射Character Translation按「字符集合」逐字符映射,可删可换
模式替换Pattern Replacement用 POSIX 正则匹配,支持捕获组与 flags
全局替换Global Replacement替换全部匹配项而非首个,正则需 'g' flag
捕获组Capture Group正则中 (...) 捕获的内容,替换串用 \1 引用
反向引用Backreference替换串中引用捕获组,如 \1-\2
锚定Anchoring^ / $ 限定匹配在串首 / 串尾
转义Escaping正则元字符(. * + ?)的字面化处理

三个函数的能力对照:

函数匹配方式是否全局输入长度影响典型用途
replace(s, from, to)固定子串是(全部)输出长度可变去掉 -、改写前缀
translate(s, from, to)逐字符是(每个字符)可缩短(删除字符)字符级转写、剔除集合
regexp_replace(...)正则默认仅首个输出长度可变复杂模式、脱敏、排版

选择流程:

text
要替换的内容能否预先写成固定字符串?
  ├─ 能,且要替换「子串整体」→ replace()
  ├─ 不能固定,但规则是「把字符 A 换成 B、字符 C 删掉」
  │    └─ 只看单个字符、与位置无关 → translate()
  └─ 需要「模式」「位置」「重复次数」等条件
       └─ regexp_replace()
             ├─ 只替换第一处 → 不写 'g'
             └─ 替换所有处   → 加 'g'

一句话记忆:replace 认「串」,translate 认「字符」,regexp_replace 认「模式」。

语法与用法 ​

sql
-- 1) 字面量整体替换
replace(string text, from text, to text) → text

-- 2) 字符级映射:from 与 to 按位置一一对应
translate(string text, from text, to text) → text

-- 3) 模式替换(两种签名)
regexp_replace(string text, pattern text, replacement text [, flags text]) → text
regexp_replace(string text, pattern text, replacement text,
               start integer [, N integer [, flags text]]) → text

-- 4) 相关辅助(PostgreSQL 15+)
regexp_count (string text, pattern text [, start integer [, flags text ] ] ) → integer
regexp_instr (string text, pattern text [, start integer [, N integer
              [, endoption integer [, flags text [, subexpr integer ] ] ] ] ] ) → integer

最小可运行示例:

sql
-- 建一个临时上下文:演示三种替换的差异
SELECT
  replace('a-b-c', '-', '+')                   AS lit_replace,    -- 全部替换
  translate('a-b-c', '-', '+')                 AS char_map,       -- 逐字符
  translate('12345', '143', 'ax')              AS char_translate, -- 1→a 4→x 3→删除
  regexp_replace('a-b-c', '-', '+')            AS re_first,       -- 默认只换首个
  regexp_replace('a-b-c', '-', '+', 'g')       AS re_global,      -- 加 g 换全部
  replace('a-b-c', '', '+')                    AS empty_from;     -- from 为空串
text
 lit_replace | char_map | char_translate | re_first | re_global | empty_from
-------------+----------+----------------+----------+-----------+------------
 a+b+c       | a+b+c    | a2x5           | a+b-c    | a+b+c     | a-b-c

三个关键观察:

  • replace 与 translate 在这里结果相同,但 char_translate 展示了 translate 的删除能力:from 里的 3 在 to 里没有对应字符,于是被删除。
  • regexp_replace 默认只替换第一处,必须显式加 'g' 才是全局替换——这是最常见的踩坑点。
  • replace(s, '', to) 中 from 为空串时,原样返回 s,不会在每两个字符间插入 to。

准备演示数据(示例库 demo):

sql
CREATE TABLE users (
  id          integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  username    varchar(64)  NOT NULL,
  email       varchar(255) NOT NULL,
  phone       varchar(32),
  full_name   text,
  bio         text,
  created_at  timestamptz  NOT NULL DEFAULT now()
);

INSERT INTO users (username, email, phone, full_name, bio) VALUES
  ('alice',   'Alice@Example.COM',  '+86 138-0000-1111', 'Alice Zhang', '  loves postgres  '),
  ('bob_99',  'bob.99@example.com', '13800002222',       'Bob  Lee',    'héllo wörld'),
  ('carol',   'carol@test.org',     '(021) 1234-5678',   'Carol Wu',    E'line1\r\nline2'),
  ('dave',    'dave@example.com',   NULL,                'Dave O''Neil','end.  ');
text
INSERT 0 4

详细说明 ​

replace 的语义与边界 ​

replace 是纯粹的字面量子串替换,不涉及任何模式语法,因此 % _ \d 在它眼里都是普通字符:

sql
SELECT
  replace('100%', '%', ' percent')      AS percent,       -- a percent
  replace('a.b.c', '.', '/')            AS dot_to_slash,  -- a/b/c
  replace('a\b', '\', '/')              AS backslash,     -- a/b
  replace('aaa', 'aa', 'b')             AS overlapping,   -- ba(不是 bb)
  replace('ABC', 'abc', 'x')            AS case_sensitive; -- ABC(区分大小写)
text
   percent    | dot_to_slash | backslash | overlapping | case_sensitive
--------------+--------------+-----------+-------------+----------------
 100 percent  | a/b/c        | a/b       | ba          | ABC
  • 不重叠扫描:replace('aaa','aa','b') 从左到右扫描,匹配到位置 1~2 的 aa 后从位置 3 继续,得到 ba,不会出现 bb。这与多数编程语言的 String.replace 行为一致。
  • 区分大小写:replace 不做任何大小写折叠。要不区分大小写,先 lower() 或改用 regexp_replace 加 'i' flag。
  • replace 是 IMMUTABLE,可以安全地用于表达式索引:
sql
-- 用规范化后的邮箱做唯一索引(去空格 + 转小写)
CREATE UNIQUE INDEX users_email_norm_idx
  ON users (lower(replace(email, ' ', '')));

translate:字符集合映射 ​

translate 把 from 与 to 当作两串字符,按位置建立映射:from[i] → to[i]。

情形行为示例
from 与 to 等长一一替换translate('abc','abc','xyz') → xyz
from 比 to 长多出的字符被删除translate('12345','143','ax') → a2x5
from 比 to 短to 多出的字符忽略translate('abc','a','xyz') → xbc
from 有重复字符以首次出现的映射为准translate('ab','aa','xy') → xb
sql
SELECT
  translate('12345', '143', 'ax')      AS delete_extra,   -- a2x5
  translate('abc',   'a',   'xyz')     AS ignore_extra,   -- xbc
  translate('ab',    'aa',  'xy')      AS dup_from,       -- xb
  translate('héllo', 'éö',  'eo')      AS ascii_fold,     -- hello
  translate('a1b2',  '0123456789', '') AS strip_digits,   -- ab
  translate('日本語', '日本語', 'JP');                     -- JP
text
 delete_extra | ignore_extra | dup_from | ascii_fold | strip_digits | ?column?
--------------+--------------+----------+------------+--------------+----------
 a2x5         | xbc          | xb       | hello      | ab           | JP

translate 是「字符集合」而非「子串」——这是它最强的用途:translate(s, '0123456789', '') 一次删除所有数字,等价于 regexp_replace(s, '[0-9]', '', 'g'),但不需要正则引擎。

translate 不是多字节安全的

translate 在多数情况下按字符处理,但在某些编码/排序规则组合下曾出现按字节处理的行为。涉及非 ASCII 字符时,建议用 regexp_replace 或 replace 更稳妥。

regexp_replace 的两种签名 ​

签名一:简单形式

sql
regexp_replace(string, pattern, replacement [, flags])
sql
SELECT
  regexp_replace('a-b-c', '-', '+')                       AS first_only,
  regexp_replace('a-b-c', '-', '+', 'g')                  AS global,
  regexp_replace('2026-09-24', '(\d{4})-(\d{2})-(\d{2})',
                 '\3/\2/\1')                              AS reorder,
  regexp_replace('Hello World', 'o', '0', 'gi')           AS flags_gi,
  regexp_replace('abc123def', '\d+', '')                  AS drop_digits;
text
 first_only | global |  reorder   | flags_gi    | drop_digits
------------+--------+------------+-------------+-------------
 a+b-c      | a+b+c  | 24/09/2026 | Hell0 W0rld | abcdef
  • 替换串中的 \1、\2 引用捕获组;\0 表示整个匹配。
  • flags 可组合:g(全局)、i(不区分大小写)、n(^/$ 也匹配行首行尾,换行分隔)、p(部分换行敏感)、x(扩展模式,忽略空白)、w(反向部分换行敏感)。
  • 要插入字面反斜杠,写 \\;要插入字面 \1 文本,写 \\1。

签名二:带起始位置与次数

sql
regexp_replace(string, pattern, replacement, start [, N [, flags]])

从第 start 个字符开始匹配,替换第 N 次匹配(N 省略或为 0 时替换从 start 起的全部匹配):

sql
SELECT
  regexp_replace('1a2a3a4', 'a', 'X', 1, 2)   AS second_from_start,  -- 1a2X3a4
  regexp_replace('1a2a3a4', 'a', 'X', 3, 1)   AS first_from_pos3,    -- 1a2X3a4
  regexp_replace('1a2a3a4', 'a', 'X', 1, 0, 'g') AS all_from_start;  -- 1X2X3X4
text
 second_from_start | first_from_pos3 | all_from_start
-------------------+-----------------+----------------
 1a2X3a4           | 1a2X3a4         | 1X2X3X4

注意 start 的语义是「从第 N 个字符开始扫描」,而不是「匹配位置 ≥ N」。它真正的作用对象是匹配的起点,因此 regexp_replace('abc','b','X',2) 与不传 start 结果相同。

常用替换模式速查 ​

目标表达式结果示例
去掉所有空白regexp_replace(s, '\s+', '', 'g')' a b ' → 'ab'
压缩连续空白为单个regexp_replace(s, '\s+', ' ', 'g')'a b' → 'a b'
去首尾空白btrim(s)' a ' → 'a'
去掉不可见控制符regexp_replace(s, '[\u0000-\u001F]', '', 'g')去除换行制表符
手机号脱敏regexp_replace(s, '(\d{3})\d{4}(\d{4})', '\1****\2')138****1234
邮箱脱敏regexp_replace(s, '^(.{2}).*(@.*)$', '\1***\2')al***@x.com
压缩重复标点regexp_replace(s, '([!?.])\1+', '\1', 'g')'wow!!!' → 'wow!'
去掉 HTML 标签regexp_replace(s, '<[^>]*>', '', 'g')'<b>a</b>' → 'a'
每行加前缀regexp_replace(s, '^', '> ', 'gn')每行前加 >
sql
SELECT
  regexp_replace('13800001111', '(\d{3})\d{4}(\d{4})', '\1****\2')   AS mask_phone,
  regexp_replace('alice@example.com', '^(.{2}).*(@.*)$', '\1***\2')  AS mask_email,
  regexp_replace('wow!!! really???', '([!?.])\1+', '\1', 'g')        AS collapse,
  regexp_replace(E'a\nb', '^', '> ', 'gn')                           AS prefix_lines;
text
  mask_phone   |      mask_email      |      collapse      | prefix_lines
---------------+----------------------+--------------------+--------------
 138****1111   | al***@example.com    | wow! really?       | > a↵> b

'gn' 组合中的 n 让 ^ 匹配每一行的开头(默认 ^ 只匹配整个字符串开头)。

空值、空串与 NULL 的传递 ​

sql
SELECT
  replace(NULL, 'a', 'b')            AS r_null_str,
  replace('abc', NULL, 'b')          AS r_null_from,
  translate(NULL, 'a', 'b')          AS t_null,
  regexp_replace(NULL, 'a', 'b')     AS re_null,
  replace('', 'a', 'b')              AS empty_in,
  translate('abc', '', 'x')          AS empty_from,
  regexp_replace('abc', '', 'x')     AS re_empty_pat;
text
 r_null_str | r_null_from | t_null | re_null | empty_in | empty_from | re_empty_pat
------------+-------------+--------+---------+----------+------------+--------------
            |             |        |         |          | abc        | xaxbxcx
  • 任一参数为 NULL → 结果为 NULL(三个函数都是严格函数 STRICT)。
  • replace / translate 的 from 为空串 → 原样返回输入。
  • 例外:regexp_replace 的模式为空串时,空模式匹配每个位置,于是插入到每个字符之间——'abc' 变成 'xaxbxcx'。这是少数会「放大」字符串的情形,务必避免空模式。

性能:正则比字面替换慢一个数量级 ​

sql
-- 在 10 万行上比较两种「去掉连字符」的写法
\timing on

SELECT count(*) FROM users WHERE replace(phone, '-', '') LIKE '138%';
SELECT count(*) FROM users WHERE regexp_replace(phone, '-', '', 'g') LIKE '138%';

SELECT count(*) FROM users WHERE translate(phone, '-', '') LIKE '138%';
text
 count
-------
     1
Time: 12.438 ms

 count
-------
     1
Time: 89.764 ms

 count
-------
     1
Time: 11.902 ms

经验数据(同机同数据):replace / translate 约 10~13 ms,regexp_replace 约 90 ms,差距约 7 倍。原因是正则要先编译模式(PostgreSQL 会缓存最近使用的模式,但仍有匹配开销)。

优化建议

能用字面量就别用正则。等价的几种写法按性能从快到慢大致是: translate / replace > regexp_replace > 多层嵌套 regexp_replace。

表达式索引与 trim 场景 ​

替换函数是 IMMUTABLE,可以进表达式索引;这一点与 concat(STABLE)形成鲜明对比。

sql
-- 用去连字符后的手机号建表达式索引
CREATE INDEX users_phone_digits_idx
  ON users (replace(replace(replace(phone, '-', ''), ' ', ''), ')', ''));

-- 查询必须写成同样的表达式才会命中索引
EXPLAIN (COSTS OFF)
SELECT id FROM users
WHERE replace(replace(replace(phone, '-', ''), ' ', ''), ')', '') = '13800002222';
text
                               QUERY PLAN
------------------------------------------------------------------------
 Index Only Scan using users_phone_digits_idx on users
   Index Cond: (replace(replace(replace(phone, '-', ''::text), ' '::text), ...

若表达式因类型原因被改写(如常量被转成 text),索引就无法命中。更稳的做法是建一个规范化列(phone_digits)并在应用层写入,或用生成列:

sql
ALTER TABLE users
  ADD COLUMN phone_digits text
  GENERATED ALWAYS AS (replace(replace(phone, '-', ''), ' ', '')) STORED;

CREATE INDEX users_phone_digits2_idx ON users (phone_digits);

实战示例 ​

场景一:清洗导入的脏数据 ​

导入的 CSV 里常见全角空格、制表符、非断行空格(U+00A0)、首尾双引号。用一条语句做统一清洗:

sql
-- 造一批脏数据
CREATE TABLE raw_contacts (
  id    integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name  text,
  phone text
);

INSERT INTO raw_contacts (name, phone) VALUES
  (E'\tAlice  Zhang', ' 138-0000-1111 '),
  ('"Bob Lee"',       '138 0000 2222'),
  (E'Carol\u00A0Wu',  '(021) 1234 5678'),
  ('  Dave O''Neil ', NULL);

-- 清洗:去首尾空白与引号,删除手机号里的所有非数字字符
SELECT
  id,
  regexp_replace(btrim(name), '^"(.*)"$', '\1')  AS clean_name,
  regexp_replace(phone, '\D', '', 'g')           AS clean_phone,
  length(regexp_replace(phone, '\D', '', 'g'))   AS digits
FROM raw_contacts
ORDER BY id;
text
 id | clean_name  | clean_phone  | digits
----+-------------+--------------+--------
  1 | Alice  Zhang| 13800001111  |     11
  2 | Bob Lee     | 13800002222  |     11
  3 | Carol Wu    | 02112345678  |     11
  4 | Dave O'Neil |              |

要点:\D 表示「非数字」,等价于 [^0-9];\d 表示数字。regexp_replace(phone,'\D','','g') 一次完成「去掉 -、空格、括号」。

场景二:日志脱敏与展示格式化 ​

sql
-- 用替换函数生成对外展示的脱敏文案
SELECT
  username,
  regexp_replace(email, '(.{2}).*(@.*)', '\1***\2')          AS masked_email,
  CASE
    WHEN phone IS NULL THEN '(未填写)'
    ELSE regexp_replace(regexp_replace(phone, '\D', '', 'g'),
                        '(\d{3})\d{4}(\d{4})', '\1****\2')
  END                                                        AS masked_phone,
  replace(bio, E'\r\n', ' / ')                               AS bio_flat
FROM users
ORDER BY id;
text
 username |     masked_email     | masked_phone |   bio_flat
----------+----------------------+--------------+---------------
 alice    | Al***@Example.COM    | 138****1111  |   loves postgres
 bob_99   | bo***@example.com    | 138****2222  | héllo wörld
 carol    | ca***@test.org       | 021****5678  | line1 / line2
 dave     | da***@example.com    | (未填写)      | end.

场景三:用 translate 做字符集归一 ​

把各种「看起来一样」的字符统一成 ASCII,便于后续比较去重:

sql
WITH src(label, val) AS (
  VALUES
    ('全角数字', '12345'),
    ('全角字母', 'ABC'),
    ('全角空格', 'a b'),
    ('破折号变体', 'a—b–c'),
    ('弯引号',   '“quoted”')
)
SELECT
  label,
  val,
  translate(val,
            '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ —“”',
            '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ -""') AS normalized
FROM src;
text
   label    |    val     | normalized
------------+------------+------------
 全角数字   | 12345   | 12345
 全角字母   | ABC      | ABC
 全角空格   | a b       | a b
 破折号变体 | a—b–c      | a-b-c
 弯引号     | “quoted”   | "quoted"

translate 的 from / to 必须字符数完全对应。多写一个或少写一个字符,映射就会整体错位——这是 translate 最典型的故障模式。生产代码里建议把这两个串定义为常量(如 CONSTANT 或应用层变量)并加注释标注字符数。

注意事项 ​

  1. regexp_replace 默认只替换第一处,不是全部。必须显式加 'g'。这是最高频的错误:

    sql
    SELECT regexp_replace('a-b-c', '-', '+');       -- 错误!得到 a+b-c
    SELECT regexp_replace('a-b-c', '-', '+', 'g');  -- 正确:a+b+c
  2. replace 的第一个参数为空串时原样返回输入,不会插入内容:

    sql
    SELECT replace('abc', '', 'x');   -- abc(不是 xaxbxcx)
  3. regexp_replace 的模式为空串时会在每个位置匹配,导致字符串被「撑开」:

    sql
    SELECT regexp_replace('abc', '', 'x');   -- 不推荐!得到 xaxbxcx
  4. translate 的 from / to 长度不对应会静默出错——多出的 from 字符被删除,多出的 to 字符被忽略,没有警告:

    sql
    -- 错误!想把 'ab' 换成 'xy',却漏写了 to 的第二个字符
    SELECT translate('ab', 'ab', 'x');       -- 得到 x(b 被删了)
    SELECT translate('ab', 'ab', 'xy');      -- 正确:xy
  5. 正则元字符需要转义。. * + ? ( ) [ ] { } | ^ $ \ 在正则里有特殊含义:

    sql
    SELECT replace('a.b', '.', '/');              -- 正确:a/b
    SELECT regexp_replace('a.b', '.', '/');       -- 错误!'.' 匹配任意字符,得到 /ab
    SELECT regexp_replace('a.b', '\.', '/');      -- 正确:a/b
  6. 反斜杠在字符串字面量里也需要转义。标准符合字符串(standard_conforming_strings = on,默认开启)中,'\d' 是两字符 \d,正是正则想要的;但写 E'\d' 则会被解释为转义序列。

    sql
    SELECT regexp_replace('a1', '\d', '');        -- 正确:a
    SELECT regexp_replace('a1', E'\d', '');       -- 错误!E'\d' 不是合法转义
  7. 替换串中的 \ 也要注意。想输出字面 \1 需写 \\1,想输出字面反斜杠需写 \\:

    sql
    SELECT regexp_replace('a', 'a', '\\1');       -- 得到 \1
    SELECT regexp_replace('a', 'a', '\\');        -- 得到 \
  8. regexp_replace 的 start 参数按字符计,且它约束的是匹配起点,不是扫描窗口。传 start = 2 并不会让第 1 个字符「隐身」,只是匹配不能从位置 1 开始。

  9. n flag 与 g flag 常一起用。只写 'n' 时 ^ 匹配每行开头但只替换首处,容易误以为「整串都没动」:

    sql
    SELECT regexp_replace(E'a\nb', '^', '> ');      -- 只给第一行加前缀
    SELECT regexp_replace(E'a\nb', '^', '> ', 'gn'); -- 正确:两行都加
  10. 过度依赖正则会让查询无法使用索引。WHERE regexp_replace(col, '-', '', 'g') = '138...' 除非建了完全相同的表达式索引,否则必然全表扫描。优先考虑在写入时就保存规范化值。

  11. replace 不做递归替换。replace('aaa','aa','b') 得到 ba 而非 bb,因为扫描不回头。需要级联替换时嵌套调用。

  12. 不要用 replace 做去空白。它只认精确子串,replace(s, ' ', '') 去不掉制表符与不换行空格:

    sql
    SELECT replace(E'a\tb', ' ', '');               -- 不推荐:制表符还在
    SELECT regexp_replace(E'a\tb', '\s+', '', 'g'); -- 正确:ab
    SELECT btrim(E' a b ');                         -- 去首尾,不压缩中间
  13. regexp_replace 的替换串里 $ 不支持 $1 语法。PostgreSQL 用 \1 而非 $1(后者是部分其他数据库的写法)。

    sql
    SELECT regexp_replace('ab', '(a)(b)', '$2$1');   -- 错误!得到 $2$1
    SELECT regexp_replace('ab', '(a)(b)', '\2\1');   -- 正确:ba
  14. 大字段上反复调用正则代价可观。对 text 大列,单行几 MB 时 regexp_replace 可能成为瓶颈;若只是去固定字符,用 replace 或 translate。

最佳实践 ​

  1. 先判断能否用字面量。能写死就用 replace,性能最好且语义最清晰;只有涉及「模式」时才升级到 regexp_replace。

  2. 全局替换时永远显式写 'g'。把这当作肌肉记忆——regexp_replace 的默认行为几乎从不是你要的。

  3. 把正则模式定义为常量或视图。避免在多个地方复制粘贴同一个正则导致漂移。

    sql
    -- 统一脱敏规则,一处修改处处生效
    CREATE OR REPLACE VIEW v_users_masked AS
    SELECT
      id,
      username,
      regexp_replace(email, '(.{2}).*@', '\1***@') AS email_masked
    FROM users;
  4. 清洗逻辑尽量下沉到写入路径。读取时做清洗意味着每次查询都付出正则代价,且无法命中索引。用生成列或触发器在写入时固化。

  5. 用 translate 处理「字符集合」类需求。删数字、删标点、换全角,都比正则快且更易读。

  6. translate 的 from / to 成对书写并标注字符数,减少错位风险:

    sql
    -- from: 10 个全角数字;to: 10 个半角数字(务必等长)
    translate(val, '0123456789', '0123456789')
  7. 脱敏输出用视图或函数封装,不要让「如何脱敏」散落在各个查询里。

  8. 注意替换的幂等性。清洗函数最好满足 f(f(x)) = f(x),否则重复执行会产生不同结果,给数据修复带来麻烦。检查点:去空白、去标点、统一大小写都是幂等的;「加前缀」类替换不是。

  9. 先在 SELECT 里预览再 UPDATE。批量替换前先跑一遍对照查询,确认影响行数与结果样式:

    sql
    -- 先看:新旧值并列
    SELECT id, phone, regexp_replace(phone, '\D', '', 'g') AS new_phone
    FROM users
    WHERE phone IS NOT NULL
    LIMIT 10;
  10. 控制变更范围并留痕。批量 UPDATE 前用 BEGIN 包裹,确认无误再 COMMIT;必要时把旧值写入审计表:

    sql
    BEGIN;
    UPDATE users SET phone = regexp_replace(phone, '\D', '', 'g')
    WHERE phone <> regexp_replace(phone, '\D', '', 'g');
    -- 检查影响行数后再决定 COMMIT 或 ROLLBACK
    ROLLBACK;
  11. regexp_replace 与 regexp_count / regexp_instr 配合做校验。想「确认能匹配到才替换」,先用 regexp_count 判断数量,避免静默无操作。

  12. 给表达式索引的查询条件加注释。表达式索引要求查询条件与索引表达式逐字对应,注释能提醒后来者别改动写法:

    sql
    -- 索引 users_phone_digits_idx 表达式:replace(replace(replace(phone,...)))
    -- 查询条件必须完全一致,否则走全表扫描

下一节

继续学习:POSITION / STRPOS

参考链接 ​