DHDataHọc
TheoryLesson 4/85 min read+10 XP

Đơ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ảngHạtKhóa duy nhất
customersMột khách hàngid
ordersMột đơn hàngid
order_itemsMột dòng sản phẩm trong một đơnid, hoặc (order_id, product_id)
Bảng log sự kiệnMột sự kiệnevent_id
Bảng tổng hợp ngàyMộ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:

code
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:

code
-- 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:

  1. Đếm dòng gốc: SELECT COUNT(*) FROM orders;
  2. Đếm khóa duy nhất: SELECT COUNT(DISTINCT id) FROM orders; — hai số phải bằng nhau ở bảng gốc.
  3. Sau JOIN: so COUNT(*) với COUNT(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:

code
-- 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:

code
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

  1. Viết một câu mô tả hạt của orders, order_items, customers.
  2. Chạy COUNT(*) và COUNT(DISTINCT id) trên orders; giải thích kết quả.
  3. JOIN orders với order_items và so COUNT(*) với COUNT(DISTINCT o.id).
  4. Tính doanh thu mỗi đơn trong CTE, rồi tính trung bình mỗi đơn.
  5. 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ứngNguyên nhânCá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 đôiJOIN thêm bảng một-nhiềuTổng hợp trước trong CTE
Trung bình đơn saiTính trên hạt dòng hàngTổng theo đơn rồi AVG
Tổng thiếuLọc sau JOIN loại mất dòngLọ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ạtViết câu "một dòng = một ___"
Số liệu lệch báo cáo khácKhá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.

Use ← → to move between lessons