Appearance
🛠 Workshop Buổi 6: Phân tích nâng cao với Window Function
Sử dụng database E-commerce từ Buổi 5, viết SQL nâng cao với Window Functions — xếp hạng, so sánh MoM, tích lũy, tỷ trọng — và thiết kế Star Schema cho Data Warehouse!
🎯 Mục tiêu
Sau khi hoàn thành workshop này, bạn sẽ:
- Dùng ROW_NUMBER + PARTITION BY để tìm TOP N sản phẩm trong mỗi category — xếp hạng trong nhóm
- Áp dụng LAG để tính MoM revenue growth — so sánh doanh thu tháng này vs tháng trước
- Tính Running Total + % of Total bằng SUM OVER — doanh thu tích lũy và tỷ trọng trong danh mục
- Thiết kế Star Schema cho database e-commerce — xác định fact table, dimension tables, grain
- Rút ra 3 insight kinh doanh từ kết quả Window Function — có số liệu cụ thể, sẵn sàng đưa vào portfolio
🧰 Công cụ & Setup
| Công cụ | Link | Ghi chú |
|---|---|---|
| DB Fiddle | db-fiddle.com | Online, không cần cài đặt — chọn PostgreSQL 15 (hỗ trợ Window Functions đầy đủ) |
| SQLite Online | sqliteonline.com | Giao diện đơn giản — SQLite 3.25+ hỗ trợ Window Functions |
| BigQuery Sandbox | cloud.google.com/bigquery/docs/sandbox | Miễn phí 1 TB/tháng — trải nghiệm cloud DW thực tế |
Setup trước buổi học
- Truy cập DB Fiddle (db-fiddle.com) → chọn PostgreSQL 15 (⚠️ Buổi 6 cần PostgreSQL, không dùng SQLite như Buổi 5 — vì PostgreSQL hỗ trợ Window Functions đầy đủ hơn)
- Copy toàn bộ phần CREATE TABLE + INSERT ở mục Dataset bên dưới → dán vào ô Schema SQL (bên trái)
- Click Run để tạo database
- Viết query ở ô Query SQL (bên phải) → click Run để xem kết quả
- Lưu file .sql trên máy: mở VS Code → dán từng query → lưu thành
HoTen_Buoi06_WindowFunction.sql
💡 Tip: Workshop Buổi 6 sử dụng cùng database từ Buổi 5 (bảng customers, products, orders, order_items) + thêm dữ liệu mới cho Window Functions. Nếu bạn đã lưu schema Buổi 5, chỉ cần thêm INSERT mới!
📦 Dataset: E-commerce Database (mở rộng)
Database mô phỏng hệ thống e-commerce tại Việt Nam gồm 4 bảng liên kết — mở rộng từ Buổi 5 với thêm dữ liệu T11–T12/2025 và T01/2026 để thực hành Window Functions.
Sơ đồ quan hệ (ERD)
┌──────────────┐ ┌──────────────┐
│ customers │ │ products │
├──────────────┤ ├──────────────┤
│ customer_id │ (PK) │ product_id │ (PK)
│ name │ │ product_name │
│ email │ │ category │
│ region │ │ price │
│ signup_date │ │ stock_qty │
└──────┬───────┘ └──────┬───────┘
│ 1:N │ 1:N
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ orders │ │ order_items │
├──────────────┤ ├──────────────┤
│ order_id │ (PK) │ item_id │ (PK)
│ customer_id │ (FK) │ order_id │ (FK)
│ order_date │ │ product_id │ (FK)
│ status │ │ quantity │
│ total_amount │ │ unit_price │
└──────┬───────┘ └──────────────┘
│ 1:N ▲
└──────────────────────┘Schema & Sample Data
Copy toàn bộ block SQL dưới đây vào ô Schema SQL của DB Fiddle (chọn PostgreSQL 15):
sql
-- =============================================
-- E-COMMERCE DATABASE — Workshop Buổi 6
-- Mở rộng từ Buổi 5 + thêm data cho Window Functions
-- 4 bảng: customers, products, orders, order_items
-- =============================================
-- 1. BẢNG CUSTOMERS — Thông tin khách hàng
CREATE TABLE customers (
customer_id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT,
region TEXT,
signup_date DATE
);
INSERT INTO customers (customer_id, name, email, region, signup_date) VALUES
(1, 'Nguyễn Văn An', 'an.nguyen@email.com', 'Bắc', '2024-01-15'),
(2, 'Trần Thị Bình', 'binh.tran@email.com', 'Nam', '2024-02-20'),
(3, 'Lê Hoàng Cường', 'cuong.le@email.com', 'Trung', '2024-03-10'),
(4, 'Phạm Minh Dũng', 'dung.pham@email.com', 'Bắc', '2024-04-05'),
(5, 'Hoàng Thị Em', 'em.hoang@email.com', 'Nam', '2024-05-18'),
(6, 'Võ Đức Phong', 'phong.vo@email.com', 'Tây Nguyên', '2024-06-22'),
(7, 'Đặng Thu Hà', 'ha.dang@email.com', 'Bắc', '2024-07-30'),
(8, 'Bùi Quang Huy', NULL, 'Trung', '2024-08-14'),
(9, 'Ngô Thị Lan', 'lan.ngo@email.com', 'Nam', '2024-09-01'),
(10, 'Trịnh Văn Mạnh', 'manh.trinh@email.com', 'Bắc', '2024-10-12'),
(11, 'Lý Thị Ngọc', NULL, 'Tây Nguyên', '2024-11-05'),
(12, 'Dương Công Ơn', 'on.duong@email.com', 'Trung', '2024-12-20'),
(13, 'Mai Thị Phượng', 'phuong.mai@email.com', 'Nam', '2025-01-08'),
(14, 'Huỳnh Thanh Quân', NULL, 'Bắc', '2025-02-14'),
(15, 'Tô Thị Rạng', 'rang.to@email.com', 'Trung', '2025-03-22');
-- 2. BẢNG PRODUCTS — Danh mục sản phẩm
CREATE TABLE products (
product_id INTEGER PRIMARY KEY,
product_name TEXT NOT NULL,
category TEXT,
price REAL,
stock_qty INTEGER
);
INSERT INTO products (product_id, product_name, category, price, stock_qty) VALUES
(101, 'Áo thun nam basic', 'Thời Trang', 250000, 120),
(102, 'Quần jean slim fit', 'Thời Trang', 450000, 80),
(103, 'Tai nghe Bluetooth', 'Điện Tử', 350000, 200),
(104, 'Sạc dự phòng 10000mAh', 'Điện Tử', 280000, 150),
(105, 'Nồi cơm điện 1.8L', 'Gia Dụng', 850000, 60),
(106, 'Bình giữ nhiệt 500ml', 'Gia Dụng', 180000, 300),
(107, 'Cà phê rang xay 500g', 'Thực Phẩm', 120000, 500),
(108, 'Trà oolong hộp 100 túi', 'Thực Phẩm', 95000, 400),
(109, 'Kem chống nắng SPF50', 'Mỹ Phẩm', 320000, 180),
(110, 'Sữa rửa mặt trà xanh', 'Mỹ Phẩm', 150000, 250),
(111, 'Laptop văn phòng 14"', 'Điện Tử', 12500000, 25),
(112, 'Bàn phím cơ gaming', 'Điện Tử', 1200000, 70);
-- 3. BẢNG ORDERS — Đơn hàng (mở rộng: 01/2025 → 01/2026)
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY,
customer_id INTEGER,
order_date DATE,
status TEXT,
total_amount REAL,
FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);
INSERT INTO orders (order_id, customer_id, order_date, status, total_amount) VALUES
-- Dữ liệu Buổi 5 (01/2025 → 10/2025)
(1001, 1, '2025-01-05', 'completed', 700000),
(1002, 2, '2025-01-12', 'completed', 1300000),
(1003, 3, '2025-01-20', 'completed', 350000),
(1004, 1, '2025-02-03', 'completed', 530000),
(1005, 4, '2025-02-14', 'completed', 850000),
(1006, 5, '2025-02-28', 'cancelled', 450000),
(1007, 6, '2025-03-05', 'completed', 280000),
(1008, 2, '2025-03-15', 'completed', 1850000),
(1009, 7, '2025-03-22', 'completed', 620000),
(1010, 8, '2025-04-01', 'completed', 350000),
(1011, 3, '2025-04-10', 'completed', 950000),
(1012, 9, '2025-04-18', 'completed', 2400000),
(1013, 10, '2025-05-02', 'completed', 180000),
(1014, 1, '2025-05-15', 'completed', 12500000),
(1015, 11, '2025-05-20', 'cancelled', 320000),
(1016, 4, '2025-06-01', 'completed', 720000),
(1017, 12, '2025-06-12', 'completed', 215000),
(1018, 5, '2025-06-25', 'completed', 1050000),
(1019, 13, '2025-07-03', 'completed', 470000),
(1020, 2, '2025-07-14', 'completed', 960000),
(1021, 14, '2025-07-28', 'completed', 350000),
(1022, 7, '2025-08-05', 'completed', 1680000),
(1023, 15, '2025-08-18', 'cancelled', 250000),
(1024, 9, '2025-08-22', 'completed', 540000),
(1025, 6, '2025-09-01', 'completed', 850000),
(1026, 10, '2025-09-15', 'completed', 3600000),
(1027, 3, '2025-09-28', 'completed', 150000),
(1028, 1, '2025-10-05', 'completed', 1900000),
(1029, 8, '2025-10-18', 'completed', 630000),
(1030, 11, '2025-10-30', 'completed', 280000),
-- Dữ liệu mới Buổi 6 (11/2025 → 01/2026)
(1031, 2, '2025-11-03', 'completed', 2200000),
(1032, 1, '2025-11-08', 'completed', 980000),
(1033, 4, '2025-11-15', 'completed', 1500000),
(1034, 7, '2025-11-20', 'completed', 450000),
(1035, 9, '2025-11-25', 'completed', 3200000),
(1036, 3, '2025-11-28', 'completed', 680000),
(1037, 5, '2025-12-02', 'completed', 1800000),
(1038, 10, '2025-12-05', 'completed', 950000),
(1039, 1, '2025-12-10', 'completed', 2500000),
(1040, 2, '2025-12-15', 'completed', 1350000),
(1041, 6, '2025-12-18', 'completed', 720000),
(1042, 13, '2025-12-22', 'completed', 480000),
(1043, 8, '2025-12-28', 'completed', 350000),
(1044, 14, '2026-01-03', 'completed', 890000),
(1045, 1, '2026-01-05', 'completed', 3200000),
(1046, 3, '2026-01-08', 'completed', 1100000),
(1047, 5, '2026-01-12', 'completed', 750000),
(1048, 9, '2026-01-15', 'completed', 2800000),
(1049, 7, '2026-01-18', 'completed', 560000),
(1050, 12, '2026-01-22', 'completed', 1650000);
-- 4. BẢNG ORDER_ITEMS — Chi tiết từng sản phẩm trong đơn hàng
CREATE TABLE order_items (
item_id INTEGER PRIMARY KEY,
order_id INTEGER,
product_id INTEGER,
quantity INTEGER,
unit_price REAL,
FOREIGN KEY (order_id) REFERENCES orders(order_id),
FOREIGN KEY (product_id) REFERENCES products(product_id)
);
INSERT INTO order_items (item_id, order_id, product_id, quantity, unit_price) VALUES
-- Dữ liệu Buổi 5
(1, 1001, 101, 2, 250000),
(2, 1001, 106, 1, 180000),
(3, 1002, 102, 2, 450000),
(4, 1002, 103, 1, 350000),
(5, 1003, 103, 1, 350000),
(6, 1004, 104, 1, 280000),
(7, 1004, 101, 1, 250000),
(8, 1005, 105, 1, 850000),
(9, 1006, 102, 1, 450000),
(10, 1007, 104, 1, 280000),
(11, 1008, 105, 1, 850000),
(12, 1008, 109, 2, 320000),
(13, 1008, 106, 1, 180000),
(14, 1009, 107, 3, 120000),
(15, 1009, 110, 1, 150000),
(16, 1009, 106, 1, 180000),
(17, 1010, 103, 1, 350000),
(18, 1011, 109, 1, 320000),
(19, 1011, 107, 2, 120000),
(20, 1011, 108, 3, 95000),
(21, 1012, 111, 1, 12500000),
(22, 1013, 106, 1, 180000),
(23, 1014, 111, 1, 12500000),
(24, 1015, 109, 1, 320000),
(25, 1016, 101, 1, 250000),
(26, 1016, 103, 1, 350000),
(27, 1016, 107, 1, 120000),
(28, 1017, 108, 1, 95000),
(29, 1017, 107, 1, 120000),
(30, 1018, 102, 1, 450000),
(31, 1018, 109, 1, 320000),
(32, 1018, 104, 1, 280000),
(33, 1019, 110, 2, 150000),
(34, 1019, 108, 1, 95000),
(35, 1020, 103, 2, 350000),
(36, 1020, 106, 1, 180000),
(37, 1021, 103, 1, 350000),
(38, 1022, 112, 1, 1200000),
(39, 1022, 101, 1, 250000),
(40, 1022, 108, 2, 95000),
(41, 1023, 101, 1, 250000),
(42, 1024, 104, 1, 280000),
(43, 1024, 110, 1, 150000),
(44, 1025, 105, 1, 850000),
(45, 1026, 111, 1, 12500000),
(46, 1027, 110, 1, 150000),
(47, 1028, 112, 1, 1200000),
(48, 1028, 102, 1, 450000),
(49, 1028, 101, 1, 250000),
(50, 1029, 107, 2, 120000),
(51, 1029, 106, 1, 180000),
(52, 1029, 108, 1, 95000),
(53, 1030, 104, 1, 280000),
-- Dữ liệu mới Buổi 6 (11/2025 → 01/2026)
(54, 1031, 111, 1, 12500000),
(55, 1031, 103, 1, 350000),
(56, 1032, 101, 2, 250000),
(57, 1032, 107, 2, 120000),
(58, 1033, 105, 1, 850000),
(59, 1033, 109, 1, 320000),
(60, 1033, 106, 1, 180000),
(61, 1034, 108, 3, 95000),
(62, 1034, 110, 1, 150000),
(63, 1035, 111, 1, 12500000),
(64, 1035, 112, 1, 1200000),
(65, 1036, 103, 1, 350000),
(66, 1036, 104, 1, 280000),
(67, 1037, 105, 1, 850000),
(68, 1037, 102, 1, 450000),
(69, 1037, 101, 2, 250000),
(70, 1038, 109, 2, 320000),
(71, 1038, 107, 1, 120000),
(72, 1039, 111, 1, 12500000),
(73, 1040, 102, 2, 450000),
(74, 1040, 108, 1, 95000),
(75, 1041, 104, 2, 280000),
(76, 1041, 106, 1, 180000),
(77, 1042, 110, 2, 150000),
(78, 1042, 107, 1, 120000),
(79, 1043, 103, 1, 350000),
(80, 1044, 112, 1, 1200000),
(81, 1045, 111, 1, 12500000),
(82, 1045, 109, 1, 320000),
(83, 1046, 105, 1, 850000),
(84, 1046, 101, 1, 250000),
(85, 1047, 102, 1, 450000),
(86, 1047, 106, 1, 180000),
(87, 1048, 111, 1, 12500000),
(88, 1048, 103, 1, 350000),
(89, 1049, 107, 2, 120000),
(90, 1049, 108, 1, 95000),
(91, 1050, 112, 1, 1200000),
(92, 1050, 104, 1, 280000);Tổng quan dữ liệu
| Bảng | Số dòng | Mô tả |
|---|---|---|
| customers | 15 | Khách hàng — 4 vùng: Bắc (5), Nam (4), Trung (3), Tây Nguyên (2), có 3 email NULL |
| products | 12 | Sản phẩm — 5 danh mục: Thời Trang, Điện Tử, Gia Dụng, Thực Phẩm, Mỹ Phẩm |
| orders | 50 | Đơn hàng — 01/2025 → 01/2026, status: completed/cancelled |
| order_items | 92 | Chi tiết đơn — liên kết order ↔ product, có quantity & unit_price |
⏱️ Thời lượng
| Phần | Thời gian | Nội dung |
|---|---|---|
| Setup & kiểm tra database | 10 phút | Tạo database trên DB Fiddle (PostgreSQL 15), chạy thử SELECT |
| Bài tập 1: TOP N per category | 25 phút | ROW_NUMBER + PARTITION BY — xếp hạng sản phẩm |
| Bài tập 2: MoM Revenue Growth | 25 phút | LAG — so sánh doanh thu theo tháng |
| Bài tập 3: Running Total + % of Total | 25 phút | SUM OVER — tích lũy + tỷ trọng |
| Bài tập Bonus: Star Schema Design | 20 phút | Thiết kế fact & dimension tables |
| Review & Feedback | 10 phút | Trình bày insight, nhận xét chéo |
| Tổng | 115 phút |
Bài tập 1: TOP N sản phẩm theo Category — ROW_NUMBER + PARTITION BY (4 query)
Hướng dẫn
Mục tiêu: Thành thạo ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...) để xếp hạng dữ liệu trong mỗi nhóm — bài toán kinh điển nhất của Window Functions.
📌 Tóm tắt pattern: CTE (tính metric + gán rank) → SELECT WHERE rank <= N
Query 1: Doanh thu mỗi sản phẩm (base query — chuẩn bị data)
Yêu cầu: Tính tổng doanh thu (quantity * unit_price) mỗi sản phẩm từ tất cả đơn hàng completed. Hiển thị product_name, category, total_revenue. Sắp xếp theo total_revenue giảm dần.
sql
-- Query 1: Doanh thu mỗi sản phẩm — base query
-- Mục đích: Chuẩn bị dữ liệu trước khi dùng Window Function
SELECT
p.product_name,
p.category,
SUM(oi.quantity * oi.unit_price) AS total_revenue
FROM order_items oi
JOIN products p ON oi.product_id = p.product_id
JOIN orders o ON oi.order_id = o.order_id
WHERE o.status = 'completed'
GROUP BY p.product_name, p.category
ORDER BY total_revenue DESC;Kết quả mong đợi (12 sản phẩm):
| product_name | category | total_revenue |
|---|---|---|
| Laptop văn phòng 14" | Điện Tử | 75,000,000 |
| Bàn phím cơ gaming | Điện Tử | 4,800,000 |
| Nồi cơm điện 1.8L | Gia Dụng | 4,250,000 |
| Tai nghe Bluetooth | Điện Tử | 3,150,000 |
| Quần jean slim fit | Thời Trang | 3,150,000 |
| ... | ... | ... |
✅ Bạn thấy 12 sản phẩm với doanh thu — đây là base data cho Window Function!
Query 2: Xếp hạng sản phẩm trong mỗi category
Yêu cầu: Thêm cột rank_in_category — xếp hạng doanh thu trong mỗi danh mục bằng ROW_NUMBER. Hiển thị cả tổng doanh thu category bên cạnh.
sql
-- Query 2: Xếp hạng sản phẩm trong mỗi category
-- Mục đích: Hiểu PARTITION BY — chia nhóm, xếp hạng riêng mỗi nhóm
SELECT
p.category,
p.product_name,
SUM(oi.quantity * oi.unit_price) AS product_revenue,
ROW_NUMBER() OVER (
PARTITION BY p.category
ORDER BY SUM(oi.quantity * oi.unit_price) DESC
) AS rank_in_category,
SUM(SUM(oi.quantity * oi.unit_price)) OVER (
PARTITION BY p.category
) AS category_total
FROM order_items oi
JOIN products p ON oi.product_id = p.product_id
JOIN orders o ON oi.order_id = o.order_id
WHERE o.status = 'completed'
GROUP BY p.category, p.product_name
ORDER BY p.category, rank_in_category;Kết quả mong đợi:
| category | product_name | product_revenue | rank_in_category | category_total |
|---|---|---|---|---|
| Điện Tử | Laptop văn phòng 14" | 75,000,000 | 1 | 84,630,000 |
| Điện Tử | Bàn phím cơ gaming | 4,800,000 | 2 | 84,630,000 |
| Điện Tử | Tai nghe Bluetooth | 3,150,000 | 3 | 84,630,000 |
| Điện Tử | Sạc dự phòng 10000mAh | 1,680,000 | 4 | 84,630,000 |
| Gia Dụng | Nồi cơm điện 1.8L | 4,250,000 | 1 | 5,870,000 |
| Gia Dụng | Bình giữ nhiệt 500ml | 1,620,000 | 2 | 5,870,000 |
| ... | ... | ... | ... | ... |
📌 Mỗi category có numbering riêng! Laptop = hạng 1 Điện Tử, Nồi cơm = hạng 1 Gia Dụng.
Query 3: TOP 2 sản phẩm bán chạy nhất mỗi category (CTE + filter)
Yêu cầu: Chỉ lấy top 2 sản phẩm có doanh thu cao nhất trong mỗi category. Dùng CTE.
sql
-- Query 3: Top 2 sản phẩm mỗi category
-- Mục đích: Pattern kinh điển — CTE + ROW_NUMBER + WHERE rank <= N
WITH ranked_products AS (
SELECT
p.category,
p.product_name,
SUM(oi.quantity * oi.unit_price) AS product_revenue,
ROW_NUMBER() OVER (
PARTITION BY p.category
ORDER BY SUM(oi.quantity * oi.unit_price) DESC
) AS rank_in_category
FROM order_items oi
JOIN products p ON oi.product_id = p.product_id
JOIN orders o ON oi.order_id = o.order_id
WHERE o.status = 'completed'
GROUP BY p.category, p.product_name
)
SELECT
category,
product_name,
product_revenue,
rank_in_category
FROM ranked_products
WHERE rank_in_category <= 2
ORDER BY category, rank_in_category;Kết quả mong đợi:
| category | product_name | product_revenue | rank_in_category |
|---|---|---|---|
| Điện Tử | Laptop văn phòng 14" | 75,000,000 | 1 |
| Điện Tử | Bàn phím cơ gaming | 4,800,000 | 2 |
| Gia Dụng | Nồi cơm điện 1.8L | 4,250,000 | 1 |
| Gia Dụng | Bình giữ nhiệt 500ml | 1,620,000 | 2 |
| Mỹ Phẩm | Kem chống nắng SPF50 | 2,560,000 | 1 |
| Mỹ Phẩm | Sữa rửa mặt trà xanh | 1,200,000 | 2 |
| Thời Trang | Quần jean slim fit | 3,150,000 | 1 |
| Thời Trang | Áo thun nam basic | 2,750,000 | 2 |
| Thực Phẩm | Cà phê rang xay 500g | 1,560,000 | 1 |
| Thực Phẩm | Trà oolong hộp 100 túi | 1,140,000 | 2 |
✅ 10 dòng = 5 category × top 2. Đây là pattern quan trọng nhất — bạn sẽ dùng hàng ngày!
Query 4: So sánh ROW_NUMBER vs RANK vs DENSE_RANK
Yêu cầu: Trong cùng 1 query, thêm cả 3 loại ranking cho category Điện Tử — để thấy sự khác biệt.
sql
-- Query 4: So sánh 3 ranking functions trên cùng data
-- Mục đích: Hiểu khi nào dùng ROW_NUMBER, RANK, DENSE_RANK
SELECT
p.category,
p.product_name,
SUM(oi.quantity * oi.unit_price) AS product_revenue,
ROW_NUMBER() OVER (
PARTITION BY p.category ORDER BY SUM(oi.quantity * oi.unit_price) DESC
) AS row_num,
RANK() OVER (
PARTITION BY p.category ORDER BY SUM(oi.quantity * oi.unit_price) DESC
) AS rank_pos,
DENSE_RANK() OVER (
PARTITION BY p.category ORDER BY SUM(oi.quantity * oi.unit_price) DESC
) AS dense_rank_pos
FROM order_items oi
JOIN products p ON oi.product_id = p.product_id
JOIN orders o ON oi.order_id = o.order_id
WHERE o.status = 'completed'
GROUP BY p.category, p.product_name
ORDER BY p.category, product_revenue DESC;📌 Quan sát: Khi 2 sản phẩm có cùng revenue → ROW_NUMBER vẫn gán khác nhau (1, 2), RANK gán cùng hạng rồi skip (1, 1, 3), DENSE_RANK gán cùng hạng không skip (1, 1, 2).
Bài tập 2: MoM Revenue Growth — LAG (3 query)
Hướng dẫn
Mục tiêu: Thành thạo LAG(column, offset) OVER (ORDER BY ...) để truy cập dòng trước — tính % tăng trưởng Month-over-Month, bài toán phổ biến nhất trong báo cáo kinh doanh.
📌 Pattern: CTE (GROUP BY tháng → tổng doanh thu) → SELECT với LAG lấy tháng trước → tính % growth
Query 5: Doanh thu theo tháng (base query)
Yêu cầu: Tính tổng doanh thu mỗi tháng từ 01/2025 đến 01/2026 (chỉ completed). Hiển thị tháng và doanh thu.
sql
-- Query 5: Doanh thu theo tháng — base cho LAG
-- Mục đích: Chuẩn bị dữ liệu monthly trước khi dùng LAG
SELECT
DATE_TRUNC('month', order_date)::DATE AS month,
SUM(total_amount) AS monthly_revenue
FROM orders
WHERE status = 'completed'
GROUP BY DATE_TRUNC('month', order_date)
ORDER BY month;Kết quả mong đợi (13 tháng):
| month | monthly_revenue |
|---|---|
| 2025-01-01 | 2,350,000 |
| 2025-02-01 | 1,380,000 |
| 2025-03-01 | 2,750,000 |
| 2025-04-01 | 3,700,000 |
| 2025-05-01 | 12,680,000 |
| 2025-06-01 | 1,985,000 |
| 2025-07-01 | 1,780,000 |
| 2025-08-01 | 2,220,000 |
| 2025-09-01 | 4,600,000 |
| 2025-10-01 | 2,810,000 |
| 2025-11-01 | 9,010,000 |
| 2025-12-01 | 8,150,000 |
| 2026-01-01 | 10,950,000 |
📌 Tháng 5 đột biến (12.68M — Laptop), T11–T1 tăng cao (mùa Tết).
Query 6: MoM Growth = doanh thu tháng hiện tại vs tháng trước
Yêu cầu: Thêm cột prev_month_revenue bằng LAG, và tính mom_growth_pct (%).
sql
-- Query 6: MoM Revenue Growth bằng LAG
-- Mục đích: So sánh doanh thu mỗi tháng với tháng trước
WITH monthly AS (
SELECT
DATE_TRUNC('month', order_date)::DATE AS month,
SUM(total_amount) AS monthly_revenue
FROM orders
WHERE status = 'completed'
GROUP BY DATE_TRUNC('month', order_date)
)
SELECT
month,
monthly_revenue,
LAG(monthly_revenue, 1) OVER (ORDER BY month) AS prev_month_revenue,
ROUND(
(monthly_revenue - LAG(monthly_revenue, 1) OVER (ORDER BY month))
* 100.0 / LAG(monthly_revenue, 1) OVER (ORDER BY month),
1
) AS mom_growth_pct
FROM monthly
ORDER BY month;Kết quả mong đợi:
| month | monthly_revenue | prev_month_revenue | mom_growth_pct |
|---|---|---|---|
| 2025-01-01 | 2,350,000 | NULL | NULL |
| 2025-02-01 | 1,380,000 | 2,350,000 | -41.3 |
| 2025-03-01 | 2,750,000 | 1,380,000 | 99.3 |
| 2025-04-01 | 3,700,000 | 2,750,000 | 34.5 |
| 2025-05-01 | 12,680,000 | 3,700,000 | 242.7 |
| ... | ... | ... | ... |
| 2025-12-01 | 8,150,000 | 9,010,000 | -9.5 |
| 2026-01-01 | 10,950,000 | 8,150,000 | 34.4 |
📌 Dòng đầu tiên (T1/2025) có
NULLcho prev_month — vì không có tháng nào trước đó. Đây là hành vi chuẩn củaLAG.
Query 7: MoM Growth + phân loại tăng/giảm + rolling average 3 tháng
Yêu cầu: Nâng cao: thêm cột phân loại (📈 Tăng / 📉 Giảm / ➡️ Ổn định) và rolling average 3 tháng.
sql
-- Query 7: MoM Growth nâng cao — phân loại + MA3
-- Mục đích: Kết hợp LAG + CASE + AVG OVER (moving average)
WITH monthly AS (
SELECT
DATE_TRUNC('month', order_date)::DATE AS month,
SUM(total_amount) AS monthly_revenue
FROM orders
WHERE status = 'completed'
GROUP BY DATE_TRUNC('month', order_date)
)
SELECT
month,
monthly_revenue,
LAG(monthly_revenue, 1) OVER (ORDER BY month) AS prev_month,
ROUND(
(monthly_revenue - LAG(monthly_revenue, 1) OVER (ORDER BY month))
* 100.0 / NULLIF(LAG(monthly_revenue, 1) OVER (ORDER BY month), 0),
1
) AS mom_growth_pct,
CASE
WHEN monthly_revenue > LAG(monthly_revenue, 1) OVER (ORDER BY month) * 1.05
THEN '📈 Tăng mạnh'
WHEN monthly_revenue < LAG(monthly_revenue, 1) OVER (ORDER BY month) * 0.95
THEN '📉 Giảm'
ELSE '➡️ Ổn định'
END AS trend,
ROUND(
AVG(monthly_revenue) OVER (
ORDER BY month
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
), 0
) AS moving_avg_3m
FROM monthly
ORDER BY month;Kết quả mong đợi:
| month | monthly_revenue | prev_month | mom_growth_pct | trend | moving_avg_3m |
|---|---|---|---|---|---|
| 2025-01-01 | 2,350,000 | NULL | NULL | ➡️ Ổn định | 2,350,000 |
| 2025-02-01 | 1,380,000 | 2,350,000 | -41.3 | 📉 Giảm | 1,865,000 |
| 2025-03-01 | 2,750,000 | 1,380,000 | 99.3 | 📈 Tăng mạnh | 2,160,000 |
| ... | ... | ... | ... | ... | ... |
📌 NULLIF tránh chia cho 0. ROWS BETWEEN 2 PRECEDING AND CURRENT ROW = moving average 3 tháng gần nhất.
Bài tập 3: Running Total + Percent of Total — SUM OVER (3 query)
Hướng dẫn
Mục tiêu: Thành thạo SUM(...) OVER (ORDER BY ...) cho running total và SUM(...) OVER (PARTITION BY ...) cho tỷ trọng % — hai kỹ thuật phân tích quan trọng nhất cho báo cáo kinh doanh.
Query 8: Running Total doanh thu tích lũy theo tháng
Yêu cầu: Tính doanh thu tích lũy (cumulative) từ T1/2025 → T1/2026. Mỗi dòng = doanh thu tháng + tổng tích lũy đến tháng đó.
sql
-- Query 8: Running Total — doanh thu tích lũy theo tháng
-- Mục đích: SUM OVER (ORDER BY) → cumulative sum
WITH monthly AS (
SELECT
DATE_TRUNC('month', order_date)::DATE AS month,
SUM(total_amount) AS monthly_revenue
FROM orders
WHERE status = 'completed'
GROUP BY DATE_TRUNC('month', order_date)
)
SELECT
month,
monthly_revenue,
SUM(monthly_revenue) OVER (
ORDER BY month
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumulative_revenue
FROM monthly
ORDER BY month;Kết quả mong đợi:
| month | monthly_revenue | cumulative_revenue |
|---|---|---|
| 2025-01-01 | 2,350,000 | 2,350,000 |
| 2025-02-01 | 1,380,000 | 3,730,000 |
| 2025-03-01 | 2,750,000 | 6,480,000 |
| 2025-04-01 | 3,700,000 | 10,180,000 |
| 2025-05-01 | 12,680,000 | 22,860,000 |
| ... | ... | ... |
| 2026-01-01 | 10,950,000 | 63,365,000 |
📌 Dòng cuối: cumulative = tổng tất cả = 63.365M. Đây là sanity check — cumulative cuối kỳ PHẢI bằng SUM toàn bộ.
Query 9: Percent of Total — tỷ trọng doanh thu sản phẩm trong category
Yêu cầu: Tính % doanh thu mỗi sản phẩm so với tổng category (dùng SUM OVER PARTITION BY). Sắp xếp theo category rồi % giảm dần.
sql
-- Query 9: Percent of Total — tỷ trọng trong mỗi category
-- Mục đích: SUM OVER (PARTITION BY) → tổng nhóm, rồi chia
WITH product_rev AS (
SELECT
p.category,
p.product_name,
SUM(oi.quantity * oi.unit_price) AS product_revenue
FROM order_items oi
JOIN products p ON oi.product_id = p.product_id
JOIN orders o ON oi.order_id = o.order_id
WHERE o.status = 'completed'
GROUP BY p.category, p.product_name
)
SELECT
category,
product_name,
product_revenue,
SUM(product_revenue) OVER (PARTITION BY category) AS category_total,
ROUND(
product_revenue * 100.0
/ SUM(product_revenue) OVER (PARTITION BY category),
1
) AS pct_of_category
FROM product_rev
ORDER BY category, pct_of_category DESC;Kết quả mong đợi:
| category | product_name | product_revenue | category_total | pct_of_category |
|---|---|---|---|---|
| Điện Tử | Laptop văn phòng 14" | 75,000,000 | 84,630,000 | 88.6 |
| Điện Tử | Bàn phím cơ gaming | 4,800,000 | 84,630,000 | 5.7 |
| Điện Tử | Tai nghe Bluetooth | 3,150,000 | 84,630,000 | 3.7 |
| Điện Tử | Sạc dự phòng 10000mAh | 1,680,000 | 84,630,000 | 2.0 |
| Gia Dụng | Nồi cơm điện 1.8L | 4,250,000 | 5,870,000 | 72.4 |
| ... | ... | ... | ... | ... |
📌 Laptop chiếm 88.6% doanh thu Điện Tử — concentration risk! Insight kinh doanh: phụ thuộc quá nhiều vào 1 sản phẩm.
Query 10: Running Total + % of Grand Total + Cumulative %
Yêu cầu: Kết hợp: doanh thu mỗi tháng + running total + % so tổng năm + cumulative % — tất cả trong 1 query.
sql
-- Query 10: All-in-one — Running Total + % + Cumulative %
-- Mục đích: Kết hợp nhiều Window Functions trong 1 query
WITH monthly AS (
SELECT
DATE_TRUNC('month', order_date)::DATE AS month,
SUM(total_amount) AS monthly_revenue
FROM orders
WHERE status = 'completed'
GROUP BY DATE_TRUNC('month', order_date)
)
SELECT
month,
monthly_revenue,
-- Running total
SUM(monthly_revenue) OVER (
ORDER BY month
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumulative_revenue,
-- % so với tổng toàn bộ
ROUND(
monthly_revenue * 100.0
/ SUM(monthly_revenue) OVER (),
1
) AS pct_of_total,
-- Cumulative % (tích lũy %)
ROUND(
SUM(monthly_revenue) OVER (
ORDER BY month
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) * 100.0
/ SUM(monthly_revenue) OVER (),
1
) AS cumulative_pct
FROM monthly
ORDER BY month;Kết quả mong đợi:
| month | monthly_revenue | cumulative_revenue | pct_of_total | cumulative_pct |
|---|---|---|---|---|
| 2025-01-01 | 2,350,000 | 2,350,000 | 3.7 | 3.7 |
| 2025-02-01 | 1,380,000 | 3,730,000 | 2.2 | 5.9 |
| 2025-03-01 | 2,750,000 | 6,480,000 | 4.3 | 10.2 |
| ... | ... | ... | ... | ... |
| 2025-12-01 | 8,150,000 | 52,415,000 | 12.9 | 82.7 |
| 2026-01-01 | 10,950,000 | 63,365,000 | 17.3 | 100.0 |
📌
SUM() OVER ()— không có ORDER BY, không có PARTITION BY → tổng toàn bộ (grand total).SUM() OVER (ORDER BY month)→ running total. Hai window functions khác nhau hoàn toàn!
Bài tập Bonus: Thiết kế Star Schema cho E-commerce (thiết kế + SQL)
Hướng dẫn
Mục tiêu: Áp dụng Kimball Dimensional Modeling — chuyển database OLTP hiện tại (4 bảng normalized) thành Star Schema (1 fact + 3 dimension tables).
📌 Bạn không cần tạo database mới — chỉ cần viết DDL (CREATE TABLE) và vẽ ERD trên giấy/tool.
Bonus Query 1: Thiết kế Star Schema — DDL
Yêu cầu: Viết SQL tạo Star Schema gồm: dim_customer, dim_product, dim_date, fact_order_items.
sql
-- =============================================
-- STAR SCHEMA DESIGN — E-commerce ShopVN
-- Chuyển từ OLTP (4 bảng) → Star Schema (1 fact + 3 dim)
-- =============================================
-- DIMENSION: dim_date — Bảng ngày tháng
CREATE TABLE dim_date (
date_key INTEGER PRIMARY KEY, -- YYYYMMDD, ví dụ: 20260115
full_date DATE NOT NULL,
day_of_week INTEGER, -- 1 = Monday ... 7 = Sunday
day_name VARCHAR(10), -- 'Monday', 'Tuesday'...
day_of_month INTEGER, -- 1–31
week_of_year INTEGER, -- 1–53
month_number INTEGER, -- 1–12
month_name VARCHAR(10), -- 'January', 'February'...
quarter INTEGER, -- 1, 2, 3, 4
year INTEGER,
is_weekend BOOLEAN DEFAULT FALSE,
is_holiday BOOLEAN DEFAULT FALSE
);
-- DIMENSION: dim_customer — Thông tin khách hàng
CREATE TABLE dim_customer (
customer_key INTEGER PRIMARY KEY, -- Surrogate key
customer_id INTEGER NOT NULL, -- Business key (từ OLTP)
customer_name TEXT NOT NULL,
email TEXT,
region TEXT,
city TEXT,
signup_date DATE,
-- SCD Type 2 fields
effective_date DATE NOT NULL,
expiry_date DATE DEFAULT '9999-12-31',
is_current BOOLEAN DEFAULT TRUE
);
-- DIMENSION: dim_product — Thông tin sản phẩm
CREATE TABLE dim_product (
product_key INTEGER PRIMARY KEY, -- Surrogate key
product_id INTEGER NOT NULL, -- Business key (từ OLTP)
product_name TEXT NOT NULL,
category TEXT,
price REAL,
brand TEXT,
-- SCD Type 1 (overwrite)
last_updated DATE
);
-- FACT: fact_order_items — Bảng sự kiện chi tiết bán hàng
-- Grain: 1 dòng = 1 sản phẩm trong 1 đơn hàng (line item)
CREATE TABLE fact_order_items (
order_item_key INTEGER PRIMARY KEY, -- Surrogate key
-- Foreign keys → dimensions
date_key INTEGER REFERENCES dim_date(date_key),
customer_key INTEGER REFERENCES dim_customer(customer_key),
product_key INTEGER REFERENCES dim_product(product_key),
-- Degenerate dimension (không cần bảng riêng)
order_id INTEGER, -- DD: mã đơn hàng
-- Measures (metrics)
quantity INTEGER,
unit_price REAL,
line_total REAL, -- quantity * unit_price
discount_amount REAL DEFAULT 0
);📌 Grain: 1 dòng = 1 line item (1 sản phẩm trong 1 đơn hàng). Đây là mức chi tiết cao nhất — có thể aggregate lên bất kỳ mức nào (theo ngày, theo sản phẩm, theo khách hàng).
Bonus Query 2: Vẽ ERD Star Schema (text-based)
┌─────────────────┐
│ dim_product │
│─────────────────│
│ product_key (PK)│──┐
│ product_id │ │
│ product_name │ │
│ category │ │
│ price │ │
│ brand │ │
└─────────────────┘ │
│
┌─────────────────┐ │ ┌────────────────────────┐
│ dim_customer │ │ │ fact_order_items │
│─────────────────│ │ │────────────────────────│
│ customer_key (PK)│───────────────┼───>│ order_item_key (PK) │
│ customer_id │ │ │ date_key (FK) ─────│──> dim_date
│ customer_name │ └───>│ customer_key (FK) ─────│──> dim_customer
│ email │ │ product_key (FK) ─────│──> dim_product
│ region │ │ order_id (DD) │
│ signup_date │ │ quantity │
│ effective_date │ │ unit_price │
│ expiry_date │ │ line_total │
│ is_current │ │ discount_amount │
└─────────────────┘ └────────────────────────┘
│
┌─────────────────┐ │
│ dim_date │ │
│─────────────────│ │
│ date_key (PK) │<──────────────────┘
│ full_date │
│ day_name │
│ month_name │
│ quarter │
│ year │
│ is_weekend │
│ is_holiday │
└─────────────────┘Bonus Query 3: Query mẫu trên Star Schema
Yêu cầu: Viết query phân tích doanh thu theo quý + category + region trên star schema (giả sử đã có data).
sql
-- Bonus Query 3: Phân tích doanh thu trên Star Schema
-- Pattern: FROM fact → JOIN dim₁ → JOIN dim₂ → JOIN dim₃
-- Lọc trên dimension, aggregate trên fact
SELECT
d.year,
d.quarter,
p.category,
c.region,
SUM(f.line_total) AS total_revenue,
COUNT(*) AS total_items_sold,
COUNT(DISTINCT f.order_id) AS total_orders,
ROUND(AVG(f.line_total), 0) AS avg_item_value,
-- Window Function trên Star Schema!
SUM(SUM(f.line_total)) OVER (
PARTITION BY d.year, p.category
ORDER BY d.quarter
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumulative_category_revenue
FROM fact_order_items f
JOIN dim_date d ON f.date_key = d.date_key
JOIN dim_product p ON f.product_key = p.product_key
JOIN dim_customer c ON f.customer_key = c.customer_key
WHERE d.year = 2025
AND c.is_current = TRUE -- Chỉ lấy dimension record hiện tại (SCD Type 2)
GROUP BY d.year, d.quarter, p.category, c.region
ORDER BY d.quarter, total_revenue DESC;📌 Lưu ý Kimball pattern: FROM fact → JOIN dim. Lọc trên dim (WHERE d.year = 2025). Aggregate trên fact (SUM(f.line_total)). GROUP BY dim attributes.
📋 Deliverable — Bài nộp
Yêu cầu nộp
| # | Nội dung | Format |
|---|---|---|
| 1 | File SQL — tất cả 10 query + 3 bonus (có comment giải thích) | HoTen_Buoi06_WindowFunction.sql |
| 2 | Bảng kết quả — screenshot hoặc copy kết quả query chính | Paste vào file Word/Google Doc |
| 3 | ERD Star Schema — vẽ tay hoặc tool (draw.io, dbdiagram.io) | Ảnh hoặc PDF |
| 4 | 3 insight kinh doanh — rút ra từ kết quả Window Function, có số liệu cụ thể | Viết trong cùng file Word/Google Doc |
Template file .sql
sql
-- =============================================
-- Workshop Buổi 6: Phân tích nâng cao với Window Function
-- Họ tên: [Điền tên]
-- Ngày: [Điền ngày]
-- =============================================
-- === BÀI TẬP 1: TOP N PER CATEGORY (ROW_NUMBER + PARTITION BY) ===
-- Query 1: Doanh thu mỗi sản phẩm (base query)
-- [Paste query ở đây]
-- Query 2: Xếp hạng sản phẩm trong mỗi category
-- [Paste query ở đây]
-- Query 3: Top 2 sản phẩm mỗi category (CTE + filter)
-- [Paste query ở đây]
-- Query 4: So sánh ROW_NUMBER vs RANK vs DENSE_RANK
-- [Paste query ở đây]
-- === BÀI TẬP 2: MoM REVENUE GROWTH (LAG) ===
-- Query 5: Doanh thu theo tháng (base query)
-- [Paste query ở đây]
-- Query 6: MoM Growth bằng LAG
-- [Paste query ở đây]
-- Query 7: MoM Growth nâng cao (phân loại + MA3)
-- [Paste query ở đây]
-- === BÀI TẬP 3: RUNNING TOTAL + % OF TOTAL (SUM OVER) ===
-- Query 8: Running Total doanh thu tích lũy
-- [Paste query ở đây]
-- Query 9: Percent of Total (tỷ trọng trong category)
-- [Paste query ở đây]
-- Query 10: All-in-one — Running Total + % + Cumulative %
-- [Paste query ở đây]
-- === BONUS: STAR SCHEMA DESIGN ===
-- Bonus 1: DDL — CREATE TABLE star schema
-- [Paste DDL ở đây]
-- Bonus 2: ERD mô tả (vẽ riêng hoặc paste text diagram)
-- Bonus 3: Query mẫu trên star schema
-- [Paste query ở đây]
-- === 3 INSIGHT ===
-- Insight 1: [Viết ở đây]
-- Insight 2: [Viết ở đây]
-- Insight 3: [Viết ở đây]3 Insight mẫu (tham khảo)
Insight 1 — Laptop văn phòng chiếm 88.6% doanh thu Điện Tử — rủi ro tập trung: Trong category Điện Tử, Laptop văn phòng 14" chiếm 88.6% tổng doanh thu (75M / 84.6M). Nếu nguồn cung Laptop gián đoạn hoặc demand giảm, doanh thu category sụt 80%+. → Kiến nghị: đẩy mạnh bán Bàn phím cơ gaming (5.7%) và Tai nghe Bluetooth (3.7%) để giảm concentration risk. Target: top 1 product ≤ 60% category revenue.
Insight 2 — Doanh thu Q4/2025 + T1/2026 chiếm 46% cả năm — mùa vụ rõ ràng: Running total cho thấy doanh thu tích lũy đến hết T9/2025 = 31.1M (49%), nhưng chỉ riêng T10–T1/2026 = 31.9M (51%). Tháng cao nhất: T5 (12.68M — outlier Laptop) và T1/2026 (10.95M — mùa Tết). → Kiến nghị: tăng inventory + budget marketing từ T10, chuẩn bị flash sale Black Friday (T11) và Tết (T12–T1).
Insight 3 — MoM growth biến động mạnh: cần moving average để đánh giá trend: MoM growth dao động từ -41.3% (T2) đến +242.7% (T5) — biến động quá lớn do outlier (đơn Laptop). Moving average 3 tháng cho bức tranh ổn định hơn: trend tổng thể tăng từ H2/2025. → Kiến nghị: báo cáo cho board dùng MA3 (moving average 3 tháng) thay vì MoM raw — tránh panic khi 1 tháng giảm do seasonal.
📝 Tiêu chí chấm
| Tiêu chí | Trọng số | Chi tiết |
|---|---|---|
| Query đúng cú pháp & chạy được | 25% | 10 query chính đều chạy không lỗi trên PostgreSQL |
| Kết quả chính xác | 25% | Output khớp với bảng kết quả mong đợi |
| Window Function đúng | 20% | ROW_NUMBER, LAG, SUM OVER dùng đúng cú pháp + đúng logic |
| Comment giải thích | 10% | Mỗi query có comment mô tả mục đích, window function dùng |
| 3 Insight có chất lượng | 15% | Có số liệu cụ thể, có so sánh, có kiến nghị hành động |
| Bonus (Star Schema) | 5% | DDL đúng Kimball pattern + ERD rõ ràng |
| Tổng | 100% |
Rubric chi tiết
| Mức | Điểm | Mô tả |
|---|---|---|
| Xuất sắc | 9–10 | 10/10 query đúng, Window Function chính xác, 3 insight sâu sắc, có bonus Star Schema |
| Tốt | 7–8 | 8–9/10 query đúng, Window Function đúng logic, 3 insight có số liệu |
| Đạt | 5–6 | 6–7/10 query đúng, hiểu concept Window Function, 2 insight cơ bản |
| Chưa đạt | < 5 | < 6 query đúng hoặc Window Function sai logic |
💡 Tips & Best Practices
Window Function Cheat Sheet
sql
-- RANKING — xếp hạng
ROW_NUMBER() OVER (PARTITION BY group ORDER BY metric DESC) -- unique rank
RANK() OVER (PARTITION BY group ORDER BY metric DESC) -- skip ties
DENSE_RANK() OVER (PARTITION BY group ORDER BY metric DESC) -- no skip
-- OFFSET — truy cập dòng khác
LAG(column, 1) OVER (ORDER BY date) -- dòng trước
LEAD(column, 1) OVER (ORDER BY date) -- dòng sau
-- AGGREGATE — tổng hợp giữ dòng
SUM(col) OVER () -- grand total
SUM(col) OVER (PARTITION BY group) -- group total
SUM(col) OVER (ORDER BY date ROWS UNBOUNDED PRECEDING) -- running total
AVG(col) OVER (ORDER BY date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) -- MA3Thứ tự thực thi SQL (nhắc lại + Window Functions)
Thứ tự VIẾT: Thứ tự CHẠY:
1. SELECT 6. SELECT (+ Window Functions)
2. FROM 1. FROM / JOIN
3. WHERE 2. WHERE
4. GROUP BY 3. GROUP BY
5. HAVING 4. HAVING
6. Window Functions 5. Window Functions (chạy SAU GROUP BY, TRƯỚC ORDER BY)
7. ORDER BY 7. ORDER BY
8. LIMIT 8. LIMIT📌 Window Functions chạy SAU GROUP BY nhưng TRƯỚC ORDER BY/LIMIT. Đó là lý do bạn có thể dùng
SUM(SUM(x)) OVER (...)— aggregate trước (GROUP BY), rồi window function trên kết quả aggregate.
Lỗi thường gặp
| Lỗi | Nguyên nhân | Cách sửa |
|---|---|---|
window function not allowed in WHERE | Window function chạy sau WHERE | Dùng CTE: tính trong CTE, lọc ở query ngoài |
window function not allowed in GROUP BY | Không thể group by window result | Dùng CTE tách logic |
| Running total = grand total | Thiếu ORDER BY trong OVER | Thêm ORDER BY date hoặc explicit frame |
| LAG trả NULL dòng đầu | Không có dòng trước | Dùng LAG(col, 1, 0) hoặc COALESCE(LAG(...), 0) |
| RANK bỏ qua thứ hạng | Dùng RANK thay vì DENSE_RANK | Đổi sang DENSE_RANK nếu không muốn skip |
| Kết quả duplicate | PARTITION BY không đủ chi tiết | Kiểm tra lại PARTITION BY + GROUP BY |
🔗 Tài liệu tham khảo
- 📖 PostgreSQL Window Functions Tutorial — Tutorial chính thức
- 📖 PostgreSQL Window Function Reference — Reference đầy đủ
- 🎮 DataLemur SQL Questions — Practice window function interviews
- 🎮 LeetCode Database Problems — SQL challenges
- 📊 Mode Analytics — Window Functions — Tutorial cho analysts
- 📝 Kimball Group — Dimensional Modeling — Star schema design tips
- 🛠 DB Fiddle — Viết & test SQL trực tuyến
- 🛠 dbdiagram.io — Vẽ ERD online miễn phí
🔗 Quay lại: Buổi 6: SQL — Window Function & Data Warehouse