Trang chủ · Blog về kiểm thử ·
SQL cho QA: câu truy vấn, kiểm tra cơ sở dữ liệu và bài phỏng vấn
SQL hữu ích khi trả lời một câu hỏi sản phẩm: order đã được lưu chưa, retry có tạo duplicate không, customer nào không có activity, và lỗi giữa chừng có để lại dữ liệu dở dang không? Bài này giải các câu hỏi đó trên một schema nhỏ thay vì liệt kê lệnh thiếu ngữ cảnh.
- Nêu business invariant
- Đọc schema và relationship
- Dự đoán expected rows
- Chạy read-only query và so sánh
| Table | Column quan trọng | Business meaning |
|---|---|---|
| customers | id, email, owner_id, created_at | Customer CRM và manager phụ trách |
| orders | id, customer_id, status, total, created_at | Order và trạng thái hiện tại |
| payments | id, order_id, external_id, status, amount | Kết quả payment attempt bên ngoài |
| activities | id, customer_id, kind, created_at | Call, email hoặc meeting trong CRM |
1. Đọc schema trước khi viết SQL
Tên table không mô tả đầy đủ dữ liệu. Hãy hỏi primary key, foreign key, column nào nullable, status nào hợp lệ và uniqueness có được database bảo vệ không. Thiếu các thông tin này, một query trông hợp lý vẫn có thể trả lời sai câu hỏi.
Trong schema mẫu, orders.customer_id và activities.customer_id trỏ tới customers.id; payments.order_id trỏ tới orders.id. Một customer có nhiều order và activity; một order có thể có nhiều payment attempt. Cardinality này giải thích vì sao JOIN có thể nhân số row.
- Viết relationship bằng lời trước khi JOIN.
- Phân biệt row không tồn tại với field có NULL.
- Xác nhận timezone và soft-delete rule.
Tự giải trước khi xem lại
Giải thích vì sao một order có nhiều payment row và cần so sánh gì để chứng minh double charge.
2. SELECT và WHERE: xác minh chính xác một thao tác
Sau POST /orders trả 201, đừng chỉ tìm theo status chung chung. Lưu order id từ response và query đúng record đó. So customer_id, status, total và created_at với request và contract. Chọn rõ column giúp evidence dễ đọc và tránh lấy dữ liệu không liên quan.
Time window hữu ích khi không có id nhưng yếu hơn vì test khác có thể tạo row tương tự. ORDER BY không lọc record; LIMIT thiếu deterministic order có thể trả row khác ở lần chạy sau.
SELECT id, customer_id, status, total, created_at FROM orders WHERE id = 1842 AND customer_id = 77;
Tự giải trước khi xem lại
Viết query lấy paid orders của customer 77 từ 2026-10-01, mới nhất trước. Dự đoán column trước khi chạy.
3. JOIN: so sánh các business object liên quan
INNER JOIN chỉ trả pair có match ở hai bên. LEFT JOIN giữ mọi row phía trái và điền NULL cho phía phải bị thiếu. Khác biệt này rất quan trọng khi tìm customer không có order hoặc order không có payment.
Filter của right table trong WHERE có thể loại NULL và biến LEFT JOIN thành INNER JOIN. Với “customer không có paid order”, đặt status trong ON rồi kiểm joined order id IS NULL.
SELECT c.id, c.email FROM customers c LEFT JOIN orders o ON o.customer_id = c.id AND o.status = 'paid' WHERE o.id IS NULL ORDER BY c.id;
Tự giải trước khi xem lại
Tìm order hoàn toàn không có payment row. Chọn left table và id cần bằng NULL.
4. NULL, empty string và missing data không giống nhau
NULL là giá trị chưa biết hoặc vắng mặt. Nó không bằng empty string và không kiểm bằng = NULL; phải dùng IS NULL. Trong sản phẩm, NULL, empty và whitespace có thể đi qua các validation path khác nhau.
Comparison với NULL cho kết quả unknown, nên WHERE không pass. COALESCE tiện cho report nhưng dùng quá sớm có thể che defect bằng cách đổi NULL bất ngờ thành default trông có vẻ hợp lệ.
SELECT id, email,
CASE
WHEN email IS NULL THEN 'null'
WHEN TRIM(email) = '' THEN 'blank'
ELSE 'present'
END AS email_state
FROM customers;
Tự giải trước khi xem lại
Thiết kế check riêng cho NULL, empty string và ba dấu cách trong customers.email, kèm expected behavior.
5. COUNT, GROUP BY và HAVING: tìm duplicate và total sai
GROUP BY tạo group; COUNT, SUM, MIN, MAX tính trong group; HAVING lọc group sau aggregate. WHERE lọc source row trước đó. Nhầm các bước này là lỗi interview phổ biến.
COUNT(*) đếm row, COUNT(column) bỏ NULL. JOIN nhiều quan hệ one-to-many có thể nhân row và làm total tăng sai. Hãy aggregate child table trước thay vì dùng DISTINCT như một bản vá không giải thích được.
SELECT LOWER(TRIM(email)) AS normalized_email, COUNT(*) AS customer_count FROM customers WHERE email IS NOT NULL GROUP BY LOWER(TRIM(email)) HAVING COUNT(*) > 1; SELECT external_id, COUNT(*) AS attempts, SUM(amount) AS charged FROM payments WHERE status = 'succeeded' GROUP BY external_id HAVING COUNT(*) > 1;
Tự giải trước khi xem lại
Trả customer có hơn ba activities trong tháng 10/2026, gồm customer id và count, sắp xếp count giảm dần.
6. Kiểm data integrity, không chỉ API response
Success response không chứng minh related data nhất quán. Order có thể thiếu line, payment có thể trỏ tới order không tồn tại, hoặc hai customer dùng email đáng lẽ unique. Foreign key, UNIQUE và CHECK bảo vệ database, nhưng application vẫn phải xử lý lỗi constraint đúng contract.
Tìm orphan row là kỹ thuật chẩn đoán, không phải quyền đọc tùy ý production data. Dùng synthetic records, chọn ít column và đính kèm identifier thay vì personal data đầy đủ.
SELECT p.id, p.order_id FROM payments p LEFT JOIN orders o ON o.id = p.order_id WHERE o.id IS NULL; SELECT id, order_id, amount FROM payments WHERE status = 'succeeded' AND amount <= 0;
Tự giải trước khi xem lại
Nêu ba invariant cho orders/payments và một read-only query có thể tìm violation cho mỗi invariant.
7. Transaction, retry và concurrency
Transaction gom các thay đổi để cùng commit hoặc cùng rollback. Hãy test failure giữa hai write liên quan: payment thành công nhưng update order thất bại thì state nào còn lại và reconciliation chạy thế nào?
Concurrent request lộ defect mà sequential check bỏ sót. Hai lần convert lead hoặc retry sau timeout có thể cùng pass kiểm tra “đã tồn tại chưa?”. Unique constraint, idempotency key hoặc locking phải giữ business invariant.
SELECT order_id, external_id, status, amount, created_at FROM payments WHERE order_id = 1842 ORDER BY created_at, id;
Tự giải trước khi xem lại
Mô tả hai request song song có thể tạo duplicate customer từ một CRM lead. Nêu invariant và database evidence mong đợi.
8. Cách giải bài SQL trong phỏng vấn
Đừng gõ ngay. Hãy diễn đạt task bằng business language, nêu table cần giữ, vẽ relationship và dự đoán một vài row. Sau khi viết query, kiểm bằng suy nghĩ với zero, one và many child rows.
Câu trả lời Middle còn nêu limitation: duplicate source data, timezone boundary, soft deletion, isolation và quyền direct DB access. Hỏi schema là bình thường; tự bịa column và cardinality mới nguy hiểm.
- Giải thích từng JOIN condition.
- Nêu khả năng duplicate output row.
- Kiểm empty input và nhiều child rows.
- Tách diagnostic SELECT khỏi cleanup script.
- Không chạy UPDATE/DELETE chỉ để tỏ ra tự tin.
Tự giải trước khi xem lại
Chuẩn bị câu trả lời nói trong năm phút và hai query. Đổi requirement từ “không có order” thành “không có paid order” rồi giải thích JOIN thay đổi thế nào.
Câu hỏi thường gặp về SQL cho QA
Manual QA có cần SQL phức tạp không?
Phần lớn công việc Junior bắt đầu bằng SELECT read-only: filter, JOIN, NULL và aggregate. Mức độ phụ thuộc sản phẩm. Gắn query với business invariant quan trọng hơn thuộc cú pháp nâng cao thiếu ngữ cảnh.
QA có thể chạy UPDATE hoặc DELETE không?
Chỉ trong environment được duyệt, với permission và cleanup plan rõ ràng. Điều tra production thường dùng read-only access. Không ứng biến destructive SQL trong interview hoặc incident.
Database result có thay API check không?
Không. API chứng minh external contract; database cung cấp evidence về stored state. Khi risk cần, hãy so cả hai và nhớ internal schema có thể thay đổi.
SQL topic nào hay gặp trong QA interview?
SELECT/WHERE, INNER và LEFT JOIN, NULL, COUNT, GROUP BY, HAVING, duplicate, subquery và transaction thường xuất hiện. Scope tùy công ty nên hãy hỏi recruiter về database và format.
SQL không phải tập hợp câu thần chú cho interview. Nó là cách biến product rule thành evidence chính xác về data. Bắt đầu từ expected rows, giữ query read-only và nói rõ kết quả chứng minh hay chưa chứng minh điều gì.