Mở đầu — vì sao bài này quan trọng
Bạn có nhớ cảm giác ngồi trong buổi họp sprint, khách hàng hỏi "Tháng trước có bao nhiêu đơn hàng bị hủy?", dev quay sang bạn nói "Chờ mình query rồi báo lại", và cả phòng phải chờ ba tiếng đồng hồ mới có con số? Đó chính là khoảnh khắc một BA fresher nhận ra: nếu tự mình biết viết vài dòng SQL, mình đã có câu trả lời ngay tại chỗ.
Tôi muốn nói thẳng với bạn điều này ngay từ đầu: SQL không phải là kỹ năng của developer mà BA "học ké cho vui". Khi tôi rà soát các tin tuyển dụng BA fresher và junior trên TopCV, ITviec và LinkedIn Việt Nam trong nửa đầu năm 2026, khoảng 7 trên 10 job mô tả "biết SQL cơ bản" hoặc "có khả năng truy vấn dữ liệu" là một lợi thế, và với các domain như banking, fintech, e-commerce thì nó gần như bắt buộc. Lý do rất thực tế:
- Tự pull data để đỡ phụ thuộc dev. Mỗi lần nhờ dev query hộ, bạn tiêu tốn thời gian của họ và làm chậm chính mình.
- Validate spec với dữ liệu thật. Bạn viết requirement "hệ thống phải hỗ trợ khách hàng có nhiều địa chỉ giao hàng" — nhưng nếu bạn query bảng và thấy 98% khách chỉ có một địa chỉ, requirement đó có còn ưu tiên không?
- Phỏng vấn. Rất nhiều vòng phỏng vấn BA fresher tại VN có một bài test SQL 15–20 phút. Không qua được là rớt.
Khái niệm cốt lõi
SQL là gì và BA dùng phần nào của nó
SQL (Structured Query Language) là ngôn ngữ để "hỏi chuyện" cơ sở dữ liệu quan hệ. Dữ liệu được lưu trong các bảng (table) gồm hàng và cột, giống một sheet Excel khổng lồ. BA hầu như chỉ cần phần đọc dữ liệu (câu lệnh SELECT) — bạn gần như không bao giờ được (và không nên) INSERT, UPDATE, DELETE trên hệ thống thật, vì đó là việc có rủi ro cao thuộc về dev/DBA.
Bộ khung 6 mệnh đề cần nhớ
Mọi câu SELECT xoay quanh 6 mệnh đề, và chúng luôn theo đúng thứ tự này:
SELECT cột_cần_lấy
FROM tên_bảng
WHERE điều_kiện_lọc_từng_dòng
GROUP BY nhóm_theo_cột
HAVING điều_kiện_lọc_trên_nhóm
ORDER BY sắp_xếp
Nếu nhớ được thứ tự này, bạn đã nắm 80% cú pháp. Máy tính thực thi theo thứ tự hơi khác (FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY), nhưng bạn chỉ cần nhớ để tránh lỗi phổ biến ở phần sau.
20 query bắt buộc — chia thành 5 nhóm
Nhóm 1 — Lấy và lọc dữ liệu (query 1–6)
-- 1. Lấy tất cả cột
SELECT * FROM customers;-- 2. Lấy cột chọn lọc (luôn ưu tiên cách này thay vì *)
SELECT customer_id, full_name, city FROM customers;
-- 3. Lọc theo điều kiện
SELECT * FROM orders WHERE status = 'cancelled';
-- 4. Lọc nhiều điều kiện
SELECT * FROM orders
WHERE status = 'paid' AND total_amount > 500000;
-- 5. Lọc theo danh sách và khoảng
SELECT * FROM orders
WHERE city IN ('Hà Nội', 'Đà Nẵng')
AND created_at BETWEEN '2026-01-01' AND '2026-03-31';
-- 6. Tìm gần đúng (LIKE) và giá trị rỗng (NULL)
SELECT * FROM customers
WHERE full_name LIKE 'Nguyễn%' AND phone IS NULL;
Nhóm 2 — Sắp xếp và giới hạn (query 7–8)
-- 7. Sắp xếp giảm dần và giới hạn số dòng
SELECT customer_id, total_amount FROM orders
ORDER BY total_amount DESC
LIMIT 10;-- 8. Loại trùng lặp
SELECT DISTINCT city FROM customers;
Nhóm 3 — Tổng hợp và nhóm (query 9–13)
-- 9. Đếm số dòng
SELECT COUNT(*) FROM orders WHERE status = 'cancelled';-- 10. Các hàm tổng hợp cơ bản
SELECT SUM(total_amount), AVG(total_amount),
MIN(total_amount), MAX(total_amount)
FROM orders;
-- 11. Nhóm theo cột
SELECT status, COUNT(*) AS so_don
FROM orders
GROUP BY status;
-- 12. Nhóm rồi lọc trên nhóm (HAVING)
SELECT customer_id, COUNT(*) AS so_don
FROM orders
GROUP BY customer_id
HAVING COUNT(*) >= 5;
-- 13. Đặt tên lại cột cho báo cáo dễ đọc (AS)
SELECT city AS thanh_pho, COUNT(*) AS so_khach
FROM customers
GROUP BY city;
Nhóm 4 — Ghép bảng (query 14–17) — đây là phần "lên trình" của BA.
-- 14. INNER JOIN: chỉ lấy dòng khớp ở cả 2 bảng
SELECT o.order_id, c.full_name, o.total_amount
FROM orders o
INNER JOIN customers c ON o.customer_id = c.customer_id;-- 15. LEFT JOIN: giữ toàn bộ bảng trái, kể cả không khớp
SELECT c.full_name, o.order_id
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id;
-- 16. Tìm dòng "mồ côi" (khách chưa từng đặt hàng)
SELECT c.customer_id, c.full_name
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE o.order_id IS NULL;
-- 17. JOIN kết hợp tổng hợp (doanh thu theo khách)
SELECT c.full_name, SUM(o.total_amount) AS tong_chi_tieu
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.full_name
ORDER BY tong_chi_tieu DESC;
Nhóm 5 — Query nâng cao mà fresher vẫn nên biết (query 18–20)
-- 18. CASE WHEN: phân loại ngay trong query
SELECT order_id,
CASE
WHEN total_amount >= 1000000 THEN 'Đơn lớn'
WHEN total_amount >= 300000 THEN 'Đơn trung bình'
ELSE 'Đơn nhỏ'
END AS phan_loai
FROM orders;-- 19. Subquery: query lồng trong query
SELECT full_name FROM customers
WHERE customer_id IN (
SELECT customer_id FROM orders WHERE total_amount > 5000000
);
-- 20. Lọc theo hàm ngày tháng (số đơn theo tháng)
SELECT EXTRACT(MONTH FROM created_at) AS thang,
COUNT(*) AS so_don
FROM orders
WHERE EXTRACT(YEAR FROM created_at) = 2026
GROUP BY EXTRACT(MONTH FROM created_at)
ORDER BY thang;
Nếu bạn đọc hiểu và viết lại được 20 query trên, bạn đã vượt qua mức yêu cầu SQL của phần lớn job BA fresher tại Việt Nam.
Tình huống thực tế
Tình huống 1 — Query cứu vãn một requirement sai ở startup fintech
Linh, BA fresher tại một fintech ví điện tử ở TP.HCM (gọi tên là PayNow cho dễ), nhận yêu cầu từ Product Owner: "Xây tính năng cho phép người dùng liên kết tối đa 5 tài khoản ngân hàng." Team dev ước lượng 3 sprint.
Trước khi viết spec chi tiết, Linh tự chạy một query đơn giản trên bảng linked_accounts của môi trường staging:
SELECT
CASE WHEN so_tk >= 3 THEN '3+' ELSE CAST(so_tk AS CHAR) END AS nhom,
COUNT(*) AS so_nguoi_dung
FROM (
SELECT user_id, COUNT(*) AS so_tk
FROM linked_accounts
GROUP BY user_id
) t
GROUP BY nhom;
Kết quả: 91% người dùng chỉ liên kết 1 tài khoản, 7% liên kết 2, và chỉ 2% có 3 tài khoản trở lên. Không một ai chạm mức 4–5.
Bài học: Linh mang con số này vào buổi refinement và đề xuất giới hạn 3 tài khoản cho phiên bản đầu, đơn giản hóa UI và giảm khối lượng test. Team tiết kiệm được một sprint. Đây là sức mạnh thực sự của SQL với BA — không phải để thay dev, mà để ra quyết định requirement dựa trên dữ liệu thật thay vì phỏng đoán.
Tình huống 2 — Bài test SQL trong vòng phỏng vấn tại một công ty e-commerce
Tuấn, sinh viên IT mới ra trường, ứng tuyển vị trí Junior BA tại một sàn thương mại điện tử. Vòng 2 có bài test 20 phút với hai bảng: customers(customer_id, full_name, city) và orders(order_id, customer_id, total_amount, status, created_at).
Đề: "Liệt kê top 5 khách hàng ở Hà Nội có tổng chi tiêu cao nhất từ đơn đã thanh toán, kèm tổng số tiền."
Tuấn viết:
SELECT c.full_name, SUM(o.total_amount) AS tong_chi_tieu
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
WHERE c.city = 'Hà Nội' AND o.status = 'paid'
GROUP BY c.full_name
ORDER BY tong_chi_tieu DESC
LIMIT 5;
Câu này gói gọn INNER JOIN, WHERE, GROUP BY, hàm SUM, ORDER BY và LIMIT — đúng những gì nhà tuyển dụng muốn kiểm tra.
Bài học: Tuấn không phải thiên tài SQL, cậu chỉ luyện đúng 20 query mẫu. Điểm khiến cậu qua vòng: sau khi viết, cậu nói thêm "Nếu 'khách Hà Nội' được xác định theo địa chỉ giao hàng chứ không phải hồ sơ khách, thì em cần join thêm bảng địa chỉ." Câu nói này thể hiện tư duy BA làm rõ requirement, không chỉ là kỹ năng gõ query — và đó chính là điều phân biệt BA với một người test SQL thuần túy.
Tình huống 3 — Đối soát dữ liệu khi UAT một dự án ngân hàng
Trong một dự án core banking tại Việt Nam, đội BA phải hỗ trợ UAT tính năng báo cáo giao dịch. Nghiệp vụ báo "Số dư tổng cuối ngày trên báo cáo lệch so với hệ thống cũ." Không ai biết lệch ở đâu.
BA junior trong team chạy query đối chiếu số giao dịch theo trạng thái:
SELECT status, COUNT(*) AS so_gd, SUM(amount) AS tong_tien
FROM transactions
WHERE DATE(created_at) = '2026-06-20'
GROUP BY status;
Phát hiện: có 3.412 giao dịch ở trạng thái 'pending' đang bị báo cáo mới tính vào tổng, trong khi hệ thống cũ chỉ tính giao dịch 'completed'.
Bài học: Chỉ một câu GROUP BY đã khoanh vùng được lỗi và biến một cuộc tranh cãi mơ hồ thành business rule rõ ràng: "Báo cáo cuối ngày chỉ tính giao dịch trạng thái completed." SQL ở đây là công cụ điều tra và giao tiếp — BA đưa ra bằng chứng số liệu thay vì cảm tính.
Hướng dẫn từng bước
Đây là quy trình tôi khuyên bạn dùng mỗi khi cần viết một query, kể cả khi phỏng vấn:
- Đọc kỹ câu hỏi nghiệp vụ, gạch chân danh từ và điều kiện. "Top 5 khách Hà Nội chi tiêu cao nhất từ đơn đã thanh toán" → danh từ: khách, đơn hàng; điều kiện: Hà Nội, đã thanh toán; yêu cầu: top 5, theo tổng chi tiêu.
- Xác định cần bảng nào. Khách ở bảng customers, đơn ở bảng orders. Cần cả hai → sẽ phải JOIN.
- Xác định khóa liên kết (join key). Hai bảng nối nhau qua customer_id. Đây là bước fresher hay quên nhất.
- Dựng khung 6 mệnh đề theo thứ tự. Viết SELECT ... FROM ... JOIN ... WHERE ... GROUP BY ... ORDER BY ... trước, rồi mới điền chi tiết.
- Điền điều kiện lọc vào WHERE, điều kiện nhóm vào HAVING. Nhớ: lọc trên từng dòng thì dùng WHERE, lọc trên kết quả đã gom nhóm (như COUNT >= 5) thì dùng HAVING.
- Chạy thử với LIMIT 10 trước. Đừng bao giờ chạy query nặng trên bảng triệu dòng mà không giới hạn — vừa chậm vừa dễ làm nghẽn hệ thống.
- Kiểm tra số liệu có "make sense" không. Nếu tổng doanh thu ra số âm hoặc số khách nhiều hơn dân số thành phố, chắc chắn query sai.
Lỗi thường gặp & mẹo
- Nhầm WHERE với HAVING. WHERE lọc trước khi gom nhóm, HAVING lọc sau. Viết
WHERE COUNT() > 5sẽ báo lỗi; phải dùngHAVING COUNT() > 5.
- Quên điều kiện JOIN → sinh tích Descartes. Nếu JOIN mà thiếu
ON ..., database sẽ ghép mọi dòng bảng này với mọi dòng bảng kia. 1.000 khách nhân 10.000 đơn thành 10 triệu dòng rác. Luôn kiểm tra mệnh đề ON.
- Bẫy NULL. So sánh
WHERE phone = NULLluôn trả về rỗng. Phải viếtWHERE phone IS NULL. NULL không bằng bất cứ giá trị nào, kể cả chính nó.
- COUNT() và COUNT(cột) khác nhau. COUNT() đếm tất cả dòng; COUNT(email) chỉ đếm dòng có email khác NULL. Trong báo cáo, chọn nhầm là ra sai số ngay.
- Cột trong SELECT phải xuất hiện trong GROUP BY hoặc nằm trong hàm tổng hợp. Đây là lỗi cú pháp fresher gặp nhiều nhất khi mới học GROUP BY.
- Mẹo lớn nhất: đừng học thuộc lòng cú pháp một cách máy móc. Hãy tự tạo một "cheat sheet" gồm đúng 20 query trên, dán cạnh màn hình, và mỗi lần gặp bài toán thực tế thì tìm mẫu gần nhất để chỉnh sửa. Sau khoảng 30–40 lần, bạn sẽ viết được mà không cần nhìn.
- Mẹo phỏng vấn: khi được hỏi query, hãy nói thành tiếng suy nghĩ của bạn ("Em cần join hai bảng qua customer_id, lọc theo status..."). Nhà tuyển dụng BA đánh giá cao tư duy làm rõ requirement hơn là tốc độ gõ.
Bài tập thực hành
Dùng hai bảng customers(customer_id, full_name, city, created_at) và orders(order_id, customer_id, total_amount, status, created_at). Hãy tự viết query cho các đề sau (thử làm trên sqliteonline.com):
- Đếm tổng số khách hàng ở Đà Nẵng.
- Liệt kê 10 đơn hàng có giá trị cao nhất, kèm tên khách hàng.
- Tính tổng doanh thu từ các đơn có status = 'paid' trong quý 1 năm 2026.
- Tìm những khách hàng đã đăng ký nhưng chưa từng đặt đơn nào (gợi ý: LEFT JOIN + IS NULL).
- Đếm số đơn theo từng trạng thái và chỉ hiển thị trạng thái có từ 100 đơn trở lên.
- Phân loại khách thành 'VIP' (tổng chi tiêu ≥ 10 triệu) và 'Thường' bằng CASE WHEN.
- Bài tập tư duy BA: giả sử bạn viết requirement "khách VIP được miễn phí ship". Hãy viết một query để ước lượng có bao nhiêu khách sẽ được hưởng chính sách này, và giải thích con số đó ảnh hưởng thế nào đến chi phí.
Tóm tắt
SQL với BA fresher không phải là môn học đáng sợ — nó là bộ 20 query mẫu bạn hoàn toàn có thể làm chủ trong vài tuần luyện tập. Ba điều cốt lõi cần mang theo:
- Nắm khung 6 mệnh đề (SELECT–FROM–WHERE–GROUP BY–HAVING–ORDER BY) và 20 query bắt buộc, đặc biệt là JOIN và GROUP BY.
- Dùng SQL để phục vụ tư duy BA: validate requirement bằng dữ liệu thật, đối soát khi UAT, và trả lời câu hỏi nghiệp vụ ngay tại chỗ thay vì chờ dev.
- Trong phỏng vấn, kết hợp query đúng với câu hỏi làm rõ requirement — đó là thứ phân biệt bạn với một ứng viên chỉ biết gõ SQL.