REPLACE() 不适用于手机号或身份证号的中间段脱敏,因其仅支持固定子串替换,无法按位置精准隐藏;正确做法是用 SUBSTR() 截取首尾、CONCAT() 拼接星号,兼顾准确性与兼容性。

直接用 REPLACE() 脱敏手机号或身份证号会出错
REPLACE() 只做固定子串替换,没法按位置隐藏中间段。比如想把 13812345678 变成 138****5678,你不能写 REPLACE(phone, '12345678', '****5678')——因为每条记录的中间四位都不同,根本没法枚举。更糟的是,如果字段里有重复数字(如 13812341234),REPLACE() 可能误替多次,结果变成 138****1234 甚至 138****1***。
它真正适合的场景只有:字段内容高度统一、且需全局替换某固定片段。例如银行卡号格式化:REPLACE(card_no, '', ' ')(加空格)不现实,但若原始数据全是 1234567890123456 这种无分隔符格式,可先用 SUBSTR() 拆再拼,REPLACE() 在这里基本没用武之地。
SUBSTR() + CONCAT() 是 MySQL 5.7 及以下最稳的脱敏组合
低版本 MySQL 没 REGEXP_REPLACE(),靠 SUBSTR() 截头取尾、CONCAT() 拼星号,逻辑清晰、兼容性好、执行快。关键点是位置从 1 开始数,中文和 emoji 不会错位(前提是列字符集为 utf8mb4)。
- 手机号(11 位,隐藏中间 4 位):
CONCAT(SUBSTR(phone, 1, 3), '****', SUBSTR(phone, 8, 4)) - 身份证号(18 位,隐藏出生年月后 8 位):
CONCAT(SUBSTR(id_card, 1, 6), '********', SUBSTR(id_card, 15, 3)) - 姓名(保留姓,其余变 *):
CONCAT(SUBSTR(name, 1, 1), REPEAT('*', CHAR_LENGTH(name) - 1))—— 注意别用REPLACE()模拟,它对长度不敏感
遇到 NULL 值时,CONCAT() 会整条返回 NULL,必须包一层 IFNULL():CONCAT(IFNULL(SUBSTR(name, 1, 1), ''), REPEAT('*', IFNULL(CHAR_LENGTH(name), 0) - 1))。
WHERE 条件里千万别套脱敏函数
只要在 WHERE 或 JOIN ON 里用了 SUBSTR()、CONCAT()、REPLACE(),哪怕只是 WHERE SUBSTR(phone, 1, 3) = '138',MySQL 就无法使用 phone 列上的索引,强制全表扫描。线上查百万级用户表,延迟直接上秒级。
正确做法是:脱敏只出现在 SELECT 列表中;查询条件永远基于原始字段。例如要查“所有手机号以 138 开头的用户”,写 WHERE phone LIKE '138%',而不是 WHERE SUBSTR(phone, 1, 3) = '138'。如果业务真需要按脱敏后形态筛选(比如“姓张”),得在应用层过滤,或建生成列预计算并加索引。
中文姓名脱敏慎用正则,SUBSTR 更可靠
有人想用 REGEXP_REPLACE(name, '^.(.+)$', '*\1') 实现“首字保留、其余打星”,但在 MySQL 5.7 下不可用;即使 8.0+,Unicode 正则支持不稳定,. 在某些版本可能不匹配中文,\u4e00-\u9fa5 范围又未必全覆盖生僻字或 emoji。而 SUBSTR() 按字符计数,只要字符集是 utf8mb4,SUBSTR(name, 1, 1) 就稳稳拿到第一个汉字,CHAR_LENGTH(name) 返回真实字数,不受编码影响。
唯一要注意的是空格和全角符号:如果姓名字段前后有空格,CHAR_LENGTH() 会把它们算进去,导致星号数量不准。上线前务必检查清洗逻辑,或加 TRIM() 包裹:CONCAT(SUBSTR(TRIM(name), 1, 1), REPEAT('*', CHAR_LENGTH(TRIM(name)) - 1))。











