神奇的 SQL ,同時實現小計與合計,閣下該如何應對

青石路發表於2024-02-01

開心一刻

  今天,小區有個很漂亮的姑娘出嫁

  我對兒子說:你要好好學習,認真寫作業,以後才能娶到這麼漂亮的老婆

  兒子好像聽明白了,思考了一會,默默的收起了作業本

  然後如釋重負的跟我說到:爸,我以後還是不娶老婆了

 環境準備

  後文要講的重點是標準 SQL ,與具體的資料庫沒關係,所以理論上來講,所有的關係型資料庫都應該支援

  但理論是理論,事實是事實,大家需要結合當下的實際情況來看問題

  關係型資料庫很多,後文主要基於 MySQL 8.0.30 來講解,偶爾會插入 PostgreSQL 14.1 ,沒有特殊說明的情況下,都是基於 MySQL 8.0.30 

   MySQL 建表 tbl_ware ,並初始化資料

神奇的 SQL ,同時實現小計與合計,閣下該如何應對
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 ,並初始化資料

神奇的 SQL ,同時實現小計與合計,閣下該如何應對
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

小計與合計

  關於 小計與合計 ,大家肯定不會陌生,甚至很熟悉

  或多或少都實現過這樣的功能,尤其是涉及到報表統計的時候, 小計與合計 是繞不過去的坎

神奇的 SQL ,同時實現小計與合計,閣下該如何應對

  那有哪些實現方式了,我們今天就來盤一盤

  GROUP BY + 應用程式彙總

  先透過資料庫層面的 GROUP BY 得到小計,類似如下

  然後透過程式程式碼對 商品類別 的小計進行一個合計

  我敢斷定,這種方式肯定是大家用的最多的方式,因為我就是這麼用的!

  但是,如果加個限制條件:只用 SQL 

  此時如何實現小計和合計,各位該如何應對?

  是不是有面試內味了?

  GROUP BY + UNION ALL

  直接上 SQL 

  這個 SQL ,大家都能看懂,我就不做過多解釋了

  補充問下,用 UNION 可以嗎

  答案是可以的,但由於兩條 SELECT 語句的聚合鍵不同,一定不會出現重複行,可以使用 UNION ALL 

   UNION ALL 和 UNION 的不同之處在於它不會對結果進行排序,所以它有更好的效能

  就從結果而言,是不是隻用 SQL 實現了 小計與合計 ?

神奇的 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 ,你認識嗎

    這是本文的重點(呼應開頭了),請繼續往下看

    你們不要懷疑我是不是在套娃,請把懷疑去掉,我就是在套娃!

神奇的 SQL ,同時實現小計與合計,閣下該如何應對

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 

神奇的 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 一樣,我們直接看案例

神奇的 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 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) 的結果中選出 商品類別 和 登記日期 各自作為聚合鍵的結果

  可以這麼實現

神奇的 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 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基礎教程》

相關文章