开心一刻

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

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

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

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

环境准备

后文要讲的
重点
是标准
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 TABLEtbl_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 '商品类别 合计'
        ELSEware_categoryEND ASware_category,CASE WHEN GROUPING(registration_date) = 1
        THEN '登记日期 合计'
        ELSE TO_CHAR(registration_date, 'YYYY-MM-DD')END    ASregistration_date,SUM(purchase_unit_price) ASpurchase_unit_pricesFROMtbl_wareGROUP BYROLLUP(ware_category,registration_date)ORDER BY ware_category DESC, registration_date;

View Code

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

CUBE

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


SELECT 
    CASE WHEN GROUPING(ware_category) = 1
        THEN '商品类别 合计'
        ELSEware_categoryEND ASware_category,CASE WHEN GROUPING(registration_date) = 1
        THEN '登记日期 合计'
        ELSE TO_CHAR(registration_date, 'YYYY-MM-DD')END    ASregistration_date,SUM(purchase_unit_price) ASpurchase_unit_pricesFROMtbl_wareGROUP BYCUBE(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 '商品类别 合计'
        ELSEware_categoryEND ASware_category,CASE WHEN GROUPING(registration_date) = 1
        THEN '登记日期 合计'
        ELSE TO_CHAR(registration_date, 'YYYY-MM-DD')END    ASregistration_date,SUM(purchase_unit_price) ASpurchase_unit_pricesFROMtbl_wareGROUP 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基础教程》

标签: none

添加新评论