Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng bạn đang ngồi trong một cuộc họp sprint review. Product Owner hỏi: "Tháng vừa rồi có bao nhiêu đơn hàng bị hủy sau khi đã thanh toán, và nhóm khách hàng nào hủy nhiều nhất?" Cả phòng im lặng. Anh dev nói "để em query thử nhưng đang bận, mai gửi nhé". Anh data analyst thì "queue của em đang 12 ticket rồi". Và bạn — Technical BA — nếu lúc đó mở laptop, gõ một câu SQL chừng năm dòng, rồi chiếu kết quả lên màn hình trong vòng ba phút, thì giá trị của bạn trong mắt cả team vừa nhảy lên một bậc.
Đó chính là lý do bài này quan trọng đến vậy. Trong toàn bộ khóa Technical BA Masterclass, SQL là kỹ năng "đòn bẩy" mạnh nhất bạn có thể học. Nó không trừu tượng như kiến trúc microservices, không xa vời như Kafka hay Kubernetes — bạn dùng được nó ngay ngày mai tại công việc hiện tại. Một Technical BA biết SQL không còn phải xếp hàng chờ người khác lấy số liệu hộ mình. Bạn tự trả lời câu hỏi nghiệp vụ, tự kiểm chứng giả định trong requirement bằng dữ liệu thật, tự phát hiện edge case trước khi nó thành bug trên production.
Bài này tập trung vào ba trụ cột nền tảng nhất: SELECT (lấy và lọc dữ liệu), JOIN (ghép dữ liệu từ nhiều bảng), và GROUP BY (tổng hợp, đếm, tính toán). Đây là 80% công việc SQL hằng ngày của một BA. Những kỹ thuật nâng cao hơn như window function, CTE, hay tối ưu index sẽ được nói riêng ở các bài sau — ở đây ta xây cho chắc phần móng.
Khái niệm cốt lõi
Cơ sở dữ liệu quan hệ nhìn dưới góc độ BA
Trước khi viết câu lệnh đầu tiên, bạn cần một mô hình tư duy đúng. Một cơ sở dữ liệu quan hệ (relational database) về bản chất là một tập hợp các bảng (table) — giống hệt các sheet trong Excel: có dòng (row, mỗi dòng là một bản ghi) và cột (column, mỗi cột là một thuộc tính). Khác biệt lớn nhất so với Excel là các bảng liên kết với nhau qua khóa (key).
Ví dụ một hệ thống thương mại điện tử như Tiki hay Shopee thường có:
- Bảng
customers: mỗi dòng một khách hàng, có cộtcustomer_id,full_name,city,created_at. - Bảng
orders: mỗi dòng một đơn hàng, có cộtorder_id,customer_id,total_amount,status,order_date. - Bảng
order_items: mỗi dòng một dòng sản phẩm trong đơn, cóorder_id,product_id,quantity,unit_price.
customer_id trong bảng orders chính là khóa ngoại (foreign key) trỏ về bảng customers. Đây là sợi dây cho phép ta ghép "đơn hàng này là của ai". Hiểu được mối quan hệ giữa các bảng là điều kiện tiên quyết để viết JOIN đúng.SELECT — câu hỏi cơ bản nhất
Mọi câu truy vấn lấy dữ liệu đều bắt đầu bằng SELECT. Cấu trúc rút gọn:
SELECT cột_cần_lấy
FROM tên_bảng
WHERE điều_kiện_lọc
ORDER BY cột_sắp_xếp
LIMIT số_dòng;
Ví dụ, lấy 10 khách hàng ở Hà Nội đăng ký gần đây nhất:
SELECT full_name, city, created_at
FROM customers
WHERE city = 'Hà Nội'
ORDER BY created_at DESC
LIMIT 10;
Mệnh đề WHERE là nơi BA thể hiện logic nghiệp vụ. Bạn có thể kết hợp điều kiện bằng AND, OR, dùng BETWEEN cho khoảng giá trị, IN cho danh sách, LIKE cho so khớp chuỗi:
WHERE order_date BETWEEN '2026-01-01' AND '2026-03-31'
AND status IN ('paid', 'shipped')
AND total_amount > 500000
JOIN — ghép dữ liệu từ nhiều bảng
Đây là kỹ năng phân biệt một người "biết SQL sơ sơ" với một người thật sự dùng được. Vì dữ liệu nằm rải rác ở nhiều bảng, để trả lời gần như mọi câu hỏi nghiệp vụ thực tế bạn phải ghép bảng.
INNER JOIN giữ lại những dòng có khớp ở cả hai bảng. Lấy tên khách hàng kèm tổng tiền từng đơn:
SELECT c.full_name, o.order_id, o.total_amount
FROM orders o
INNER JOIN customers c ON o.customer_id = c.customer_id;
Lưu ý chữ o và c — đó là alias (bí danh) giúp câu lệnh ngắn gọn và rõ ràng. Điều kiện ghép nằm sau ON.
LEFT JOIN giữ toàn bộ dòng của bảng bên trái, kể cả khi không tìm thấy dòng khớp ở bảng phải (khi đó các cột bên phải sẽ là NULL). Đây là JOIN cực kỳ quan trọng với BA, vì nó giúp tìm ra "cái còn thiếu". Ví dụ: tìm khách hàng chưa từng đặt đơn nào:
SELECT c.full_name
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE o.order_id IS NULL;
Nắm chắc sự khác nhau giữa INNER và LEFT JOIN sẽ tránh cho bạn vô số lỗi đếm thiếu hoặc đếm thừa số liệu.
GROUP BY — tổng hợp và trả lời câu hỏi "bao nhiêu"
Phần lớn câu hỏi của business đều có dạng tổng hợp: bao nhiêu đơn, doanh thu bao nhiêu, trung bình mỗi khách chi bao nhiêu. GROUP BY gom các dòng có cùng giá trị lại thành nhóm, rồi áp dụng hàm tổng hợp (aggregate function): COUNT(), SUM(), AVG(), MIN(), MAX().
Doanh thu và số đơn theo từng tỉnh thành:
SELECT c.city,
COUNT(o.order_id) AS so_don,
SUM(o.total_amount) AS doanh_thu
FROM orders o
INNER JOIN customers c ON o.customer_id = c.customer_id
WHERE o.status = 'paid'
GROUP BY c.city
ORDER BY doanh_thu DESC;
Khi muốn lọc sau khi đã tổng hợp, ta dùng HAVING (không phải WHERE). Ví dụ chỉ lấy các tỉnh có trên 1.000 đơn:
GROUP BY c.city
HAVING COUNT(o.order_id) > 1000
Phân biệt WHERE (lọc dòng trước khi gom nhóm) và HAVING (lọc nhóm sau khi gom) là một trong những điểm gây nhầm lẫn nhất với người mới — ta sẽ nói kỹ ở phần lỗi thường gặp.
Tình huống thực tế
Tình huống 1 — BA tự kiểm chứng requirement tại một ví điện tử
Một BA tại ví điện tử giả định "MoMoPay" nhận yêu cầu từ business: "Khách hàng nạp tiền lần đầu thường nạp khoảng 200.000đ, hãy đặt gói khuyến mãi mặc định ở mức đó." Nghe rất hợp lý, nhưng BA này không vội viết spec. Cô mở SQL và kiểm chứng:
SELECT AVG(amount) AS trung_binh,
MIN(amount) AS thap_nhat,
MAX(amount) AS cao_nhat,
COUNT(*) AS so_giao_dich
FROM transactions
WHERE type = 'topup'
AND is_first_topup = TRUE
AND created_at >= '2026-01-01';
Kết quả: trung bình là 187.000đ, nhưng vì có vài giao dịch nạp 5 triệu kéo con số lên. Cô viết thêm một truy vấn phân nhóm theo khoảng giá trị và phát hiện median (giá trị giữa) thực ra chỉ khoảng 100.000đ, với hơn 60% giao dịch dưới 150.000đ. Hóa ra giả định ban đầu sai lệch vì bị các giao dịch lớn làm nhiễu.
Bài học: Trung bình (AVG) dễ đánh lừa khi dữ liệu có giá trị ngoại lai. Một BA biết SQL không chỉ lấy được số, mà còn biết đặt nghi vấn với số. Nhờ kiểm chứng, gói khuyến mãi mặc định được đặt ở 100.000đ, sát với hành vi thật và tăng tỷ lệ nhận khuyến mãi đáng kể.
Tình huống 2 — Truy vết đơn hàng "ma" tại một sàn TMĐT
Tại một sàn thương mại điện tử ở TP.HCM, bộ phận vận hành báo cáo có hiện tượng đơn hàng hiển thị "đã giao" nhưng khách phản ánh chưa nhận được. Trước đây mỗi lần như vậy phải nhờ dev export log, mất cả ngày. Lần này BA của team tự vào điều tra. Anh nghi ngờ trạng thái đơn không khớp giữa bảng orders và bảng shipments:
SELECT o.order_id, o.status AS order_status, s.status AS ship_status
FROM orders o
LEFT JOIN shipments s ON o.order_id = s.order_id
WHERE o.status = 'delivered'
AND (s.status IS NULL OR s.status <> 'delivered')
AND o.order_date >= '2026-06-01';
Câu lệnh trả về 47 đơn có vấn đề: trạng thái ở bảng đơn là "delivered" nhưng bảng vận chuyển hoặc không có bản ghi, hoặc vẫn đang "in_transit". Đây là bằng chứng cụ thể về một bug đồng bộ trạng thái giữa hai service. BA mang đúng 47 order_id này cho team dev — không còn chuyện "em không tái hiện được lỗi".
Bài học: LEFT JOIN kết hợp với điều kiện IS NULL hoặc so sánh chéo là vũ khí để phát hiện dữ liệu không nhất quán giữa các bảng. Một BA biết tự dựng truy vấn này rút ngắn thời gian điều tra từ một ngày xuống mười phút, và biến mô tả bug mơ hồ thành danh sách bằng chứng chính xác.
Tình huống 3 — Phân khúc khách hàng cho chiến dịch tại chuỗi cà phê
Một chuỗi cà phê giả định "An Coffee" với app tích điểm muốn chạy chiến dịch win-back cho khách hàng "đang nguội lạnh" — từng mua nhiều nhưng gần đây vắng bóng. BA phụ trách dùng GROUP BY để dựng phân khúc:
SELECT c.customer_id, c.full_name,
COUNT(o.order_id) AS tong_don,
SUM(o.total_amount) AS tong_chi_tieu,
MAX(o.order_date) AS lan_mua_gan_nhat
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.full_name
HAVING COUNT(o.order_id) >= 10
AND MAX(o.order_date) < '2026-04-01';
Truy vấn lọc ra những khách đã mua từ 10 đơn trở lên (khách trung thành) nhưng lần mua gần nhất đã trước tháng 4 (gần ba tháng không quay lại). Danh sách 1.250 khách này được chuyển thẳng cho team marketing để gửi voucher cá nhân hóa.
Bài học: GROUP BY cộng HAVING cho phép BA tự xây logic phân khúc khách hàng phức tạp mà không cần công cụ BI đắt tiền hay nhờ data team. Đây chính là tinh thần "tự lấy data để trả lời câu hỏi business" mà nghề Technical BA hướng tới.
Hướng dẫn từng bước
Khi đối mặt một câu hỏi nghiệp vụ và cần viết SQL, hãy theo quy trình sau thay vì gõ ngẫu hứng:
Bước 1 — Diễn đạt câu hỏi bằng tiếng Việt thật rõ. Ví dụ: "Trong quý 1/2026, mỗi thành phố có bao nhiêu đơn đã thanh toán và doanh thu là bao nhiêu, sắp xếp theo doanh thu giảm dần."
Bước 2 — Xác định cần dữ liệu từ bảng nào. Đơn hàng nằm ở orders, thành phố của khách nằm ở customers. Vậy cần JOIN hai bảng qua customer_id.
Bước 3 — Dựng khung câu lệnh theo thứ tự logic. Một mẹo hữu ích: viết theo thứ tự FROM → JOIN → WHERE → GROUP BY → SELECT → ORDER BY, dù khi đọc thì SELECT đứng đầu. Hãy xác định nguồn dữ liệu và cách lọc trước, rồi mới quyết định lấy cột gì.
Bước 4 — Bắt đầu nhỏ rồi mở rộng dần. Chạy thử SELECT * FROM orders LIMIT 5; để xem dữ liệu thật trông như thế nào, tên cột chính xác là gì, trạng thái có những giá trị nào. Đừng bao giờ viết một câu lệnh dài rồi mới chạy.
Bước 5 — Thêm điều kiện lọc và kiểm tra số lượng dòng. Thêm WHERE, chạy lại, xem số dòng có hợp lý không. Nếu kết quả 0 dòng hoặc nhiều bất thường, dừng lại kiểm tra ngay.
Bước 6 — Thêm tổng hợp. Đưa GROUP BY và các hàm COUNT, SUM vào sau cùng, khi bạn đã chắc tập dữ liệu thô là đúng.
Bước 7 — Đối chiếu kết quả với một con số bạn đã biết. Nếu tổng doanh thu quý 1 ra 50 tỷ trong khi báo cáo tài chính ghi 5 tỷ, gần như chắc chắn bạn đã JOIN sai và nhân đôi dữ liệu. Luôn có một "mỏ neo" để kiểm tra độ tin cậy.
Lỗi thường gặp & mẹo
Nhầm WHERE với HAVING. WHERE lọc từng dòng trước khi gom nhóm; HAVING lọc nhóm sau khi đã tổng hợp. Bạn không thể viết WHERE COUNT() > 10 — sẽ báo lỗi. Phải dùng HAVING COUNT() > 10.
JOIN nhân đôi dữ liệu (fan-out). Khi JOIN với một bảng có nhiều dòng khớp (ví dụ một đơn có nhiều order_items), số dòng sẽ phình lên và SUM(total_amount) bị tính lặp. Mẹo: trước khi tin con số tổng, hãy chạy SELECT COUNT(*) trước và sau JOIN để kiểm tra số dòng có thay đổi bất ngờ không.
Bẫy NULL. NULL không bằng bất cứ thứ gì, kể cả chính nó. WHERE status = NULL luôn trả về rỗng; phải viết WHERE status IS NULL. Tương tự, COUNT(column) bỏ qua NULL còn COUNT(*) đếm tất cả — một khác biệt nhỏ nhưng làm sai lệch báo cáo.
Quên rằng COUNT(DISTINCT ...) khác COUNT(...). Đếm số khách hàng mua hàng, không phải số đơn hàng, phải dùng COUNT(DISTINCT customer_id).
Mẹo an toàn: Trên môi trường production, luôn thêm LIMIT khi khám phá và tuyệt đối không chạy UPDATE/DELETE nếu bạn chỉ cần đọc. Lý tưởng nhất là BA chỉ có quyền read-only trên một bản replica của database, không bao giờ gõ lệnh trực tiếp lên database chính.
Mẹo định dạng: Đặt mỗi mệnh đề trên một dòng, dùng alias rõ nghĩa, đặt tên cột kết quả bằng AS để người đọc hiểu ngay. SQL dễ đọc là SQL ít lỗi.
Bài tập thực hành
Giả sử bạn có ba bảng customers, orders, order_items như mô tả ở đầu bài. Hãy tự viết SQL cho các yêu cầu sau (tự dựng dữ liệu mẫu trên DB Fiddle hoặc SQLite để chạy thử):
- Liệt kê 20 đơn hàng có giá trị cao nhất trong tháng 6/2026, kèm tên khách hàng.
- Tính tổng doanh thu theo từng tháng của năm 2026 (gợi ý: nhóm theo tháng của
order_date). - Tìm tất cả khách hàng đã đăng ký nhưng chưa từng đặt đơn nào (dùng LEFT JOIN).
- Với mỗi khách hàng, tính số đơn và giá trị đơn trung bình; chỉ giữ lại khách có giá trị trung bình trên 300.000đ.
- Tìm 5 sản phẩm bán chạy nhất theo số lượng (dùng
order_items, hàmSUM(quantity)). - Thử thách: Tìm những khách hàng có ít nhất một đơn ở trạng thái "cancelled" sau khi đã "paid" — đây là dạng câu hỏi điều tra mà BA hay gặp.
Tóm tắt
SQL là kỹ năng đòn bẩy lớn nhất của một Technical BA: nó cho bạn khả năng tự trả lời câu hỏi nghiệp vụ bằng dữ liệu thật, ngay lập tức, không phải xếp hàng chờ dev hay data analyst. Trong bài này, ta đã đi qua ba trụ cột nền tảng: SELECT với WHERE để lấy và lọc dữ liệu; JOIN (đặc biệt là INNER và LEFT) để ghép dữ liệu từ nhiều bảng và phát hiện những gì còn thiếu hoặc không nhất quán; và GROUP BY cùng các hàm tổng hợp với HAVING để trả lời mọi câu hỏi dạng "bao nhiêu, trung bình, tổng cộng".
Qua ba tình huống thực tế — kiểm chứng giả định khuyến mãi ở ví điện tử, truy vết đơn hàng "ma" ở sàn TMĐT, và phân khúc khách hàng win-back ở chuỗi cà phê — bạn đã thấy SQL không phải kỹ thuật khô khan mà là công cụ ra quyết định. Điều phân biệt một BA giỏi không nằm ở việc viết được câu lệnh phức tạp, mà ở chỗ biết đặt nghi vấn với số liệu, biết kiểm chứng kết quả bằng một mỏ neo đã biết, và biết tránh các bẫy NULL, fan-out, hay nhầm WHERE/HAVING. Hãy luyện tập đến khi viết SQL trở thành phản xạ — đó là nền móng cho mọi kỹ năng nâng cao ở các bài tiếp theo.