这两年我接手的数据清洗和报表需求里,十个有七个都在跟字符串纠缠。要么是从旧系统导出来的字段带着一堆前后空格和换行,要么是手机号、身份证、订单号混在一个字段里需要拆开,要么是编码规则不统一导致排序结果怎么看怎么别扭。每次遇到这种活儿,我就翻出MySQL那一组字符串函数,三下五除二拼一条SQL搞定。字符串函数在MySQL里属于那种平时不太显眼、但真要处理数据时能救命的东西,它解决的核心问题就是:怎么对文本字段做裁剪、拼接、查找、替换、格式化,以及怎么让这些操作在SQL里直接完成,而不是把数据捞回应用程序里用Java或Python慢慢磨。这篇文章不打算挨个罗列函数文档,而是按照实际使用场景把这些函数串起来讲,适合刚接触MySQL没多久、被各种字符串字段搞到头大的同学,也适合写了好几年SQL但从没系统整理过这类函数的老手。
1. 先搞清楚你到底需要哪类字符串函数
字符串函数在MySQL里的数量接近五十个,看着一大堆,其实真正高频用得上的没有那么多。我自己的习惯是先把它们按“想对字符串做什么”分成四类,用的时候直接到对应分类里找,比对着文档从头翻到尾高效得多。
1.1 按操作目的把函数分类
第一类是拼接与拆分,典型代表是CONCAT、CONCAT_WS、SUBSTRING、SUBSTRING_INDEX。这类函数解决的是“把字段拼起来”和“把字段拆开”的问题。比如客户表里姓和名分两列存,报表要显示全名;或者地址字段里同时包含了省市区,需要单独把省拎出来。
第二类是查找与定位,典型代表是LOCATE、INSTR、POSITION、FIELD。这类函数解决的是“某个子串到底在哪个位置”的问题。平时做数据分析时,经常需要判断某个字段里是否包含某个关键词,或者根据关键词出现的位置去截取后面的内容。
第三类是替换与格式化,典型代表是REPLACE、INSERT、TRIM、LPAD、RPAD、UPPER、LOWER。这类函数解决的是“文本长得不好看,需要调整一下”的问题。比如导出的手机号有的带横杠、有的不带,统一去掉横杠;或者编码长度不齐,左边补零。
第四类是编码与比较,典型代表是CHAR_LENGTH、LENGTH、STRCMP、REGEXP。这类函数解决的是“这个字段到底有多长、两个字符串怎么比较、是否符合某种模式”的问题。它们经常出现在WHERE条件里做筛选,或者用于判断数据质量。
1.2 为什么组合使用比单函数更有价值
单独看每个函数都挺简单,真正拉开效率差距的是组合使用。举个例子,客户地址字段存的是“广东省深圳市南山区科技园路1号”,你想把省份单独取出来,光用一个SUBSTRING搞不定,得先LOCATE找出“省”的位置,再配合SUBSTRING才能截出来。类似这种多函数嵌套的写法,才是处理真实数据时的常态。
我自己写SQL有个习惯:先判断需要哪几步——“先找到目标位置,再截取,最后去空格”,然后把每一步对应的函数写出来,嵌套在一起。这个思路比背函数文档管用得多。下面几章就按这个思路,把实战中最高频的几组函数逐一拆开讲。
2. 拼接与截取:处理字段组合的看家本领
2.1 CONCAT和CONCAT_WS拼接的技巧与NULL陷阱
CONCAT的作用是把多个字符串首尾相连。用法非常直白:
SELECT CONCAT('Hello', ' ', 'World'); -- 结果:Hello World实际业务里最常见的场景是把姓名、地址、订单号等字段拼成一个完整描述。我遇到过最典型的案例是把订单表和用户表联查后,需要生成一个“用户名-订单号-商品名”的合并字段,直接CONCAT三个字段就行。
但CONCAT有一个特别容易踩的坑:只要参与拼接的任何一个字段是NULL,整个结果就变成NULL。这跟很多人的直觉不一样,也是实际开发中线上数据报错、报表显示空白的常见元凶。解决办法有两个:一是用IFNULL先把NULL转成空字符串,二是改用CONCAT_WS。
SELECT CONCAT_WS('-', 'A', NULL, 'C'); -- 结果:A-CCONCAT_WS里的WS是WITH SEPARATOR的意思,第一个参数指定分隔符,后面的参数是要拼接的内容。它的好处有两个:NULL参数会被自动忽略,而不是让整个结果变NULL;分隔符只需要写一次,代码更简洁。比如拼“2024-12-01”这种日期格式,用CONCAT_WS('-', year_col, month_col, day_col)就很方便。
2.2 SUBSTRING系截取:从正数、负数到按分隔符截取
SUBSTRING是字符串截取的核心函数,基本语法是SUBSTRING(str, pos, len),表示从第pos个位置开始取len个字符。注意MySQL的字符串位置是从1开始数的,不是从0开始,这个跟很多编程语言不一样,容易搞混。
SELECT SUBSTRING('abcdef', 2, 3); -- 结果:bcdpos还可以传负数,表示从字符串末尾往前数位置。SUBSTRING('abcdef', -3, 2)的结果是de,因为从右边数第3个字符是d,往后取2个字符。
真正处理业务数据时,SUBSTRING_INDEX比SUBSTRING更常用。它的语法是SUBSTRING_INDEX(str, delim, count),按照分隔符把字符串拆成若干段,然后返回第count段之前的内容。count为正数时从左往右数,为负数时从右往左数。
SELECT SUBSTRING_INDEX('www.example.com', '.', 2); -- 结果:www.example SELECT SUBSTRING_INDEX('www.example.com', '.', -1); -- 结果:com这个函数在处理带分隔符的字段时极其好用。比如在一列里存了“品牌-型号-容量”,想提取容量,可以嵌套使用SUBSTRING_INDEX取最后一段;想提取品牌型号,先取前两段,再取第一段。
2.3 CHAR_LENGTH和LENGTH:字符数和字节数的区别必须分清
这两个函数看起来都跟长度有关,实际上差很多。LENGTH返回的是字符串的字节数,CHAR_LENGTH返回的是字符数。对于英文字母和数字,两者结果一致,因为每个字符占1个字节;但对于中文、表情符号这类多字节字符,LENGTH的值会比CHAR_LENGTH大,具体大多少取决于字符集。
SELECT LENGTH('数据库'), CHAR_LENGTH('数据库'); -- 在utf8下:LENGTH=9,CHAR_LENGTH=3utf8字符集下一个汉字占3个字节,所以“数据库”三个字的LENGTH是9,CHAR_LENGTH是3。如果在utf8mb4下存了emoji表情,一个表情占4个字节,差距更明显。
这个区别在写校验逻辑时很关键。比如限制用户昵称最长20个字符,应该用CHAR_LENGTH而不是LENGTH,否则中文用户还没输入几个字就被拦下来了。判断字段是否有脏数据时,也可以用LENGTH和CHAR_LENGTH不一致来判断字段里是否含中文或特殊字符。
3. 查找与替换:处理脏数据的核心手段
3.1 LOCATE、INSTR和POSITION的定位用法
这类函数解决的是“我的字符串里有没有某个子串,如果有,它在哪里”。LOCATE是最常用的一个,语法是LOCATE(substr, str),返回子串第一次出现的位置,找不到时返回0。
SELECT LOCATE('world', 'hello world hello'); -- 结果:7INSTR(str, substr)和LOCATE的功能基本一致,只是参数顺序相反。POSITION(substr IN str)也是同样的作用,写法更接近英文习惯。三者选一个用就行,我习惯用LOCATE,因为参数顺序跟直觉一致,先写要找的子串,再写从哪里找。
LOCATE在WHERE条件里经常配合判断使用:WHERE LOCATE('关键字', content) > 0。不过需要注意,用一个简单的LIKE '%关键字%'也能实现,但如果要基于子串位置做进一步的截取或判断,LOCATE就是不可替代的了。
3.2 REPLACE做批量替换:UPDATE场景下的威力
REPLACE的语法是REPLACE(str, from_str, to_str),把字符串里的from_str全部替换成to_str。它是处理历史脏数据的一把好手。
我处理过一个经典订单:手机号字段里既有“138-1234-5678”这种带横杠的,又有“138 1234 5678”这种带空格的,还有不带任何格式的。要统一格式,一条UPDATE就行:
UPDATE users SET phone = REPLACE(REPLACE(phone, '-', ''), ' ', '') WHERE phone LIKE '%-%' OR phone LIKE '% %';这种操作比先把数据导出来、用程序处理完再导回去高效得多。注意REPLACE是替换所有匹配项,不是只替换第一个,这点在业务上要清楚。另一个容易忽略的点:REPLACE也可以用于修正历史数据中的错别字或规则变更,比如把旧编码“BJ”统一替换成“BEIJING”,一秒钟完成全表更新。
有一点要提醒:如果REPLACE的目标字段上有索引,大规模UPDATE会触发索引更新,锁表时间可能较长。生产环境大表做这种操作,最好避开业务高峰期,或者分批执行。
3.3 TRIM系列:去除空格之外的额外能力
TRIM最常见的用法是去掉字符串前后的空格。LTRIM只去左边,RTRIM只去右边。实际业务里导出的Excel字段经常带着换行符和回车符,直接用TRIM去不掉,需要配合REPLACE把换行符替换掉。
SELECT TRIM(' abc '); -- 结果:abcTRIM还有一个比较少有人用的扩展语法:TRIM(removed_str FROM str),可以去掉指定字符。比如去掉字符串首尾的“#”号:
SELECT TRIM('#' FROM '###abc###'); -- 结果:abc这在处理被特定符号包裹的字段时很实用。还有平时做报表时经常遇到字符串左右长短不齐,用LPAD和RPAD可以补齐到指定长度。LPAD的意思是LEFT PAD,在字符串左边填充指定字符;RPAD在右边填充。比如编号字段统一补成8位:
SELECT LPAD('123', 8, '0'); -- 结果:000001234. 排序、UPDATE和存储过程里的字符串函数实战
4.1 ORDER BY配合字符串函数解决排序错乱
字符串函数不只是用来SELECT查询,在ORDER BY里也经常要做手脚。MySQL默认排序是按照字符集规则排的,数字字符串排序会按字典序排,所以“10”会排在“9”前面,这是很多人遇到过的坑。
比如版本号字段存的是“1.0.9”、“1.0.10”,直接ORDER BY版本号升序,结果是1.0.10排在1.0.9前面。要按真实数字语义排,可以配合REPLACE去掉小数点后转成数值,或者用CAST转成DECIMAL。更通用的办法是把版本号拆成多段再排,但实现会比较绕。
实际业务里最常见的需求是“从字符串中提取数字后再排序”,比如门牌号“A-101”、“A-102”。低版本MySQL没有REGEXP_REPLACE,要提取数字稍微费劲;MySQL 8.0开始可以用REGEXP_REPLACE把非数字字符替换掉,然后转成数值排序:
SELECT * FROM rooms ORDER BY CAST(REGEXP_REPLACE(room_code, '[^0-9]', '') AS UNSIGNED);4.2 UPDATE结合REPLACE修正全表数据
前面提过用REPLACE清洗手机号,这里补充一个更复杂一点的场景:把地址字段里的“XX市”统一改成“XX省XX市”。比如一批数据里存的是“深圳市南山区”,需要变成“广东省深圳市南山区”,但省名跟市名不是一个字段,就得根据固定的市名列表逐条处理。
这种场景下用UPDATE加REPLACE依然高效,但要注意匹配的准确性,防止把“深圳市”错误匹配到“深州市”这类相近地名。如果规则比较复杂,我的建议是在UPDATE之前先SELECT统计一下受影响的行数,确认无误后再执行UPDATE。
还有一个实际操作中的经验:UPDATE之前先备份原表,或者把原值复制到一个新的临时列,便于出错时回滚。特别是影响的数据量比较大时,这种回退手段是必须的。
4.3 存储过程和GROUP BY里的字符串函数组合
存储过程是另一个字符串函数大量出没的地方。我之前写过一个批量清理日志数据的存储过程,流程就是先用SUBSTRING_INDEX解析日志文件名里的日期,再判断日期是否在保留范围内,最后拼接DELETE语句删除对应表的数据。中间用了至少五六个字符串函数,包括CONCAT、SUBSTRING_INDEX、DATE_FORMAT配合使用。
GROUP BY里用字符串函数的场景也很常见。比如按地址字段里的省份分组统计用户数,就需要先截取出省份字段再用它分组。由于MySQL的GROUP BY可以在SELECT后面使用别名,写法可以很简洁:
SELECT SUBSTRING_INDEX(address, '省', 1) AS province, COUNT(*) AS user_count FROM users GROUP BY province;这里先截取省份,再用province作为别名分组。需要注意GROUP BY中使用别名是MySQL允许的扩展功能,其他数据库不一定支持。
5. 正则匹配、大小写与编码:进阶场景不能乱来
5.1 REGEXP做灵活匹配:LIKE的加强版
LIKE查询只能做简单的通配符匹配,正则表达式则能处理更复杂的模式。MySQL里用REGEXP关键字(同义词是RLIKE)做正则匹配,比如判断手机号是否合法:
SELECT * FROM users WHERE phone REGEXP '^1[3-9][0-9]{9}$';这条语句能筛选出符合大陆手机号基本格式的记录。再比如找到所有以数字开头、以字母结尾的编码字段。正则的灵活性远高于LIKE,但要注意性能问题。正则匹配没法利用普通索引,每行都要全表扫描判断,数据量大时查询会明显变慢。
5.2 大小写转换与排序规则(COLLATE)的关系
UPPER和LOWER函数可以对字符串做大小写转换,这在做不区分大小写的匹配时很有用。比如用户输入的用户名可能是大写或小写,统一转成小写再比较可以避免漏查:
SELECT * FROM users WHERE LOWER(username) = LOWER('Admin');但这里有个性能隐患:左边用了函数后,索引失效。更推荐的做法是把查询条件也转成小写后跟字段比较,或者应用“字段函数”改为“条件函数”。另外MySQL的排序规则本身也影响大小写比较,例如utf8mb4_general_ci和utf8mb4_bin对字母大小写是否敏感就不同。前者大小写不敏感,后者大小写敏感。平常开发时要注意表的COLLATE设置,否则明明两个值看起来一样,WHERE却匹配不到。
5.3 字符串函数在WHERE条件里的索引失效问题
这是字符串函数使用中最值得注意的性能问题。只要WHERE里对字段用了函数,MySQL就难以利用该字段上的索引,因为它需要先对每一行做函数运算,再比较结果。这在数据量小的表上感觉不明显,在千万级大表上就是灾难。
-- 索引失效写法 SELECT * FROM t WHERE LEFT(name, 3) = '张'; -- 可以考虑的替代写法(范围条件,索引可用) SELECT * FROM t WHERE name LIKE '张%';我遇到过不少案例是把这种写法写进了线上接口,结果分页查询越来越慢,直到超时才排查出来。所以写SQL时有一个习惯:尽量避免在WHERE子句中对索引列应用函数,如果实在需要用,优先考虑改写为范围查询或者等值查询;或者建立函数索引(MySQL 8.0支持在索引中使用表达式)来缓解。
6. 高频问题排查速查:从报错到结果异常
6.1 字符串函数常见坑对照表
用字符串函数时遇到的问题,大多不是函数本身报错,而是结果跟预期不一致。我把实战中遇到的高频问题整理成了下面这张表。
| 问题现象 | 常见原因 | 解决方案 |
|---|---|---|
| CONCAT结果变成NULL | 某个参数为NULL | 用IFNULL包裹参数,或改用CONCAT_WS |
| 中文字符串长度不对 | 用了LENGTH而不是CHAR_LENGTH | 长度校验用CHAR_LENGTH |
| 排序结果数字错乱 | 字符串按字典序排列 | 用CAST/REGEXP_REPLACE转为数值排序 |
| 两端空格去不掉 | 存在换行符或制表符 | 先用REPLACE去掉\r\n再TRIM |
| SUBSTRING截取结果少一位 | 位置从1开始而不是0 | 检查pos参数是否从1开始数 |
| 大小写匹配不到 | 排序规则大小写不敏感 | 改用utf8mb4_bin或使用COLLATE指定规则 |
| WHERE用函数后变慢 | 索引失效 | 改写为范围条件或函数索引 |
| 版本号排序不对 | 点分字符串字典序问题 | 拆段转数值再排序 |
6.2 我的排查习惯与两个保底技巧
遇到字符串函数结果不对时,我的第一步永远是单独SELECT出来看,不直接套UPDATE。比如怀疑某个字段里有没有看不见的字符,就把字段转成十六进制看它的二进制内容:
SELECT HEX(column_name) FROM table_name WHERE id = 1;这个办法能直接看到不可见字符的编码,例如空格是20,换行是0A,回车是0D。很多时候数据“看似干净”实际不干净,用HEX一查就现原形。
第二个保底技巧是在做复杂字符串处理之前,先用SELECT把处理后的结果查出来检查,确认无误后再用同样的表达式放进UPDATE。这种做法虽然多一步操作,但能避免错误UPDATE写坏整张表。
还有一次排查乱了半天,发现是字符集问题:表的字符集是latin1,英文字符正常,中文全变成了乱码,当时用的LENGTH、SUBSTRING这些函数都基于当前字符集解析,结果自然全错。后来统一改成utf8mb4后问题消失。这里给各位提个醒:遇到莫名其妙的中文截断或长度不对,先用下面这条语句看一下表的字符集:
SHOW CREATE TABLE table_name;6.3 实际走一遍:从需求到SQL的过程
最后用一个完整的例子串一遍今天讲的这些函数。假设有个需求:从订单表里提取出客户地址的省份,按省份统计订单数量,并生成“省份-数量”的拼接字段,同时只统计数量大于100的省份,按数量降序排列。
地址字段存的是“XX省XX市XX区XX路XX号”这类格式。
先写统计SQL:
SELECT SUBSTRING_INDEX(address, '省', 1) AS province, COUNT(*) AS order_count FROM orders GROUP BY province HAVING order_count > 100 ORDER BY order_count DESC;如果要生成拼接字段,就再套一层CONCAT或CONCAT_WS:
SELECT CONCAT(SUBSTRING_INDEX(address, '省', 1), '-', COUNT(*)) AS province_order FROM orders GROUP BY province HAVING COUNT(*) > 100 ORDER BY COUNT(*) DESC;这个例子看着不复杂,但它把SUBSTRING_INDEX、CONCAT、GROUP BY、HAVING、ORDER BY全部串起来了,而且每一步背后都有明确的业务意图,不是生搬硬套。
就我个人的经验而言,MySQL字符串函数的学习节奏应该是“先掌握最核心的七八个,再到具体业务场景里补充”,不需要一开始就把文档啃完。真正让我熟练起来的,不是看文档,而是不断处理类似“地址解析”“手机号清洗”“编码转换”这种需求。每处理一次,对函数组合的理解就深一层。下次再遇到字符串相关的需求,你可以先别急着写代码,停下来想一想:这个操作用哪几个函数能实现?组合顺序是什么?NULL怎么处理?字符集有没有影响?想清楚这几步,SQL基本就写对了。