mysql 8.0原生不支持pivot/unpivot,动态行转列必须通过存储过程拼接sql+prepare执行:先查出动态列值,再用group_concat拼sum(if())字段列表,最后组装并执行select...group by语句。

MySQL 8.0 本身不支持 PIVOT 或 UNPIVOT 关键字,所谓“动态行列转换”必须靠拼接 SQL 字符串 + 预处理语句(PREPARE/EXECUTE)实现,且必须在存储过程中完成——直接在应用层拼 SQL 再执行,会丢失列元信息,MyBatis 等框架也难以映射结果集。
为什么不能用视图或普通查询做动态行转列
动态的核心是“列名来自数据”,比如 plan_date 是表里查出来的日期值('2022-07-01', '2022-07-02'…),不是写死的字符串。而 MySQL 视图、普通 SELECT、函数都不允许把变量当列别名;GROUP_CONCAT 只能拼字符串,不能让结果集自动拥有新列结构。
-
SELECT @col := 'name' FROM dual—— 这个@col是值,不是列名 -
SELECT ${col} FROM t—— 应用层模板语法,MySQL 服务端根本不认识 - 试图用
JSON_OBJECTAGG模拟 PIVOT —— 返回的是单个 JSON 字段,不是多列,前端仍需解析,没解决“列结构动态生成”问题
存储过程里拼 SQL 的关键三步
所有可行的动态行转列存储过程,都绕不开这三步:查出要转的列值 → 拼成 SUM(IF(...)) AS `xxx` 字段列表 → 组装完整 SELECT ... FROM ... GROUP BY 并执行。漏掉任意一步都会报错或返回空结果。
- 先用
SELECT DISTINCT plan_date INTO @cols_str获取唯一日期,但注意:必须用GROUP_CONCAT+CONCAT逐个拼字段表达式,不能只拼列名 - 拼字段时要用反引号包裹列别名:
CONCAT('SUM(IF(plan_date = ''', plan_date, ''', num, 0)) AS `', plan_date, '`'),否则日期含短横线(如2022-07-01)会导致 SQL 解析失败 - 最终
@sql字符串必须以SELECT ... FROM ... GROUP BY ...开头,且GROUP BY字段要覆盖所有非聚合列(比如base_id,product_code),否则ONLY_FULL_GROUP_BY模式下直接报错
MyBatis 调用该存储过程的注意事项
MyBatis 无法从存储过程元数据中推断动态列,所以 <resultmap></resultmap> 必须用 column="*" 或全字段 resultType="map" 接收,不能写死 property 映射。
- Mapper XML 中调用要加
{call proc_name(?, ?)},参数用mode="IN"或mode="INOUT",不能用#{}直接插值,否则 SQL 注入且日期格式错乱 - 若存储过程有多个
SELECT输出(比如先SELECT COUNT(*)再主查询),MyBatis 默认只取第一个结果集,需在 JDBC URL 加allowMultiQueries=true并用statementType="CALLABLE" - MySQL 8.0 默认开启
sql_mode=STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION,拼接时若@sql为空(比如日期范围无数据),PREPARE stmt FROM @sql会报ERROR 1064,必须加IF LENGTH(@sql) > 0 THEN ... END IF判断
容易被忽略的兼容性细节
MySQL 8.0 对用户变量行为做了调整:在 SELECT 里赋值(如 @var := expr)不再保证执行顺序,尤其在 GROUP BY 或窗口函数附近极易出错。安全做法是只在 SET 或单独 SELECT ... INTO 中操作变量。
- 不要写
SELECT @sql := CONCAT(@sql, ...) FROM (...)来累积拼接,改用SELECT GROUP_CONCAT(... SEPARATOR ',') INTO @sql FROM (...) -
PREPARE语句作用域是会话级,不能跨连接复用;如果用连接池(如 HikariCP),每次调用都要重新PREPARE+DEALLOCATE,否则可能报ERROR 1243(Unknown prepared statement handler) - 日期字段用
BETWEEN时,注意plan_date类型是DATE还是DATETIME:前者可直接BETWEEN '2022-07-01' AND '2022-07-03',后者必须写成BETWEEN '2022-07-01 00:00:00' AND '2022-07-03 23:59:59',否则丢数据











