Menu
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Đang tải...

Bài 14 — Custom SQL Queries (Mixpanel JQL, Amplitude)

Product Analytics Mixpanel and Amplitude Bài 14/60

Mở đầu — vì sao bài này quan trọng

Nếu bạn đã học đến bài này, chắc hẳn bạn đã quen với việc dựng Funnel, xem Retention Curve, tạo Dashboard bằng giao diện kéo-thả của Mixpanel hay Amplitude. Đó là 80% công việc phân tích hằng ngày của một Product Manager. Nhưng sẽ đến một ngày — và tin tôi đi, ngày đó đến sớm hơn bạn nghĩ — giao diện kéo-thả sẽ "chịu thua" câu hỏi của bạn.

Ví dụ: "Trong số những user đã mua hàng lần đầu, có bao nhiêu người mà giá trị đơn hàng thứ hai lớn hơn đơn hàng đầu tiên?" Hay: "Tính khoảng thời gian trung bình giữa sự kiện add_to_cartpurchase, nhưng chỉ cho những user đã xem ít nhất 3 sản phẩm khác nhau trước đó." Những câu hỏi kiểu này liên quan tới việc so sánh giữa các event, tính toán tùy biến, hoặc logic điều kiện phức tạp mà không một nút bấm nào trên giao diện cover được.

Đây chính là lúc bạn cần đến custom query — viết code truy vấn trực tiếp trên dữ liệu event. Với Mixpanel, công cụ đó là JQL (JavaScript Query Language) và người kế nhiệm của nó. Với Amplitude, đó là các phép tính tùy chỉnh và khả năng kết nối SQL qua warehouse. Bài học hôm nay sẽ dạy bạn cách "vượt qua giới hạn của giao diện" một cách có kỷ luật, để khi sếp hỏi một câu hỏi hóc búa, bạn không phải trả lời "công cụ không làm được", mà là "để em viết một query".

Điều quan trọng cần nhấn mạnh ngay từ đầu: custom query là con dao hai lưỡi. Nó cực kỳ mạnh nhưng cũng dễ tốn kém tài nguyên và dễ cho ra kết quả sai nếu bạn không hiểu mô hình dữ liệu. Vì vậy bài này không chỉ dạy cú pháp, mà dạy cả tư duy khi nào nên dùng và dùng sao cho an toàn.

Khái niệm cốt lõi

Khi nào thực sự cần custom query

Trước khi mở trình soạn thảo code, hãy tự hỏi ba câu:

  • Giao diện built-in có làm được không? Nếu Funnel, Segmentation, hoặc Insights chuẩn đã trả lời được câu hỏi, đừng viết query. Query khó bảo trì hơn, khó chia sẻ hơn, và tốn công hơn nhiều.
  • Câu hỏi có liên quan tới quan hệ giữa nhiều event không? Ví dụ: so sánh property của event A với event B trong cùng một user. Đây là điểm yếu kinh điển của giao diện kéo-thả.
  • Có cần phép tính tùy biến không? Ví dụ tính median (trung vị) thay vì mean, hoặc một công thức doanh thu đặc thù của công ty bạn.
Nếu ít nhất một trong hai câu sau là "có", thì custom query là công cụ đúng.

Ba nhóm use case chính

Nhóm 1 — Cross-event analysis (phân tích chéo giữa các event). Đây là nhóm phổ biến nhất. Bạn cần "nối" nhiều event của cùng một user theo thời gian, rồi so sánh chúng. Ví dụ: đơn hàng thứ hai có lớn hơn đơn đầu không, hay user có quay lại trong 24 giờ sau khi gặp lỗi không.

Nhóm 2 — Custom calculation (tính toán tùy chỉnh). Giao diện chỉ cho bạn vài phép tính có sẵn: count, sum, average, unique. Khi cần median, percentile 90, tỷ lệ có điều kiện, hay công thức LTV riêng, bạn phải tự viết.

Nhóm 3 — Data audit / debug (kiểm tra chất lượng dữ liệu). Khi nghi ngờ tracking sai, bạn dùng query để "soi" từng event thô, đếm các giá trị property bất thường, tìm event bị trùng.

Mixpanel JQL — bản chất là JavaScript

JQL (JavaScript Query Language) là cách Mixpanel cho phép bạn viết một hàm JavaScript chạy trực tiếp trên server của Mixpanel, xử lý luồng event thô. Mô hình tư duy giống hệt các thao tác trên mảng trong JavaScript: bạn có một luồng dữ liệu, rồi groupBy, filter, reduce nó.

Cấu trúc một query JQL điển hình:

function main() {
  return Events({
    from_date: "2026-06-01",
    to_date: "2026-06-27",
    event_selectors: [{ event: "purchase" }]
  })
  .groupByUser(function(state, events) {
    // gom tất cả event của một user lại và xử lý
    var total = state ? state.total : 0;
    return { total: total + events.length };
  })
  .filter(function(row) {
    return row.value.total >= 2; // chỉ giữ user mua từ 2 lần
  });
}

Ba khối bạn phải nhớ:

  • Events(...): nguồn dữ liệu. Luôn khai báo from_date, to_date càng hẹp càng tốt — đây là yếu tố sống còn về chi phí và tốc độ.
  • .groupByUser(...): gom event theo từng user, xử lý theo thứ tự thời gian. Đây là nơi sức mạnh cross-event nằm ở đó.
  • .reduce(...): gộp kết quả cuối để trả về con số tổng hợp.
Lưu ý thực tế năm 2026: Mixpanel đã chuyển hướng khuyến khích dùng Custom Events, Custom Properties, và API truy vấn cho phần lớn nhu cầu, và JQL bị coi là legacy trên một số gói. Tuy nhiên tư duy JQL vẫn là nền tảng vàng để hiểu cross-event analysis, nên chúng ta vẫn học nó như một mô hình khái niệm.

Amplitude — không có "JQL", mà có SQL qua warehouse

Amplitude tiếp cận khác. Thay vì một ngôn ngữ script riêng, Amplitude cung cấp:

  • Custom Formulas trong Event Segmentation: bạn gõ công thức toán học trực tiếp, ví dụ UNIQUES(A) / UNIQUES(B) để tính một tỷ lệ tùy biến, hay dùng các hàm như PROPSUM, PROPAVG, PERCENTILE.
  • Data TablesCustom Events: định nghĩa event ảo từ tổ hợp nhiều event.
  • SQL thật thông qua tính năng warehouse-native / kết nối với data warehouse (Snowflake, BigQuery). Khi dữ liệu event đã đồng bộ về warehouse, bạn viết SQL chuẩn ANSI.
Nói cách khác: triết lý của Amplitude là "giữ 90% trong giao diện, còn lại thì đẩy xuống SQL trong warehouse của bạn" — chủ đề đồng bộ warehouse này sẽ được đào sâu ở Bài 25, nên ở đây ta chỉ chạm tới phần viết query.

Tình huống thực tế

Ví dụ 1 — Tiki: "Đơn thứ hai có lớn hơn đơn đầu không?"

Đội Growth của một sàn thương mại điện tử kiểu Tiki muốn hiểu hành vi upsell tự nhiên: trong số khách mua lần thứ hai, bao nhiêu phần trăm chi tiêu nhiều hơn lần đầu? Con số này giúp họ quyết định có nên đẩy mạnh gợi ý sản phẩm cao cấp hơn ở lần mua thứ hai không.

Giao diện Funnel của Mixpanel không trả lời được, vì nó không so sánh được property của event thứ nhất với event thứ hai. Đây là bài toán cross-event điển hình. Đội dùng JQL:

function main() {
  return Events({
    from_date: "2026-04-01",
    to_date: "2026-06-27",
    event_selectors: [{ event: "purchase" }]
  })
  .groupByUser(function(state, events) {
    state = state || { orders: [] };
    events.forEach(function(e) {
      state.orders.push(e.properties.order_value);
    });
    return state;
  })
  .filter(function(r) { return r.value.orders.length >= 2; })
  .reduce(function(acc, rows) {
    var bigger = 0, total = 0;
    rows.forEach(function(r) {
      total++;
      if (r.value.orders[1] > r.value.orders[0]) bigger++;
    });
    return { bigger: bigger, total: total };
  });
}

Kết quả: 4.230 khách mua từ 2 lần trở lên, trong đó 2.480 người có đơn thứ hai lớn hơn — khoảng 58,6%. Insight này khiến đội quyết định bật gợi ý "combo nâng cấp" ngay sau đơn đầu.

Bài học rút ra: cross-event analysis là lý do số một khiến PM phải học query. Không một nút bấm nào so sánh được "event thứ nhất với event thứ hai của cùng user".

Ví dụ 2 — MoMo: median thay vì average cho thời gian nạp tiền

Một ví thanh toán kiểu MoMo muốn đo "thời gian trung bình từ lúc mở app đến lúc hoàn tất giao dịch". Đội PM ban đầu dùng average trên giao diện Amplitude và ra con số 47 giây — nghe khá tệ. Nhưng họ nghi ngờ: có lẽ vài user để app mở nền cả tiếng đồng hồ đã kéo trung bình lên.

Họ chuyển sang Custom Formula với hàm percentile trong Amplitude, tính median (p50)p90. Kết quả: median chỉ 12 giây, còn p90 là 38 giây — con số 47 giây trước đó là do một đuôi dài các phiên bất thường kéo lên. Trải nghiệm thực tế của đa số người dùng tốt hơn nhiều so với con số average gây hiểu lầm.

Với đội có warehouse, cùng phép tính này viết bằng SQL rất gọn:

SELECT
  PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY duration_sec) AS median_sec,
  PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY duration_sec) AS p90_sec
FROM app_open_to_payment
WHERE event_date BETWEEN '2026-06-01' AND '2026-06-27';

Bài học rút ra: average là "cái bẫy" kinh điển trong analytics. Khi phân phối lệch (skewed) — mà hầu hết dữ liệu hành vi đều lệch — median và percentile phản ánh trải nghiệm thật hơn. Đây là custom calculation mà giao diện mặc định thường không cho bạn.

Ví dụ 3 — Startup SaaS: audit event bị trùng

Một startup SaaS ở TP.HCM phát hiện số liệu signup_completed cao bất thường sau một lần deploy. Nghi ngờ tracking bị gọi hai lần, đội PM dùng query để đếm số event signup_completed trên mỗi user trong một khung giờ hẹp:

function main() {
  return Events({
    from_date: "2026-06-20",
    to_date: "2026-06-21",
    event_selectors: [{ event: "signup_completed" }]
  })
  .groupByUser(function(state, events) {
    return (state || 0) + events.length;
  })
  .filter(function(r) { return r.value > 1; }); // user có > 1 lần signup
}

Query trả về 812 user có hơn một event signup_completed trong vòng 24 giờ — điều bất khả về mặt logic nghiệp vụ. Nguyên nhân: một useEffect bị gọi hai lần sau khi nâng cấp React. Nhờ query này, đội định vị lỗi trong nửa buổi thay vì tranh cãi số liệu cả tuần.

Bài học rút ra: query không chỉ để tìm insight, mà còn là công cụ debug dữ liệu mạnh nhất của PM. Trước khi tin một con số, hãy biết cách "soi" event thô đằng sau nó.

Hướng dẫn từng bước

Đây là quy trình tôi khuyên bạn áp dụng mỗi khi định viết một custom query:

  • Viết câu hỏi bằng tiếng Việt trước. Diễn đạt thật rõ: đối tượng là ai, khung thời gian nào, phép tính gì. Nếu bạn không viết được câu hỏi rõ ràng bằng lời, bạn sẽ không viết được query đúng.
  • Kiểm tra giao diện built-in một lần nữa. Thử Funnel, Segmentation, Custom Formula. Nếu làm được, dừng lại — đừng viết query.
  • Xác định đây là cross-event, custom calculation, hay audit. Việc phân loại này quyết định cấu trúc query. Cross-event thì trọng tâm là groupByUser; custom calculation thì trọng tâm là công thức trong reduce hoặc formula; audit thì trọng tâm là filter và đếm.
  • Giới hạn khung thời gian thật hẹp khi thử nghiệm. Với JQL, luôn bắt đầu bằng vài ngày dữ liệu. Với SQL, thêm LIMIT và điều kiện ngày. Đây là bước quan trọng nhất để không đốt tài nguyên và không chờ query chạy hàng phút.
  • Chạy thử, kiểm tra vài dòng kết quả bằng mắt. Lấy 5–10 dòng, đối chiếu thủ công với một user bạn biết rõ. Đừng bao giờ tin kết quả tổng hợp nếu chưa từng kiểm tra một mẫu nhỏ.
  • Mở rộng khung thời gian và chạy thật. Sau khi chắc logic đúng trên mẫu nhỏ, mới mở rộng ngày tháng.
  • Đối chiếu chéo với một nguồn khác. So con số query với một chart built-int gần tương đương. Nếu lệch quá xa, gần như chắc chắn query của bạn có lỗi logic — thường là ở chỗ lọc trùng hoặc múi giờ.

Lỗi thường gặp & mẹo

Lỗi 1 — Không giới hạn khung thời gian. Query quét toàn bộ lịch sử event dễ chạy hàng chục giây tới hàng phút, có thể chạm giới hạn tài nguyên của gói. Mẹo: luôn đặt from_date/to_date hẹp, mở rộng dần.

Lỗi 2 — Quên rằng property có thể null hoặc thiếu. Nếu order_value không được gửi ở vài event, r.value.orders[0] có thể undefined, khiến phép so sánh cho kết quả sai âm thầm. Mẹo: luôn kiểm tra tồn tại (if (e.properties.order_value != null)) trước khi tính.

Lỗi 3 — Nhầm múi giờ. Mixpanel và Amplitude có thể lưu timestamp theo UTC hoặc theo project timezone. Ở Việt Nam (UTC+7), một event lúc 23h có thể bị đếm sang ngày hôm sau nếu bạn không để ý. Mẹo: xác nhận timezone của project trước khi tin bất kỳ con số theo ngày nào.

Lỗi 4 — Dùng average khi nên dùng median. Như ví dụ MoMo, average dễ bị đuôi dài kéo lệch. Mẹo: với mọi chỉ số thời lượng, giá trị đơn hàng, số phiên — hãy nhìn median và p90 song song với average.

Lỗi 5 — Đếm trùng trong cross-event. Khi gom event theo user, dễ vô tình cộng gộp cả những event trùng. Mẹo: trong bước audit, luôn kiểm tra "một user có thể có bao nhiêu event tối đa" trước khi tính tỷ lệ.

Mẹo chung: đối xử với query như code sản xuất — đặt tên biến rõ ràng, ghi chú câu hỏi gốc ngay đầu query, và lưu lại các query hay dùng vào một nơi chung cho cả đội. Một thư viện query dùng lại được sẽ tiết kiệm cho đội bạn hàng chục giờ mỗi quý.

Bài tập thực hành

  • Cross-event cơ bản. Viết (bằng lời hoặc bằng JQL) một query trả lời: "Trong số user đã add_to_cart, bao nhiêu phần trăm hoàn tất purchase trong vòng 30 phút?" Xác định khối groupByUser và điều kiện thời gian bạn cần.
  • Custom calculation. Với công cụ bạn đang dùng, tính medianp90 của một chỉ số thời lượng bất kỳ (ví dụ thời gian hoàn tất onboarding). So sánh với average và viết 2–3 câu giải thích vì sao chúng khác nhau.
  • Data audit. Chọn một event quan trọng trong sản phẩm của bạn và viết query đếm số user có event đó xuất hiện bất thường nhiều lần trong một ngày. Nếu tìm thấy bất thường, mô tả giả thuyết nguyên nhân.
  • Ra quyết định. Với mỗi bài tập trên, ghi một dòng: "Nếu kết quả là X thì tôi sẽ làm gì?" Query chỉ có giá trị khi nó dẫn tới hành động.

Tóm tắt

Custom query là công cụ để bạn vượt qua giới hạn của giao diện kéo-thả — và bạn nên dùng nó một cách có kỷ luật, không phải mặc định. Ba trường hợp cần đến query là: cross-event analysis (so sánh nhiều event của cùng user), custom calculation (median, percentile, công thức riêng), và data audit (soi event thô để debug).

Với Mixpanel, tư duy nền tảng là JQL — một hàm JavaScript với EventsgroupByUserreduce, trong đó việc giới hạn khung thời gian là sống còn. Với Amplitude, con đường là Custom Formulas trong giao diện cho các phép tính, và SQL thật khi dữ liệu đã đồng bộ về warehouse.

Ba bài học lớn từ các tình huống thực tế: cross-event là lý do số một để học query (Tiki), median thường trung thực hơn average (MoMo), và query là công cụ debug dữ liệu mạnh nhất của PM (startup SaaS). Cuối cùng, hãy luôn nhớ quy trình: viết câu hỏi bằng lời, kiểm tra giao diện trước, giới hạn thời gian, kiểm tra mẫu nhỏ bằng mắt, rồi mới mở rộng. Một query đúng bắt đầu từ một câu hỏi rõ ràng, không phải từ dòng code đầu tiên.