Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng bạn là BA cho một ứng dụng đặt đồ ăn. Sếp gọi bạn vào phòng họp và hỏi một câu rất đơn giản: "Tháng này chúng ta đốt 2 tỷ tiền quảng cáo kéo được 50.000 lượt tải app, nhưng doanh thu chẳng tăng. Người ta rơi rụng ở đâu vậy?". Nếu bạn chỉ đưa ra con số "tỷ lệ chuyển đổi 1,2%", bạn vừa trả lời sai trọng tâm. Sếp không cần một con số tổng — sếp cần biết người dùng rơi rụng ở bước nào để biết nên sửa cái gì.
Đó chính là lý do tồn tại của funnel analysis (phân tích phễu). Funnel là công cụ giúp BA biến một câu hỏi mơ hồ ("vì sao không bán được hàng?") thành một bức tranh cụ thể từng bước: bao nhiêu người vào, bao nhiêu người đăng ký, bao nhiêu người thêm vào giỏ, bao nhiêu người thanh toán thành công. Khi nhìn được tỷ lệ rớt (drop-off) tại từng bước, bạn không còn đoán mò — bạn chỉ vào đúng cái bước đang chảy máu và nói với team sản phẩm: "Đây, 70% người dùng bỏ cuộc ngay ở màn nhập địa chỉ giao hàng".
Trong nghề BA, funnel query là một trong những kỹ năng "ra tiền" nhanh nhất. Nó trực tiếp gắn với doanh thu, với trải nghiệm người dùng, và là ngôn ngữ chung giữa BA, product manager và growth team. Bài này sẽ dạy bạn cách viết funnel query bằng SQL từ con số không, hiểu bản chất của nó, và tránh những cái bẫy khiến nhiều BA cho ra con số sai mà không hề hay biết.
Khái niệm cốt lõi
Funnel là gì?
Funnel (phễu) là một chuỗi các bước có thứ tự mà người dùng phải đi qua để đạt được một mục tiêu. Hình dạng của nó giống cái phễu vì ở mỗi bước, một phần người dùng sẽ rơi rụng — bước sau luôn nhỏ hơn hoặc bằng bước trước.
Một funnel e-commerce điển hình:
visit (xem sản phẩm) → add_to_cart (thêm giỏ) → checkout (bắt đầu thanh toán) → purchase (mua thành công)
100.000 → 30.000 → 12.000 → 8.000
Hai chỉ số quan trọng nhất bạn rút ra từ funnel:
- Conversion rate (tỷ lệ chuyển đổi) giữa hai bước: ví dụ từ visit sang add_to_cart là 30.000 / 100.000 = 30%.
- Drop-off rate (tỷ lệ rơi rụng): phần còn lại, tức 70% người xem sản phẩm nhưng không thêm vào giỏ.
Hai loại funnel BA cần phân biệt
Đây là điểm cực kỳ quan trọng mà nhiều người mới bỏ qua:
1. Funnel theo sự kiện độc lập (mỗi bước đếm riêng). Bạn chỉ đơn giản đếm số người unique đã thực hiện mỗi hành động trong một khoảng thời gian, không quan tâm thứ tự. Loại này dễ viết, nhanh, đủ dùng cho báo cáo tổng quan. Nhược điểm: nó có thể đếm cả những người làm bước 3 mà chưa từng làm bước 1 (ví dụ user cũ quay lại mua thẳng).
2. Funnel theo thứ tự (ordered / sequential funnel). Bước sau chỉ đếm những người đã thực hiện bước trước đó, và theo đúng trình tự thời gian. Loại này chính xác hơn nhiều cho việc tối ưu sản phẩm, nhưng query phức tạp hơn vì bạn phải ràng buộc "ai đã làm A rồi mới làm B".
Trong thực tế, BA bắt đầu bằng loại 1 để có cái nhìn nhanh, rồi chuyển sang loại 2 khi cần điều tra sâu một bước cụ thể.
Cấu trúc dữ liệu đầu vào
Phần lớn funnel được xây trên một bảng events (sự kiện) có dạng dài (long format), mỗi dòng là một hành động:
| user_id | event_name | event_time |
|---|---|---|
| 1001 | visit | 2026-06-01 09:12 |
| 1001 | add_to_cart | 2026-06-01 09:15 |
| 1002 | visit | 2026-06-01 10:01 |
| 1001 | purchase | 2026-06-01 09:20 |
user_id rồi kiểm tra xem họ đã đi tới bước nào.Tình huống thực tế
Ví dụ 1 — Tiki: phễu mua hàng và phát hiện "điểm nghẽn ô địa chỉ"
Giả sử bạn là BA tại một sàn TMĐT kiểu Tiki. Trong tháng 6/2026, đội growth chạy chiến dịch sale giữa năm và muốn biết phễu mua hàng đang nghẽn ở đâu. Bạn có bảng events với các event_name: view_product, add_to_cart, start_checkout, purchase.
Bạn viết một funnel cơ bản đếm số user unique từng bước:
SELECT
COUNT(DISTINCT CASE WHEN event_name = 'view_product' THEN user_id END) AS step1_view,
COUNT(DISTINCT CASE WHEN event_name = 'add_to_cart' THEN user_id END) AS step2_cart,
COUNT(DISTINCT CASE WHEN event_name = 'start_checkout' THEN user_id END) AS step3_checkout,
COUNT(DISTINCT CASE WHEN event_name = 'purchase' THEN user_id END) AS step4_purchase
FROM events
WHERE event_time >= '2026-06-01' AND event_time < '2026-07-01';
Kết quả:
| step1_view | step2_cart | step3_checkout | step4_purchase |
|---|---|---|---|
| 420.000 | 138.000 | 95.000 | 41.000 |
Bài học: funnel không cho bạn câu trả lời "vì sao", nhưng nó chỉ chính xác chỗ cần đào sâu, giúp bạn không lãng phí thời gian tối ưu nhầm bước.
Ví dụ 2 — Be/Grab: phễu đặt xe theo đúng thứ tự thời gian
Bây giờ bạn là BA tại một app gọi xe kiểu Be. Funnel đặt xe: open_app → set_destination (chọn điểm đến) → request_ride (bấm đặt) → ride_completed (chuyến hoàn tất). Lần này bạn cần funnel theo đúng thứ tự vì một user có thể mở app nhiều lần trong ngày, và bạn chỉ muốn đếm những người đi đúng trình tự.
Cách phổ biến là dùng phép tự kết hợp dựa trên thời điểm đầu tiên của mỗi bước:
WITH first_step AS (
SELECT
user_id,
MIN(CASE WHEN event_name = 'open_app' THEN event_time END) AS t_open,
MIN(CASE WHEN event_name = 'set_destination' THEN event_time END) AS t_dest,
MIN(CASE WHEN event_name = 'request_ride' THEN event_time END) AS t_request,
MIN(CASE WHEN event_name = 'ride_completed' THEN event_time END) AS t_complete
FROM events
WHERE event_time >= '2026-06-01' AND event_time < '2026-06-08'
GROUP BY user_id
)
SELECT
COUNT(t_open) AS s1_open,
COUNT(CASE WHEN t_dest > t_open THEN 1 END) AS s2_dest,
COUNT(CASE WHEN t_request > t_dest THEN 1 END) AS s3_request,
COUNT(CASE WHEN t_complete > t_request THEN 1 END) AS s4_complete
FROM first_step;
Điểm cốt lõi: mệnh đề t_dest > t_open đảm bảo bạn chỉ đếm những người chọn điểm đến sau khi đã mở app — đúng tinh thần funnel theo thứ tự. Giả sử kết quả trong tuần đầu tháng 6:
| s1_open | s2_dest | s3_request | s4_complete |
|---|---|---|---|
| 210.000 | 180.000 | 165.000 | 120.000 |
Bài học: với hành vi lặp lại nhiều lần (mở app, tìm kiếm), funnel theo thứ tự cho con số trung thực hơn hẳn so với đếm độc lập. Dùng MIN(...) để lấy mốc thời gian đầu tiên rồi so sánh là một pattern bạn sẽ dùng đi dùng lại.
Ví dụ 3 — MoMo: phễu onboarding ví điện tử và bài toán cửa sổ thời gian
Bạn là BA cho một ví điện tử kiểu MoMo. Phễu kích hoạt người dùng mới: register (đăng ký) → verify_otp (xác thực OTP) → link_bank (liên kết ngân hàng) → first_transaction (giao dịch đầu tiên). Khác biệt ở đây: bạn quan tâm đến việc người dùng hoàn tất các bước trong vòng 7 ngày kể từ lúc đăng ký, vì nếu họ lề mề quá lâu thì coi như đã mất.
WITH steps AS (
SELECT
user_id,
MIN(CASE WHEN event_name = 'register' THEN event_time END) AS t_reg,
MIN(CASE WHEN event_name = 'verify_otp' THEN event_time END) AS t_otp,
MIN(CASE WHEN event_name = 'link_bank' THEN event_time END) AS t_bank,
MIN(CASE WHEN event_name = 'first_transaction' THEN event_time END) AS t_txn
FROM events
GROUP BY user_id
)
SELECT
COUNT(t_reg) AS s1_register,
COUNT(CASE WHEN t_otp > t_reg AND t_otp <= t_reg + INTERVAL '7 days' THEN 1 END) AS s2_otp,
COUNT(CASE WHEN t_bank > t_otp AND t_bank <= t_reg + INTERVAL '7 days' THEN 1 END) AS s3_bank,
COUNT(CASE WHEN t_txn > t_bank AND t_txn <= t_reg + INTERVAL '7 days' THEN 1 END) AS s4_txn
FROM steps;
Cú pháp t_reg + INTERVAL '7 days' là PostgreSQL; trên MySQL bạn dùng DATE_ADD(t_reg, INTERVAL 7 DAY). Giả sử kết quả:
| s1_register | s2_otp | s3_bank | s4_txn |
|---|---|---|---|
| 80.000 | 76.000 | 31.000 | 22.000 |
Bài học: funnel thực tế gần như luôn cần một cửa sổ thời gian (conversion window). Một người đăng ký hôm nay rồi 3 tháng sau mới giao dịch thì không nên tính là "đã chuyển đổi mượt". Luôn hỏi stakeholder: "Chúng ta cho người dùng bao lâu để hoàn tất?".
Hướng dẫn từng bước
Đây là quy trình bạn nên theo mỗi khi được giao một funnel mới:
Bước 1 — Định nghĩa các bước cùng stakeholder. Trước khi gõ một dòng SQL, hãy ngồi với PM/growth và thống nhất chính xác mỗi bước là event_name nào. "Đăng ký" là lúc bấm nút hay lúc tạo xong tài khoản? Mơ hồ ở đây sẽ làm sai cả báo cáo.
Bước 2 — Xác định loại funnel. Hỏi: cần đếm độc lập (nhanh, tổng quan) hay cần đúng thứ tự thời gian (chính xác, để tối ưu)? Và có cần conversion window không?
Bước 3 — Viết phiên bản đơn giản trước. Luôn bắt đầu bằng COUNT(DISTINCT CASE WHEN ...) để có con số tổng nhanh, kiểm tra xem các bước có giảm dần hợp lý không.
Bước 4 — Nâng cấp lên funnel theo thứ tự nếu cần. Dùng pattern MIN(CASE WHEN ...) gom mốc thời gian mỗi bước về một dòng/user, rồi so sánh t_sau > t_truoc.
Bước 5 — Tính tỷ lệ chuyển đổi. Bọc query trên trong một lớp ngoài rồi chia bước sau cho bước trước. Ví dụ:
SELECT
s1_register,
ROUND(100.0 * s2_otp / NULLIF(s1_register, 0), 1) AS reg_to_otp_pct,
ROUND(100.0 * s3_bank / NULLIF(s2_otp, 0), 1) AS otp_to_bank_pct,
ROUND(100.0 * s4_txn / NULLIF(s3_bank, 0), 1) AS bank_to_txn_pct
FROM funnel_counts;
Lưu ý NULLIF(..., 0) để tránh lỗi chia cho 0.
Bước 6 — Phân tách (segment) để tìm insight. Funnel tổng chỉ là khởi đầu. Hãy thêm GROUP BY theo nền tảng (iOS/Android), theo thành phố, theo nguồn quảng cáo. Thường thì điểm nghẽn chỉ xuất hiện ở một phân khúc cụ thể — ví dụ chỉ rớt mạnh trên Android.
Lỗi thường gặp & mẹo
Quên DISTINCT khiến đếm trùng. Nếu bạn dùng COUNT(user_id) thay vì COUNT(DISTINCT user_id), một người xem sản phẩm 10 lần sẽ bị tính thành 10. Funnel theo người dùng gần như luôn cần DISTINCT (hoặc gom về một dòng/user trước).
Bước sau lớn hơn bước trước. Nếu thấy step3 > step2, gần như chắc chắn bạn đang đếm độc lập và có những người vào thẳng bước sau mà không qua bước trước (ví dụ user cũ mua lại). Hoặc bạn quên ràng buộc thứ tự thời gian. Funnel đúng phải giảm dần đều.
Quên conversion window. Nếu không giới hạn thời gian, bạn sẽ tính cả những chuyển đổi xảy ra hàng tháng sau đó, làm tỷ lệ trông đẹp một cách giả tạo. Luôn thống nhất cửa sổ thời gian.
So sánh dùng >= hay >? Khi hai event có thể xảy ra trong cùng một giây (hệ thống ghi log thô), > có thể loại nhầm. Cân nhắc dùng >= hoặc so sánh kèm event_id để xử lý các trường hợp đồng thời.
Khác biệt MySQL vs PostgreSQL. Phép cộng ngày khác nhau: PostgreSQL dùng INTERVAL '7 days', MySQL dùng DATE_ADD(t, INTERVAL 7 DAY). FILTER (WHERE ...) chỉ có trên PostgreSQL — đây là cách viết gọn hơn CASE WHEN mà bạn nên học:
COUNT(*) FILTER (WHERE t_otp > t_reg) AS s2_otp -- chỉ PostgreSQL
Mẹo trình bày: stakeholder không đọc bảng số khô. Hãy luôn kèm cả con số tuyệt đối lẫn tỷ lệ phần trăm, và highlight bước rớt mạnh nhất. Một câu kết luận kiểu "70% người dùng bỏ cuộc ở bước X, tương đương 28.000 đơn mỗi tháng" mạnh hơn nhiều một bảng số trần trụi.
Bài tập thực hành
Cho bảng events(user_id, event_name, event_time) của một app học trực tuyến với các bước: view_course → start_trial → complete_lesson_1 → subscribe.
- Cơ bản: Viết query đếm số user unique ở mỗi bước trong tháng 5/2026, dùng
COUNT(DISTINCT CASE WHEN ...).
- Tỷ lệ: Mở rộng query trên để tính tỷ lệ chuyển đổi giữa từng cặp bước liên tiếp, làm tròn 1 chữ số thập phân, có xử lý chia cho 0.
- Theo thứ tự: Viết lại funnel sao cho mỗi bước chỉ đếm những người đã thực hiện bước trước đó theo đúng trình tự thời gian (dùng pattern
MIN(CASE WHEN ...)rồi so sánh mốc thời gian).
- Conversion window: Thêm ràng buộc rằng người dùng phải hoàn tất
subscribetrong vòng 14 ngày kể từview_course.
- Segment: Thêm
GROUP BY platform(giả sử bảng có cộtplatform) để xem funnel khác nhau thế nào giữa iOS và Android, và viết một câu nhận xét về bước nghẽn nặng nhất ở mỗi nền tảng.
Tóm tắt
Funnel analysis là cách BA biến câu hỏi mơ hồ "tại sao không chuyển đổi?" thành câu trả lời cụ thể "rớt ở bước nào, bao nhiêu phần trăm". Những điểm cần nhớ:
- Funnel là chuỗi bước có thứ tự, bước sau luôn nhỏ hơn hoặc bằng bước trước; hai chỉ số chính là conversion rate và drop-off rate.
- Phân biệt funnel đếm độc lập (nhanh, tổng quan, dùng
COUNT(DISTINCT CASE WHEN ...)) và funnel theo thứ tự (chính xác, dùngMIN(CASE WHEN ...)rồi so sánh mốc thời gian). - Thực tế gần như luôn cần conversion window — giới hạn thời gian để hoàn tất chuyển đổi.
- Tránh các bẫy: quên
DISTINCT, bước sau lớn hơn bước trước, quên cửa sổ thời gian, và khác biệt cú pháp ngày giữa MySQL/PostgreSQL. - Sức mạnh thật sự đến khi bạn segment funnel theo nền tảng, thành phố, nguồn quảng cáo — vì điểm nghẽn thường ẩn trong một phân khúc cụ thể.