Đơn vị phân tích và hạt dữ liệu
Bạn sẽ học được
- "Hạt dữ liệu" (grain) là gì và vì sao nó quyết định mọi con số.
- Cách xác định một dòng trong bảng đại diện cho cái gì.
- Vì sao JOIN làm nhân dòng và nhân doanh thu.
- Cách đếm đúng bằng
COUNT(DISTINCT ...). - Quy trình kiểm tra hạt trước khi phân tích.
Khái niệm cốt lõi
Hạt dữ liệu trả lời câu hỏi: một dòng trong bảng đại diện cho cái gì? Một đơn hàng? Một sản phẩm trong đơn? Một khách hàng? Một ngày?
Mọi phép đếm và tổng chỉ đúng khi bạn biết hạt. Cùng một câu "có bao nhiêu đơn?" cho hai kết quả khác nhau nếu bảng thực chất ở hạt đơn hàng hay hạt dòng hàng.
| Bảng | Hạt | Khóa duy nhất |
|---|---|---|
customers | Một khách hàng | id |
orders | Một đơn hàng | id |
order_items | Một dòng sản phẩm trong một đơn | id, hoặc (order_id, product_id) |
| Bảng log sự kiện | Một sự kiện | event_id |
| Bảng tổng hợp ngày | Một ngày × một chiều | (date, dimension) |
Trước khi viết truy vấn, viết một câu: "Bảng này có một dòng cho mỗi ___." Nếu không điền được, chưa nên phân tích.
Vì sao JOIN nhân dòng
orders có một dòng mỗi đơn. order_items có nhiều dòng mỗi đơn. Khi JOIN, mỗi đơn xuất hiện lặp lại theo số dòng hàng:
SELECT o.id, o.order_date, oi.product_id, oi.quantity
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
ORDER BY o.id;Một đơn có ba sản phẩm cho ba dòng. Kết quả này đúng cho phân tích theo sản phẩm, nhưng sai nếu bạn đếm đơn:
-- Sai: đếm số dòng sau JOIN, không phải số đơn
SELECT COUNT(*) FROM orders o JOIN order_items oi ON oi.order_id = o.id;
-- Đúng: đếm đơn duy nhất
SELECT COUNT(DISTINCT o.id) FROM orders o JOIN order_items oi ON oi.order_id = o.id;Tương tự, SUM(oi.quantity * oi.unit_price) đúng vì mỗi dòng hàng chỉ xuất hiện một lần. Nhưng nếu JOIN thêm một bảng một-nhiều khác (ví dụ bảng khuyến mãi theo đơn), mỗi dòng hàng lại nhân lên và doanh thu bị cộng trùng.
Kiểm tra trước khi tin một con số
Ba bước kiểm tra nhanh:
- Đếm dòng gốc:
SELECT COUNT(*) FROM orders; - Đếm khóa duy nhất:
SELECT COUNT(DISTINCT id) FROM orders;— hai số phải bằng nhau ở bảng gốc. - Sau JOIN: so
COUNT(*)vớiCOUNT(DISTINCT o.id). Nếu chênh, JOIN đã nhân dòng.
Với doanh thu, kiểm tra chéo bằng hai đường: tổng từ order_items và tổng từ một bảng đã tổng hợp sẵn (nếu có). Hai đường lệch nhau là dấu hiệu nhân dòng hoặc thiếu bộ lọc.
Tổng hợp về đúng hạt
Khi phân tích ở hạt khác với hạt bảng, tổng hợp trước rồi mới JOIN:
-- Doanh thu mỗi đơn, gộp từ dòng hàng
WITH don_hang AS (
SELECT order_id, SUM(quantity * unit_price) AS doanh_thu
FROM order_items
GROUP BY order_id
)
SELECT c.city, SUM(dh.doanh_thu) AS doanh_thu
FROM orders o
JOIN don_hang dh ON dh.order_id = o.id
JOIN customers c ON c.id = o.customer_id
WHERE o.status = 'completed'
GROUP BY c.city;Cách này tránh nhân dòng vì don_hang đã ở hạt một dòng mỗi đơn trước khi JOIN với khách.
Ngược lại, khi cần trung bình trên mỗi đơn, phải tính tổng theo đơn trước rồi mới lấy trung bình:
WITH don_hang AS (
SELECT order_id, SUM(quantity * unit_price) AS doanh_thu
FROM order_items
GROUP BY order_id
)
SELECT AVG(doanh_thu) FROM don_hang;AVG(quantity * unit_price) trên bảng dòng hàng là trung bình mỗi dòng sản phẩm, không phải mỗi đơn — hai con số khác nhau và thường bị dùng lẫn.
Hạt thời gian
Cùng vấn đề với thời gian: dữ liệu theo ngày, nhưng câu hỏi theo tháng. Nếu chỉ cộng doanh thu theo ngày rồi chia trung bình, bạn đang tính trung bình ngày có dữ liệu, không phải trung bình mỗi ngày trong tháng. Tháng có 30 ngày nhưng chỉ 20 ngày bán được hàng sẽ bị thổi phồng.
Quy tắc: xác định hạt của câu hỏi trước, tổng hợp về hạt đó, rồi mới tính tiếp.
Thực hành
- Viết một câu mô tả hạt của
orders,order_items,customers. - Chạy
COUNT(*)vàCOUNT(DISTINCT id)trênorders; giải thích kết quả. - JOIN
ordersvớiorder_itemsvà soCOUNT(*)vớiCOUNT(DISTINCT o.id). - Tính doanh thu mỗi đơn trong CTE, rồi tính trung bình mỗi đơn.
- Tính trung bình mỗi dòng sản phẩm và so sánh với kết quả trên.
Lỗi thường gặp
| Triệu chứng | Nguyên nhân | Cách sửa |
|---|---|---|
| Số đơn cao hơn thực tế | JOIN một-nhiều rồi COUNT(*) | COUNT(DISTINCT order_id) |
| Doanh thu bị nhân đôi | JOIN thêm bảng một-nhiều | Tổng hợp trước trong CTE |
| Trung bình đơn sai | Tính trên hạt dòng hàng | Tổng theo đơn rồi AVG |
| Tổng thiếu | Lọc sau JOIN loại mất dòng | Lọc trước khi JOIN hoặc dùng LEFT JOIN |
| Không giải thích được số | Chưa xác định hạt | Viết câu "một dòng = một ___" |
| Số liệu lệch báo cáo khác | Khác hạt hoặc khác bộ lọc | Đối chiếu định nghĩa và nguồn |
Tóm tắt
- Hạt là "một dòng đại diện cho cái gì" — xác định trước khi tính.
- JOIN một-nhiều nhân dòng; đếm bằng
COUNT(DISTINCT ...). - Tổng hợp về hạt cần phân tích trước khi JOIN tiếp.
- Trung bình mỗi đơn khác trung bình mỗi dòng hàng.
- Hạt thời gian cũng quan trọng: trung bình mỗi ngày có dữ liệu khác mỗi ngày trong tháng.
Dùng ← → để chuyển bài