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|
+---+----+-------+-------+------+--------+-----+
相关推荐
一 乐2 小时前
酒店客房预订|基于springboot + vue酒店客房预订系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端
TG:@yunlaoda360 云老大2 小时前
华为云国际站代理商跨境容灾是如何实现的?
服务器·数据库·华为云
么么...2 小时前
在 Ubuntu 上安装 Docker 并部署 MySQL 容器
linux·运维·经验分享·笔记·mysql·ubuntu·docker
兴趣互联2 小时前
PostgresSQL数据库如何使用navicat创建分区表
数据库·postgresql
·云扬·2 小时前
Linux系统下MySQL服务器关键配置优化指南
linux·服务器·mysql
·云扬·3 小时前
MySQL分页查询优化:从基础到进阶实践
数据库·mysql·oracle
冉冰学姐3 小时前
SSM校园二手交易平台系统o86a5(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面
数据库·校园二手交易平台·ssm 框架
于归pro3 小时前
Redis 基础命令、核心概念与安装验证完整指南
数据库·redis·缓存
程序猿20233 小时前
索引的使用及设计规则
mysql