GROUP BY 本身不处理时间,必须配合日期函数提取年月日等粒度再分组;否则按完整时间戳分组将导致每行独立,无业务意义。

MySQL 中用 DATE_FORMAT 提取年月做分组
MySQL 最常用也最直观的方式是用 DATE_FORMAT 把 order_date 截成 '%Y-%m' 格式,确保同一自然月的数据归为一组:
- 错误写法:
GROUP BY order_date→ 每条记录时间都不同,每行一条,等于没分组 - 正确写法:
GROUP BY DATE_FORMAT(order_date, '%Y-%m')→ 得到 "2023-01"、"2023-02" 这样的字符串键 - 注意:如果字段是
DATETIME或TIMESTAMP,DATE_FORMAT可直接用;但若字段存的是字符串(如'2023/01/05'),得先用STR_TO_DATE转成日期类型再格式化 - 性能提示:该表达式无法走索引,大数据量时建议在表上建生成列 + 索引,例如:
ALTER TABLE `order` ADD COLUMN order_month VARCHAR(7) GENERATED ALWAYS AS (DATE_FORMAT(order_date, '%Y-%m')) STORED;,再对order_month建索引
SQL Server 中用 YEAR() 和 MONTH() 多字段组合分组
SQL Server 不推荐拼字符串(如 CAST(YEAR(SaleDate) AS VARCHAR) + '-' + RIGHT('0' + CAST(MONTH(SaleDate) AS VARCHAR), 2)),容易出错且难排序。稳妥做法是直接多列分组:
-
GROUP BY YEAR(SaleDate), MONTH(SaleDate)→ 返回两列:年、月,天然可排序,也避免了字符串比较的隐式转换风险 - 若需输出为
'2023-01'格式,可在 SELECT 中用FORMAT(SaleDate, 'yyyy-MM'),但 GROUP BY 子句里仍用函数组合,别混用 - 坑点:当
SaleDate是DATETIME2且含时区时,先用AT TIME ZONE转换,否则跨时区数据可能被分错月,例如:GROUP BY YEAR(SaleDate AT TIME ZONE 'China Standard Time'), MONTH(SaleDate AT TIME ZONE 'China Standard Time')
多维度时间分组:人 + 天、地区 + 月等复合场景
真实业务极少只按单一时间维度统计,通常要叠加业务维度,比如“每个销售员每天的订单数”或“华东区每月销售额”。这时 GROUP BY 必须列出所有分组字段:
- 示例(员工+日期):
GROUP BY employee_name, DATE(work_date)—— 注意 MySQL 中DATE()比DATE_FORMAT(work_date, '%Y-%m-%d')更轻量,且能走索引(如果work_date上有索引) - 示例(区域+年月):
GROUP BY region, YEAR(order_date), MONTH(order_date),比拼字符串更安全,也方便后续按年份范围筛选 - 关键约束:SELECT 中所有非聚合字段(如
employee_name、region)必须完整出现在 GROUP BY 列表中,否则报错(严格模式下) - 容易忽略的点:如果某字段允许 NULL(如
region IS NULL),它会自成一组,常导致统计口径偏差,建议显式过滤:WHERE region IS NOT NULL
补全缺失月份:GROUP BY 本身做不到,必须构造维度表
GROUP BY 只返回有数据的分组,比如 2023 年只有 1 月、3 月、5 月有订单,结果就三行。但报表常要求显示连续 12 个月(含 0)。这不是 GROUP BY 的问题,而是数据建模问题:
- 不能靠
GROUP BY加COALESCE解决,因为没数据的月份根本不出现在结果集里 - 正确解法:先构造一个完整月份列表(可用递归 CTE、日历表或 UNION 手动生成),再 LEFT JOIN 到你的汇总结果
- MySQL 8.0+ 示例片段:
WITH RECURSIVE months AS (SELECT 1 AS m UNION ALL SELECT m+1 FROM months WHERE m - 这个逻辑和 GROUP BY 是两层事:GROUP BY 负责聚合,补全逻辑负责兜底展示

















