Menu
ESC

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

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

Đang tải...

VN context: schema fintech MoMo/ZaloPay transaction

SQL and Data Analysis cho BA Bài 42/60

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

Nếu bạn làm BA trong ngành fintech ở Việt Nam — MoMo, ZaloPay, VNPay, ShopeePay hay bất kỳ ví điện tử nào — thì dữ liệu giao dịch (transaction) chính là trái tim của hệ thống. Mỗi lần ai đó nạp tiền, chuyển khoản, thanh toán hóa đơn điện, mua thẻ game, hay nhận hoàn tiền (cashback), một dòng record được sinh ra. Hàng chục triệu dòng mỗi ngày. Người nắm được cách dữ liệu này tổ chức và truy vấn nó đúng cách sẽ trả lời được những câu hỏi mà cả ban giám đốc lẫn đội rủi ro (risk) đều cần: ví này có đang sống không, tiền chảy đi đâu, có dấu hiệu gian lận không, người dùng mới giữ chân được bao lâu.

Khác với e-commerce (bài 40) hay ride-hailing (bài 41), schema fintech có những đặc thù riêng mà nếu không hiểu, bạn sẽ viết query sai một cách nguy hiểm: một giao dịch luôn có hai phía (người gửi và người nhận), tiền phải cân bằng kế toán (double-entry), trạng thái giao dịch thay đổi theo thời gian (pending → success → failed → reversed), và mỗi giao dịch dính chặt với các vấn đề tuân thủ (compliance), KYC, chống rửa tiền (AML). Bài này tập trung dựng và đọc hiểu một schema fintech mô phỏng MoMo/ZaloPay, để các bài phân tích phía sau (RFM, churn, fraud) có nền tảng dữ liệu chuẩn. Hiểu schema trước, viết query sau — đó là tư duy của một BA giỏi.

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

Schema mô phỏng MoMo/ZaloPay

Một ví điện tử tối thiểu cần các bảng sau. Tôi viết chuẩn để bạn copy chạy thử trên PostgreSQL hoặc MySQL (chỉ khác vài kiểu dữ liệu nhỏ).

CREATE TABLE users (
  id          BIGINT PRIMARY KEY,
  phone       VARCHAR(20) UNIQUE NOT NULL,
  email       VARCHAR(255),
  full_name   VARCHAR(255),
  kyc_level   SMALLINT DEFAULT 0,        -- 0=chưa định danh, 1=cơ bản, 2=đầy đủ eKYC
  city        VARCHAR(100),
  created_at  TIMESTAMP NOT NULL,
  status      VARCHAR(20) DEFAULT 'active' -- active, suspended, closed
);

CREATE TABLE wallets ( id BIGINT PRIMARY KEY, user_id BIGINT REFERENCES users(id), balance DECIMAL(18,2) DEFAULT 0, -- số dư, dùng DECIMAL không dùng FLOAT currency CHAR(3) DEFAULT 'VND', created_at TIMESTAMP NOT NULL );

CREATE TABLE transactions ( id BIGINT PRIMARY KEY, txn_code VARCHAR(40) UNIQUE, -- mã giao dịch hiển thị cho user sender_wallet BIGINT REFERENCES wallets(id), receiver_wallet BIGINT REFERENCES wallets(id), amount DECIMAL(18,2) NOT NULL, fee DECIMAL(18,2) DEFAULT 0, txn_type VARCHAR(30) NOT NULL, -- topup, transfer, payment, withdraw, cashback, refund channel VARCHAR(30), -- bank, atm, qr, billing, app status VARCHAR(20) NOT NULL, -- pending, success, failed, reversed merchant_id BIGINT, -- NULL nếu là chuyển tiền P2P created_at TIMESTAMP NOT NULL, completed_at TIMESTAMP -- NULL khi chưa xong );

CREATE TABLE merchants ( id BIGINT PRIMARY KEY, name VARCHAR(255), category VARCHAR(50), -- food, ride, ecommerce, billing, gaming, telco city VARCHAR(100), onboarded_at TIMESTAMP );

Bốn đặc thù bạn buộc phải nhớ

1. DECIMAL chứ không phải FLOAT. Tiền tệ phải dùng DECIMAL(18,2). Nếu dùng FLOAT/DOUBLE, phép cộng 0.1 + 0.2 cho ra 0.30000000000004 — sai số tích lũy qua hàng triệu giao dịch sẽ làm lệch sổ. Trong fintech, lệch một đồng cũng là lỗi nghiêm trọng.

2. Giao dịch có hai phía. sender_walletreceiver_wallet đều trỏ về bảng wallets. Khi tính "user A đã chi bao nhiêu", bạn nhìn vào các dòng sender_wallet = ví của A. Khi tính "user A nhận về bao nhiêu", bạn nhìn receiver_wallet. Nhầm hai cái này là lỗi kinh điển của BA mới.

3. Trạng thái và thời gian. Một giao dịch topup từ ngân hàng có thể ở pending vài giây tới vài phút. Báo cáo doanh thu chỉ tính status = 'success'. Nếu bạn quên lọc, bạn đếm cả tiền chưa về thực — con số phồng lên ảo.

4. txn_type quyết định ý nghĩa dòng tiền. topup là tiền vào hệ thống từ ngân hàng; withdraw là tiền ra; transfer/payment là tiền luân chuyển nội bộ; cashback/refund là tiền nền tảng trả lại. Tổng GMV (Gross Merchandise Value) thường chỉ tính payment, không tính topup (nếu cộng topup vào là thổi phồng quy mô).

Tình huống thực tế

Ví dụ 1 — MoMo và bài toán "doanh thu thật" cuối tháng

Giả định bạn là BA tại một ví giống MoMo. Đội tài chính hỏi: "Tháng 5/2026 chúng ta xử lý bao nhiêu tiền thanh toán merchant, và thu được bao nhiêu phí?" Một BA non tay viết:

SELECT SUM(amount) FROM transactions
WHERE created_at >= '2026-05-01' AND created_at < '2026-06-01';

Kết quả ra 4.200 tỷ đồng. Nghe oách, nhưng sai. Con số này gộp cả topup (tiền nạp vào ví, không phải doanh thu), cả giao dịch failed, cả pending. Query đúng phải là:

SELECT
  COUNT(*)                          AS so_giao_dich,
  SUM(amount)                       AS tong_gmv,
  SUM(fee)                          AS tong_phi
FROM transactions
WHERE txn_type = 'payment'
  AND status = 'success'
  AND completed_at >= '2026-05-01'
  AND completed_at <  '2026-06-01';

Lưu ý tôi lọc theo completed_at chứ không phải created_at — vì một giao dịch tạo lúc 23:59 ngày 31/5 nhưng hoàn tất 00:01 ngày 1/6 thì về mặt kế toán thuộc tháng 6. Bài học: trong fintech, "doanh thu" luôn = một txn_type cụ thể + status = success + lọc theo thời điểm hoàn tất. Ba điều kiện, thiếu cái nào cũng sai.

Ví dụ 2 — ZaloPay và việc cân sổ double-entry

Đội kế toán ở một ví giống ZaloPay nghi ngờ hệ thống đang "rò" tiền. Vì mỗi giao dịch trừ ví người gửi và cộng ví người nhận đúng bằng nhau (trừ phí), tổng số dư toàn hệ thống cộng lại phải khớp với tổng tiền nạp ròng. BA viết query kiểm tra một ngày bất kỳ:

SELECT
  SUM(CASE WHEN txn_type = 'topup'    THEN amount ELSE 0 END) AS tien_nap,
  SUM(CASE WHEN txn_type = 'withdraw' THEN amount ELSE 0 END) AS tien_rut,
  SUM(CASE WHEN txn_type = 'cashback' THEN amount ELSE 0 END) AS cashback_tra,
  SUM(fee)                                                    AS phi_thu_ve
FROM transactions
WHERE status = 'success'
  AND completed_at >= '2026-05-20'
  AND completed_at <  '2026-05-21';

Tiền vào ròng của hệ thống trong ngày = tien_nap - tien_rut + cashback_tra. Nếu so với mức tăng tổng balance trong bảng wallets mà lệch, có vấn đề: hoặc trigger cập nhật số dư bị sót, hoặc có giao dịch reversed không được hoàn nguyên đúng. Bài học: schema fintech cho phép BA làm "kiểm toán dữ liệu" mà không cần đụng tới hệ thống kế toán chính thức — đây là kỹ năng cực kỳ giá trị, vì BA thường là người đầu tiên phát hiện bất thường.

Ví dụ 3 — Phát hiện giao dịch P2P bất thường (chống rửa tiền)

Đội risk của một ví ở TP.HCM muốn tìm những tài khoản nhận tiền chuyển khoản P2P (txn_type = 'transfer', merchant_id IS NULL) với tần suất bất thường — dấu hiệu của tài khoản trung gian rửa tiền. BA viết:

SELECT
  w.user_id,
  u.phone,
  COUNT(*)        AS so_lan_nhan,
  SUM(t.amount)   AS tong_nhan,
  COUNT(DISTINCT t.sender_wallet) AS so_nguoi_gui_khac_nhau
FROM transactions t
JOIN wallets w ON t.receiver_wallet = w.id
JOIN users   u ON w.user_id = u.id
WHERE t.txn_type = 'transfer'
  AND t.merchant_id IS NULL
  AND t.status = 'success'
  AND t.completed_at >= '2026-05-01'
  AND t.completed_at <  '2026-06-01'
GROUP BY w.user_id, u.phone
HAVING COUNT(*) > 100
   AND COUNT(DISTINCT t.sender_wallet) > 50
ORDER BY tong_nhan DESC;

Một tài khoản cá nhân nhận tiền từ 50+ người khác nhau, hơn 100 lần trong một tháng, rất đáng nghi. Bài học: chính vì giao dịch có hai phía và liên kết qua wallets, BA có thể truy ngược danh tính người dùng từ ví. Đây là lý do receiver_walletwallets.user_idusers là chuỗi JOIN bạn sẽ dùng đi dùng lại trong fintech.

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

Khi tiếp cận một schema fintech mới, làm theo trình tự này:

Bước 1 — Đọc kiểu dữ liệu cột tiền. Kiểm tra amount, fee, balance có phải DECIMAL không. Nếu thấy FLOAT, ghi chú lại ngay — mọi báo cáo tổng tiền sẽ có sai số nhỏ.

Bước 2 — Vẽ sơ đồ quan hệ. transactions nối tới wallets hai lần (sender và receiver), wallets nối tới users, transactions nối tới merchants. Tự vẽ ra giấy để khỏi nhầm chiều JOIN.

Bước 3 — Liệt kê giá trị thực của các cột phân loại. Đừng đoán txn_type có những giá trị gì. Chạy:

SELECT txn_type, status, COUNT(*)
FROM transactions
GROUP BY txn_type, status
ORDER BY COUNT(*) DESC;

Bạn sẽ thấy ngay tỉ lệ failed, các txn_type ít gặp, và phát hiện giá trị rác (ví dụ txn_type = 'TopUp' lẫn với 'topup' do hệ thống không chuẩn hóa).

Bước 4 — Xác định "chân lý" về thời gian. Hỏi nghiệp vụ: báo cáo dùng created_at hay completed_at? Với doanh thu, gần như luôn là completed_at.

Bước 5 — Viết query nháp với LIMIT. Luôn thử trên tập nhỏ trước:

SELECT t.txn_code, t.amount, t.txn_type, t.status, u.phone
FROM transactions t
JOIN wallets w  ON t.sender_wallet = w.id
JOIN users   u  ON w.user_id = u.id
WHERE t.txn_type = 'payment'
ORDER BY t.created_at DESC
LIMIT 20;

Nhìn 20 dòng thật, đối chiếu với hiểu biết nghiệp vụ, rồi mới bỏ LIMIT để chạy toàn bộ.

Bước 6 — Phủ các điều kiện lọc cốt lõi. Với mọi báo cáo fintech, mặc định thêm status = 'success' trừ khi đề bài yêu cầu phân tích cả thất bại.

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

Lỗi 1: Quên lọc status. Đây là lỗi số một. Báo cáo doanh thu gộp cả pendingfailed cho con số ảo. Mẹo: tạo thói quen mỗi query tiền đều có dòng AND status = 'success'.

Lỗi 2: Nhầm sender và receiver. Tính "chi tiêu của user" mà JOIN nhầm vào receiver_wallet thì bạn đang đo tiền họ nhận. Mẹo: đặt tên alias rõ ràng, ví dụ JOIN wallets sw ON t.sender_wallet = sw.id để chữ sw nhắc bạn đây là phía gửi.

Lỗi 3: Cộng topup vào GMV. GMV (tổng giá trị thanh toán) chỉ nên tính payment. Cộng topup vào sẽ thổi phồng quy mô gấp đôi vì người dùng nạp rồi mới tiêu. Mẹo: luôn hỏi định nghĩa GMV của công ty trước khi tính.

Lỗi 4: NULL ở completed_at. Giao dịch pendingfailedcompleted_at IS NULL. Nếu lọc completed_at < '2026-06-01', dòng NULL tự động bị loại (điều kiện so sánh với NULL trả về unknown). Điều này thường đúng ý bạn, nhưng phải ý thức được — đừng để nó âm thầm loại mất dữ liệu bạn cần.

Lỗi 5: Quên fee khi cân sổ. Với transfer có phí, ví người gửi bị trừ amount + fee, người nhận chỉ được amount, phần fee chảy về ví hệ thống. Bỏ qua fee làm lệch khi kiểm tra cân bằng.

Mẹo bonus — bảo mật PII: số phonefull_name là dữ liệu nhạy cảm. Khi xuất báo cáo gửi ra ngoài đội, che bớt: CONCAT(LEFT(phone, 4), '**', RIGHT(phone, 2)). Bài 57 sẽ đi sâu, nhưng tập thói quen từ bây giờ.

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

Dùng schema ở trên, hãy viết query cho các yêu cầu sau (tự tạo dữ liệu mẫu hoặc dùng dataset của lớp):

  • Top 10 merchant theo doanh thu trong tháng 5/2026: chỉ tính payment thành công, JOIN sang bảng merchants, nhóm theo tên merchant, sắp xếp giảm dần theo tổng amount.
  • Tỉ lệ thất bại theo channel: với mỗi channel, tính số giao dịch failed chia tổng số giao dịch, ra phần trăm. Channel nào tệ nhất? (Gợi ý: dùng CASE WHEN trong AVG hoặc SUM.)
  • Người dùng "ngủ đông": liệt kê user status = 'active' nhưng không có giao dịch success nào trong 30 ngày gần nhất. (Gợi ý: LEFT JOIN từ users sang transactions qua wallets, lọc dòng không khớp.)
  • Tổng cashback đã trả theo category merchant: ví đã chi bao nhiêu tiền cashback cho từng nhóm ngành (food, gaming, telco...)?
  • Nâng cao — phát hiện lệch sổ: cho một ngày, tính tổng tiền nạp ròng vào hệ thống và so sánh ý tưởng với mức tăng tổng balance. Viết câu query tính tiền nạp ròng (topup - withdraw + cashback - refund_ra).
Hãy viết, chạy, và quan trọng nhất: nhìn 10–20 dòng kết quả đầu, tự hỏi "con số này có hợp lý không?". Tư duy phản biện với chính output của mình là dấu hiệu của một BA trưởng thành.

Tóm tắt

Schema fintech kiểu MoMo/ZaloPay xoay quanh bốn bảng cốt lõi: users, wallets, transactions, merchants. Bốn nguyên tắc bạn phải khắc cốt ghi tâm: dùng DECIMAL cho tiền (không FLOAT); giao dịch có hai phía sender/receiver nối qua wallets; báo cáo tiền luôn lọc status = 'success' và đúng txn_type; phân biệt rõ thời điểm tạo (created_at) và hoàn tất (completed_at). Khi gặp schema mới, hãy đọc kiểu dữ liệu, vẽ quan hệ, liệt kê giá trị phân loại thực tế, rồi thử query với LIMIT trước khi chạy toàn bộ.

Ba tình huống thực tế — tính doanh thu thật của MoMo, cân sổ double-entry của ZaloPay, và phát hiện tài khoản rửa tiền — cho thấy cùng một schema phục vụ ba mục tiêu rất khác nhau: tài chính, kiểm toán, và rủi ro. Nắm vững schema này, bạn đã sẵn sàng cho các bài phân tích chuyên sâu phía sau như RFM, churn và fraud detection, nơi mọi query đều bắt đầu từ chính bốn bảng bạn vừa làm chủ.