神奇的 SQL ,同时实现小计与合计,阁下该如何应对

开心一刻

今天,小区有个很漂亮的姑娘出嫁

我对儿子说:你要好好学习,认真写作业,以后才能娶到这么漂亮的老婆

儿子好像听明白了,思考了一会,默默的收起了作业本

然后如释重负的跟我说到:爸,我以后还是不娶老婆了

环境准备

后文要讲的重点是标准 SQL ,与具体的数据库没关系,所以理论上来讲,所有的关系型数据库都应该支持

但理论是理论,事实是事实,大家需要结合当下的实际情况来看问题

关系型数据库很多,后文主要基于 MySQL 8.0.30 来讲解,偶尔会插入 PostgreSQL 14.1 ,没有特殊说明的情况下,都是基于 MySQL 8.0.30

MySQL 建表 tbl_ware ,并初始化数据

复制代码
CREATE TABLE `tbl_ware` (
  `ware_id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '商品id',
  `ware_name` VARCHAR(100) NOT NULL COMMENT '商品名称',
  `ware_category` VARCHAR(100) NOT NULL COMMENT '商品类别',
  `sale_unit_price` INT COMMENT '销售单价',
  `purchase_unit_price` INT COMMENT '进货单价',
  `registration_date` DATE COMMENT '登记日期',
  PRIMARY KEY (`ware_id`) USING BTREE
) ENGINE=InnoDB COMMENT='产品';

View Code

PostgreSQL 建表 tbl_ware ,并初始化数据

复制代码
CREATE TABLE tbl_ware (
  ware_id INT PRIMARY KEY,
  ware_name VARCHAR(100) NOT NULL,
  ware_category VARCHAR(100) NOT NULL,
  sale_unit_price INT,
  purchase_unit_price INT,
  registration_date DATE
);

INSERT INTO tbl_ware VALUES 
(1, 'T恤衫', '衣服', 100, 50, '2023-12-11'),
(2, '打孔器', '办公用品', 25, 10, '2023-12-13'),
(3, '运动T恤', '衣服', 150, 50, '2023-12-10'),
(4, '菜刀', '厨房用具', 75, 30, '2023-12-15'),
(5, '高压锅', '厨房用具', 600, 200, '2023-12-15'),
(6, '叉子', '厨房用具', 7, 3, NULL),
(7, '菜板', '厨房用具', 98, 30, '2023-12-12'),
(8, '圆珠笔', '办公用品', 5, 2, '2023-12-15'),
(9, '带帽卫衣', '衣服', 150, 90, NULL),
(10, '砍骨刀', '厨房用具', 150, 69, '2023-12-13'),
(11, '羽绒服', '衣服', 800, 200, NULL);

View Code

小计与合计

关于 小计与合计 ,大家肯定不会陌生,甚至很熟悉

或多或少都实现过这样的功能,尤其是涉及到报表统计的时候, 小计与合计 是绕不过去的坎

那有哪些实现方式了,我们今天就来盘一盘

GROUP BY + 应用程序汇总

先通过数据库层面的 GROUP BY 得到小计,类似如下

然后通过程序代码对 商品类别 的小计进行一个合计

我敢断定,这种方式肯定是大家用的最多的方式,因为我就是这么用的!

但是,如果加个限制条件:只用 SQL

此时如何实现小计和合计,各位该如何应对?

是不是有面试內味了?

GROUP BY + UNION ALL

直接上 SQL

这个 SQL ,大家都能看懂,我就不做过多解释了

补充问下,用 UNION 可以吗

答案是可以的,但由于两条 SELECT 语句的聚合键不同,一定不会出现重复行,可以使用 UNION ALL

UNION ALL 和 UNION 的不同之处在于它不会对结果进行排序,所以它有更好的性能

就从结果而言,是不是只用 SQL 实现了 小计与合计 ?

但是,这可恶的 但是 来了

执行 2 次几乎相同的 SELECT 语句,再将其结果进行连接,你们不觉得繁琐吗?

在我看来不仅繁琐,效率也会因为繁琐而低下

面试官又会接着问了:在只用 SQL 的前提下,有没有更合适的实现方法?

此时,各位又该如何应对?

ROLLUP

我就不卖关子了,直接上绝招

斗胆问一句,这算实现了吗?

可能有小伙伴会说:这不能算实现了,没看到那么明显的 Null 吗?

如果非要较真的话,这么说也有道理,但是假若我们在展现层(比如前端)将 Null 当 合计 处理了?

为什么我不说在后端将 Null 处理成 合计 ?

如果我们在后端将 Null 处理成 合计 ,为什么不直接用方式: GROUP BY + 应用程序汇总 ?

不过, Null 看着着实不爽,关键是坑还多:神奇的 SQL 之温柔的陷阱 → 三值逻辑 与 NULL !

那就把它干掉,调整下 SQL

这下完美了吧,从结果上来看是完美了

但从整体上来看,我觉得还不够完美,主要有 2 点

1、 WITH ROLLUP 是 MySQL 的独有写法

ROLLUP 的标准写法是 GROUP BY ROLLUP(列名1,列名2,...) ,例如在 PostgreSQL 实现小计与合计

主流的关系型数据库( Oracle 、 SQL Server 、 DB2 、 PostgreSQL )都是按 SQL 标准来实现的

唯独 MySQL 没有按标准来,她发挥了她的小任性,用 WITH ROLLUP 坚持了自己的个性

2、 GROUPING 、 ROLLUP ,你认识吗

这是本文的重点(呼应开头了),请继续往下看

你们不要怀疑我是不是在套娃,请把怀疑去掉,我就是在套娃!

GROUPING

考虑到 MySQL 8.0.30 不支持 CUBE 和 GROUPING SETS ,所以后面的 SQL 都基于 PostgreSQL 14.1

GROUPING 不会单独使用,往往会结合 ROLLUP 、 CUBE 和 GROUPING SETS 其中之一来使用

ROLLUP

关于 ROLLUP ,前面已经演示了一个案例

商品类别 值 NULL 的那一行,没有聚合键,也就相当于没有 GROUP BY 子句,这时会得到全部数据的 合计行

该合计行记录称为 超级分组记录(super group row) ,虽然听上去很屌,但还是希望大家把它当做未使用 GROUP BY 的 合计行 来理解

正是因为 合计行 的 ware_category 列的键值不明确,所以会默认使用 NULL

前面的案例只有一个聚合列,如果再加一列 registration_date ,会是什么结果?

就问你们看的懵不懵?

反正我有 2 点比较懵:

1、每一行记录的含义是什么?

2、这么多 Null ,分别表示什么

关于懵点 1,如果大家细看的话,还是能看明白每一行记录的含义的

至此,相信大家对 ROLLUP 的作用有一定感觉了

总结下, ROLLUP 作用就如其名一样,能够得到像从小计到合计,从最小的聚合级开始,聚合单位逐渐扩大的结果

GROUP BY ROLLUP(ware_category) 时,那么结果就是以 ware_category 归类的 小计 加上这些 小计 的 合计 ,一共 3 + 1 = 4 条记录

GROUP BY ROLLUP(ware_category,registration_date) 时,那么结果就是以 ware_category,registration_date 归类的 小计 加上 GROUP BY ROLLUP(ware_category) 的结果,一共 9 + 4 = 13 条记录

如果聚合列有 3 列,大家还能明白每一行记录的含义吗

关于懵点 2, Null 看着确实难受,关键是难以区分:到底是值是 Null ,还是超级分组记录的 Null

所以为了避免混淆, SQL 标准就规定用 GROUPING 函数来判断超级分组记录的 NULL

如果 GROUPING 函数的值是 1,则表示是超级分组记录,0 则表示其他情况

我们调整下 SQL

复制代码
SELECT 
    CASE WHEN GROUPING(ware_category) = 1
        THEN '商品类别 合计'
        ELSE ware_category
    END AS ware_category, 
    CASE WHEN GROUPING(registration_date) = 1
        THEN '登记日期 合计'
        ELSE TO_CHAR(registration_date, 'YYYY-MM-DD')
    END    AS registration_date,
    SUM(purchase_unit_price) AS purchase_unit_prices
FROM tbl_ware
GROUP BY ROLLUP(ware_category,registration_date)
ORDER BY ware_category DESC, registration_date;

View Code

这样看着是不是清晰很多?

CUBE

语法和 ROLLUP 一样,我们直接看案例

复制代码
SELECT 
    CASE WHEN GROUPING(ware_category) = 1
        THEN '商品类别 合计'
        ELSE ware_category
    END AS ware_category, 
    CASE WHEN GROUPING(registration_date) = 1
        THEN '登记日期 合计'
        ELSE TO_CHAR(registration_date, 'YYYY-MM-DD')
    END    AS registration_date,
    SUM(purchase_unit_price) AS purchase_unit_prices
FROM tbl_ware
GROUP BY CUBE(ware_category,registration_date)
ORDER BY ware_category DESC, registration_date;

View Code

与 ROLLUP 的结果相比, CUBE 结果多了几行记录,而这几行记录就是 GROUP BY(registration_date) 的聚合记录

所谓 CUBE ,就是将 GROUP BY 子句中的聚合键的 所有可能组合 的聚合结果集中到一个结果集中的功能

因此,组合的个数就 2 的 n 次方(n 是聚合键的个数)

本例中,聚合键有 2 个( ware_category,registration_date ),所以组合个数就是 2 的 2 次方,即 4 个

如果再添加 1 个变为 3 个聚合键的话,那么组合的个数就是 2 的 3 次方,即 8 个

反观 ROLLUP ,组合个数就是 n + 1

提个疑问, ROLLUP 的结果一定包含在 CUBE 的结果之中吗?

GROUPING SETS

该运算符主要用于从 ROLLUP 或者 CUBE 的结果中取出部分记录

例如,如果希望从 GROUP BY CUBE(ware_category,registration_date) 的结果中选出 商品类别 和 登记日期 各自作为聚合键的结果

可以这么实现

复制代码
SELECT 
    CASE WHEN GROUPING(ware_category) = 1
        THEN '商品类别 合计'
        ELSE ware_category
    END AS ware_category, 
    CASE WHEN GROUPING(registration_date) = 1
        THEN '登记日期 合计'
        ELSE TO_CHAR(registration_date, 'YYYY-MM-DD')
    END    AS registration_date,
    SUM(purchase_unit_price) AS purchase_unit_prices
FROM tbl_ware
GROUP BY GROUPING SETS (ware_category,registration_date);

View Code

提个问题,有 Null 的哪一行记录表示什么?

相比 ROLLUP 和 CUBE 相比, GROUPING SETS 的使用场景特别少,有所了解即可

总结

GROUPING

作用很明显,就是为了区分 超级分组记录 的 NULL 和原始数据 NULL

说白了,就是为了标识出 合计 记录

ROLLUP

做个等价替换,方便大家理解

GROUP BY ROLLUP(ware_category,registration_date) 等价于

如果是 3 个聚合键了,等价情况是怎么样的?

CUBE

同样做个等价替换

GROUP BY CUBE(ware_category,registration_date) 等价于

如果是 3 个聚合键了,等价情况又是怎么样的?

参考

《SQL基础教程》

相关推荐
friend_ship14 天前
Vite与Vue Cli的区别与详解
vue.js·webpack·rollup·vite·vue脚手架·vue cli
闻风听雨4 个月前
yearrecord——一个类似痕迹墙的React数据展示组件
react·rollup·js·组件库
HaanLen5 个月前
axios源码打包关于rollup.config.js文件分析
javascript·ecmascript·axios·rollup·打包工具
牙膏上的小苏打23338 个月前
Unity Mesh简化为Cube mesh
unity·合并·mesh·cube
暂时先用这个名字8 个月前
UE5制作一条底部挂着物体的悬垂的绳子
ue5·虚幻·cube·unreal engine 5·cable·constraint·虚幻5
陨石猎人9 个月前
Webpack和Rollup区别、使用场景、如何选择
前端·webpack·rollup
俊刚、1 年前
rollup 原理解析
前端·rollup
_血手人屠_1 年前
npm发布js工具包
前端·javascript·typescript·npm·rollup·jest·eslint