最近经常在工作中遇到统计数据报告场景,其中要求某个分组的小计或合计,在此记录下MySQL中实现小计的方法。
场景描述
假设我们有一张销售业绩表 sales_records,包含字段:部门 (dept)、员工 (user)、销售额 (amount)。我们需要统计:
- 每个部门下每个员工的销售额(明细)。
- 每个部门的销售额(部门小计)。
- 全公司的总销售额(总计)。
一、 MySQL 中的实现方案
建表与初始化数据
sql
-- 创建测试表
CREATE TABLE sales_records (
id INT AUTO_INCREMENT PRIMARY KEY,
dept VARCHAR(50) NOT NULL COMMENT '部门',
user_name VARCHAR(50) NOT NULL COMMENT '员工姓名',
amount DECIMAL(10, 2) NOT NULL COMMENT '销售额'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 插入测试数据
INSERT INTO sales_records (dept, user_name, amount) VALUES
('研发部', '张三', 1000.00),
('研发部', '李四', 2000.00),
('研发部', '王五', 1500.00),
('销售部', '赵六', 3000.00),
('销售部', '钱七', 4000.00),
('市场部', '孙八', 2500.00);
方法一:UNION ALL
sql
-- 1. 明细统计
SELECT dept, user_name, SUM(amount) as total_amount, 1 as sort_type
FROM sales_records
GROUP BY dept, user_name
UNION ALL
-- 2. 部门小计
SELECT dept, '小计' as user_name, SUM(amount), 2 as sort_type
FROM sales_records
GROUP BY dept
UNION ALL
-- 3. 总计
SELECT '总计' as dept, '小计' as user_name, SUM(amount), 3 as sort_type
FROM sales_records
ORDER BY FIELD(dept, '总计') , dept, sort_type, user_name;
- UNION ALL: 将多个查询结果集合并
- FIELD(dept, '总计'):当 dept 是"总计"时返回 1,否则返回 0
执行结果:
bash
+----+---------+------------+---------+
|dept|user_name|total_amount|sort_type|
+----+---------+------------+---------+
|市场部 |孙八 |2500.00 |1 |
|市场部 |小计 |2500.00 |2 |
|研发部 |张三 |1000.00 |1 |
|研发部 |李四 |2000.00 |1 |
|研发部 |王五 |1500.00 |1 |
|研发部 |小计 |4500.00 |2 |
|销售部 |赵六 |3000.00 |1 |
|销售部 |钱七 |4000.00 |1 |
|销售部 |小计 |7000.00 |2 |
|总计 |小计 |14000.00 |3 |
+----+---------+------------+---------+
方法二:ROLLUP
WITH ROLLUP 的作用是在 GROUP BY 的分组基础上,自动向上层级进行聚合。WITH ROLLUP 的名字来源于"卷轴"。它的逻辑非常直观:从最细粒度的分组开始,一级一级向上卷动计算,直到最顶层。
假设你的 SQL 是:
GROUP BY A, B, C WITH ROLLUP
数据库内部会实际上帮你执行以下 4 种 组合的聚合(按顺序):
- GROUP BY A, B, C (最细粒度:明细)
- GROUP BY A, B (去掉 C:按 A,B 小计)
- GROUP BY A (去掉 B:按 A 小计)
- GROUP BY () (去掉 A:总计)
sql
SELECT
dept,
user_name,
SUM(amount) as total_amount
FROM
sales_records
GROUP BY
dept, user_name
WITH ROLLUP;
执行结果
bash
+----+---------+------------+
|dept|user_name|total_amount|
+----+---------+------------+
|市场部 |孙八 |2500.00 |
|市场部 |null |2500.00 |
|研发部 |张三 |1000.00 |
|研发部 |李四 |2000.00 |
|研发部 |王五 |1500.00 |
|研发部 |null |4500.00 |
|销售部 |赵六 |3000.00 |
|销售部 |钱七 |4000.00 |
|销售部 |null |7000.00 |
|null|null |14000.00 |
+----+---------+------------+
- GROUP BY dept, user_name WITH ROLLUP: MySQL 会先按 (dept, user_name) 分组,然后按 (dept) 分组(即 user_name 为 NULL),最后全表聚合(dept 和 user_name 均为 NULL)。
优化:使用 GROUPING 函数优化
sql
SELECT
IF(GROUPING(dept), '总计', dept) AS dept,
IF(GROUPING(user_name), '小计', user_name) AS user_name,
SUM(amount) as total_amount
FROM
sales_records
GROUP BY
dept, user_name
WITH ROLLUP;
- 小计和总计行显示为 NULL,显示不友好。在 MySQL 8.0 中,我们可以使用
GROUPING()函数来判断当前行是否是聚合行,并结合if函数进行替换。
执行结果
sql
+----+---------+------------+
|dept|user_name|total_amount|
+----+---------+------------+
|市场部 |孙八 |2500.00 |
|市场部 |小计 |2500.00 |
|研发部 |张三 |1000.00 |
|研发部 |李四 |2000.00 |
|研发部 |王五 |1500.00 |
|研发部 |小计 |4500.00 |
|销售部 |赵六 |3000.00 |
|销售部 |钱七 |4000.00 |
|销售部 |小计 |7000.00 |
|总计 |小计 |14000.00 |
+----+---------+------------+
方式三:使用窗口函数
sql
SELECT
dept AS 部门,
user_name AS 员工姓名,
amount AS 销售额,
SUM(amount) OVER (PARTITION BY dept) AS 部门销售额小计,
ROUND(amount * 100.0 / SUM(amount) OVER (PARTITION BY dept), 2) AS 占部门比例,
SUM(amount) OVER () AS 公司销售额总计,
ROUND(amount * 100.0 / SUM(amount) OVER (), 2) AS 占公司比例
FROM sales_records
ORDER BY
部门销售额小计 DESC,
销售额 DESC;
- SUM(...) OVER(PARTITION BY dept): 这里的"小计"是附在每一行后面的。它不会减少行数,只是增加列。这在做"占比分析"时比较有用。
执行结果
bash
+---+----+-------+-------+------+--------+-----+
|部门 |员工姓名|销售额 |部门销售额小计|占部门比例 |公司销售额总计 |占公司比例|
+---+----+-------+-------+------+--------+-----+
|销售部|钱七 |4000.00|7000.00|57.14 |14000.00|28.57|
|销售部|赵六 |3000.00|7000.00|42.86 |14000.00|21.43|
|研发部|李四 |2000.00|4500.00|44.44 |14000.00|14.29|
|研发部|王五 |1500.00|4500.00|33.33 |14000.00|10.71|
|研发部|张三 |1000.00|4500.00|22.22 |14000.00|7.14 |
|市场部|孙八 |2500.00|2500.00|100.00|14000.00|17.86|
+---+----+-------+-------+------+--------+-----+