DATABASE + SQL — Nội dung ôn tập cho Fresher Backend (VPBank)
1. Danh sách chủ đề con theo độ ưu tiên
Ưu tiên CAO (gần như chắc chắn bị hỏi)
- Transaction & ACID: giải thích từng chữ A-C-I-D với ví dụ chuyển tiền. Đây là câu "signature" của phỏng vấn ngân hàng.
- SQL cơ bản viết trên giấy: SELECT, WHERE, JOIN (INNER/LEFT/RIGHT/FULL), GROUP BY + HAVING, ORDER BY, subquery. Bài kinh điển: "tìm khách hàng có tổng giao dịch lớn nhất", "top N", "tìm bản ghi trùng".
- Index: B-tree hoạt động thế nào (mức khái niệm), composite index và quy tắc leftmost prefix, các trường hợp index KHÔNG được dùng (hàm trên cột, LIKE '%x', kiểu dữ liệu không khớp, cột selectivity thấp).
- Isolation levels & anomalies: dirty read, non-repeatable read, phantom read; 4 mức Read Uncommitted → Serializable; default của MySQL (Repeatable Read) và PostgreSQL/Oracle (Read Committed).
- Locking & double-spend: pessimistic lock (
SELECT ... FOR UPDATE) vs optimistic lock (version column); race condition khi 2 request cùng trừ số dư.
Ưu tiên TRUNG BÌNH
- Deadlock: là gì, ví dụ 2 transaction khóa chéo 2 tài khoản, cách phòng tránh (khóa theo thứ tự cố định, transaction ngắn, timeout/retry).
- Normalization: 1NF/2NF/3NF ở mức giải thích được + khi nào chấp nhận denormalize (báo cáo, read-heavy).
- NoSQL vs SQL: khi nào dùng gì; tại sao core banking vẫn dùng RDBMS.
- Redis: use case (cache, session, rate limiting, distributed lock, OTP với TTL); cache-aside pattern; vấn đề cache và DB không nhất quán.
- Kiểu dữ liệu cho tiền: DECIMAL/NUMERIC, tuyệt đối không dùng FLOAT/DOUBLE.
- DELETE vs TRUNCATE vs DROP; WHERE vs HAVING; UNION vs UNION ALL — các câu so sánh nhanh hay dùng để "khởi động".
Ưu tiên THẤP (biết khái niệm là đủ, fresher ít bị đào sâu)
- Clustered vs non-clustered index, covering index.
- EXPLAIN/execution plan (chỉ cần biết dùng để xem query có ăn index không).
- Window functions (ROW_NUMBER, RANK) — điểm cộng nếu biết, thường không bắt buộc với fresher.
- Partitioning, replication, sharding — chỉ cần nói được 1-2 câu khái niệm.
- MVCC — nói được "PostgreSQL/MySQL InnoDB dùng MVCC nên read không block write" là đủ ấn tượng.
2. Câu hỏi phỏng vấn khả năng cao + hướng trả lời
Câu 1: Giải thích ACID. Cho ví dụ với giao dịch chuyển tiền.
Hướng trả lời:
- Atomicity: tất cả hoặc không gì cả. Chuyển 500k từ A sang B gồm 2 bước (trừ A, cộng B) — nếu cộng B lỗi thì bước trừ A phải rollback, không được để tiền "bốc hơi".
- Consistency: DB đi từ trạng thái hợp lệ này sang trạng thái hợp lệ khác — tổng tiền toàn hệ thống không đổi, constraint
balance >= 0 luôn đúng.
- Isolation: các transaction chạy đồng thời không thấy trạng thái dở dang của nhau — người khác không đọc được số dư "đã trừ A nhưng chưa cộng B".
- Durability: đã commit là không mất, kể cả mất điện/crash (nhờ WAL/redo log).
- Ăn điểm: gắn CẢ 4 chữ vào đúng một ví dụ chuyển tiền xuyên suốt; nhắc rollback và commit đúng ngữ cảnh; nói thêm Durability đạt được nhờ write-ahead log.
- Lỗi cần tránh: học vẹt định nghĩa mà không có ví dụ; nhầm Consistency của ACID với Consistency trong CAP theorem; nói "Isolation nghĩa là các transaction chạy tuần tự" (thực tế là chạy song song nhưng như thể tách biệt, mức độ tùy isolation level).
Câu 2: Phân biệt INNER JOIN, LEFT JOIN, RIGHT JOIN, FULL OUTER JOIN.
Hướng trả lời:
- INNER JOIN: chỉ lấy bản ghi khớp ở cả 2 bảng.
- LEFT JOIN: lấy hết bảng trái, bảng phải không khớp thì NULL — ví dụ: lấy tất cả khách hàng kể cả người chưa có giao dịch nào.
- RIGHT JOIN: ngược lại LEFT; FULL OUTER: lấy hết cả 2 phía (MySQL không hỗ trợ trực tiếp, phải UNION).
- Vẽ sơ đồ Venn hoặc bảng 3-4 dòng minh họa nếu được đưa giấy.
- Ăn điểm: nêu use case thật — "đếm số giao dịch của TẤT CẢ khách hàng, kể cả người 0 giao dịch → phải LEFT JOIN + COUNT(t.id) chứ không COUNT(*)"; biết rằng điều kiện lọc bảng phải đặt ở ON và ở WHERE cho kết quả khác nhau với LEFT JOIN.
- Lỗi cần tránh: dùng COUNT(*) làm dòng NULL bị đếm thành 1; quên rằng WHERE trên cột bảng phải sẽ biến LEFT JOIN thành INNER JOIN.
Câu 3: WHERE khác HAVING thế nào? Viết query: tìm các khách hàng có tổng tiền chuyển trong tháng > 100 triệu.
Hướng trả lời:
- WHERE lọc từng dòng TRƯỚC khi GROUP BY; HAVING lọc trên kết quả tổng hợp SAU GROUP BY.
- WHERE không dùng được aggregate function; HAVING thì được.
- Query mẫu:
SELECT customer_id, SUM(amount) AS total
FROM transactions
WHERE txn_date >= '2026-06-01' AND txn_date < '2026-07-01'
AND type = 'TRANSFER'
GROUP BY customer_id
HAVING SUM(amount) > 100000000;
- Ăn điểm: giải thích thứ tự thực thi logic FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY; đặt điều kiện ngày vào WHERE (lọc sớm, ít dữ liệu hơn) thay vì HAVING.
- Lỗi cần tránh: viết
WHERE SUM(amount) > ...; dùng MONTH(txn_date) = 6 khiến index trên txn_date không dùng được (bonus nếu tự chỉ ra điều này).
Câu 4: Index là gì? B-tree index hoạt động ra sao? Khi nào index KHÔNG được dùng?
Hướng trả lời:
- Index như mục lục sách: cấu trúc dữ liệu phụ giúp tìm dòng nhanh mà không quét cả bảng (full table scan) — O(log n) thay vì O(n).
- B-tree: cây cân bằng, node sắp thứ tự, hỗ trợ tốt cả tìm chính xác (=) và tìm khoảng (BETWEEN, <, >), ORDER BY.
- Trade-off: tốn dung lượng, làm chậm INSERT/UPDATE/DELETE vì phải cập nhật index — không phải cứ đánh index là tốt.
- Index không được dùng khi: bọc hàm quanh cột (
WHERE YEAR(created_at) = 2026), LIKE '%abc' (wildcard đầu), so sánh khác kiểu dữ liệu (implicit cast), cột selectivity quá thấp (cột giới tính), hoặc không tuân leftmost prefix của composite index.
- Composite index
(a, b): dùng được cho điều kiện trên a hoặc a AND b, KHÔNG dùng được nếu chỉ lọc theo b.
- Ăn điểm: ví dụ ngân hàng — bảng transactions hàng trăm triệu dòng, index
(account_id, txn_date) cho truy vấn sao kê; nhắc từ khóa EXPLAIN để kiểm tra.
- Lỗi cần tránh: nói "index luôn làm nhanh mọi thứ"; không nêu được cost khi ghi; nhầm index với primary key (PK tự có index nhưng index không nhất thiết là PK).
Câu 5: Kể tên các isolation level và anomaly tương ứng.
Hướng trả lời:
- Dirty read: đọc dữ liệu chưa commit của transaction khác — ví dụ đọc số dư đã bị trừ bởi giao dịch sau đó rollback → ra quyết định trên tiền "ảo".
- Non-repeatable read: trong 1 transaction đọc cùng 1 dòng 2 lần ra 2 giá trị khác nhau (dòng bị transaction khác UPDATE + commit xen giữa).
- Phantom read: cùng 1 câu query chạy 2 lần ra SỐ DÒNG khác nhau (bị INSERT xen giữa) — khác non-repeatable ở chỗ là dòng mới xuất hiện chứ không phải dòng cũ đổi giá trị.
- 4 mức: Read Uncommitted (cho phép cả 3) → Read Committed (chặn dirty read) → Repeatable Read (chặn thêm non-repeatable) → Serializable (chặn cả phantom, chậm nhất).
- Default: PostgreSQL/Oracle/SQL Server = Read Committed; MySQL InnoDB = Repeatable Read.
- Ăn điểm: vẽ bảng 4x3 (level × anomaly); ví dụ ngân hàng cho từng anomaly (đọc số dư dở dang, tính lãi 2 lần ra 2 số, báo cáo tổng cuối ngày bị lệch do có giao dịch INSERT thêm); biết MySQL Repeatable Read chặn phần lớn phantom nhờ gap lock/MVCC (nói ngắn gọn là đủ, không cần sâu).
- Lỗi cần tránh: nhầm non-repeatable read với phantom read (đây là bẫy phổ biến nhất); nói "cứ để Serializable cho an toàn" mà không nhắc cái giá về hiệu năng/throughput.
Câu 6: Hai request cùng lúc rút tiền từ 1 tài khoản số dư 1 triệu, mỗi request rút 800k. Làm sao tránh cả hai cùng thành công (double-spend)?
Hướng trả lời:
- Nêu rõ race condition: cả 2 cùng đọc balance = 1.000.000, cùng thấy đủ tiền, cùng trừ → số dư âm hoặc mất tiền. Pattern lỗi: check-then-act không atomic.
- Cách 1 — Pessimistic lock:
SELECT balance FROM accounts WHERE id = ? FOR UPDATE trong transaction — request thứ hai phải chờ, đọc số dư mới nhất, thấy không đủ tiền → từ chối.
- Cách 2 — Optimistic lock: thêm cột
version; UPDATE accounts SET balance = balance - 800000, version = version + 1 WHERE id = ? AND version = ? — nếu affected rows = 0 thì retry hoặc báo lỗi.
- Cách 3 — Atomic update có điều kiện:
UPDATE accounts SET balance = balance - 800000 WHERE id = ? AND balance >= 800000, kiểm tra affected rows.
- So sánh: pessimistic hợp khi contention cao và không được phép sai (tiền → thường chọn cái này); optimistic hợp khi xung đột hiếm, throughput cao.
- Ăn điểm: dùng đúng từ "race condition", "check-then-act", "lost update"; nói rõ đọc balance rồi check ở tầng Java KHÔNG đủ vì giữa đọc và ghi có khoảng hở; nhắc thêm idempotency key để chống double-submit từ phía client. Đây là câu ăn điểm nặng nhất ở ngân hàng.
- Lỗi cần tránh: trả lời "dùng synchronized trong Java" (chỉ đúng với 1 instance, chết khi chạy nhiều instance/microservice); quên kiểm tra affected rows sau UPDATE.
Câu 7: Deadlock là gì? Cho ví dụ và cách phòng tránh.
Hướng trả lời:
- Hai (hay nhiều) transaction chờ lock của nhau vòng tròn, không ai tiến được.
- Ví dụ kinh điển ngân hàng: T1 chuyển tiền A→B (khóa A rồi xin khóa B), T2 đồng thời chuyển B→A (khóa B rồi xin khóa A) → kẹt chéo.
- DB tự phát hiện và chọn một transaction làm "victim" để rollback; ứng dụng phải bắt lỗi và retry.
- Phòng tránh: luôn khóa tài nguyên theo THỨ TỰ cố định (ví dụ theo account_id tăng dần), giữ transaction ngắn, đặt lock timeout, giảm phạm vi lock.
- Ăn điểm: nêu đúng giải pháp "sắp xếp thứ tự khóa theo id" — interviewer ngân hàng rất chờ ý này; nói thêm ứng dụng cần cơ chế retry vì deadlock không thể loại bỏ 100%.
- Lỗi cần tránh: nhầm deadlock với lock wait/blocking thông thường; nói "tăng timeout là hết deadlock" (timeout chỉ là giảm thiệt hại, không phòng được).
Câu 8: Normalization là gì? Nêu 1NF, 2NF, 3NF. Khi nào chấp nhận denormalize?
Hướng trả lời:
- Chuẩn hóa = tổ chức bảng để giảm dư thừa dữ liệu và tránh anomaly khi insert/update/delete.
- 1NF: giá trị nguyên tử, không lưu danh sách trong 1 ô (không lưu "SĐT1, SĐT2" trong 1 cột).
- 2NF: đạt 1NF + thuộc tính non-key phụ thuộc toàn bộ khóa chính (loại phụ thuộc một phần khóa composite).
- 3NF: đạt 2NF + không có phụ thuộc bắc cầu (tên chi nhánh không nằm trong bảng account mà tách sang bảng branch, account chỉ giữ branch_id).
- Denormalize khi read-heavy/báo cáo: chấp nhận dư thừa để bớt JOIN, ví dụ bảng tổng hợp số dư cuối ngày cho reporting.
- Ăn điểm: mỗi dạng chuẩn một ví dụ ngắn cụ thể thay vì định nghĩa suông; nói được trade-off "chuẩn hóa tốt cho ghi và tính đúng, denormalize tốt cho đọc".
- Lỗi cần tránh: đọc định nghĩa hàn lâm mà không cho nổi ví dụ; tuyên bố "càng chuẩn hóa càng tốt" tuyệt đối.
Câu 9: SQL vs NoSQL — khác nhau gì, khi nào dùng gì? Vì sao core banking dùng RDBMS?
Hướng trả lời:
- SQL: schema cố định, quan hệ, JOIN, transaction ACID mạnh, scale dọc là chính. NoSQL: schema linh hoạt, scale ngang dễ, nhiều loại (document — MongoDB, key-value — Redis, column — Cassandra, graph — Neo4j).
- Dùng SQL khi: dữ liệu quan hệ chặt, cần transaction đa bảng, tính đúng tuyệt đối — tài khoản, số dư, sổ cái giao dịch.
- Dùng NoSQL khi: dữ liệu phi cấu trúc/thay đổi nhanh, khối lượng cực lớn, chấp nhận eventual consistency — log, session, cache, notification, lịch sử hành vi người dùng.
- Core banking chọn RDBMS vì: chuyển tiền cần atomic trên nhiều bản ghi, không chấp nhận eventual consistency với số dư ("số dư gần đúng" là không tồn tại trong ngân hàng), yêu cầu audit/đối soát.
- Ăn điểm: câu "với tiền, consistency quan trọng hơn availability" + nhắc CAP theorem đúng 1 câu; nêu kiến trúc thực tế: RDBMS cho core + Redis/Mongo cho các phần phụ trợ (không phải chọn một trong hai).
- Lỗi cần tránh: nói "NoSQL không có transaction" tuyệt đối (MongoDB 4.0+ có multi-document transaction, chỉ là không phải sở trường); nói "NoSQL nhanh hơn SQL" chung chung không điều kiện.
Câu 10: Redis là gì, dùng làm gì trong hệ thống banking? Cache-aside hoạt động thế nào?
Hướng trả lời:
- In-memory key-value store, cực nhanh (dữ liệu ở RAM), hỗ trợ TTL, các cấu trúc: string, hash, list, set, sorted set.
- Use case: cache dữ liệu đọc nhiều ít đổi (danh mục chi nhánh, tỷ giá), session/token, OTP với TTL 2-5 phút, rate limiting (chặn brute-force đăng nhập), distributed lock, sorted set cho leaderboard/ranking.
- Cache-aside: đọc cache trước → miss thì đọc DB rồi ghi vào cache kèm TTL → hit thì trả luôn. Khi dữ liệu đổi: update DB rồi xóa (invalidate) cache key.
- Vấn đề cần biết: cache và DB có thể lệch nhau tạm thời → KHÔNG cache số dư tài khoản, số dư luôn đọc từ DB.
- Ăn điểm: câu "tôi sẽ không cache balance vì yêu cầu strong consistency, chỉ cache dữ liệu tra cứu ít thay đổi" — thể hiện tư duy ngân hàng; nhắc OTP + TTL là ví dụ rất tự nhiên; biết Redis single-thread xử lý lệnh nên các lệnh đơn là atomic.
- Lỗi cần tránh: coi Redis là database chính để lưu dữ liệu giao dịch; quên đặt TTL; không nêu được chiến lược invalidation nào.
Câu 11: Optimistic lock vs Pessimistic lock — khác nhau và chọn thế nào?
Hướng trả lời:
- Pessimistic: giả định sẽ có xung đột → khóa trước khi đọc/sửa (
SELECT ... FOR UPDATE), người khác phải chờ. An toàn nhưng giảm throughput, có nguy cơ deadlock.
- Optimistic: giả định hiếm xung đột → không khóa, khi ghi mới kiểm tra version/timestamp; nếu version đổi → fail và retry. Throughput cao, nhưng phải xử lý retry.
- Chọn pessimistic khi contention cao + nghiệp vụ không được sai: trừ số dư, gỡ hạn mức.
- Chọn optimistic khi conflict hiếm: cập nhật thông tin hồ sơ khách hàng, cấu hình.
- Ăn điểm: viết được cả 2 câu SQL minh họa (FOR UPDATE và UPDATE ... WHERE version = ?); nếu dùng JPA/Hibernate thì nhắc
@Version và PESSIMISTIC_WRITE — cộng điểm với stack Java của ngân hàng.
- Lỗi cần tránh: nói optimistic lock "không có lock nên không an toàn" (nó an toàn, chỉ là phát hiện xung đột lúc ghi); quên nói phải xử lý khi update fail (retry/báo user).
Câu 12: Viết query tìm bản ghi trùng / tìm giá trị lớn thứ 2 (bài viết trên giấy kinh điển).
Hướng trả lời:
- Tìm trùng (ví dụ CMND bị đăng ký 2 lần):
SELECT national_id, COUNT(*)
FROM customers
GROUP BY national_id
HAVING COUNT(*) > 1;
- Lương/giao dịch lớn thứ 2:
SELECT MAX(amount) FROM transactions
WHERE amount < (SELECT MAX(amount) FROM transactions);
-- hoặc: ORDER BY amount DESC LIMIT 1 OFFSET 1
-- hoặc (điểm cộng): DENSE_RANK() OVER (ORDER BY amount DESC)
- Ăn điểm: đưa được 2 cách trở lên và nói trade-off (subquery chạy mọi DB; LIMIT/OFFSET gọn nhưng syntax khác nhau giữa DB; window function xử lý được "thứ N" tổng quát và giá trị trùng).
- Lỗi cần tránh: viết SQL sai thứ tự mệnh đề; dùng alias của SELECT trong WHERE; hoảng khi bị yêu cầu viết tay — nên luyện viết 10-15 query ra giấy trước phỏng vấn.
Câu 13: DELETE, TRUNCATE, DROP khác nhau gì? PRIMARY KEY vs UNIQUE?
Hướng trả lời:
- DELETE: xóa từng dòng, có WHERE, ghi log từng dòng, rollback được trong transaction, trigger chạy.
- TRUNCATE: xóa toàn bộ dữ liệu, nhanh, thường là DDL, reset auto-increment, nhiều DB không rollback được (Oracle/MySQL auto-commit).
- DROP: xóa cả cấu trúc bảng.
- PRIMARY KEY: duy nhất + NOT NULL + mỗi bảng 1 cái; UNIQUE: duy nhất nhưng cho phép NULL và có thể nhiều constraint.
- Ăn điểm: nhắc góc audit của ngân hàng — dữ liệu giao dịch gần như không bao giờ DELETE vật lý mà dùng soft delete/status vì yêu cầu lưu vết và đối soát.
- Lỗi cần tránh: nói TRUNCATE "giống DELETE không WHERE" mà không nêu khác biệt về log/rollback/trigger.
Câu 14: Lưu tiền trong database nên dùng kiểu dữ liệu gì? Tại sao không dùng FLOAT?
Hướng trả lời:
- Dùng DECIMAL/NUMERIC với precision/scale rõ ràng, ví dụ
DECIMAL(19,2) (VND có thể DECIMAL(19,0)).
- FLOAT/DOUBLE là số dấu phẩy động nhị phân, không biểu diễn chính xác nhiều số thập phân → 0.1 + 0.2 != 0.3, sai số tích lũy qua hàng triệu giao dịch là thảm họa đối soát.
- Trong Java tương ứng dùng BigDecimal, không dùng double cho tiền; so sánh BigDecimal bằng
compareTo chứ không ==.
- Ăn điểm: đây là câu "lọc người" của ngân hàng — nói ngay được ví dụ 0.1 + 0.2 và chữ "sai số tích lũy/đối soát" là ghi điểm mạnh; nhắc luôn cả tầng ứng dụng (BigDecimal) cho đồng bộ.
- Lỗi cần tránh: trả lời FLOAT "vì nó là số thực"; dùng
new BigDecimal(0.1) từ double thay vì từ String (nếu bị hỏi lấn sang Java).
Câu 15: Trong 1 transaction Java/Spring, nếu bước giữa ném exception thì chuyện gì xảy ra? Commit/Rollback hoạt động ra sao?
Hướng trả lời:
- Transaction bắt đầu (BEGIN) → các lệnh SQL → COMMIT nếu thành công, ROLLBACK nếu lỗi; mọi thay đổi trước rollback bị hủy như chưa từng xảy ra.
- Với Spring
@Transactional: mặc định rollback với RuntimeException/unchecked, KHÔNG tự rollback với checked exception (trừ khi cấu hình rollbackFor).
- Ví dụ chuyển tiền: debit A thành công, credit B ném exception → toàn bộ rollback, số dư A trở về ban đầu.
- Lưu ý
@Transactional chỉ hoạt động qua proxy — gọi method nội bộ trong cùng class thì annotation không có tác dụng (điểm cộng lớn nếu tự nêu).
- Ăn điểm: nối được kiến thức DB (BEGIN/COMMIT/ROLLBACK) với framework (Spring @Transactional) — đúng hình mẫu backend ngân hàng dùng Java/Spring; nêu bẫy checked exception.
- Lỗi cần tránh: nói "cứ có @Transactional là mọi exception đều rollback"; nghĩ rằng transaction tự retry khi lỗi.
3. Ghi chú chiến thuật riêng cho phỏng vấn ngân hàng
- Mọi câu trả lời nên "neo" vào ví dụ chuyển tiền/số dư: interviewer ngân hàng đánh giá cao ứng viên tự động map lý thuyết sang nghiệp vụ tiền — dù không bị yêu cầu.
- 3 câu gần như chắc chắn xuất hiện: ACID với ví dụ chuyển tiền (Câu 1), double-spend/race condition (Câu 6), isolation levels (Câu 5). Luyện nói trôi 3 câu này trước.
- Luyện viết SQL tay trên giấy: JOIN + GROUP BY + HAVING + subquery, top N, tìm trùng — VPBank và các bank Việt Nam rất hay cho viết trên giấy hoặc bảng.
- Từ khóa nên chủ động dùng (thể hiện tư duy banking): audit trail, đối soát (reconciliation), idempotency, soft delete,
SELECT FOR UPDATE, affected rows, DECIMAL/BigDecimal, "không cache số dư".
- Câu thòng an toàn khi bị hỏi quá sâu: mô tả nguyên lý + thừa nhận chưa vận hành thực tế + nói cách sẽ kiểm chứng (EXPLAIN, viết test tái hiện race condition) — với fresher, thái độ này ăn điểm hơn trả lời liều.