题解 | 统计所有课程参加培训人次
统计所有课程参加培训人次
https://www.nowcoder.com/practice/98aad5807cf34a3b960cc8a70ce03f53
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+ |
存储过程 | 最灵活,可处理复杂逻辑 | 需要执行权限,较复杂 | 所有版本 |
查看24道真题和解析