Skip to content

🛠 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ẽ:

  1. 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
  2. Áp dụng LAG để tính MoM revenue growth — so sánh doanh thu tháng này vs tháng trước
  3. Tính Running Total + % of Total bằng SUM OVER — doanh thu tích lũy và tỷ trọng trong danh mục
  4. Thiết kế Star Schema cho database e-commerce — xác định fact table, dimension tables, grain
  5. 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ụLinkGhi chú
DB Fiddledb-fiddle.comOnline, không cần cài đặt — chọn PostgreSQL 15 (hỗ trợ Window Functions đầy đủ)
SQLite Onlinesqliteonline.comGiao diện đơn giản — SQLite 3.25+ hỗ trợ Window Functions
BigQuery Sandboxcloud.google.com/bigquery/docs/sandboxMiễn phí 1 TB/tháng — trải nghiệm cloud DW thực tế

Setup trước buổi học

  1. 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)
  2. 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)
  3. Click Run để tạo database
  4. Viết query ở ô Query SQL (bên phải) → click Run để xem kết quả
  5. 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ảngSố dòngMô tả
customers15Khách hàng — 4 vùng: Bắc (5), Nam (4), Trung (3), Tây Nguyên (2), có 3 email NULL
products12Sản phẩm — 5 danh mục: Thời Trang, Điện Tử, Gia Dụng, Thực Phẩm, Mỹ Phẩm
orders50Đơn hàng — 01/2025 → 01/2026, status: completed/cancelled
order_items92Chi tiết đơn — liên kết order ↔ product, có quantity & unit_price

⏱️ Thời lượng

PhầnThời gianNội dung
Setup & kiểm tra database10 phútTạo database trên DB Fiddle (PostgreSQL 15), chạy thử SELECT
Bài tập 1: TOP N per category25 phútROW_NUMBER + PARTITION BY — xếp hạng sản phẩm
Bài tập 2: MoM Revenue Growth25 phútLAG — so sánh doanh thu theo tháng
Bài tập 3: Running Total + % of Total25 phútSUM OVER — tích lũy + tỷ trọng
Bài tập Bonus: Star Schema Design20 phútThiết kế fact & dimension tables
Review & Feedback10 phútTrình bày insight, nhận xét chéo
Tổng115 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_namecategorytotal_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.8LGia Dụng4,250,000
Tai nghe BluetoothĐiện Tử3,150,000
Quần jean slim fitThời Trang3,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:

categoryproduct_nameproduct_revenuerank_in_categorycategory_total
Điện TửLaptop văn phòng 14"75,000,000184,630,000
Điện TửBàn phím cơ gaming4,800,000284,630,000
Điện TửTai nghe Bluetooth3,150,000384,630,000
Điện TửSạc dự phòng 10000mAh1,680,000484,630,000
Gia DụngNồi cơm điện 1.8L4,250,00015,870,000
Gia DụngBình giữ nhiệt 500ml1,620,00025,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:

categoryproduct_nameproduct_revenuerank_in_category
Điện TửLaptop văn phòng 14"75,000,0001
Điện TửBàn phím cơ gaming4,800,0002
Gia DụngNồi cơm điện 1.8L4,250,0001
Gia DụngBình giữ nhiệt 500ml1,620,0002
Mỹ PhẩmKem chống nắng SPF502,560,0001
Mỹ PhẩmSữa rửa mặt trà xanh1,200,0002
Thời TrangQuần jean slim fit3,150,0001
Thời TrangÁo thun nam basic2,750,0002
Thực PhẩmCà phê rang xay 500g1,560,0001
Thực PhẩmTrà oolong hộp 100 túi1,140,0002

✅ 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):

monthmonthly_revenue
2025-01-012,350,000
2025-02-011,380,000
2025-03-012,750,000
2025-04-013,700,000
2025-05-0112,680,000
2025-06-011,985,000
2025-07-011,780,000
2025-08-012,220,000
2025-09-014,600,000
2025-10-012,810,000
2025-11-019,010,000
2025-12-018,150,000
2026-01-0110,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:

monthmonthly_revenueprev_month_revenuemom_growth_pct
2025-01-012,350,000NULLNULL
2025-02-011,380,0002,350,000-41.3
2025-03-012,750,0001,380,00099.3
2025-04-013,700,0002,750,00034.5
2025-05-0112,680,0003,700,000242.7
............
2025-12-018,150,0009,010,000-9.5
2026-01-0110,950,0008,150,00034.4

📌 Dòng đầu tiên (T1/2025) có NULL cho prev_month — vì không có tháng nào trước đó. Đây là hành vi chuẩn của LAG.


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:

monthmonthly_revenueprev_monthmom_growth_pcttrendmoving_avg_3m
2025-01-012,350,000NULLNULL➡️ Ổn định2,350,000
2025-02-011,380,0002,350,000-41.3📉 Giảm1,865,000
2025-03-012,750,0001,380,00099.3📈 Tăng mạnh2,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:

monthmonthly_revenuecumulative_revenue
2025-01-012,350,0002,350,000
2025-02-011,380,0003,730,000
2025-03-012,750,0006,480,000
2025-04-013,700,00010,180,000
2025-05-0112,680,00022,860,000
.........
2026-01-0110,950,00063,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:

categoryproduct_nameproduct_revenuecategory_totalpct_of_category
Điện TửLaptop văn phòng 14"75,000,00084,630,00088.6
Điện TửBàn phím cơ gaming4,800,00084,630,0005.7
Điện TửTai nghe Bluetooth3,150,00084,630,0003.7
Điện TửSạc dự phòng 10000mAh1,680,00084,630,0002.0
Gia DụngNồi cơm điện 1.8L4,250,0005,870,00072.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:

monthmonthly_revenuecumulative_revenuepct_of_totalcumulative_pct
2025-01-012,350,0002,350,0003.73.7
2025-02-011,380,0003,730,0002.25.9
2025-03-012,750,0006,480,0004.310.2
...............
2025-12-018,150,00052,415,00012.982.7
2026-01-0110,950,00063,365,00017.3100.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 dungFormat
1File SQL — tất cả 10 query + 3 bonus (có comment giải thích)HoTen_Buoi06_WindowFunction.sql
2Bảng kết quả — screenshot hoặc copy kết quả query chínhPaste vào file Word/Google Doc
3ERD Star Schema — vẽ tay hoặc tool (draw.io, dbdiagram.io)Ảnh hoặc PDF
43 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 được25%10 query chính đều chạy không lỗi trên PostgreSQL
Kết quả chính xác25%Output khớp với bảng kết quả mong đợi
Window Function đúng20%ROW_NUMBER, LAG, SUM OVER dùng đúng cú pháp + đúng logic
Comment giải thích10%Mỗi query có comment mô tả mục đích, window function dùng
3 Insight có chất lượng15%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ổng100%

Rubric chi tiết

MứcĐiểmMô tả
Xuất sắc9–1010/10 query đúng, Window Function chính xác, 3 insight sâu sắc, có bonus Star Schema
Tốt7–88–9/10 query đúng, Window Function đúng logic, 3 insight có số liệu
Đạt5–66–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) -- MA3

Thứ 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ỗiNguyên nhânCách sửa
window function not allowed in WHEREWindow function chạy sau WHEREDùng CTE: tính trong CTE, lọc ở query ngoài
window function not allowed in GROUP BYKhông thể group by window resultDùng CTE tách logic
Running total = grand totalThiếu ORDER BY trong OVERThêm ORDER BY date hoặc explicit frame
LAG trả NULL dòng đầuKhông có dòng trướcDùng LAG(col, 1, 0) hoặc COALESCE(LAG(...), 0)
RANK bỏ qua thứ hạngDùng RANK thay vì DENSE_RANKĐổi sang DENSE_RANK nếu không muốn skip
Kết quả duplicatePARTITION BY không đủ chi tiếtKiểm tra lại PARTITION BY + GROUP BY

🔗 Tài liệu tham khảo


🔗 Quay lại: Buổi 6: SQL — Window Function & Data Warehouse