一、concat() 函数
先来看一个最常用的场景:当我们需要将多个字段拼接成一个字符串时,该如何操作?MySQL 提供了三个函数来解决这类问题:concat、concat_ws 和 group_concat。它们各自有明确的适用场景,如果选错函数反而会导致结果异常。
concat() 的功能非常直接——将多个字符串连接成一个字符串。语法是 concat(str1, str2, …),返回结果为连接参数产生的字符串。但有一个容易踩的坑:如果任何一个参数为 null,则整个返回值就是 null。
举例1

select CONCAT(id,name,email) as userinfo from users;

结果中有一行显示为 null,原因就是 users 表中那行的 email 值为 null。因此使用 concat 时,务必先确认拼接的字段里没有 null 值,否则整条记录都会变成空值,前功尽弃。
举例2
上面的结果里,三个字段(id、name、email)直接拼在一起,没有任何分隔符,看起来非常不直观。加上逗号作为分隔符后效果会好很多:
select CONCAT(id,',',name,',',email) as userinfo from users;

二、concat_ws() 函数
每次使用 concat 都需要手动在每个字段之间插入逗号,如果字段较多会非常繁琐。于是 concat_ws() 应运而生——con cat with separator,可以一次性指定分隔符,大大简化了操作。
语法:concat_ws(separator, str1, str2, …)。第一个参数就是分隔符。注意:分隔符不能为 null,如果设置为 null,结果会直接返回 null。
举例
用 concat_ws() 将分隔符指定为逗号:
select CONCAT_WS(',',id,name,email) as userinfo from users;

至于为什么把分隔符写成 null 后结果全部变成 null,建议你亲自动手测试一下,来验证这个特性。

三、group_concat() 函数
前面两个函数都是在单行内拼接字段,但在实际工作中经常需要将多行数据按某个分组归并到一起。例如,按姓名分组,把同一个人的所有 ID 拼成一行。这时候 group_concat() 就派上用场了。
先回顾一下基本规则:在有 group by 的查询中,select 指定的字段要么包含在 group by 后面,要么包含在聚合函数中。否则会报错或者结果不准确。
举例1

这个查询查的是 name 相同的人中最小的 id。如果我们要查 name 相同的人所有的 id 呢?当然可以这样:

但这样同一个名字会出现多次,数据冗余,而且不够直观。有没有更优雅的方法?——使用 group_concat()。
group_concat() 的功能:将 group by 产生的同一个分组中的值连接起来,返回一个字符串结果。
语法:
group_concat([DISTINCT] 要连接的字段 [Order BY ASC/DESC 排序字段] [Separator '分隔符'])
通过 distinct 可以排除重复值;通过 order by 可以对结果进行排序;separator 用来指定分隔符,缺省为逗号。
举例2
用 group_concat() 配合 group by 显示相同名字的人的 id 号:

还可以把 id 从大到小排序,并用 _ 作为分隔符:

上面只显示了 id,如果还想同时显示每个分组内的 email 呢?

示例
GROUP_CONCAT([DISTINCT] expr [,expr ...] [ORDER BY {unsigned_integer | col_name | expr} [ASC | DESC] [,col_name ...]] [SEPARATOR str_val])
测试数据

1. 查询所有学生的选课信息
SELECT s.stu_id AS studentId, s.stu_name AS studentName, c.course_id AS courseId, c.course_name AS studentCourseFROM student sLEFT JOIN stu_course scON s.stu_id = sc.stu_idLEFT JOIN course cON sc.course_id = c.course_id

2 使用 GROUP_CONCAT() 函数将结果分组归集
SELECT s.stu_id AS studentId, s.stu_name AS studentName, GROUP_CONCAT(c.course_id) AS courseId, GROUP_CONCAT(c.course_name) AS studentCourseFROM student sLEFT JOIN stu_course scON s.stu_id = sc.stu_idLEFT JOIN course cON sc.course_id = c.course_idGROUP BY studentId

注意:使用 GROUP_CONCAT 时,数值类型的数据会被转化成二进制 BLOB 类型,这可能导致显示异常。稳妥的做法是用 CAST(expr AS type) 或 CONVERT(expr, type) 将数值转成字符串:
SELECT s.stu_id AS studentId, s.stu_name AS studentName, GROUP_CONCAT(CAST(c.course_id AS CHAR)) AS courseId, GROUP_CONCAT(c.course_name) AS studentCourseFROM student sLEFT JOIN stu_course scON s.stu_id = sc.stu_idLEFT JOIN course cON sc.course_id = c.course_idGROUP BY studentId

3 若要将第一条记录中 courseId 按照从小到大的方式归集
可以在 GROUP_CONCAT() 里加上 ORDER BY:
SELECT s.stu_id AS studentId, s.stu_name AS studentName, GROUP_CONCAT(CAST(c.course_id AS CHAR) ORDER BY c.course_id) AS courseId, GROUP_CONCAT(c.course_name) AS studentCourseFROM student sLEFT JOIN stu_course scON s.stu_id = sc.stu_idLEFT JOIN course cON sc.course_id = c.course_idGROUP BY studentId

不过这里有一个小遗憾:虽然 courseId 这一列按从小到大排序了,但另一列 studentCourse 对应的数据并没有跟着一起变化。这算是 GROUP_CONCAT 的一个局限——各字段的排序是独立进行的,无法联动。如果哪位知道怎么让另一列也跟着变,不妨分享一下经验。
4 改变分隔符
GROUP_CONCAT 默认分隔符是逗号,用 SEPARATOR 关键字可以换成其他符号:
SELECT s.stu_id AS studentId, s.stu_name AS studentName, GROUP_CONCAT(CAST(c.course_id AS CHAR) ORDER BY c.course_id SEPARATOR '/') AS courseId, GROUP_CONCAT(c.course_name SEPARATOR '|') AS studentCourseFROM student sLEFT JOIN stu_course scON s.stu_id = sc.stu_idLEFT JOIN course cON sc.course_id = c.course_idGROUP BY studentId

数据库
CREATE TABLE `users` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, `email` varchar(255) CHARACTER SET utf8 DEFAULT NULL, `email_verified_at` timestamp NULL DEFAULT NULL, `password` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, `remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, `deleted_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=50078 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;INSERT INTO `users` (`id`, `name`, `email`, `email_verified_at`, `password`, `remember_token`, `created_at`, `updated_at`, `deleted_at`) VALUES ('77', '', NULL, NULL, '', NULL, NULL, NULL, NULL);INSERT INTO `users` (`id`, `name`, `email`, `email_verified_at`, `password`, `remember_token`, `created_at`, `updated_at`, `deleted_at`) VALUES ('5577', '', '55@qq.com', NULL, '', NULL, NULL, NULL, NULL);INSERT INTO `users` (`id`, `name`, `email`, `email_verified_at`, `password`, `remember_token`, `created_at`, `updated_at`, `deleted_at`) VALUES ('50010', 'willem', '11@qq.com', NULL, '', NULL, NULL, NULL, '2022-04-07 14:03:11');INSERT INTO `users` (`id`, `name`, `email`, `email_verified_at`, `password`, `remember_token`, `created_at`, `updated_at`, `deleted_at`) VALUES ('50012', 'wgchen', '22@qq.com', NULL, '', NULL, NULL, NULL, '2022-04-07 14:03:16');INSERT INTO `users` (`id`, `name`, `email`, `email_verified_at`, `password`, `remember_token`, `created_at`, `updated_at`, `deleted_at`) VALUES ('50018', 'ycc', '33@qq.com', NULL, '', NULL, NULL, NULL, '2022-04-07 14:03:23');INSERT INTO `users` (`id`, `name`, `email`, `email_verified_at`, `password`, `remember_token`, `created_at`, `updated_at`, `deleted_at`) VALUES ('50077', '77', '', NULL, '', NULL, NULL, NULL, '2022-04-07 14:06:39');
案例
1 MySQL 中某个字段前或后添加字符
UPDATE optional_productSET cate_path = concat('|',cate_path,'|')

2 MySQL 中批量替换某个字段的值:replace
例如,表 mxd_goods 中字段 goods_pic 的值需要全部去掉前缀 goods/。

执行以下语句:
update mxd_goods set goods_pic= replace(goods_pic, 'goods/', '');
注意:一定要加上 where id = 数字,否则就是全表修改,后果非常严重。

那怎么撤销这次修改呢?
UPDATE mxd_goodsSET goods_pic = REPLACE (goods_pic, 'g_', 'goods/g_')WHEREgoods_id = 1849;
3 substring(a,b,c) 截取字符串
substring(a,b,c):第一个参数 a 是被截取的字符串,第二个参数 b 是起始位置(从1开始),第三个参数 c 是要截取的长度。
举例:
SELECT SUBSTRING("hello",2,2);
结果是从 'hello' 的第二个字符 'e' 开始截取两个字符,得到 "el"。

4 MySQL 删除最后一个字符
如果想只删除最后一个字符(比如移除多余的逗号),用 SUBSTRING 只能显示修改后的结果,不会更新实际数据。如果需要更新列的值,可以用 CONCAT(LEFT(…), '') 的方式:
UPDATESET = CONCAT(LEFT( , CHAR_LENGTH( ) -1), '')WHERE ;
也可以用 SUBSTR 配合 CHAR_LENGTH 来查询:
SELECTSUBSTR(title,1,CHAR_LENGTH(title) - 1)FROMhhir_crm_email_send_contentWHEREid = 14;

项目案例
SELECT`record`.`id`,`record`.`fund_name`,`record`.`report_type`,`record`.`number`,`record`.`year`,`record`.`quarter`,`record`.`notice_time`,`record`.`customer_sub_name`,`record`.`legal_subject_name`,`record`.`send_status`,`record`.`created_at`,`report_send`.`report_name`,`send_content`.`title` AS `email_title`,GROUP_CONCAT(`contact_link`.`contact_name`,concat('(', `contact_link`.`contact_email`, ');') SEPARATOR '') AS contactsFROM`hhir_crm_email_record` AS `record`LEFT JOIN `hhir_crm_email_report_send` AS `report_send` ON `report_send`.`email_record_id` = `record`.`id`AND `report_send`.`valid` = 1LEFT JOIN `hhir_crm_email_send_content` AS `send_content` ON `send_content`.`email_record_id` = `record`.`id`AND `send_content`.`valid` = 1LEFT JOIN `hhir_crm_email_contact_link` AS `contact_link` ON `contact_link`.`email_record_id` = `record`.`id`AND `contact_link`.`valid` = 1WHERE`record`.`valid` = 1GROUP BY `record`.`id`

SELECTid,GROUP_CONCAT(title,concat('(', cc_emails, ');') SEPARATOR '') AS email_titleFROMhhir_crm_email_send_contentWHEREid IN (12, 13)


总结
三个函数各有侧重:concat 适合简单拼接,但需要留意 null 值;concat_ws 更加简洁优雅,省去了重复写分隔符的麻烦;group_concat 则是分组聚合的利器,尤其适合处理一对多关系的数据归并。掌握它们之后,日常的字符串处理工作会变得顺手很多。
