MySQL中实现“小计”与“合计”

最近经常在工作中遇到统计数据报告场景,其中要求某个分组的小计或合计,在此记录下MySQL中实现小计的方法。

场景描述

假设我们有一张销售业绩表 sales_records,包含字段:部门 (dept)、员工 (user)、销售额 (amount)。我们需要统计:

  1. 每个部门下每个员工的销售额(明细)。
  2. 每个部门的销售额(部门小计)。
  3. 全公司的总销售额(总计)。

一、 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 种 组合的聚合(按顺序):

  1. GROUP BY A, B, C (最细粒度:明细)
  2. GROUP BY A, B (去掉 C:按 A,B 小计)
  3. GROUP BY A (去掉 B:按 A 小计)
  4. 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|
+---+----+-------+-------+------+--------+-----+
相关推荐
闪电悠米23 分钟前
黑马点评-Redis 消息队列-03_stream_consumer_group
开发语言·数据库·redis·分布式·缓存·junit·lua
DIY源码阁1 小时前
JavaSwing航班订票管理系统 - MySQL版
数据库·mysql
浪客灿心2 小时前
项目篇:模块设计与实现
数据库·c++
流星白龙4 小时前
【MySQL高阶】26.事务(1)
数据库·mysql
三十..4 小时前
Redis 核心原理与高可用架构实践
运维·数据库·redis
这个DBA有点耶5 小时前
索引优化深潜(下):索引合并、ICP 与索引设计的实战法则
数据库·mysql·架构
用户3074596982075 小时前
EXPLAIN 执行计划 完全精通指南
mysql
努力努力再努力wz5 小时前
【内存管理与高并发内存池系列】从 mmap 到 malloc:文件映射、匿名映射与 glibc 内存分配机制详解
linux·c语言·数据结构·数据库·c++·qt·链表
JdSnE27zv5 小时前
Qt 操作SQLite数据库
数据库·qt·sqlite
tedcloud1235 小时前
HyperFrames部署教程:用HTML生成MP4视频
前端·数据库·人工智能·html·音视频