1.方法一:直接计算
方法一最简单,就是官方解题的方法,计算每个员工参加的培训课程数量,并求和得到总的人次。
每个员工参加的培训课程数量可以通过处理`course`列得到,即根据逗号拆分字段,对字符块计数。
代码如下(示例):
select sum(num) AS staff_nums from( select case when course is null then 0 else length(course) - length(replace(course,',','')) + 1 end AS num from cultivate_tb ) t ;
2.方法二:先拆分再计算
方法一不太灵活,只能算出来每个用户选课数量以及总选课人次,但如果我想计算每门课的选课人次就没办法了。所以为了让操作更灵活,可以把表格展开,也就是把每个用户的`course`字段里的1条多选课记录展开多条单选课记录。
展开方法主要有以下4种:
- 使用 SUBSTRING_INDEX 函数
- 递归CTE(MySQL 8.0+)
- 使用JSON函数
- 存储过程
以下解题方法参考了deepseek回答。
2.1 使用 SUBSTRING_INDEX 函数
1.适用场景:
- 已知最大拆分数量,比如这道题已知总共3门课,所以最大拆分数就是3
- MySQL 5.7及以上版本
- 数据量不大
2.代码示例
WITH staff_course_details as ( SELECT staff_id, SUBSTRING_INDEX(SUBSTRING_INDEX(course, ',', numbers.n), ',', -1) AS split_value FROM cultivate_tb JOIN ( SELECT 1 AS n UNION ALL SELECT 2 UNION ALL SELECT 3 -- 已知总共3门课程,所以最大可能的分隔数量为3 ) numbers ON CHAR_LENGTH(course) - CHAR_LENGTH(REPLACE(course, ',', '')) >= numbers.n - 1 ) select count(*) as staff_nums from staff_course_details ;
3.步骤说明
(1)数字辅助表:
(SELECT 1 AS n UNION ALL SELECT 2 UNION ALL...)
创建一个临时数字序列,表示可能的拆分位置。比如1表示第一个值,2表示第二个值等。
(2)连接条件:
ON CHAR_LENGTH(comma_column) - CHAR_LENGTH(REPLACE(comma_column, ',', '')) >= numbers.n - 1
计算逗号数量,确保只连接有足够多分隔值的行。例如"course1,course2,course3"有2个逗号,可以拆分成3个值。
(3)SUBSTRING_INDEX嵌套:
`substring_index(str, delim, count)
`函数用法:
- `
str
` 是原始字符串 - `
delim
` 是作为分隔符的字符。如果指定的分隔符在字符串中不存在,或者其出现的次数超过了实际分隔符的出现次数,substring_index 函数将返回整个原始字符串。 - `
count
` 指定了要返回的子字符串的数量。如果是正数,则从字符串的开头开始计数;如果是负数,则从字符串的末尾开始计数。
SUBSTRING_INDEX(SUBSTRING_INDEX(comma_column, ',', numbers.n), ',', -1)
内层`SUBSTRING_INDEX(comma_column, ',', n)
`获取前n个分隔的部分
外层`SUBSTRING_INDEX(..., ',', -1)
`从结果中取最后一个部分。
执行流程示例:
对于值 "apple,banana,orange":
- 当n=1:内层→"apple",外层→"apple"
- 当n=2:内层→"apple,banana",外层→"banana"
- 当n=3:内层→"apple,banana,orange",外层→"orange"
4.补充说明
这种方法适用于“数据量不大”的情况,具体量化标准是什么呢?怎么才算数据量不大?没有绝对的数值,但可以从记录数量、分隔值数量、字符串长度等维度来进行评估。
实际建议阈值:
数据特征 | 安全范围 | 风险范围 |
总行数 | <5,000行 | >50,000行 |
每行分隔值 | <10个 | >20个 |
字符串长度 | <512字符 | >2048字符 |
数字辅助表 | <15个数字 | >20个数字 |
2.2 递归CTE(MySQL 8.0+)
1.适用场景
- MySQL 8.0及以上版本
- 未知最大拆分数量
- 大数据量性能较好
2.代码示例
WITH RECURSIVE splitter AS ( -- 基础部分:获取第一个值 SELECT staff_id, course, SUBSTRING_INDEX(course, ',', 1) AS split_value, SUBSTRING(course, LENGTH(SUBSTRING_INDEX(course, ',', 1)) + 2) AS remainder, 1 AS position FROM cultivate_tb UNION ALL -- 递归部分:继续拆分剩余部分 SELECT staff_id, remainder, SUBSTRING_INDEX(remainder, ',', 1), SUBSTRING(remainder, LENGTH(SUBSTRING_INDEX(remainder, ',', 1)) + 2), position + 1 FROM splitter WHERE remainder != '' ) SELECT COUNT(*) AS staff_nums FROM ( SELECT staff_id, split_value, position FROM splitter WHERE split_value != '' ) t ;
3.步骤说明
(1)基础部分:
- 获取原始字段的第一个逗号分隔值
- 计算剩余部分(remainder)
- 设置初始位置为1
(2)递归部分:
- 从上一步的remainder继续拆分
- 位置(position)递增
- 直到remainder为空停止
(3)结果筛选:
- 过滤掉空值
- 返回ID、拆分值和位置
执行流程示例:
对于值 "a,b,c":
- 基础→ split_value="a", remainder="b,c", position=1
- 递归1→ split_value="b", remainder="c", position=2
- 递归2→ split_value="c", remainder="", position=3
- 终止(remainder为空)
2.3 使用JSON函数
1.适用场景
- MySQL 5.7及以上版本
- 数据格式较规范
- 需要简洁的解决方案
2.代码示例
WITH staff_course_details AS ( SELECT t.staff_id, JSON_UNQUOTE(JSON_EXTRACT(CONCAT('["', REPLACE(t.course, ',', '","'), '"]'), CONCAT('$[', n.n, ']'))) AS split_value, n.n + 1 AS position FROM cultivate_tb t JOIN ( SELECT 0 AS n UNION ALL SELECT 1 UNION ALL SELECT 2 ) n ON n.n <= LENGTH(t.course) - LENGTH(REPLACE(t.course, ',', '')) ) SELECT COUNT(*) AS staff_nums FROM staff_course_details ;
3.步骤说明
(1)JSON数组转换:
CONCAT('["', REPLACE(t.comma_column, ',', '","'), '"]')
将"a,b,c"转换为JSON数组格式:["a","b","c"]
(2)JSON_EXTRACT提取指定索引元素:
JSON_EXTRACT(..., CONCAT('$[', n.n, ']'))
提取数组中指定索引的元素,如$[0]表示第一个元素
(3)JSON_UNQUOTE:去除JSON字符串的引号
(4)数字辅助表:与方法一类似,确定需要拆分的最大数量
2.4 存储过程
1.适用场景
- 需要复杂处理逻辑
- 需要重复使用
- 有存储过程执行权限
ps: 在牛客网上运行不了以下代码,可能是因为权限问题,会报错“SQL_ERROR_INFO: Not allow to drop”。所以下列代码仅供学习使用。
2.代码示例
DELIMITER // CREATE PROCEDURE split_comma_column() BEGIN -- 变量声明 DECLARE done INT DEFAULT FALSE; DECLARE v_id INT; DECLARE v_comma_column TEXT; DECLARE v_split_value TEXT; DECLARE v_position INT; -- 创建临时表存储结果 DROP TEMPORARY TABLE IF EXISTS temp_split_results; CREATE TEMPORARY TABLE temp_split_results ( id INT, split_value VARCHAR(255), position INT ); -- 清空临时表 TRUNCATE TABLE temp_split_results; -- 声明游标遍历原表 DECLARE cur CURSOR FOR SELECT staff_id, course FROM cultivate_tb; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE; OPEN cur; read_loop: LOOP FETCH cur INTO v_id, v_comma_column; IF done THEN LEAVE read_loop; END IF; SET v_position = 1; -- 循环拆分每个值 WHILE LENGTH(v_comma_column) > 0 DO -- 获取第一个分隔值 SET v_split_value = SUBSTRING_INDEX(v_comma_column, ',', 1); -- 存入结果表 INSERT INTO temp_split_results VALUES (v_id, v_split_value, v_position); -- 准备下一轮 SET v_position = v_position + 1; -- 移除已处理的部分 SET v_comma_column = SUBSTRING(v_comma_column, LENGTH(v_split_value) + 2)); -- 终止条件检查 IF v_comma_column = v_split_value THEN SET v_comma_column = ''; END IF; END WHILE; END LOOP; CLOSE cur; -- 返回结果 SELECT * FROM temp_split_results; END // DELIMITER ; -- 创建临时表结构匹配存储过程的输出 CREATE TEMPORARY TABLE IF NOT EXISTS split_results ( id INT, split_value VARCHAR(255), position INT ); -- 调用存储过程并插入结果到临时表 INSERT INTO split_results CALL split_comma_column() ; -- 执行统计查询( SELECT COUNT(*) as staff_nums FROM split_results ;
3.步骤说明
(1)游标遍历:逐行处理原表数据
(2)WHILE循环:对每行的逗号分隔值循环拆分
(3)SUBSTRING_INDEX:获取当前第一个值
(4)字符串截取:移除已处理部分
(5)结果存储:将每个拆分值存入临时表
3.总结
文章一共使用了两大类方法解答题目,其中方法一是最简单的,但是灵活性不够,方法二中又使用了4个方法,先把表格展开成不能分割的记录,再进行统计,灵活度比较高。
需要注意的是,存储过程的方法可能因为权限问题,在牛客网上运行报错。
方法二可总结如下:
方法 | 优点 | 缺点 | 适用版本 |
SUBSTRING_INDEX | 简单直观 | 需要知道最大拆分数量 | 所有版本 |
递归CTE | 无需知道最大数量,性能好 | 仅MySQL 8.0+ | 8.0+ |
JSON函数 | 代码简洁 | 对异常格式处理较弱 | 5.7+ |
存储过程 | 最灵活,可处理复杂逻辑 | 需要执行权限,较复杂 | 所有版本 |