MySQL 行转列 (Pivot) 的 N 种实现方式:静态、动态与 GROUP_CONCAT 详解
MySQL中的"行转列"是一个经典且非常实用的数据处理技巧,通常用于将数据从一种易于存储的格式转换为一种更易于阅读和报告的格式。下面我将详细解释其概念、方法和应用场景。
核心概念:什么是行转列?
- **行数据**:通常指数据库中最自然的存储格式,每一行代表一条独立的记录。
| 学生姓名 | 科目 | 成绩 |
| :------- | :--- | :--- |
| 张三 | 语文 | 90 |
| 张三 | 数学 | 85 |
| 李四 | 语文 | 95 |
| 李四 | 数学 | 70 |
- **列数据**(行转列后的目标):将某一列的唯一值(如`科目`)转换为新的列名,并将其对应的值(如`成绩`)填充到新列下。
| 学生姓名 | 语文 | 数学 |
| :------- | :--- | :--- |
| 张三 | 90 | 85 |
| 李四 | 95 | 70 |
这种转换也称为**数据透视(PIVOT)**。
---
### 实现行转列的几种方法
假设我们有上文的成绩表 `scores`。
方法一:使用 `CASE WHEN` + 聚合函数(最通用、最灵活)
这是最经典和兼容性最好的方法,适用于几乎所有版本的 MySQL。
**思路**:
1. 使用 `GROUP BY` 按唯一标识(如`学生姓名`)分组,确保一行一个学生。
2. 对每个需要转换的列值,使用 `CASE WHEN`(或 `IF`)语句判断并提取对应的值。
3. 使用 `MAX` 或 `MIN`、`SUM` 等聚合函数对提取出的值进行聚合(通常用于确保每个分组下只有一个非空值)。
**SQL 示例**:
```sql
SELECT
学生姓名,
MAX(CASE WHEN 科目 = '语文' THEN 成绩 ELSE NULL END) AS 语文,
MAX(CASE WHEN 科目 = '数学' THEN 成绩 ELSE NULL END) AS 数学
-- , ...可以继续添加其他科目
FROM scores
GROUP BY 学生姓名;
```
**结果**:
| 学生姓名 | 语文 | 数学 |
| :------- | :--- | :--- |
| 张三 | 90 | 85 |
| 李四 | 95 | 70 |
**为什么用 `MAX`?**
因为 `GROUP BY` 后,每个学生的每门科目理论上只有一条记录。`MAX` 的作用是取回那条唯一记录的值。如果不用聚合函数,SQL 会报错。你也可以用 `MIN` 或 `SUM`,效果相同。
方法二:使用 `IF()` + 聚合函数(MySQL 的简洁写法)
`IF(condition, value_if_true, value_if_false)` 是 `CASE WHEN` 的简化版,逻辑更清晰。
**SQL 示例**:
```sql
SELECT
学生姓名,
MAX(IF(科目 = '语文', 成绩, NULL)) AS 语文,
MAX(IF(科目 = '数学', 成绩, NULL)) AS 数学
FROM scores
GROUP BY 学生姓名;
```
效果与方法一完全相同。
方法三:使用 `GROUP_CONCAT`(另一种形式的“行转列”)
有时“转列”的目的不是生成多个新列,而是将多行数据**合并到一个单元格**中,用分隔符连接。这时 `GROUP_CONCAT` 非常有用。
**SQL 示例**:
```sql
SELECT
学生姓名,
GROUP_CONCAT(科目, ':', 成绩 SEPARATOR '; ') AS 成绩单
FROM scores
GROUP BY 学生姓名;
```
**结果**:
| 学生姓名 | 成绩单 |
| :------- | :----------------- |
| 张三 | 语文:90; 数学:85 |
| 李四 | 语文:95; 数学:70 |
---
高级应用:动态行转列
上面的方法有个巨大缺陷:**需要手动编写每个要转换的列值(如‘语文’、‘数学’)**。如果科目是不确定的(例如随时会增加‘英语’、‘物理’),上面的 SQL 就无法满足需求。
这时需要使用**存储过程(Stored Procedure)** 来**动态**生成 SQL 语句。
**思路**:
1. 查询出所有需要转换为列名的唯一值(如所有科目)。
2. 使用字符串拼接函数(如 `CONCAT`, `GROUP_CONCAT`)构造出包含所有 `CASE WHEN` 语句的 SQL 字符串。
3. 使用预处理语句(`PREPARE` & `EXECUTE`)来执行这个动态生成的 SQL 字符串。
**示例代码**:
```sql
-- 1. 定义变量存储动态生成的SQL
SET @sql = NULL;
-- 2. 查询所有不重复的科目,并拼接成 MAX(CASE WHEN...) 的格式
SELECT
GROUP_CONCAT(DISTINCT
CONCAT(
'MAX(IF(科目 = ''',
科目,
''', 成绩, NULL)) AS ',
CONCAT('`', 科目, '`') -- 用反引号包裹科目名,防止它是关键字
)
) INTO @sql
FROM scores;
-- 3. 拼接完整的SQL语句
SET @sql = CONCAT('SELECT 学生姓名, ', @sql, ' FROM scores GROUP BY 学生姓名');
-- 4. 预处理并执行动态SQL
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
```
**执行结果**:无论科目如何变化,这段代码都会自动生成一个将所有科目作为列名的透视表。
| 学生姓名 | 语文 | 数学 | 英语 | (其他任何科目...) |
| :------- | :--- | :--- | :--- | :------------------ |
| 张三 | 90 | 85 | 92 | ... |
| 李四 | 95 | 70 | 88 | ... |
---
### 总结与选择
| 方法 | 适用场景 | 优点 | 缺点 |
| :----------------- | :--------------------------------------------- | :--------------------------- | :----------------------------- |
| **`CASE WHEN`/`IF`** | 要转换的列值是**固定的、已知的**(如科目固定) | 简单、直观、性能好 | 无法自动适应列值的变化 |
| **`GROUP_CONCAT`** | 需要将多行数据合并到一个字段中显示 | 非常灵活,不会改变表结构 | 结果不是一个规整的二维表 |
| **动态SQL** | 要转换的列值是**动态的、不确定的**(如科目可变) | 全自动,一劳永逸 | 编写复杂,需要用到存储过程和预处理 |
**核心要点**:
1. **行转列的本质是条件判断和分组聚合**。
2. 静态转换使用 `CASE WHEN`/`IF` + `MAX` + `GROUP BY`。
3. 动态转换需要使用存储过程拼接 SQL,复杂度较高。
4. 在选择方法前,一定要明确需求中的“列”是否是固定的。
另外搭配便捷的MYSQL备份工具,可定时备份、异地备份,MYSQL导出导入。可本地连接LINUX里的MYSQL,简单便捷。可以大大地提高工作效率喔。