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+

存储过程

最灵活,可处理复杂逻辑

需要执行权限,较复杂

所有版本