MySQL字符串函数实战:拼接截取、数据清洗与脱敏全解析 昨天帮运营拉一份用户名单需求其实很简单——手机号中间四位打码、昵称去掉首尾空格、再从邮箱里把用户名和域名拆出来。我刚写完几个 MySQL 字符串函数的组合旁边新来的同事看了一眼就愣住了原来 SQL 里还能这么拆字符串说实话MySQL 字符串函数我用了三四年最深的感受是函数本身不难背难的是知道在什么场景用哪个以及那些文档里不会写、一跑就翻车的隐藏规则。这篇笔记不按官方手册的目录顺序来而是按我平时写查询、做数据清洗时真实遇到的场景组织。从最常用的拼接和截取到容易混淆的字节与字符计算再到定位查找的选型、清理格式化的细节最后用一个完整的脱敏清洗案例把这些函数串起来。不管你是刚接触 SQL 的新手还是写过不少业务查询的老手这篇应该都能帮你少踩几个坑。1. 拼接不只是 CONCATCONCAT_WS、NULL 与隐式转换1.1 CONCAT 最容易被 NULL 摆一道CONCAT(str1, str2, ...)是字符串拼接最基础的函数但它有一个很多人知道、却依然会踩的规则只要任何一个参数是 NULL整个结果就是 NULL。SELECT CONCAT(user_, NULL); -- 结果为 NULL SELECT CONCAT(user_, 10001); -- 结果为 user_10001 SELECT CONCAT(NULL, -, abc); -- 结果为 NULL为什么会这样因为 MySQL 遵循 SQL 的三值逻辑NULL 表示未知。一个未知的值拼在任何字符串后面结果依然是未知所以干脆整体返回 NULL。这在业务上非常恶心比如你拼一个用户展示名中间某个字段没填整行数据都显示空白。解决思路也很直接拼接前先处理 NULL。SELECT CONCAT(IFNULL(first_name, ), , IFNULL(last_name, )); SELECT CONCAT(COALESCE(remark, 暂无备注), - , nickname);IFNULL只能处理两个参数COALESCE可以给一串候选值取第一个非 NULL 的。我的习惯是只要拼接的字段可能为空一律先包一层 COALESCE省得后面排查数据时莫名其妙少了一整列。1.2 CONCAT_WS带分隔符拼接强烈推荐日常使用CONCAT_WS(separator, str1, str2, ...)的第一个参数是分隔符后面是你要拼的内容。它最大的优势是分隔符只出现在非 NULL 参数之间NULL 参数会被自动跳过不会像 CONCAT 那样一票否决。SELECT CONCAT_WS(-, 2025, 01, 01); -- 2025-01-01 SELECT CONCAT_WS(-, 2025, NULL, 01); -- 2025-01 SELECT CONCAT_WS(, , 北京, 上海, NULL, 广州); -- 北京, 上海, 广州看第二行和第三行NULL 直接被忽略分隔符也不会多出来。这个特性在做标签拼接、地址拼接、展示列拼接时非常省心。但有两个细节要注意分隔符本身如果是 NULL结果还是 NULL。这个逻辑不难理解分隔符都没有了拼接自然无从谈起。CONCAT_WS 是变长参数不能直接传一个数组进去。如果你需要把一列多行的值拼成一行那是聚合函数GROUP_CONCAT的活别混了。SELECT GROUP_CONCAT(nickname SEPARATOR 、) FROM users GROUP BY dept_id;GROUP_CONCAT在分组内做拼接SEPARATOR指定分隔符默认是逗号。这个函数和 CONCAT_WS 看起来像使用场景完全不同一个是横向拼字段一个是纵向拼行。1.3 拼接数字时的隐式转换CONCAT 遇到数字参数会自动转成字符串所以CONCAT(订单号: , order_id)这种写法没问题。但有个隐藏问题当数字是浮点时转换结果可能不符合你的预期。SELECT CONCAT(金额: , 12345.678); -- 金额: 12345.678 SELECT CONCAT(金额: , 1e20); -- 金额: 1e20部分浮点数会变成科学计数法某些大浮点数在转换成字符串时会显示成科学计数法这对金额展示来说完全不可接受。正确的做法是先用FORMAT或CAST把数字规范化再参与拼接。后面第 5 节讲格式化时会细说。个人经验能选 CONCAT_WS 就不选 CONCAT尤其是多个字段拼接时NULL 处理规则能少很多事。CONCAT 只适合那种必须让 NULL 暴露出来的场景比如排查数据质量问题。2. 字节和字符不是一回事LENGTH、CHAR_LENGTH 与中文编码2.1 同样一个中LENGTH 是 3CHAR_LENGTH 是 1这是新手最容易栽的坑。LENGTH(str)返回字符串的字节长度CHAR_LENGTH(str)返回字符个数。在utf8mb4字符集下一个汉字占 3 个字节一个 Emoji 占 4 个字节而英文字母和数字只占 1 个字节。SELECT CHAR_LENGTH(你好) AS chars, LENGTH(你好) AS bytes; -- chars 2, bytes 6 SELECT CHAR_LENGTH(A中) AS chars, LENGTH(A中) AS bytes; -- chars 2, bytes 4 SELECT CHAR_LENGTH(hello) AS chars, LENGTH(hello) AS bytes; -- chars 5, bytes 5如果你拿LENGTH去判断用户昵称是否超过 10 个字符遇到中文用户会出现严重的误判——明明只有 5 个汉字LENGTH已经返回 15 了。所有涉及字数限制的场景都应该用 CHAR_LENGTH。2.2 LEFT、RIGHT、SUBSTRING 都是按字符切不按字节切与此对应LEFT(str, n)、RIGHT(str, n)、SUBSTRING(str, pos, len)这些截取函数第二个参数全部是字符数不是字节数。这点和 LENGTH 混在一起会让人十分混乱。SELECT LEFT(你好world, 3); -- 你好w截取的是 3 个字符不是 3 个字节 SELECT RIGHT(你好world, 3); -- rld我在帮别人排查数据时经常看到这样的 SQLWHERE LENGTH(name) 20本意是限制姓名长度结果把一堆正常的中文姓名也筛出来了。正确的写法是WHERE CHAR_LENGTH(name) 20。先搞清楚字节和字符的差异截取和长度判断才不会出偏差。2.3 为什么 VARCHAR 字段设计要关注字节很多人不理解为什么 VARCHAR(255) 在某些字符集下只能建那么长的索引。因为 InnoDB 索引键前缀有长度限制一般按字节计算utf8mb4 下一个字符最多 4 字节因此一个 VARCHAR(255) 的列做索引时索引键最大可能占用 255 × 4 1020 字节。如果表里索引列很多很容易撞上索引长度上限。实操上的建议是存储路径、URL、长文本标签这类字符串能别设太长就别设太长。不是每个业务字段都需要 VARCHAR(255)很多场景 VARCHAR(64) 绰绰有余。这个习惯配合后面讲到的索引优化能帮你省掉不少麻烦。3. 拆字段的三种姿势SUBSTRING_INDEX、SUBSTRING 与正则的取舍3.1 SUBSTRING_INDEX一刀切到第 n 个分隔符SUBSTRING_INDEX(str, delim, count)是我做数据清洗时使用频率最高的函数。它从字符串里按分隔符计数count 为正数时从左往右数count 为负数时从右往左数。返回值是到第 count 个分隔符为止/之后的那一段。SELECT SUBSTRING_INDEX(aexample.com, , 1); -- a SELECT SUBSTRING_INDEX(aexample.com, , -1); -- example.com SELECT SUBSTRING_INDEX(2025-01-01, -, 2); -- 2025-01 SELECT SUBSTRING_INDEX(user-1001-dept, -, 1); -- user这个函数提取邮箱用户名、域名、日期前缀、路径中某一段时堪称神器。比如日志里存了api/v1/users/10001你想取最右边的 IDSELECT SUBSTRING_INDEX(api/v1/users/10001, /, -1); -- 10001有个重要陷阱当分隔符不存在时函数返回整个字符串。SELECT SUBSTRING_INDEX(no-delimiter-here, , 1); -- 返回 no-delimiter-here而不是报错也就是说如果数据里没有邮箱域名的提取结果会是整个原始字符串。所以做提取前最好先确认数据确实包含分隔符否则下游统计会把脏数据当成合法域名。可以用LOCATE先判断或直接加WHERE email LIKE %%过滤。想取中间段怎么办嵌套两层-- 取 user-1001-dept 中间那一段 1001 SELECT SUBSTRING_INDEX(SUBSTRING_INDEX(user-1001-dept, -, 2), -, -1);先取前两段得到user-1001再从右边取第一段得到1001。这种嵌套看着别扭但实际用起来很稳定比正则函数直观得多。3.2 SUBSTRING 和 SUBSTR 是同义词但起始位置从 1 开始SUBSTRING(str, pos, len)和SUBSTR(str, pos, len)功能完全一致还有一个 SQL 标准写法SUBSTRING(str FROM pos FOR len)三种都能用。最经典的错误是取第一个字符时用了SUBSTRING(str, 0, 1)。结果返回空字符串。因为这个函数的索引从 1 开始计数不是从 0。正确的写法是SELECT SUBSTRING(abc, 1, 1); -- a SELECT SUBSTRING(abc, 2); -- bc SELECT LEFT(abc, 1); -- a如果你是从 Python、JavaScript 切过来的特别容易在这个位置卡一下。两者的下标语义完全不同SQL 字符串位置一律从 1 开始。3.3 正则函数能不用尽量不用MySQL 8.0 提供了REGEXP_SUBSTR、REGEXP_REPLACE、REGEXP_INSTR等函数功能很强但我一般只在用 SUBSTRING_INDEX 实在搞不定时才用。原因有两点正则可读性差几个月后回来看代码REGEXP_SUBSTR(name, ^[a-z])得想半天当初是干什么用的。性能开销大正则匹配无法走索引对全表数据做正则提取数据量一大性能就崩。举一个 SUBSTRING_INDEX 替代正则的例子从order_20250101_100中提取日期部分20250101。正则写法是REGEXP_SUBSTR(str, [0-9]{8})而用 SUBSTRING_INDEX 可以先按_拆取第二段SELECT SUBSTRING_INDEX(SUBSTRING_INDEX(order_20250101_100, _, 2), _, -1);虽然嵌套看起来啰嗦但执行效率通常优于正则也没那么难维护。优先用低成本的字符串函数正则作为兜底方案这是我个人的一个原则。4. 查找定位函数别记混LOCATE、INSTR、POSITION 与 LIKE 怎么选4.1 四个函数三种返回值参数顺序还不同字符串查找这一组新手非常容易把参数顺序搞反。先把用法列清楚函数语法返回值找不到时LOCATELOCATE(substr, str, [pos])首次出现的位置从 1 开始0INSTRINSTR(str, substr)首次出现的位置从 1 开始0POSITIONPOSITION(substr IN str)首次出现的位置从 1 开始0LIKEstr LIKE pattern1 表示匹配0 表示不匹配0注意看参数顺序LOCATE是子串在前原串在后INSTR是原串在前子串在后。这两个恰恰是反的特别容易写混。POSITION(substr IN str)其实等价于LOCATE(substr, str)只是写法更接近自然语言。SELECT LOCATE(abc, xxabcxx); -- 3 SELECT INSTR(xxabcxx, abc); -- 3 SELECT POSITION(abc IN xxabcxx); -- 34.2 LOCATE 的第三个参数从指定位置继续找LOCATE 比 INSTR 多一个参数可以在查找时指定起始位置。这在处理第二次出现的场景时非常实用。比如字符串a.b.c.d你想找点号第二次出现的位置可以这样SELECT LOCATE(., a.b.c.d, LOCATE(., a.b.c.d) 1); -- 4先用内层的 LOCATE 找到第一个点号位置 2然后从第 3 位开始找下一个点号返回 4。这种写法在做路径解析、协议解析时很有用。4.3 LIKE 和 LOCATE 的定位差异LIKE 返回值是 0 或 1适合直接放在 WHERE 条件里做过滤SELECT * FROM users WHERE nickname LIKE %张三%;LOCATE 返回的是位置除了判断是否存在还可以顺便拿到在哪里。如果你的业务需要知道子串在字段里的具体位置比如根据位置再去做截取那用 LOCATE 更合适。SELECT email, LOCATE(, email) AS at_pos, SUBSTRING(email, 1, LOCATE(, email) - 1) AS username FROM users;这个 SQL 的逻辑是先找到的位置再截取它前面的部分作为用户名。用 LOCATE 拿到位置后直接传给 SUBSTRING两个函数配合得非常好。我的选择标准只判断存不存在用LIKE或LOCATE(...) 0语义都很清晰。需要位置信息配合截取用LOCATE。多个分隔符要精确定位优先考虑 SUBSTRING_INDEX 而不是反复 LOCATE后者容易嵌套成天书。4.4 一个容易误判的语义0 表示没找到很多从 0 起步语言转过来的同学看到LOCATE找不到返回 0会下意识觉得返回 0 是不是代表第一个字符千万注意SQL 里位置从 1 开始0 是不存在的专属返回值。所以判断时应该写WHERE LOCATE(abc, name) 0不要直接WHERE LOCATE(abc, name)。虽然 MySQL 里 0 在布尔上下文就是 false但为了可读性显式写 0更安全。4.5 如果要做复杂模式匹配LIKE 支持%和_但要做正则式的复杂匹配比如手机号以 13x 开头就该用REGEXPSELECT * FROM users WHERE phone REGEXP ^13[0-9]{9}$;REGEXP返回的也是 1 或 0和 LIKE 类似但语法更灵活。代价是性能这一点在最后一章会专门讲。5. 清理和格式化TRIM 家族、大小写、填充补位与金额显示5.1 TRIM 不只是去空格还能去指定字符TRIM(str)默认去掉字符串两端的空格LTRIM(str)只去左边RTRIM(str)只去右边。日常数据清洗里最常见的用法是UPDATE users SET nickname TRIM(nickname) WHERE nickname TRIM(nickname);MySQL 8.0 开始TRIM 还支持指定移除的字符SELECT TRIM(x FROM xxxabcxxx); -- abc SELECT TRIM(LEADING 0 FROM 00123); -- 123 SELECT TRIM(TRAILING 0 FROM 1.2300); -- 1.23这里的逻辑是从左边或右边连续移除指定的字符不是全局删除。比如TRIM(TRAILING 0 FROM 1.2300)会去掉末尾连续的 0得到1.23但不会影响字符串中间的 0。有一个容易忽略的点TRIM 默认只处理空格字符对于\t、\n、\r这些不可见字符不管用。清洗文本数据时经常需要先REPLACE(str, \t, )、REPLACE(str, \n, )再做 TRIM。SELECT TRIM(REPLACE(REPLACE(raw_text, \n, ), \t, ));5.2 UPPER / LOWER 与排序规则的微妙关系UPPER(str)和LOWER(str)是大小写转换最简单的方式SELECT UPPER(mysql); -- MYSQL SELECT LOWER(MySQL); -- mysql但有一个背景知识一定要知道MySQL 的默认排序规则通常对大小写不敏感。比如utf8mb4_0900_ai_ci这种排序规则下WHERE email AB.COM和WHERE email ab.com查出来的结果是一样的因为字符串比较时大小写被视为相同。那 UPPER / LOWER 什么时候才有用当你把列显式改成binary或使用utf8mb4_bin排序规则时比较变成大小写敏感这时候可能需要 LOWER 把两边统一。另一种场景是生成一个新的全小写列用于分组统计SELECT LOWER(domain) AS domain_lower, COUNT(*) FROM emails GROUP BY LOWER(domain);5.3 LPAD / RPAD 补位的反直觉细节LPAD(str, len, padstr)和RPAD(str, len, padstr)用于在字符串左侧或右侧填充指定字符达到固定长度。经典场景是订单号补零SELECT LPAD(123, 8, 0); -- 00000123 SELECT RPAD(123, 8, 0); -- 12300000这里有一个非常反直觉的规则第二个参数是最终总长度不是补多少个字符。LPAD(123, 8, 0) 的意思是把结果补到 8 位而不是补 8 个 0。更坑的是如果原字符串长度已经超过 len函数会做截断。SELECT LPAD(123456789, 3, 0); -- 123前面被截掉了 SELECT RPAD(123456789, 3, 0); -- 123后面被截掉了这意味着如果你对一个可能超过 8 位的订单号直接LPAD(id, 8, 0)超过的部分会被静默截断。订单号直接丢数据这是线上事故级别的 bug。更稳妥的写法是加一个判断SELECT IF(LENGTH(id) 8, id, LPAD(id, 8, 0)) FROM orders;5.4 FORMAT金额显示的规范姿势FORMAT(X, D)把数字转为字符串并保留 D 位小数同时带上千分位分隔符SELECT FORMAT(1234567.891, 2); -- 1,234,567.89 SELECT FORMAT(1234567.891, 0); -- 1,234,568四舍五入 SELECT FORMAT(1234.5, 2); -- 1,234.50注意两点返回结果是字符串不是数字。如果后续还要参与运算记得CAST(FORMAT(x, 2) AS DECIMAL(10,2))转回去。它自带四舍五入和 ROUND 有点类似但 FORMAT 的语义是展示ROUND 的语义是计算。简单说要给人看的用 FORMAT要参与计算的用 ROUND。再配合拼接场景SELECT CONCAT(¥, FORMAT(price, 2)) AS display_price FROM products;我之前见过有人直接CONCAT(¥, price)价格一上万显示就会变得乱七八糟。先 FORMAT 再拼接展示效果稳定很多。6. 一次真实的数据清洗任务手机号脱敏、昵称清理与域名统计6.1 需求与表结构纸上谈兵到此为止接下来用一个完整案例把这些函数串起来。假设有一张模拟用户表users字段如下CREATE TABLE users ( id BIGINT PRIMARY KEY, nickname VARCHAR(50), phone VARCHAR(20), email VARCHAR(100), created_at DATETIME );业务方提了三个需求导出一份手机号脱敏的用户清单手机号中间四位用****代替。昵称可能存在首尾空格需要清理。按邮箱域名统计用户数量方便做用户来源分析。6.2 先写 SELECT 确认再动手 UPDATE第一次做清洗的人最容易犯的错是直接UPDATE跑完看不见数据变化也不知道自己改错了多少行。我的习惯是先用 SELECT 把结果查出来肉眼确认没问题再执行 UPDATE。手机号脱敏SELECT id, CONCAT(LEFT(phone, 3), ****, RIGHT(phone, 4)) AS masked_phone FROM users;这里的逻辑很简单LEFT(phone, 3)取前 3 位RIGHT(phone, 4)取后 4 位中间用****拼接。输出效果是138****5678。昵称清理SELECT id, nickname AS old_value, TRIM(nickname) AS new_value FROM users WHERE nickname TRIM(nickname);WHERE nickname TRIM(nickname)可以精确找出清理前后不一致的行这样你能确认到底有多少条数据被影响。注意如果nickname本身是 NULL这个条件会把 NULL 排除掉因为 NULL 和任何值比较都是未知。如果想覆盖 NULL 的情况要再加OR nickname IS NOT NULL之类的条件业务上自己判断。检索确认无误后执行 UPDATEUPDATE users SET nickname TRIM(nickname) WHERE nickname TRIM(nickname);更新昵称的同时执行手机号脱敏的 UPDATEUPDATE users SET phone CONCAT(LEFT(phone, 3), ****, RIGHT(phone, 4)) WHERE CHAR_LENGTH(phone) 11;为什么加CHAR_LENGTH(phone) 11因为如果某个手机号数据本身不合法、长度不对脱敏会得到一串莫名其妙的字符串。先卡住长度只处理标准 11 位手机号稳妥得多。6.3 邮箱域名统计统计邮箱域名的核心是用SUBSTRING_INDEX(email, , -1)取出后面的部分再分组计数。SELECT SUBSTRING_INDEX(email, , -1) AS domain, COUNT(*) AS user_count FROM users WHERE email LIKE %% GROUP BY domain ORDER BY user_count DESC;WHERE email LIKE %%是为了过滤没有的脏数据避开 SUBSTRING_INDEX 遇到无分隔符时返回整个字符串的坑。如果你还要顺便清洗邮箱两边空格可以包一层 TRIM或者干脆先执行一次 UPDATE 把 email 标准化。6.4 把复杂逻辑收敛成函数当多个业务方都依赖手机号脱敏这种逻辑时把 SQL 片段写到处散落不是好主意。MySQL 支持自定义函数可以把脱敏逻辑放进一个函数里CREATE FUNCTION mask_phone(p_phone VARCHAR(20)) RETURNS VARCHAR(20) DETERMINISTIC RETURN CONCAT(LEFT(p_phone, 3), ****, RIGHT(p_phone, 4));之后所有查询都写SELECT id, mask_phone(phone) FROM users;这样规则只维护一处。DETERMINISTIC表示同一输入永远返回同一输出这个声明对函数优化和建索引都有帮助。自定义函数不可滥用但对于多处复用的固定字符串规则它确实能降低维护成本。6.5 清洗后的验证数据更新完别急着交付。几个简单的验证 SQL 能快速发现异常-- 检查手机号脱敏后格式是否符合预期 SELECT phone FROM users WHERE phone NOT REGEXP ^[0-9]{3}\*{4}[0-9]{4}$; -- 昵称清理后是否还有多余空格 SELECT nickname FROM users WHERE nickname LIKE % % OR nickname LIKE % OR nickname LIKE % ; -- 邮箱域名统计是否为 0 行 SELECT domain, COUNT(*) FROM ( SELECT SUBSTRING_INDEX(email, , -1) AS domain FROM users WHERE email LIKE %% ) t GROUP BY domain HAVING COUNT(*) 0;用正则去反查不符合规则的数据比正向验证更高效。脱敏后如果还能找到连续 4 位星号之外的原始手机号模式说明规则没跑对或者有脏数据混进来了。7. 性能与正确性的底线索引失效、隐式转换和排序规则7.1 WHERE 子句里对列用函数索引基本就废了这是字符串函数最容易被忽略的代价。下面这些写法看着没毛病但实际上都会让索引失效引发全表扫描-- 无法走索引 SELECT * FROM users WHERE LENGTH(nickname) 10; SELECT * FROM users WHERE UPPER(email) AB.COM; SELECT * FROM users WHERE CONCAT(last_name, first_name) 张三;原因很简单索引是按原始列值组织的你把它包进函数后索引里的顺序对函数结果没有参考价值优化器只能放弃索引。小表无所谓大表这是灾难。对策之一是生成列。MySQL 5.7 以上可以创建一个存储生成列把函数结果事先算好再对这个列建索引ALTER TABLE users ADD COLUMN email_lower VARCHAR(100) GENERATED ALWAYS AS (LOWER(email)) STORED; CREATE INDEX idx_email_lower ON users(email_lower);之后查询WHERE email_lower ab.com就能正常走索引。代价是存储空间换来的是查询性能适合那种高频查询场景。7.2 字符串与数字的隐式转换MySQL 在比较字符串和数字时会做隐式类型转换。这个转换方向经常造成两个问题索引失效和精度丢失。-- 手机号列是 VARCHAR 类型 SELECT * FROM users WHERE phone 13800138000;这条 SQL 中13800138000是数字字面量MySQL 会把 phone 列转换为数字再比较。一旦对列做转换索引就废了。更严重的是超过一定位数的数字在转换过程中可能出现精度问题尤其是用浮点表示大整数时末尾几位可能对不上。正确的写法是保持字符串类型一致SELECT * FROM users WHERE phone 13800138000;再看一个更隐蔽的例子SELECT 2 02; -- 返回 1因为 02 被转成了数字 2 SELECT 10abc 10; -- 返回 1非数字部分被忽略10abc 10返回 1 这件事应该能直观感受到隐式转换的野性。所以业务查询里能用字符串就用字符串别依赖 MySQL 的智能转换。7.3 字符集和排序规则不一致可能直接报错字符串函数运算时如果两个参数来自不同的字符集或排序规则MySQL 可能报错Illegal mix of collations比如一张历史表用utf8mb3新表用utf8mb4JOIN 时对两个表的字符串列做 CONCAT就会触发这类错误。解决办法是显式统一字符集SELECT CONCAT(old_column COLLATE utf8mb4_0900_ai_ci, new_column) FROM old_table JOIN new_table ON ...或者用CONVERTSELECT CONCAT(CONVERT(old_column USING utf8mb4), new_column)在建表时统一用utf8mb4并统一排序规则能省掉后续大量麻烦。临时的跨表比对用COLLATE显式指定即可。7.4 不是所有字符串处理都必须放在 SQL 里最后说一点系统层面的经验。SQL 的字符串函数适合做轻量级、规则简单、在数据源就近完成的操作比如字段裁剪、格式统一、简单脱敏。但如果清洗规则很复杂比如要解析多层嵌套的 JSON、需要循环判断多个分隔符、根据上下文决定保留哪一段这些工作交给 Python、Java 等外部脚本处理会更清晰。原因有二SQL 写复杂逻辑很难调试也没有成熟的测试环境。大数据量下一个嵌套五层的函数表达式会明显拉高单条查询耗时。我的习惯是能用一条简单 SQL 说清楚的规则就用 SQL超过两三层嵌套或者需要条件判断分支的拉出来交给脚本。这样数据库压力更小脚本逻辑也更可控。回头看看这些年写过的 SQL字符串函数本身不复杂真正让人在线上栽跟头的几乎都是字符集不一致、隐式转换、函数包裹索引列这些函数之外的问题。函数是兵器字符集和类型是内功。内功不稳再好的兵器也会伤到自己。