Một grouping set là một bộ cột dùng để nhóm, viết trong ngoặc đơn: (brand, segment), (brand), (segment), ().
Với 2 cột brand và segment, có 4 cách nhóm:
Grouping set
Câu truy vấn tương ứng
Kết quả
(brand, segment)
GROUP BY brand, segment
4 dòng chi tiết
(brand)
GROUP BY brand
ABC=300, XYZ=400
(segment)
GROUP BY segment
Basic=500, Premium=200
()
SELECT sum(quantity)
700
2.1. Cách làm thủ công — dài và chậm
SELECT brand, segment, sum(quantity) FROM sales GROUP BY brand, segmentUNION ALLSELECT brand, NULL, sum(quantity) FROM sales GROUP BY brandUNION ALLSELECT NULL, segment, sum(quantity) FROM sales GROUP BY segmentUNION ALLSELECT NULL, NULL, sum(quantity) FROM sales;
4. Hàm GROUPING() — phân biệt NULL thật và NULL tổng hợp
Vấn đề: trong kết quả trên, NULL ở cột brand có thể là:
NULL do dòng tổng hợp (không nhóm theo brand), hoặc
NULL thật sự có trong dữ liệu.
Hàm GROUPING(cot) giải quyết điều này:
Trả về 0 nếu cột đó có trong grouping set hiện tại.
Trả về 1 nếu cột đó không trong grouping set (tức là đang tổng hợp).
SELECT GROUPING(brand) AS g_brand, GROUPING(segment) AS g_segment, brand, segment, sum(quantity)FROM salesGROUP BY GROUPING SETS ((brand), (segment), ())ORDER BY brand, segment;
g_segment = 0 → dòng này là tổng con theo segment.
Cả hai = 1 → dòng tổng chung.
4.1. Dùng GROUPING() để đặt nhãn dễ đọc
SELECT CASE WHEN GROUPING(brand) = 1 THEN '— Tất cả nhãn hiệu —' ELSE brand END AS nhan_hieu, CASE WHEN GROUPING(segment) = 1 THEN '— Tất cả phân khúc —' ELSE segment END AS phan_khuc, sum(quantity) AS so_luongFROM salesGROUP BY GROUPING SETS ((brand, segment), (brand), (segment), ())ORDER BY GROUPING(brand), GROUPING(segment), brand, segment;
nhan_hieu | phan_khuc | so_luong----------------------+----------------------+---------- ABC | Basic | 200 ABC | Premium | 100 XYZ | Basic | 300 XYZ | Premium | 100 ABC | — Tất cả phân khúc — | 300 XYZ | — Tất cả phân khúc — | 400 — Tất cả nhãn hiệu — | Basic | 500 — Tất cả nhãn hiệu — | Premium | 200 — Tất cả nhãn hiệu — | — Tất cả phân khúc — | 700
Sắp xếp theo GROUPING(...) trước đảm bảo các dòng chi tiết nằm trên, tổng con ở giữa, tổng chung cuối cùng.
SELECT brand, segment, sum(quantity)FROM salesGROUP BY ROLLUP (brand, segment)ORDER BY brand, segment;
brand | segment | sum-------+---------+----- ABC | Basic | 200 ABC | Premium | 100 ABC | [NULL] | 300 ← tổng con của ABC XYZ | Basic | 300 XYZ | Premium | 100 XYZ | [NULL] | 400 ← tổng con của XYZ [NULL]| [NULL] | 700 ← tổng chung(7 rows)
So với GROUPING SETS đầy đủ (9 dòng), ROLLUPthiếu hai dòng tổng theo segment — vì ROLLUP chỉ đi theo một chiều.
ROLLUP dùng cho dữ liệu có thứ bậc
Ví dụ điển hình: ROLLUP(quoc_gia, tinh, huyen) — huyện thuộc tỉnh, tỉnh thuộc quốc gia. Tổng của một huyện lẻ không có ý nghĩa nếu không biết nó thuộc tỉnh nào.
Thứ tự cột rất quan trọng: ROLLUP(brand, segment) ≠ ROLLUP(segment, brand).
5.1. Rollup một phần
Có thể cố định một số cột, chỉ rollup phần còn lại:
SELECT brand, segment, sum(quantity)FROM salesGROUP BY brand, ROLLUP (segment)ORDER BY brand, segment;
Không có dòng tổng chung, vì brand luôn có mặt trong mọi grouping set.
5.2. Ví dụ thực tế — doanh thu theo năm/quý/tháng
SELECT EXTRACT(YEAR FROM payment_date) AS nam, EXTRACT(QUARTER FROM payment_date) AS quy, EXTRACT(MONTH FROM payment_date) AS thang, round(sum(amount), 2) AS doanh_thuFROM paymentGROUP BY ROLLUP ( EXTRACT(YEAR FROM payment_date), EXTRACT(QUARTER FROM payment_date), EXTRACT(MONTH FROM payment_date))ORDER BY nam, quy, thang;
Cho ra: doanh thu từng tháng → tổng từng quý → tổng từng năm → tổng tất cả.
6. CUBE — mọi tổ hợp
CUBE(c1, c2) sinh ra tất cả các tổ hợp con của các cột:
CUBE(c1, c2) → (c1, c2)
(c1)
(c2)
()
Với n cột, CUBE sinh ra 2ⁿ grouping set.
SELECT brand, segment, sum(quantity)FROM salesGROUP BY CUBE (brand, segment)ORDER BY brand, segment;
Đây chính là 9 dòng của GROUPING SETS đầy đủ ở mục 3 — CUBE(a, b) là cách viết tắt của GROUPING SETS ((a,b), (a), (b), ()).
6.1. Cube một phần
SELECT brand, segment, sum(quantity)FROM salesGROUP BY brand, CUBE (segment)ORDER BY brand, segment;
6.2. Cảnh báo về số lượng nhóm
Số cột
ROLLUP
CUBE
2
3
4
3
4
8
4
5
16
5
6
32
10
11
1.024
CUBE tăng theo hàm mũ
CUBE với 10 cột trên bảng 10 triệu dòng sẽ tạo ra hàng trăm triệu dòng kết quả. Chỉ dùng CUBE với 2–4 cột, hoặc chỉ định chính xác các grouping set bạn cần bằng GROUPING SETS.
7. So sánh ba cách
Sinh ra
Số grouping set
Dùng khi
GROUPING SETS (...)
Đúng những gì bạn liệt kê
Tùy bạn
Cần kiểm soát chính xác
ROLLUP(a, b, c)
Cắt dần từ phải
n + 1
Dữ liệu có thứ bậc
CUBE(a, b, c)
Mọi tổ hợp
2ⁿ
Phân tích đa chiều
Quan hệ tương đương:
ROLLUP (a, b) ≡ GROUPING SETS ((a, b), (a), ())CUBE (a, b) ≡ GROUPING SETS ((a, b), (a), (b), ())
Có thể kết hợp trong cùng một GROUP BY:
GROUP BY GROUPING SETS ( ROLLUP (nam, quy), CUBE (vung, kenh), (san_pham))
8. Ứng dụng: báo cáo doanh thu DVD Rental
SELECT CASE WHEN GROUPING(co.country) = 1 THEN '★ TOÀN CẦU' ELSE co.country END AS quoc_gia, CASE WHEN GROUPING(ci.city) = 1 THEN '☆ Cả nước' ELSE ci.city END AS thanh_pho, count(DISTINCT c.customer_id) AS so_khach, round(sum(p.amount), 2) AS doanh_thuFROM payment pJOIN customer c ON c.customer_id = p.customer_idJOIN address a ON a.address_id = c.address_idJOIN city ci ON ci.city_id = a.city_idJOIN country co ON co.country_id = ci.country_idWHERE co.country IN ('Vietnam', 'Japan', 'China')GROUP BY ROLLUP (co.country, ci.city)ORDER BY GROUPING(co.country), co.country, GROUPING(ci.city), ci.city;
Kết quả: doanh thu từng thành phố → tổng từng quốc gia → tổng toàn bộ, tất cả trong một lần quét.
9. Bài tập
Dùng CSDL dvdrental:
Đếm số phim theo rating và rental_duration, kèm tổng con cho từng rating và tổng chung. Dùng ROLLUP.
Làm lại bài 1 nhưng dùng CUBE — kết quả khác nhau ở điểm nào?
Tính doanh thu theo staff_id và tháng, kèm tổng theo nhân viên và tổng chung.
Dùng GROUPING SETS để lấy chỉ hai loại tổng: theo rating và theo rental_duration, không cần chi tiết và không cần tổng chung.
Thêm nhãn “TỔNG” thay cho NULL trong kết quả bài 1.
CUBE(a, b, c, d, e) tạo ra bao nhiêu grouping set? Còn ROLLUP thì sao?
Đáp án
1.
SELECT rating, rental_duration, count(*) AS so_phimFROM filmGROUP BY ROLLUP (rating, rental_duration)ORDER BY rating, rental_duration;
2.
SELECT rating, rental_duration, count(*) AS so_phimFROM filmGROUP BY CUBE (rating, rental_duration)ORDER BY rating, rental_duration;
CUBE có thêm các dòng tổng theo rental_duration (bỏ qua rating) — tức (NULL, 3), (NULL, 4)… mà ROLLUP không có.
3.
SELECT p.staff_id, date_trunc('month', p.payment_date)::date AS thang, round(sum(p.amount), 2) AS doanh_thuFROM payment pGROUP BY ROLLUP (p.staff_id, date_trunc('month', p.payment_date))ORDER BY p.staff_id, thang;
4.
SELECT rating, rental_duration, count(*) AS so_phimFROM filmGROUP BY GROUPING SETS ((rating), (rental_duration))ORDER BY GROUPING(rating), rating, rental_duration;
Đây chính là lúc GROUPING SETS hơn hẳn ROLLUP/CUBE: bạn chỉ định đúng những gì cần.
5.
SELECT CASE WHEN GROUPING(rating) = 1 THEN 'TỔNG' ELSE rating::text END AS xep_hang, CASE WHEN GROUPING(rental_duration) = 1 THEN 'TỔNG' ELSE rental_duration::text END AS so_ngay_thue, count(*) AS so_phimFROM filmGROUP BY ROLLUP (rating, rental_duration)ORDER BY GROUPING(rating), rating, GROUPING(rental_duration), rental_duration;
Chú ý rating::text và rental_duration::text — CASE yêu cầu mọi nhánh cùng kiểu.