1. concat()函数是什么,为什么大家都在用
做MySQL开发的人,几乎每天都会碰到这种需求:把几个字段拼成一个字段输出。比如用户表里last_name和first_name分开存,前端要显示完整姓名;订单表里省份、城市、详细地址分开存,后台要导出一列完整地址;商品表里名称、规格、单位各是各的字段,报表里却要合成一列“商品描述”。这些场景的核心操作就是字符串拼接,而MySQL里最基础、最常用的拼接函数就是concat()。
concat()函数的作用非常直接:把两个或多个字符串参数按顺序连接成一个字符串。语法也特别简单:
CONCAT(str1, str2, str3, ...)括号里写几个参数都行,函数会从左到右依次拼接,返回拼接后的结果。它解决的核心问题就是把分散在不同字段、不同列里的数据,快速组合成一条完整的信息,省去了在应用层循环拼接的麻烦。
这篇东西适合所有跟MySQL打交道的人看。刚入门的新人能搞懂concat的基本用法和理解它和CONCAT_WS、GROUP_CONCAT的区别;做了两三年的开发可以重点看NULL值处理、类型转换和性能优化这些坑;即使是DBA或者数据分析师,遇到报表导出、字段合并这类需求时,文里整理的排查思路也能直接参考。
我在实际项目里用concat的地方很多,而且踩过的坑也不算少。最典型的就是拼接结果突然变成NULL,排查半天发现是某个字段本来就是NULL,而concat函数对NULL的处理非常“轴”,只要有一个参数是NULL,整个结果就是NULL。这个特性让很多第一次用的人懵住,所以下面我会花不少篇幅讲清楚NULL的处理方案。
2. concat()的核心用法与应用场景
2.1 语法拆解:参数类型与返回值
先看concat()的参数。从官方文档的定义来说,concat()接受一个或多个参数,参数可以是字符串类型,也可以是能隐式转换为字符串的类型,比如整数、小数、日期时间等。函数执行时会把每个参数都先转成字符串,然后按顺序拼接。
举个例子:
SELECT CONCAT('MySQL', ' ', '8.0'); -- 结果:MySQL 8.0 SELECT CONCAT(2024, '-', 12, '-', 31); -- 结果:2024-12-31第二个例子里,整数2024、12、31都被隐式转成了字符串再拼接。这种自动转换虽然方便,但也容易带来隐患,后面我会单独说类型转换的坑。
concat()的返回值是字符串类型。如果所有参数都为NULL,会返回NULL;如果所有参数都是非NULL,返回拼接后的字符串;如果参数里有任何一个为NULL,前面说了,结果也会是NULL。这一点是concat最需要记住的行为特征。
2.2 适用场景:从用户表到报表字段
我在工作中总结下来,concat()主要解决这几类问题:
第一类:人的信息整合。用户表把姓和名拆开存,查询时要想显示全名,直接写CONCAT(last_name, first_name)就行。同样的情况还有联系方式,有些表把区号、座机号、分机号分三个字段存,导出通讯录时就要拼成一串。
第二类:地址信息整合。省、市、区、街道分别建字段是规范化设计的常见做法,但业务上经常需要完整地址。用CONCAT_WS或者concat把省市区街道串起来,几秒钟搞定。
第三类:业务编号与描述生成。比如订单号+状态拼接成可读性更强的日志,商品名称+规格+单位拼成报表里的完整商品描述,这些都是在SQL层直接完成,不需要把数据捞到应用层再循环处理。
第四类:与CASE WHEN、IF等条件判断结合,做动态拼接。比如根据订单状态字段拼出“已支付-金额”这种描述,或者在导出文件时拼上固定的表头、标记字符。
这些场景的共同特点是:数据本身就是结构化存储的,但展示端需要的是合并后的信息。用concat在数据库里完成,代码简洁,性能可控,也方便后续维护。
3. 关键细节:NULL值、分隔符与同族函数对比
3.1 踩坑率最高的NULL值问题
先说NULL。这是concat()绕不开的重点。
直接看效果:
SELECT CONCAT('MySQL', NULL, '8.0'); -- 结果:NULL明明有两个非空字符串,但中间夹了一个NULL,整个结果就是NULL。这在业务上非常致命。比如说,你拼一个地址:CONCAT(province, city, district, detail),只要district字段是空的,这一整行地址就全没了。
为什么会这样?因为NULL在SQL里的语义是“未知”,MySQL对concat的处理策略是:既然有一个参数未知,那整个拼接结果也算未知,干脆返回NULL。这不是bug,是设计如此。
解决方案主要有三种:
第一种,用IFNULL()把NULL转成空字符串:
SELECT CONCAT(IFNULL(province, ''), IFNULL(city, ''), IFNULL(district, ''));第二种,用COALESCE(),它和IFNULL类似,但可以接受多个参数,返回第一个非NULL值:
SELECT CONCAT(COALESCE(province, ''), COALESCE(city, ''));第三种,直接用CONCAT_WS(),这个函数专门处理分隔符拼接,而且会自动跳过NULL值,后面会详细讲。
我在实际项目里更推荐在字段设计层面预防:如果这个字段未来可能为空,那拼接时从一开始就加上IFNULL或COALESCE包装。别等到线上数据出现了NULL,报表导出全为空,才回来改SQL。
3.2 CONCAT_WS:带分隔符拼接的正确姿势
CONCAT_WS的全称是CONCAT With Separator,语法跟concat不太一样:
CONCAT_WS(separator, str1, str2, ...)第一个参数是分隔符,后面的参数是要拼接的内容。它的核心优势有二:一是分隔符只在非空字符串之间生效,二是自动跳过NULL值。
举个例子:
SELECT CONCAT_WS('-', '2024', '12', '31'); -- 结果:2024-12-31 SELECT CONCAT_WS('-', '2024', NULL, '31'); -- 结果:2024-31注意第二个例子,NULL被自动跳过了,而且不会留下多余的分隔符。这简直是为地址拼接、姓名拼接这类场景量身定做的。
再对比一下用concat怎么实现同样效果:
SELECT CONCAT('2024', '-', '12', '-', '31');如果中间某个字段变成NULL,结果就是NULL,而且分隔符的位置也很容易乱。所以当我需要拼接多个字段、并且字段可能为空时,会优先选CONCAT_WS而不是手动在concat里塞一堆分隔符。
但是要注意,CONCAT_WS的分隔符本身如果是NULL,结果也是NULL。另外分隔符只能有一个,如果要在不同位置用不同分隔符,那还是得回到concat配合CASE WHEN来处理。
3.3 同族对比:concat、CONCAT_WS与GROUP_CONCAT
很多新手会把这三个函数搞混,这里统一说一下。
concat()是基础拼接,把参数一个个连起来,不处理分隔符和NULL,适合字段少、确定非空的场景。
CONCAT_WS()是带分隔符的拼接,自动跳过NULL,适合“多个字段+同一种分隔符”的场景,比如地址、联系方式、导出文件名。
GROUP_CONCAT()则是分组拼接,它按某个分组把多行的某个字段拼成一行,支持DISTINCT去重、ORDER BY排序、SEPARATOR指定分隔符。比如查一个部门里所有员工姓名,拼成一列:
SELECT dept_id, GROUP_CONCAT(emp_name SEPARATOR '、') FROM employee GROUP BY dept_id;这个函数不属于concat()这个问题的直接范畴,但实际工作中经常会搭配使用,所以这里一并提一下。记住一点:GROUP_CONCAT有默认长度限制,默认是1024字节,超出会被截断,后面排查部分会讲怎么调整。
4. 实操过程:从需求到SQL的完整落地
4.1 用户姓名拼接的真实案例
先看一个最常见也最简单的场景。假设用户表结构是这样的:
| id | last_name | first_name |
|---|---|---|
| 1 | 张 | 小明 |
| 2 | 李 | NULL |
需求是查询时展示完整姓名。
直接写:
SELECT id, CONCAT(last_name, first_name) AS full_name FROM users;第一条数据没问题,结果是“张小明”。第二条数据就出问题了,因为first_name是NULL,整个full_name变成NULL。
解决方式是用CONCAT_WS:
SELECT id, CONCAT_WS('', last_name, first_name) AS full_name FROM users;CONCAT_WS的第一个参数是分隔符,这里不需要分隔符,就传空字符串。这样即使first_name为NULL,也能正常返回“李”。
如果是英文名,中间通常要加空格:
SELECT id, CONCAT_WS(' ', first_name, last_name) AS full_name FROM users;这样输出的是“Xiaoming Zhang”这种格式,比较符合英文习惯。
4.2 地址拼接:CONCAT_WS的典型用法
地址类需求是concat家族函数用得最舒服的地方。假设省市区详细地址分别存了四个字段:
SELECT CONCAT_WS('', COALESCE(province, ''), COALESCE(city, ''), COALESCE(district, ''), COALESCE(detail, '') ) AS full_address FROM user_address;其实这里用CONCAT_WS加COALESCE是双保险。CONCAT_WS本身已经能跳过NULL,再套COALESCE是为了防止某个字段是NULL时出现拼接断层。比如province有值、city为NULL、district有值,不用COALESCE的话,结果会直接变成“省区”中间少了市,视觉上能看出来,但如果字段之间没有分隔符,就很容易连在一起产生歧义。
所以更稳妥的写法是给每个组成部分加一个固定前缀或者干脆把分隔符做成层级结构:
SELECT CONCAT_WS('', COALESCE(province, ''), COALESCE(city, ''), COALESCE(district, ''), COALESCE(detail, '') ) AS full_address FROM user_address;这是我自己常用的写法。如果你希望地址中间带空格或逗号,可以把CONCAT_WS的第一个参数改成' '或者', ',但要注意省市区之间用同样的分隔符,有时候看起来会不够美观,这个看业务怎么要求。
4.3 报表字段拼接:动态描述与格式化输出
第三个场景是报表场景,需要把商品名称、规格、单位、库存拼接成一段可读的文本。比如:
SELECT CONCAT_WS(' ', product_name, CONCAT('规格:', spec), CONCAT('单位:', unit), CONCAT('库存:', stock) ) AS product_desc FROM product;这里用了嵌套concat,目的是让每个字段带上自己的标签。这种写法在数据导出、Excel报表、打印标签时特别实用。
还有一种情况是配合CASE WHEN做状态描述。比如订单表里有个status字段,存的是数字,但业务上需要显示“已支付-金额:xxx元”这种描述:
SELECT order_no, CONCAT( CASE status WHEN 1 THEN '待支付' WHEN 2 THEN '已支付' WHEN 3 THEN '已发货' ELSE '未知' END, '-金额:', amount, '元' ) AS order_desc FROM orders;这样就把状态转换和字段拼接结合起来了,一段SQL直接输出业务需要的描述文本,不需要在Java或者Python代码里再写一遍if else。
4.4 与UPDATE结合:批量修改字段值
除了查询,concat也经常用在UPDATE语句里。比如给所有用户的昵称加上统一前缀:
UPDATE users SET nickname = CONCAT('VIP-', nickname);或者根据已有字段生成一个新的冗余字段:
UPDATE product SET full_name = CONCAT_WS(' ', product_name, spec, unit);这类操作在数据清洗、历史数据订正时用得很多。但要注意:如果nickname本身是NULL,更新后也会变成NULL,所以最好先处理NULL再更新:
UPDATE users SET nickname = CONCAT('VIP-', COALESCE(nickname, ''));4.5 性能优化:函数与索引的取舍
很多人在讨论SQL优化时都会提到“不要在索引列上使用函数”,concat也一样。如果查询条件里写了WHERE CONCAT(last_name, first_name) = '张小明',那这个查询基本无法走索引,只能全表扫描。
原因很简单:索引是基于原始列值构建的,对列值进行函数处理后,原来的索引顺序就失效了。你相当于让数据库把每一行的字段先拼一遍,再跟目标字符串比较。
遇到这种查询,更合理的做法是调整查询条件:
WHERE last_name = '张' AND first_name = '小明'这样两个字段都能分别走索引,效率高得多。如果业务上确实需要频繁按“完整姓名”查询,那就干脆在表里加一个full_name冗余字段,写入时用concat维护,查询时直接等值匹配,性能最好。
concat本身作为SELECT后面的表达式,对查询性能的影响主要是CPU和内存开销。数据量大时,拼接操作会消耗不少计算资源,尤其不要在大表上无节制地拼接大量字段。能裁剪的字段就裁剪,能在应用层做的展示拼接也可以放到前端,数据库层只负责把数据查出来。
5. 常见问题与排查技巧实录
5.1 拼接结果全部是NULL?先查字段值
这个问题我在前面反复强调过,但还是要单独列一条。遇到拼接结果是NULL,第一反应应该是:是不是哪个参数是NULL?用下面的SQL快速定位:
SELECT CONCAT(a, b, c) AS result, a, b, c FROM your_table;把原始字段都查出来,直接看哪个是NULL。如果是业务上允许为空的字段,就用IFNULL、COALESCE或者CONCAT_WS来规避。
另外有一种情况是字段本身不是NULL,而是空字符串''。空字符串跟NULL不一样,concat会正常参与拼接,只是拼出来的结果看起来少了一段。这种问题一般靠肉眼检查字段值就能发现。
5.2 数字拼接:为什么结果不对
前面提到concat会自动做隐式类型转换,这在大多数情况下是好事,但也会带来迷惑。比如:
SELECT CONCAT(1, 2, 3); -- 结果:123这里1、2、3被转成字符串拼成了“123”,不是你直觉里的数字6。如果你想要数值相加,应该用+号。反过来,有时候你在拼接时写了加号,比如:
SELECT 'MySQL' + '8.0'; -- 结果:8在MySQL默认模式下,字符串会被转成数字,转不了的就取0,所以'MySQL'变成0,'8.0'变成8,结果是8。这就是为什么很多人会混淆concat和加号。记住一句话:concat是字符串拼接,加号是数值相加,两者完全不是一回事。
还有一个容易踩的细节是日期时间类型。直接拼接日期字段时,MySQL会按默认格式转字符串,比如'2024-12-31'。但如果你的字段是DATETIME类型,拼接出来会带时间部分,不一定符合预期。这时候用DATE_FORMAT先格式化再拼,或者用CAST转换为指定格式的字符串。
5.3 GROUP_CONCAT结果被截断
前面提过GROUP_CONCAT有默认长度限制,默认是1024字节。当你分组拼接的字符串比较多时,会发现结果不完整,被硬生生截断。这种情况的典型表现是:明明有几十个值,拼出来只有一小段。
解决办法是在会话级别调整参数:
SET SESSION group_concat_max_len = 102400;如果想让所有连接都生效,需要修改MySQL配置文件,在[mysqld]下加:
group_concat_max_len = 102400然后重启MySQL。这个值可以根据业务需要设置,一般设成102400(100KB)基本够用。注意这个参数是字节数,如果拼接的内容包含中文,一个中文字符占3个字节(utf8mb4字符集下),要预留空间。
5.4 中文乱码与字符集问题
拼接时遇到中文乱码,很多人第一反应是连接字符集问题,但有时候是拼接本身的锅。比如表和字段是utf8mb4,但连接的字符集是latin1,拼出来的中文在客户端显示就会乱。
排查方式很直接:
SHOW VARIABLES LIKE 'character_set%';确保character_set_client、character_set_connection、character_set_results都是utf8mb4。如果连接用的是JDBC,连接串里加上characterEncoding=utf8。
除了连接层面,还要注意字段本身的字符集。如果某个字段是latin1,另一个是utf8mb4,concat拼接时可能会因为字符集不一致报错或产生乱码,可以用CONVERT函数统一一下:
SELECT CONCAT(CONVERT(name USING utf8mb4), note) FROM your_table;一般开发库的默认字符集都是utf8mb4,这个问题在现代MySQL版本里出现得不算多,但老项目里很常见。
5.5 拼接后排序、GROUP BY需要注意什么
拼接后的结果是一个计算字段,直接对它排序没问题:
SELECT CONCAT_WS('-', year, month) AS ym FROM log ORDER BY ym;但要注意,这个排序是基于拼接结果的字符串排序,不是基于原始字段的数值排序。比如year是2024,month是1和12,拼出来是'2024-1'和'2024-12',字符串排序时'2024-1'会排在'2024-12'前面,这正好符合预期。但如果拼的是'2024-01'这种补零格式,结果又不一样了。如果你要的是严格的时序排序,最好还是用原始字段排序,别依赖拼接结果。
GROUP BY拼接字段也是类似逻辑。MySQL的ONLY_FULL_GROUP_BY模式默认开启之后,GROUP BY后面只能写分组字段或聚合函数,所以通常不会直接对concat字段做GROUP BY。如果业务上非要按拼接结果分组,可以考虑在子查询里先拼好,再在外面分组。
5.6 其它实战小技巧
说几个我在实践中觉得有用的细节。
第一个,CONCAT_WS的分隔符可以是空字符串,这在不需要分隔符但想自动跳过NULL时特别好用。
第二个,需要拼接的值很多且都有固定前缀后缀时,可以用CONCAT_WS嵌套concat来保证可读性:
SELECT CONCAT_WS(' ', '姓名:', CONCAT_WS('', last_name, first_name)) FROM users;第三个,拼接后的结果要参与后续条件过滤时,一般会包装成子查询,因为WHERE里不能直接引用SELECT别名(除非用HAVING或者嵌套一层)。很多人第一次写:
SELECT CONCAT_WS('', first_name, last_name) AS full_name FROM users WHERE full_name = '小明';会报错“Unknown column 'full_name'”。正确写法是:
SELECT full_name FROM ( SELECT CONCAT_WS('', first_name, last_name) AS full_name FROM users ) t WHERE full_name = '小明';或者直接写成:
SELECT CONCAT_WS('', first_name, last_name) AS full_name FROM users HAVING full_name = '小明';不过HAVING在有大结果集时效率不如WHERE,能用子查询优先用子查询。
第四个,在存储过程或者动态SQL里拼字符串时,concat也经常用来构造SQL语句。比如根据条件动态拼WHERE子句:
SET @sql = CONCAT('SELECT * FROM orders WHERE status = ', status_val); PREPARE stmt FROM @sql; EXECUTE stmt;这种用法在报表系统里很常见,但要注意SQL注入风险,用参数绑定更安全。
6. 写在最后的经验之谈
跟字符串拼接打了这么多年交道,我个人的体会是:concat看起来简单,真正用好的关键在于理解MySQL对NULL和类型转换的处理逻辑。很多线上问题,比如导出地址为空、报表字段消失、更新后数据变NULL,追究到最后都是NULL值没处理干净。所以我现在写SQL都有一个习惯:只要字段有可能为空,拼接时要么用CONCAT_WS,要么用IFNULL/COALESCE包一层,多写几个字符,省掉后面排查的半天时间。
另外一个建议是,写拼接SQL的时候尽量在数据库客户端里先跑一遍,把结果集肉眼检查一遍再上到生产环境。字符串拼接这种东西,光看代码很难发现问题,实际数据一跑,NULL、字符集、类型转换的问题全都会现出原形。宁可多花一分钟验证,也别等到业务方反馈数据不对再来查。
最后补充一个小技巧:如果你经常要写地址拼接、姓名拼接这类SQL,可以把常用的拼接逻辑封装成视图或者存储过程,这样业务方查询时直接SELECT视图字段就行,不用每次重复写一长串CONCAT_WS和COALESCE,维护起来也轻松很多。