Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng một buổi sáng thứ Hai. Sếp của bạn nhắn trong nhóm: "DAU tuần này giảm 12% so với tuần trước, mọi người check giúp xem chuyện gì xảy ra." Một Growth PM giỏi sẽ không đợi data analyst rảnh tay, không submit ticket rồi chờ ba ngày. Họ tự mở trình truy vấn lên, gõ vài dòng SQL, lọc theo platform, theo nguồn acquisition, theo cohort đăng ký — và trong vòng hai mươi phút đã biết được con số giảm đến từ nhóm người dùng Android tải app qua một chiến dịch quảng cáo vừa hết ngân sách. Đó là khác biệt giữa người ra quyết định và người chờ người khác ra quyết định cho mình.
SQL (Structured Query Language) là ngôn ngữ để "hỏi chuyện" cơ sở dữ liệu. Với Growth PM, nó không phải kỹ năng "có thì tốt" mà là kỹ năng nền tảng. Lý do rất đơn giản: phần lớn câu hỏi growth — bao nhiêu người active, retention cohort tháng 3 ra sao, funnel rớt ở bước nào, người dùng trả tiền có hành vi gì khác người dùng free — đều là những câu hỏi mà câu trả lời nằm trong database, và SQL là con đường ngắn nhất để lấy nó ra. Bạn không cần trở thành kỹ sư dữ liệu. Bạn cần đủ giỏi để tự trả lời 80% câu hỏi thường gặp mà không phải làm phiền ai.
Trong bài này, chúng ta tập trung hẹp vào SQL như một công cụ thực hành cho Growth PM: những truy vấn phải-biết (DAU, retention, funnel), cú pháp lõi đủ dùng, và cách tránh các lỗi khiến con số sai mà bạn không hề hay biết. Đây là bài về viết câu hỏi và đọc câu trả lời, không phải về thiết kế hệ thống analytics hay chọn công cụ (những phần đó nằm ở các bài khác trong khóa).
Khái niệm cốt lõi
Mô hình tư duy: event và user
Hầu hết database growth xoay quanh hai loại bảng. Thứ nhất là bảng events — mỗi dòng là một hành động: một lần mở app, một lần click nút, một lần thanh toán. Một dòng event điển hình có user_id, event_name, event_time, và vài thuộc tính kèm theo (platform, screen, giá trị giao dịch...). Thứ hai là bảng users (hay dim_users) — mỗi dòng là một người dùng với thông tin tĩnh: ngày đăng ký, kênh acquisition, quốc gia, gói cước.
Gần như mọi câu hỏi growth đều là một biến thể của việc đếm hoặc nhóm event, rồi gắn chúng với thuộc tính user. Khi bạn nắm được khung tư duy này, SQL trở nên rất logic.
Bộ khung sáu mệnh đề
Một câu lệnh SELECT đầy đủ chạy theo thứ tự logic sau (nhớ thứ tự này giúp bạn tránh nhiều lỗi):
SELECT cột muốn lấy
FROM bảng nguồn
WHERE lọc dòng trước khi nhóm
GROUP BY nhóm theo cột nào
HAVING lọc sau khi đã nhóm
ORDER BY sắp xếp kết quả
Database đọc FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY. Đây là lý do bạn không dùng được alias đặt trong SELECT ở mệnh đề WHERE — lúc WHERE chạy, SELECT chưa được tính.
Truy vấn phải-biết số 1: Daily Active Users
Đây là truy vấn nền tảng nhất:
SELECT DATE(event_time) AS day,
COUNT(DISTINCT user_id) AS dau
FROM events
WHERE event_time >= '2026-06-01'
GROUP BY DATE(event_time)
ORDER BY day;
Điểm mấu chốt là COUNT(DISTINCT user_id). Nếu bạn quên DISTINCT, bạn đang đếm số lần hoạt động chứ không phải số người. Một người mở app 30 lần sẽ bị tính thành 30, và DAU của bạn phồng lên một cách vô nghĩa.
Truy vấn phải-biết số 2: stickiness DAU/MAU
Từ DAU, chỉ cần một bước nữa là ra chỉ số gắn bó:
SELECT COUNT(DISTINCT CASE WHEN event_time >= DATE_SUB(CURRENT_DATE, INTERVAL 1 DAY)
THEN user_id END) AS dau,
COUNT(DISTINCT user_id) AS mau
FROM events
WHERE event_time >= DATE_SUB(CURRENT_DATE, INTERVAL 30 DAY);
Tỷ lệ DAU/MAU cho biết trung bình một người dùng tháng quay lại bao nhiêu ngày. 0,5 nghĩa là họ dùng app khoảng 15/30 ngày — rất cao. Mạng xã hội thường 0,5+, app công cụ có thể chỉ 0,1–0,2.
Truy vấn phải-biết số 3: retention theo cohort
Đây là nơi SQL bắt đầu thể hiện sức mạnh. Ta cần nối bảng user (lấy ngày đăng ký) với bảng event (xem họ có quay lại không):
SELECT u.signup_date,
COUNT(DISTINCT u.user_id) AS cohort_size,
COUNT(DISTINCT CASE WHEN DATE(e.event_time) = DATE_ADD(u.signup_date, INTERVAL 7 DAY)
THEN e.user_id END) AS retained_d7
FROM users u
LEFT JOIN events e ON u.user_id = e.user_id
GROUP BY u.signup_date
ORDER BY u.signup_date;
LEFT JOIN cực kỳ quan trọng ở đây: nó giữ lại cả những user không có event nào sau ngày đăng ký. Nếu bạn dùng INNER JOIN, những người bỏ đi ngay sẽ biến mất khỏi mẫu số, và retention của bạn bị thổi phồng giả tạo.
Tình huống thực tế
Ví dụ 1 — Tiki và bài toán DAU "ảo"
Giả định một Growth PM tại một sàn thương mại điện tử quy mô như Tiki phát hiện DAU dashboard báo 1,2 triệu nhưng doanh thu và đơn hàng không tương xứng. Cô viết một truy vấn kiểm tra:
SELECT event_name, COUNT(*) AS events, COUNT(DISTINCT user_id) AS users
FROM events
WHERE DATE(event_time) = '2026-06-20'
GROUP BY event_name
ORDER BY events DESC;
Kết quả lộ ra rằng phần lớn "active" đến từ một event push_received (người dùng nhận thông báo đẩy, kể cả khi không mở). Định nghĩa DAU đang đếm cả hành động thụ động. Cô sửa lại định nghĩa active thành "có ít nhất một event chủ động" bằng WHERE event_name IN ('app_open','search','add_to_cart','purchase'), và DAU thực rơi xuống còn 740 nghìn — con số khớp với thực tế kinh doanh.
Bài học: SQL không chỉ để lấy số, mà để kiểm tra định nghĩa. Trước khi tin một chỉ số, hãy hỏi: nó được tính từ những event nào? Một dòng GROUP BY event_name đơn giản thường phơi bày những giả định sai mà không ai để ý.
Ví dụ 2 — App giao đồ ăn và funnel checkout rớt
Một Growth PM tại một startup giao đồ ăn ở TP.HCM muốn biết funnel đặt hàng rớt ở đâu. Anh viết:
SELECT
COUNT(DISTINCT CASE WHEN event_name='view_restaurant' 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='checkout_start' THEN user_id END) AS step3_checkout,
COUNT(DISTINCT CASE WHEN event_name='order_placed' THEN user_id END) AS step4_order
FROM events
WHERE DATE(event_time) = '2026-06-22';
Kết quả: 50.000 → 28.000 → 9.000 → 8.200. Tỷ lệ rớt lớn nhất nằm giữa add_to_cart (28k) và checkout_start (9k) — chỉ 32% người thêm vào giỏ bắt đầu thanh toán. Đào sâu bằng cách thêm GROUP BY platform, anh thấy nhóm Android rớt nặng hơn nhiều. Hóa ra nút "Đặt hàng" trên Android bị che một phần bởi thanh điều hướng trên vài dòng máy phổ biến. Sửa giao diện xong, conversion bước này tăng từ 32% lên 47%.
Bài học: Funnel viết bằng CASE WHEN trong một truy vấn duy nhất cho bạn bức tranh tổng thể nhanh chóng. Nhưng con số tổng hợp luôn che giấu sự thật — chỉ khi GROUP BY theo platform/kênh/quốc gia thì điểm đau thực sự mới hiện ra.
Ví dụ 3 — Phân khúc người dùng trả phí để tìm Aha Moment
Một Growth PM ở một SaaS công cụ thiết kế nghi ngờ rằng người dùng dùng tính năng "cộng tác nhóm" có khả năng trả phí cao hơn. Cô kiểm chứng bằng SQL:
SELECT
CASE WHEN c.collab_users > 0 THEN 'used_collab' ELSE 'no_collab' END AS segment,
COUNT(DISTINCT u.user_id) AS total,
COUNT(DISTINCT CASE WHEN u.plan != 'free' THEN u.user_id END) AS paid,
ROUND(100.0 * COUNT(DISTINCT CASE WHEN u.plan != 'free' THEN u.user_id END)
/ COUNT(DISTINCT u.user_id), 1) AS conv_rate
FROM users u
LEFT JOIN (SELECT user_id, COUNT(*) AS collab_users
FROM events WHERE event_name='invite_teammate' GROUP BY user_id) c
ON u.user_id = c.user_id
GROUP BY 1;
Kết quả: nhóm dùng cộng tác có conversion 18%, nhóm không dùng chỉ 4%. Phát hiện này dẫn tới việc đưa lời mời cộng tác lên sớm trong onboarding, và conversion tổng thể tăng đáng kể trong quý sau.
Bài học: Subquery (truy vấn lồng) cho phép bạn tạo ra phân khúc dựa trên hành vi, rồi so sánh tỷ lệ chuyển đổi giữa các phân khúc. Đây là cách Growth PM dùng SQL để kiểm chứng giả thuyết, không chỉ báo cáo số.
Hướng dẫn từng bước
Khi đứng trước một câu hỏi growth bất kỳ, hãy đi theo quy trình sau:
Bước 1 — Dịch câu hỏi kinh doanh thành câu hỏi dữ liệu. "Người dùng mới có giữ chân tốt không?" trở thành "Trong nhóm đăng ký tuần này, bao nhiêu phần trăm có ít nhất một event vào ngày thứ 7?". Câu hỏi càng cụ thể, SQL càng dễ.
Bước 2 — Xác định bảng và cột cần dùng. Bạn cần users cho ngày đăng ký, events cho hành động. Tìm hiểu schema trước — đừng đoán tên cột.
Bước 3 — Viết khung WHERE trước, lọc nhỏ. Luôn thêm điều kiện thời gian (WHERE event_time >= ...) ngay từ đầu. Chạy thử trên một ngày dữ liệu để truy vấn nhanh và dễ kiểm tra, rồi mới mở rộng phạm vi.
Bước 4 — Thêm GROUP BY và hàm tổng hợp. Quyết định bạn đang đếm gì (COUNT(DISTINCT user_id) cho người, COUNT(*) cho lần, SUM(amount) cho doanh thu) và nhóm theo chiều nào.
Bước 5 — Kiểm tra tỉnh táo (sanity check). Tổng số có hợp lý không? So với dashboard chính thức có gần khớp không? Nếu retention ra 95% hoặc DAU lớn hơn tổng số user, chắc chắn có lỗi JOIN hoặc thiếu DISTINCT.
Bước 6 — Tinh chỉnh và phân khúc. Khi con số tổng đã đúng, thêm GROUP BY platform/channel/country để tìm câu chuyện ẩn bên dưới.
Một mẹo về LIMIT: luôn thêm LIMIT 100 khi khám phá dữ liệu thô để tránh kéo về hàng triệu dòng làm treo công cụ. Bỏ nó ra chỉ khi bạn cần kết quả tổng hợp cuối cùng.
Lỗi thường gặp & mẹo
Quên DISTINCT khi đếm người. Lỗi phổ biến nhất. COUNT(user_id) đếm số dòng, COUNT(DISTINCT user_id) đếm số người. Khi đếm người dùng, gần như luôn cần DISTINCT.
Dùng INNER JOIN khi cần LEFT JOIN trong retention. INNER JOIN âm thầm loại bỏ những user không có event, khiến mẫu số nhỏ lại và retention bị thổi phồng. Khi tính tỷ lệ giữ chân, mẫu số phải là toàn bộ cohort, nên dùng LEFT JOIN từ bảng users.
Nhân đôi dòng do JOIN nhiều-nhiều. Khi nối hai bảng mà cả hai đều có nhiều dòng cho một user, kết quả nhân lên (fan-out). Triệu chứng: SUM(amount) lớn gấp nhiều lần thực tế. Mẹo: trước khi JOIN, hãy gộp (aggregate) một bên về mức một-dòng-một-user bằng subquery, như ví dụ 3.
Bẫy NULL trong điều kiện. WHERE country != 'VN' sẽ loại bỏ cả những dòng có country là NULL, vì NULL không so sánh được. Nếu muốn giữ chúng, viết WHERE country != 'VN' OR country IS NULL.
Nhầm múi giờ. DATE(event_time) cắt theo UTC. Người dùng Việt Nam hoạt động lúc 1 giờ sáng giờ Việt Nam sẽ bị tính sang ngày hôm trước theo UTC. Khi báo cáo cho thị trường Việt Nam, hãy chuyển múi giờ: DATE(CONVERT_TZ(event_time,'UTC','+07:00')). Sai lầm này khiến DAU theo ngày lệch một cách khó hiểu.
Lọc sai chỗ giữa WHERE và HAVING. WHERE lọc dòng trước khi nhóm; HAVING lọc nhóm sau khi tổng hợp. Muốn lọc "user có hơn 5 đơn hàng" thì điều kiện đó phải nằm ở HAVING, không phải WHERE.
Mẹo đọc nhanh: khi nhận một truy vấn lạ, đọc theo thứ tự FROM → WHERE → GROUP BY → SELECT, không đọc từ trên xuống. Bạn sẽ hiểu nó đang làm gì nhanh hơn nhiều.
Bài tập thực hành
Hãy tự viết SQL cho các yêu cầu sau (giả sử có bảng users(user_id, signup_date, channel, country, plan) và events(user_id, event_name, event_time, platform)):
- DAU theo platform cho 14 ngày gần nhất, sắp xếp theo ngày rồi platform. Kiểm tra: tổng các platform trong một ngày có gần bằng DAU tổng không?
- Retention D1 theo kênh acquisition. Với mỗi
channel, tính cohort_size và số user có event vào đúng ngày sau đăng ký. Kênh nào giữ chân tốt nhất? (Nhớ dùng LEFT JOIN.)
- Funnel onboarding bốn bước (
signup→complete_profile→first_action→day7_return) trong một truy vấn dùngCASE WHEN. Bước nào rớt nặng nhất?
- Phân khúc nâng cao: so sánh tỷ lệ trả phí giữa nhóm user có ít nhất 3 event trong 7 ngày đầu và nhóm có ít hơn. Giả thuyết "early engagement dẫn tới paid conversion" có đúng không?
Tóm tắt
SQL là kỹ năng đòn bẩy lớn nhất mà một Growth PM có thể tự trang bị: nó biến bạn từ người chờ data thành người tự tìm câu trả lời trong vài phút. Bạn không cần biết tất cả — chỉ cần thành thạo một bộ nhỏ: SELECT, WHERE, GROUP BY, JOIN, COUNT(DISTINCT ...) và CASE WHEN. Với bộ này bạn đã trả lời được phần lớn câu hỏi về DAU, retention cohort, funnel và phân khúc hành vi.
Ba điểm cần khắc cốt: thứ nhất, luôn COUNT(DISTINCT user_id) khi đếm người. Thứ hai, dùng LEFT JOIN khi tính retention để không đánh mất những người đã rời đi khỏi mẫu số. Thứ ba, mọi con số tổng hợp đều che giấu một câu chuyện — GROUP BY theo platform, kênh hay quốc gia là cách bạn lôi sự thật ra ánh sáng. Và quan trọng nhất, hãy luôn làm một bước kiểm tra tỉnh táo: một con số đẹp nhưng sai còn nguy hiểm hơn không có con số nào. SQL cho bạn quyền năng tự trả lời câu hỏi — hãy dùng nó một cách kỷ luật.