Bài tập tổng hợp RFM
Bạn sẽ học được
- Ghép ba chỉ số RFM trong một truy vấn có
JOIN,GROUP BYvà hàm tổng hợp. - Vì sao
COUNT(DISTINCT o.id)là bắt buộc khi đã nốiorder_items. - Đặt bộ lọc
statusđúng chỗ. - Kiểm chứng kết quả bằng tổng trước khi nộp.
- Cách đọc bảng RFM để tìm khách hàng quan trọng.
Khái niệm cốt lõi
Đề bài RFM — Gộp ba chỉ số:
Trả về
name,recency_days,frequency,monetarycho từng khách (bỏ đơncancelled); sắp theomonetarygiảm dần.
Ba chỉ số:
| Chỉ số | Câu hỏi | Công thức |
|---|---|---|
| R — Recency | Mua lần cuối cách đây bao lâu? | DATE '2026-01-01' - MAX(order_date) |
| F — Frequency | Mua bao nhiêu lần? | COUNT(DISTINCT o.id) |
| M — Monetary | Tổng chi bao nhiêu? | SUM(quantity * unit_price) |
Mốc 2026-01-01 là ngày chốt dữ liệu, không phải CURRENT_DATE. Đây là điểm quan trọng: bài chấm so sánh với đáp án cố định, nên dùng CURRENT_DATE sẽ ra số khác mỗi ngày và không bao giờ khớp.
Đáp án:
SELECT c.name,
DATE '2026-01-01' - MAX(o.order_date) AS recency_days,
COUNT(DISTINCT o.id) AS frequency,
SUM(oi.quantity * oi.unit_price) AS monetary
FROM customers c
JOIN orders o ON o.customer_id = c.id AND o.status <> 'cancelled'
JOIN order_items oi ON oi.order_id = o.id
GROUP BY c.id, c.name
ORDER BY monetary DESC;Kết quả 10 dòng:
| name | recency_days | frequency | monetary |
|---|---|---|---|
| Bình Trần | 181 | 3 | 6750000.00 |
| Giang Đặng | 92 | 2 | 5490000.00 |
| Chi Lê | 51 | 2 | 4270000.00 |
| Minh Dương | 145 | 1 | 4030000.00 |
| An Nguyễn | 315 | 2 | 3530000.00 |
| Dũng Phạm | 240 | 1 | 2150000.00 |
| Khánh Hồ | 264 | 1 | 1640000.00 |
| Linh Ngô | 197 | 1 | 1190000.00 |
| Em Võ | 304 | 1 | 640000.00 |
| Nga Lý | 109 | 1 | 580000.00 |
Bốn quyết định trong câu lệnh
Một, lọc status trong ON. Điều kiện o.status <> 'cancelled' nằm ở mệnh đề ON của JOIN orders. Với INNER JOIN, đặt ở ON hay WHERE cho cùng kết quả. Nhưng đặt ở ON là thói quen đúng: nếu sau này đổi sang LEFT JOIN để giữ cả khách chưa mua, điều kiện ở WHERE sẽ biến LEFT JOIN thành INNER JOIN và làm mất khách. Giữ điều kiện gắn với bảng được nối.
Hai, COUNT(DISTINCT o.id). Đây là điểm dễ sai nhất. Luồng dữ liệu:
customers 1 ─── orders N ─── order_items MJOIN order_items nhân mỗi đơn thành nhiều dòng — mỗi sản phẩm một dòng. Với Bình Trần, 3 đơn có thể thành 6 dòng hàng. COUNT(*) sẽ trả 6 thay vì 3. COUNT(DISTINCT o.id) đếm số đơn duy nhất.
Hãy kiểm chứng bằng tổng:
SELECT SUM(frequency) AS tong_don
FROM ( ... câu đáp án ... ) t;Kết quả phải bằng 15 — đúng số đơn không bị huỷ. Nếu ra 29 (bằng số dòng order_items), bạn đã dùng COUNT(*).
Ba, GROUP BY c.id, c.name. Gom theo khóa chính của khách. PostgreSQL cho phép chỉ gom theo c.id vì name phụ thuộc hàm vào khóa chính, nhưng ghi cả hai giúp câu lệnh rõ nghĩa và chạy được trên hệ khác. Không gom theo name một mình: hai khách trùng tên sẽ bị gộp thành một.
Bốn, ORDER BY monetary DESC. Bài đánh dấu orderSensitive, nên thứ tự dòng được so sánh. monetary ở đây không trùng giá trị nào trên bộ công khai, nên DESC là đủ xác định. Trên dữ liệu thật, hãy thêm khóa phụ (ORDER BY monetary DESC, name) để kết quả ổn định — nhưng với bài chấm này, làm theo đúng đề.
Kiểm chứng ba con số
Trước khi nộp, chạy truy vấn tổng hợp:
SELECT COUNT(DISTINCT c.id) AS khach,
COUNT(DISTINCT o.id) AS don,
SUM(oi.quantity * oi.unit_price) AS doanh_thu
FROM customers c
JOIN orders o ON o.customer_id = c.id AND o.status <> 'cancelled'
JOIN order_items oi ON oi.order_id = o.id;Kết quả kỳ vọng: 10 | 15 | 30270000.
Ba con số này xác nhận:
- 10 khách có ít nhất một đơn hợp lệ (trong 12 khách, 2 người chưa từng mua).
- 15 đơn không bị huỷ (tổng 16 đơn, 1
cancelled). - 30.270.000 tổng doanh thu — bằng đúng tổng cột
monetarycủa bảng kết quả.
Nếu tổng monetary trong kết quả khác 30.270.000, hãy kiểm tra theo thứ tự: bộ lọc trạng thái, điều kiện nối, phép nhân quantity * unit_price. Đừng bỏ qua đơn pending: đề chỉ loại cancelled, nên đơn chờ xử lý vẫn được tính.
Đọc bảng RFM
Nhìn bảng kết quả, ba nhóm nổi lên:
- Bình Trần chi nhiều nhất (6,75 triệu), mua 3 lần, nhưng 181 ngày chưa quay lại. Đây là khách giá trị cao đang có dấu hiệu nguội — ưu tiên chăm sóc.
- Chi Lê mới mua cách đây 51 ngày, chi 4,27 triệu, 2 đơn. Khách đang hoạt động tốt, ứng viên cho upsell.
- An Nguyễn chi 3,53 triệu nhưng đã 315 ngày không mua. Giá trị lịch sử cao, rủi ro rời bỏ cao.
Một bảng RFM chỉ có giá trị khi được phân nhóm. Bài rfm-scores dùng NTILE(5) để chấm điểm; bài này dừng ở việc tính đúng ba chỉ số thô — nền tảng cho mọi bước sau.
Thực hành
- Nộp đáp án cho bài RFM — Gộp ba chỉ số (
rfm-combined). - Đổi
COUNT(DISTINCT o.id)thànhCOUNT(*)và xem cộtfrequencythay đổi thế nào. - Bỏ điều kiện
status <> 'cancelled'và tính lại tổng doanh thu — chênh bao nhiêu? - Thêm cột
so_monđếm số dòng hàng và so sánh vớifrequency. - Sắp theo
recency_daystăng dần và xác định ba khách "nóng" nhất.
Lỗi thường gặp
| Triệu chứng | Nguyên nhân | Cách sửa |
|---|---|---|
frequency quá lớn | COUNT(*) sau khi nối order_items | COUNT(DISTINCT o.id) |
| Doanh thu cao hơn kỳ vọng | Quên loại đơn cancelled | Thêm điều kiện status |
| Có khách 0 đơn trong kết quả | Dùng LEFT JOIN | Đề yêu cầu khách đã mua — dùng INNER JOIN |
recency_days đổi mỗi ngày | Dùng CURRENT_DATE | Dùng mốc cố định 2026-01-01 |
| Hai khách trùng tên bị gộp | GROUP BY c.name | Gom theo c.id |
| Sai thứ tự | Thiếu ORDER BY monetary DESC | Thêm sắp xếp đúng chiều |
monetary NULL | SUM trên nhóm rỗng hoặc quantity NULL | Kiểm tra dữ liệu và phép nối |
Tóm tắt
- RFM = recency, frequency, monetary; mốc chốt cố định
2026-01-01. COUNT(DISTINCT o.id)bắt buộc vìorder_itemsnhân dòng.- Lọc
cancelled, gom theoc.id, sắp theomonetary DESC. - Kiểm chứng bằng
10 | 15 | 30270000trước khi nộp. - Bảng thô là nền tảng; bước tiếp theo là chấm điểm và phân nhóm.
Use ← → to move between lessons