Appearance
Báo cáo Doanh số/Thực thu — Giải thích chỉ số & đề xuất bổ sung
Tài liệu này giải thích bản chất các chỉ số đang hiển thị trên dashboard
/r/reports/actual_revenue_report_group, đối chiếu với báo cáoNhóm chu kỳ khách hàng, và đề xuất bổ sung 2 cột để dashboard rõ nghĩa hơn cho ban điều hành.
1. Bối cảnh
Sếp đặt câu hỏi về cách hiểu chỉ số Tỉ lệ khách phát sinh doanh số = 35,33% trên dashboard tháng 5/2026. Sau khi kiểm tra code/SQL backend, có 2 vấn đề cần làm rõ:
- Tooltip hiện tại đang mô tả sai bản chất công thức (đã có wording fix riêng — xem mục 6).
- Dashboard thiếu 2 chỉ số phụ trợ giúp giải thích nhanh "tỉ lệ chuyển đổi đang ở mức nào".
- Cần đảm bảo không có hiểu nhầm giữa số khách báo cáo
Doanh số/Thực thu(18.126) với số khách báo cáoChu kỳ khách hàng(7.281).
2. Diễn giải con số 35,33%
Dùng số liệu thật trên dashboard kỳ 01/05–31/05/2026:
| Chỉ số hiển thị | Giá trị | Ý nghĩa kỹ thuật |
|---|---|---|
| Lượt khách đến | 20.184 | COUNT(*) FROM all_customer_visits — đếm lượt ghé (1 khách đi 3 lần = 3 lượt) |
| Số khách hàng | 18.126 | SELECT COUNT(*) FROM (SELECT DISTINCT customer_id FROM all_customer_visits) — gom distinct customer rồi đếm số dòng. Với dữ liệu sạch tương đương COUNT(DISTINCT customer_id), nhưng nếu có visit với customer_id IS NULL thì cộng thêm 1 (NULL vẫn được DISTINCT giữ lại 1 dòng). |
| Tỉ lệ khách phát sinh doanh số | 35,33% | Lượt có đơn / Tổng lượt |
Suy ngược tử số:
20.184 lượt × 35,33% ≈ 7.131 lượt→ Trong 20.184 lượt khách bước vào cửa hàng kỳ này, có ~7.131 lượt đã chốt được đơn (cờ all_customer_visits.is_zero_order = false), còn ~13.053 lượt ghé nhưng đi về không mua.
Cách đọc cho ban điều hành
"Cứ 100 lượt khách ghé spa, có khoảng 35 lượt rút ví mua dịch vụ, 65 lượt còn lại chỉ ghé rồi về."
Bổ sung — góc nhìn theo khách distinct (đối chiếu chéo với báo cáo Chu kỳ khách hàng)
Báo cáo Chu kỳ khách hàng → Chu Kỳ Mua Hàng cùng kỳ hiển thị 7.281 khách distinct có đơn. Từ đó suy ra:
| Góc nhìn | Công thức | Giá trị |
|---|---|---|
| Tỉ lệ chuyển đổi tại điểm chạm | 7.131 lượt có đơn / 20.184 lượt | 35,33% (dashboard hiện tại) |
| Tỉ lệ khách distinct mua (gần đúng) | 7.281 khách mua / 18.126 khách ghé | ~40,17% (ước tính, chưa hiển thị) |
⚠️ Lưu ý không phép trừ trực tiếp:
18.126(khách distinct có visit) và7.281(khách distinct có order) đến từ 2 bảng nguồn khác nhau (all_customer_visitsvsorder) với filter ngày khác nhau (visit_datevsorder.created_at). KHÔNG kết luận trực tiếp18.126 − 7.281 = 10.845 khách chỉ ghé không mua— con số đó cần xác định bằng query giao tập visits/buyers (xem SQL kiểm chứng mục 6.2).Tương tự, KHÔNG so sánh trực tiếp
7.281(khách distinct) với7.131(lượt visit có đơn) — khác đơn vị (khách vs lượt), khác bảng nguồn.
🔍 Insight về tỉ lệ 40,17% vs 35,33%: mẫu số "lượt" (20.184) lớn hơn mẫu số "khách distinct" (18.126) vì khách ghé nhiều lần được đếm nhiều lượt. Tử số 7.131 và 7.281 có cùng bậc độ lớn (cả hai đo "đã mua") nhưng đến từ 2 bảng khác nhau, không subtract trực tiếp. Nếu muốn 1 con số duy nhất cho "khách distinct mua", phải query đối soát từ visit table và order table như mục 6.2.
3. Lưu ý quan trọng (đừng nhầm)
| Nhầm lẫn thường gặp | Thực tế |
|---|---|
| "35% khách hàng đã mua dịch vụ" (như tooltip cũ đang viết) | Không. Là 35% lượt ghé có mua |
| Mẫu số = 18.126 (số khách hàng) | Sai. Mẫu số = 20.184 (số lượt ghé) |
| Tử số = số khách distinct có đơn | Sai. Tử số = số lượt có đơn (1 khách mua 2 lần đếm 2) |
Hệ quả nghiệp vụ: Nếu khách VIP đi spa 10 lần/tháng và mua cả 10 lần, họ đóng góp 10 vào cả tử và mẫu. Còn khách walk-in lần đầu không mua thì +1 vào mẫu, +0 vào tử. Vì vậy tỷ lệ này đo hiệu quả chuyển đổi tại điểm chạm, KHÔNG phải tỷ lệ khách mua / khách ghé.
4. Đối chiếu với chỉ số liền kề trên cùng dashboard
| Chỉ số | Giá trị | Mẫu số dùng để chia |
|---|---|---|
| Thực thu trung bình lượt khách đến | 937.398đ | Tổng lượt khách (20.184) — khớp logic với tỉ lệ chuyển đổi |
| Doanh thu TB trên khách ghé (gợi ý tự tính) | ~1.043.939đ/khách ghé | Thực thu 18.920.442.000đ / Số khách ghé 18.126 |
| Doanh thu TB trên khách có đơn (gợi ý tự tính, gần đúng) | ~2.598.193đ/khách có đơn | Thực thu 18.920.442.000đ / Số khách có đơn 7.281 |
⚠️ Cả 2 con số "TB trên khách" trên đều gần đúng vì tử số (thực thu, filter theo
paid_at) và mẫu số (khách distinct, filter theovisit_datehoặcorder.created_at) đến từ các filter ngày khác nhau (xem mục 6.4). Để có con số chính xác cần thống nhất filter ngày trước khi chia.
→ Ba cách tính khác nhau (theo lượt vs theo khách ghé vs theo khách có đơn) phục vụ 3 câu hỏi khác nhau:
- "Mỗi lần khách bước vào tạo ra bao nhiêu doanh thu?" → 937.398đ/lượt
- "Trung bình 1 khách hàng (đã từng ghé) đóng góp bao nhiêu?" → ~1.043.939đ/khách ghé (gần đúng)
- "Trung bình 1 khách có đơn trong kỳ đóng góp bao nhiêu?" → ~2.598.193đ/khách có đơn (gần đúng)
5. Đề xuất bổ sung 2 cột vào dashboard
Hiện tại dashboard chỉ hiển thị tỉ lệ %, không hiển thị tử số / mẫu số tuyệt đối. Người đọc phải tự suy ngược, dễ sai. Đề xuất bổ sung 2 cột mới ngay bên cạnh "Lượt khách đến" và "Số khách hàng":
Phương án A — Thêm 2 cột theo "lượt" (khuyến nghị)
┌────────────────────┬────────────────────┬────────────────────┬────────────────────┐
│ Lượt khách đến │ Số khách hàng │ Lượt có phát sinh │ Lượt ghé không mua │
│ 20.184 │ 18.126 │ ~7.131 │ ~13.053 │
│ ↑ Tăng X% │ ↑ Tăng Y% │ ↑ Tăng Z% │ ↓ Giảm W% │
└────────────────────┴────────────────────┴────────────────────┴────────────────────┘| Cột mới | Tử số | Mẫu số | Diễn giải |
|---|---|---|---|
| Lượt khách có phát sinh đơn | COUNT(*) FROM all_customer_visits WHERE is_zero_order = false | — (con số tuyệt đối) | Số lần khách bước vào và móc ví mua hàng |
| Lượt khách ghé không mua | COUNT(*) FROM all_customer_visits WHERE is_zero_order = true | — (con số tuyệt đối) | Số lần khách ghé chỉ tham khảo/tư vấn, không mua. Là target nhắm để team CS, telesales follow-up upsell. |
Ưu điểm A:
- Số liệu đã có sẵn trong PostgreSQL function (tử số
conversion_visitsđang được tính ngầm) → backend effort thấp. - Khớp logic với tỉ lệ chuyển đổi 35,33% — sếp tự cộng/chia ra được.
- Cột "Lượt ghé không mua" là chỉ số hành động rõ ràng cho team CSKH.
Phương án B — Thêm 2 cột theo "khách distinct"
┌────────────────────┬────────────────────┬────────────────────┬────────────────────┐
│ Số khách hàng │ Khách có mua │ Khách ghé không │ Tỉ lệ khách mua │
│ 18.126 │ ~X (cần tính) │ ~Y │ X/18.126 = ?% │
└────────────────────┴────────────────────┴────────────────────┴────────────────────┘Ưu điểm B: trả lời câu hỏi "có bao nhiêu khách (người) thực sự mua" — gần với cách sếp đọc thông thường.
Nhược điểm B: cần backend thay đổi — viết thêm SQL CTE để đếm DISTINCT customer_id từ all_customer_visits có và không có is_zero_order=false. Khác với cách customer_cycle đếm từ bảng order (xem mục 6).
Khuyến nghị
Triển khai phương án A trước (effort thấp, khớp với tỉ lệ chuyển đổi 35,33%). Phương án B có thể làm sau nếu sếp cần đối chiếu khách distinct (xem mục 6 để hiểu vì sao 2 báo cáo đang đếm khác nhau).
6. Đối chiếu "Số khách mua hàng" giữa 2 báo cáo
Đây là điểm dễ gây tranh cãi: 2 báo cáo đang đếm "khách mua hàng" theo cách KHÁC NHAU, dù tên gọi nghe giống nhau.
6.1 So sánh chi tiết
| Tiêu chí | Báo cáo A — Doanh số/Thực thu | Báo cáo B — Chu kỳ khách hàng |
|---|---|---|
| Card | "Số khách hàng" = 18.126 | "Số lượng khách bắt đầu mua" (sẽ đổi thành "Số khách có phát sinh đơn trong kỳ") = 7.281 |
| Bảng nguồn | all_customer_visits (bảng lượt ghé) | order (bảng đơn hàng) |
| Field filter ngày | visit_date | order.created_at |
| Loại trừ gì | KHÔNG loại trừ (kể cả visit 0đ) | Loại đơn order_canceled, prepaid_canceled |
| Filter loại | KHÔNG (visit không có kind) | Chỉ {service, cosmetic, prepaid} — loại bỏ internal_material, order_transfer |
| Customer NULL/rỗng | Có thể bao gồm — SELECT DISTINCT customer_id FROM all_visits giữ NULL như 1 giá trị riêng → +1 vào count nếu visit table có row NULL | Loại bỏ trong Go (if order.CustomerID != "") |
| Cách đếm | COUNT(DISTINCT customer_id) từ visit | COUNT(DISTINCT customer_id) từ order (qua len(orderMap) trong Go) |
6.2 Vì sao 2 số khác nhau (18.126 vs 7.281)?
18.126 khách có VISIT trong tháng 5
↓
trừ những khách chỉ ghé mà không mua
trừ khách có visit nhưng đơn được tạo qua tháng 6 (do thanh toán hôm sau)
trừ khách có visit nhưng đơn bị huỷ
trừ khách có visit nhưng đơn thuộc kind internal_material/transfer
cộng những khách order online không qua visit (nếu có)
↓
7.281 khách có ĐƠN không-huỷ thuộc {service, cosmetic, prepaid} trong tháng 5→ KHÔNG được kết luận 18.126 − 7.281 = 10.845 khách chỉ ghé không mua vì 2 con số đến từ 2 bảng khác nhau, filter ngày khác nhau, không cùng tập records. Để biết con số thật của tập "khách ghé nhưng không mua", phải query đối soát giao tập (xem SQL kiểm chứng dưới).
→ Tương tự, KHÔNG so sánh 7.281 (khách distinct từ order) với 7.131 (lượt visit có đơn) — hai con số khác đơn vị (khách vs lượt) và khác bảng nguồn.
Truy vấn kiểm chứng đề xuất (chạy trên prod để biết tỉ trọng thật):
sql
-- Bao nhiêu khách distinct có visit nhưng KHÔNG có đơn không-huỷ trong cùng kỳ?
WITH visits AS (
SELECT DISTINCT customer_id FROM all_customer_visits
WHERE visit_date BETWEEN '2026-05-01' AND '2026-05-31 23:59:59'
AND customer_id IS NOT NULL
),
buyers AS (
SELECT DISTINCT customer_id FROM "order"
WHERE created_at BETWEEN '2026-05-01' AND '2026-05-31 23:59:59'
AND order_service_status NOT IN ('order_canceled', 'prepaid_canceled')
AND order_kind IN ('service', 'cosmetic', 'prepaid')
AND customer_id IS NOT NULL
)
SELECT
(SELECT COUNT(*) FROM visits) AS visited,
(SELECT COUNT(*) FROM buyers) AS bought,
(SELECT COUNT(*) FROM visits v WHERE NOT EXISTS -- chỉ ghé không mua
(SELECT 1 FROM buyers b WHERE b.customer_id = v.customer_id)) AS visit_only,
(SELECT COUNT(*) FROM buyers b WHERE NOT EXISTS -- chỉ mua không ghé (online order?)
(SELECT 1 FROM visits v WHERE v.customer_id = b.customer_id)) AS bought_only;Kết quả thực mới biết được bao nhiêu khách "chỉ ghé không mua" + "ghé nhưng đơn rơi sang tháng sau" + "đơn bị huỷ" + edge cases khác — KHÔNG suy ra được từ phép trừ trực tiếp 18.126 − 7.281.
6.3 Cả 2 số đều ĐÚNG — chỉ là đo 2 hiện tượng khác nhau
| Báo cáo | Số khách | Trả lời câu hỏi |
|---|---|---|
A — Doanh số/Thực thu | 18.126 | "Có bao nhiêu khách (người khác nhau) đã GHÉ spa trong kỳ?" |
B — Chu kỳ khách hàng | 7.281 | "Có bao nhiêu khách (người khác nhau) đã MUA trong kỳ?" |
Nếu sếp hỏi "tháng này có bao nhiêu khách" — câu trả lời phụ thuộc vào ý sếp muốn đo traffic (ghé) hay conversion (mua).
Tỉ lệ chuyển đổi theo khách distinct (ước tính, chưa chính xác): 7.281 / 18.126 ≈ 40,17% — số này chỉ gần đúng vì tử (7.281) và mẫu (18.126) đến từ 2 bảng khác nhau, filter ngày khác nhau. Để có con số chính xác cần query đối soát giao tập như mục 6.2. Tuy nhiên đủ để thấy: cứ 100 khách ghé spa thì khoảng ~40 khách distinct có ít nhất 1 đơn — cao hơn 35,33% (theo lượt) vì khách ghé nhiều lần được mẫu "lượt" đếm nhiều.
6.4 Cảnh báo logic cần kiểm chứng
Có 3 điểm bất nhất giữa 2 báo cáo cần lưu ý khi đối chiếu:
| # | Bất nhất | Tác động |
|---|---|---|
| 1 | A bao gồm customer_id NULL (DISTINCT giữ 1 dòng NULL), B thì loại (if order.CustomerID != "") | Nếu visit có row customer_id IS NULL → A phồng nhẹ +1 so với B |
| 2 | A filter theo visit_date, B filter theo order.created_at | Khách ghé 31/5, thanh toán 1/6 → có trong A tháng 5, có trong B tháng 6 |
| 3 | A không filter trạng thái (vì visit không có status), B loại trừ canceled | Khách ghé + mua + sau đó huỷ đơn → có trong A, không có trong B |
Không phải bug, nhưng cần ghi trong tooltip để PO/BA hiểu khi đối chiếu chéo.
7. Action items đề xuất
| # | Việc | Ưu tiên | Effort |
|---|---|---|---|
| 1 | Đổi tooltip + label wording trên dashboard (chốt: "Số khách có phát sinh đơn trong kỳ") | 🔴 Cao — đang gây hiểu nhầm | ~1 ngày — chi tiết wording ở audit doc đi kèm |
| 2 | Bổ sung 2 cột "Lượt có phát sinh đơn" + "Lượt ghé không mua" (Phương án A) | 🟡 Trung — cần PRD nhỏ | ~2 ngày |
| 3 | Bổ sung dòng giải thích phương pháp luận trong header mỗi báo cáo: "Số khách báo cáo này đếm theo bảng visit; báo cáo Chu kỳ khách hàng đếm theo bảng đơn — không nên cộng/trừ trực tiếp." | 🟢 Thấp | <0.5 ngày |
| 4 | (Defer) Phương án B — thêm chỉ số khách distinct có mua trong báo cáo A | Chờ feedback sếp | Cần PRD M-size |
| 5 | (Defer) Tách "Khách mua lần đầu" thành chỉ số riêng nếu sếp muốn theo dõi cohort acquisition (xem ghi chú dưới) | Chờ confirm intent | PRD M-size |
Ghi chú về "Khách mua lần đầu" (first-time buyer)
Trong quá trình audit, có phát hiện: card "Số lượng khách bắt đầu mua" ở báo cáo Chu kỳ khách hàng đang KHÔNG đếm khách mua lần đầu dù tên gọi gợi ý vậy. Hiện đang đếm tất cả khách có đơn (gồm khách cũ quay lại).
→ PO đã chốt giữ nguyên logic và đổi label thành "Số khách có phát sinh đơn trong kỳ" cho khớp với thực tế.
→ Nếu sau này business cần thật sự đo "khách mua lần đầu" (cohort acquisition), cần PRD riêng để thêm chỉ số mới, không sửa chỉ số hiện tại.