Tám tuần qua bạn tập trung viết query đúng. Tuần này thêm chiều thứ hai: viết query nhanh.
Trong mock interview, sau khi bạn đưa lời giải đúng, interviewer thường hỏi follow-up:
"Nếu bảng có 100 triệu dòng, query này có chạy được không?"
"Bạn sẽ index như thế nào để tối ưu?"
"EXPLAIN trên query này, bạn mong đợi thấy gì?"
"Có cách nào viết khác để optimizer làm tốt hơn không?"
Đây là lúc phân tách candidate junior với senior. Tuần này trang bị cho bạn câu trả lời.
Sau tuần 9 bạn cần làm được
Đọc EXPLAIN output cơ bản — phân biệt seq scan, index scan, hash join, nested loop.
Viết CREATE INDEX phù hợp cho query thường gặp (single-col, composite).
Nhận diện 5 anti-pattern phổ biến và biết cách sửa.
Liệt kê khác biệt phương ngữ MySQL / PostgreSQL / SQL Server cho 6 nhóm function chính.
Ghi chú
Tuần này thiên về kiến thức nền, không phải LeetCode. Bài tập sẽ thiên về local testing với DB Fiddle hoặc PostgreSQL local.
1.2
1.2 Mental model — storage, plan, execution
Khi bạn viết một query, ba thứ xảy ra trong DB engine:
Parse — kiểm tra cú pháp, tên cột/bảng tồn tại.
Plan — optimizer chọn execution plan: scan strategy, join order, join algorithm. Đây là phần EXPLAIN cho bạn xem.
Execute — chạy plan, đọc data từ disk/cache, áp filter, ghép, aggregate.
Tốc độ query phụ thuộc chính vào số trang disk phải đọc — không phải số dòng output. Một bảng 1M dòng có index phù hợp có thể đọc 100 trang. Cùng bảng full scan đọc 100,000 trang. Khác biệt 1000×.
Hai biến quan trọng nhất
Selectivity của predicate — predicate giữ lại 0.1% dòng vs 50% dòng. 0.1% có lợi từ index; 50% có khi seq scan nhanh hơn.
Cardinality của table — bảng 1000 dòng full scan 1ms; bảng 100M dòng full scan có thể nhiều giây hoặc phút.
Quy tắc nắm tay
"Index hữu ích nhất khi query trả về < 5–10% số dòng bảng." Trên đó, optimizer thường chọn full scan — vì đọc tuần tự nhanh hơn random reads qua index.
2. Index Basics
2.1
2.1 B-tree index — cấu trúc và hoạt động
B-tree index
Cấu trúc cây cân bằng (balanced tree). Tìm kiếm, chèn, xóa đều O(log n). Là loại index mặc định của hầu hết RDBMS.
Tìm id = 250: từ root → node giữa (250 ≥ 200) → leaf. 3 lần đọc disk thay vì scan toàn bảng.
B-tree hỗ trợ tốt
Equality: WHERE col = X
Range: WHERE col > X, BETWEEN
Prefix LIKE: WHERE col LIKE 'A%'
ORDER BY: tránh sort khi sort key trùng index
KHÔNG hỗ trợ tốt: LIKE '%X', LIKE '%X%', function trên cột.
2.2
2.2 Composite index — column order quan trọng
Composite index
Index trên nhiều cột, ví dụ (country, city, signup_date). Thứ tự cột là quan trọng — index hoạt động như "sort multi-key".
Quy tắc "leftmost prefix"
Index (A, B, C) hỗ trợ:
WHERE A = ... ✓
WHERE A = ... AND B = ... ✓
WHERE A = ... AND B = ... AND C = ... ✓ (toàn bộ)
WHERE A = ... AND C = ... ✓ chỉ phần A; B không sort được nên C lookup mỗi A-value
WHERE B = ... ✗ (không có A — index vô dụng)
WHERE C = ... ✗
Quy tắc thiết kế composite index
Sắp xếp cột theo thứ tự: equality predicate trước, range/sort cuối.
-- Query: WHERE country = 'VN' AND signup_date > '2024-01-01' ORDER BY signup_date
CREATE INDEX idx_country_date ON customers(country, signup_date);
-- (country) trước vì là equality, (signup_date) sau cho range + sort
Cảnh báo
Index (signup_date, country) KHÔNG dùng được cho query trên — vì range trên cột đầu phá khả năng dùng cột thứ hai để lọc tiếp. Order matters.
2.3
2.3 Hash index và các loại khác
Loại index
Tốt cho
Yếu ở
Phương ngữ
B-tree
Equality, range, sort, LIKE prefix
LIKE '%X'
Mặc định mọi RDBMS
Hash
Equality (O(1))
Range, sort
PostgreSQL, MySQL Memory engine
Bitmap
Cột low-cardinality (giới tính, country)
OLTP với nhiều update
Oracle, SQL Server (filtered)
GIN/GiST
Full-text, JSON, array
Query đơn giản
PostgreSQL
Full-text
Tìm word trong text
Substring nhỏ
MySQL, PostgreSQL, SQL Server
Khi nào dùng hash index?
Hiếm. PostgreSQL có hash index nhưng B-tree thường tốt tương đương cho equality trên data thông thường. Lý do duy nhất chọn hash: cột rất nhiều giá trị unique và bạn KHÔNG cần range query.
Trong interview
99% câu hỏi index xoay quanh B-tree. Biết tồn tại các loại khác là đủ. Đừng đề xuất bitmap index trừ khi cụ thể đang dùng Oracle.
2.4
2.4 Khi index không giúp (hoặc làm chậm)
Index hữu ích KÉM khi
Query trả về > 10% bảng (full scan thường nhanh hơn).
Cột có cardinality thấp (boolean, status với 3 giá trị) — selectivity kém.
Predicate dùng function trên cột (xem slide 4.1).
Implicit type conversion (WHERE id = '5' với id INT — đôi khi optimizer cast và bỏ index).
Index làm CHẬM khi
Bảng có nhiều INSERT/UPDATE/DELETE — mỗi thay đổi phải update tất cả index.
Quá nhiều index trên cùng bảng — tốn disk, tốn memory cache.
Index trên cột thay đổi liên tục (vd last_seen trên user activity) — fragmentation.
Quy tắc thực tế
Tạo index trên: PK (auto), FK, cột hay xuất hiện trong WHERE/JOIN/ORDER BY của query phổ biến.
Mỗi thêm 1 index: cân nhắc cost vs benefit.
"Index everything" là anti-pattern. Database không phải SSD vô hạn.
3. EXPLAIN — đọc execution plan
3.1
3.1 EXPLAIN — cú pháp và mục đích
EXPLAIN trả về kế hoạch thực thi (mà optimizer chọn). Không thực thi query, chỉ show plan.
EXPLAIN ANALYZE (PostgreSQL/MySQL 8.0+) chạy thật và đo thời gian — quan trọng hơn cho debug performance.
-- PostgreSQL / MySQL 8+
EXPLAIN SELECT * FROM orders WHERE customer_id = 42;
EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 42;
-- SQL Server
SET SHOWPLAN_TEXT ON;
SELECT * FROM orders WHERE customer_id = 42;
Output PostgreSQL EXPLAIN — cần biết đọc
QUERY PLAN
─────────────────────────────────────────────────────────────────────
Index Scan using idx_customer on orders (cost=0.42..8.44 rows=10 width=64)
Index Cond: (customer_id = 42)
Đọc:
Index Scan using idx_customer — engine dùng index idx_customer.
cost=0.42..8.44 — startup cost..total cost (đơn vị tương đối, không phải ms).
rows=10 — ước lượng số dòng trả về.
Index Cond — predicate được áp ngay tại index lookup.
3.2
3.2 Scan types — đọc ngắn gọn
Scan type
Ý nghĩa
Khi nào tốt/xấu
Seq Scan / Full Table Scan
Đọc tuần tự cả bảng
Tốt nếu query trả >10% bảng. Xấu nếu bảng lớn và predicate selective.
Index Scan
Đi qua index, lấy row pointer, đọc bảng
Tốt cho query selective. Xấu nếu trả nhiều dòng (random IO trên bảng đắt).
Index Only Scan
Toàn bộ data trong index, không đụng bảng
Nhanh nhất. Cần index phủ hết cột query (covering index).
Bitmap Index Scan
Build bitmap từ index, sau đó fetch dòng
Tốt cho OR và AND nhiều predicate.
Cảnh báo Seq Scan trên bảng lớn
Khi EXPLAIN trả về Seq Scan trên bảng > 1M dòng và bạn mong đợi index — kiểm tra:
Index có tồn tại không? (\d table_name trong psql)
Có function trên cột? (slide 4.1)
Statistics có cũ không? (ANALYZE table_name)
Predicate có quá lỏng (chọn >10% dòng)?
3.3
3.3 Join algorithms — ba loại chính
Algorithm
Cách hoạt động
Tốt khi
Nested Loop Join
Với mỗi dòng A, scan B tìm match. O(n×m) hoặc O(n×log m) nếu B có index.
A nhỏ, B có index trên join key. OLTP.
Hash Join
Build hash table trên A, probe B từng dòng. O(n+m).
Cả hai bảng vừa, không có index trên join key. OLAP.
Merge Join
Sort cả hai theo join key, merge. O(n log n + m log m).
Cả hai đã sorted theo join key (vì index hoặc ORDER BY).
Plan đọc thực tế
Hash Join (cost=12.50..1234.00 rows=5000 width=128)
Hash Cond: (o.customer_id = c.customer_id)
-> Seq Scan on orders o (cost=0.00..200.00 rows=10000 width=64)
-> Hash (cost=10.00..10.00 rows=200 width=64)
-> Seq Scan on customers c (cost=0.00..10.00 rows=200 width=64)
Khi follow-up "query này dùng join algorithm nào?", thường trả lời: "Tùy size của bảng. Nếu A nhỏ và B có index trên join key, optimizer chọn nested loop. Hai bảng lớn, hash join. Đã sort hoặc clustered, merge join. EXPLAIN sẽ cho biết chính xác."
3.4
3.4 EXPLAIN ANALYZE — actual vs estimated
EXPLAIN cho ước lượng. EXPLAIN ANALYZE chạy thật và so sánh.
Index Scan using idx_customer on orders
(cost=0.42..8.44 rows=10 width=64)
(actual time=0.024..0.156 rows=8 loops=1)
Index Cond: (customer_id = 42)
Planning Time: 0.123 ms
Execution Time: 0.184 ms
Phần in đậm (actual time=...) là số đo thực tế. So sánh:
rows estimated vs actual — nếu lệch >10× → statistics cũ. Chạy ANALYZE table_name;
actual time — startup..total per loop. Nhân với loops cho tổng.
Buffers (thêm EXPLAIN (ANALYZE, BUFFERS)) — số trang đọc từ cache (shared hit) vs disk (read). Cache hit cao = tốt.
Lưu ý EXPLAIN ANALYZE
Thực sự chạy query — với DELETE, UPDATE, INSERT, query CHẠY THẬT và sửa data. Trên production, bao trong transaction rồi rollback:
BEGIN;
EXPLAIN ANALYZE DELETE FROM orders WHERE order_date < '2020-01-01';
ROLLBACK;
4. Anti-patterns thường gặp
4.1
4.1 Function trên cột đã index
Anti-pattern phổ biến nhất. Đặt function lên cột → optimizer không dùng index.
SAI
-- Function trên cột → seq scan
WHERE YEAR(order_date) = 2024
WHERE LOWER(email) = 'a@b.com'
WHERE order_date + INTERVAL 7 DAY > NOW()
ĐÚNG
-- Predicate có thể dùng index
WHERE order_date >= '2024-01-01'
AND order_date < '2025-01-01'
WHERE email = LOWER('a@b.com')
-- (giả sử email đã lowercase)
WHERE order_date > NOW() - INTERVAL 7 DAY
Functional index — workaround
Khi không tránh được function (ví dụ case-insensitive search):
-- PostgreSQL/SQL Server hỗ trợ
CREATE INDEX idx_email_lower ON users(LOWER(email));
-- Bây giờ WHERE LOWER(email) = '...' DÙNG được index
MySQL từ 8.0+ hỗ trợ functional index. Trước đó phải tạo cột generated.
4.2
4.2 SELECT * vs cột cụ thể
SELECT * tiện cho ad-hoc query. Nhưng trong production code có ba vấn đề:
Băng thông và memory — kéo về cột không cần. Bảng có cột TEXT/BLOB lớn → đắt.
Phá covering index — index có thể phủ hết cột query (index only scan, nhanh nhất). SELECT * ép phải đọc bảng gốc.
Code fragility — schema thay đổi (thêm cột) → query có behavior khác mà người viết không lường được.
Covering index ví dụ
CREATE INDEX idx_cust_status_total ON orders(customer_id, status, total);
-- Index only scan (nhanh nhất) — không đọc bảng
SELECT customer_id, status, total FROM orders WHERE customer_id = 42;
-- Index scan + table fetch — đọc thêm bảng
SELECT * FROM orders WHERE customer_id = 42;
-- (vì cần order_id, order_date, ... không có trong index)
Trong code review và interview: cột cụ thể là norm, SELECT * chỉ trong scratch query.
4.3
4.3 OR vs UNION ALL với cột khác nhau
Khi predicate có OR trên hai cột khác nhau (mỗi cột có index riêng), optimizer thường không dùng được cả hai index — fallback seq scan.
Có thể chậm
-- Hai cột có index, nhưng OR
SELECT * FROM customers
WHERE email = 'a@b.com'
OR phone = '0901234567';
-- Optimizer thường chọn seq scan
Tối ưu rõ ràng
SELECT * FROM customers
WHERE email = 'a@b.com'
UNION ALL
SELECT * FROM customers
WHERE phone = '0901234567'
AND email IS DISTINCT FROM 'a@b.com';
-- (loại trùng nếu cùng row có cả hai)
Khi nào áp dụng
Trên dữ liệu lớn và mỗi predicate selective riêng.
PostgreSQL có "Bitmap OR" tự động — optimizer hiện đại tốt hơn ngày xưa.
MySQL ít làm Bitmap OR — UNION ALL còn rõ ràng hơn.
Trong interview, đề cập: "Tôi sẽ kiểm tra EXPLAIN. Nếu thấy Seq Scan, refactor sang UNION ALL với điều kiện loại trùng."
4.4
4.4 Anti-pattern khác — quick reference
Anti-pattern
Vấn đề
Fix
DISTINCT để fix join nhân dòng
JOIN sai cardinality, DISTINCT che bug
Sửa JOIN (thêm điều kiện), hoặc EXISTS thay JOIN
Implicit type conversion
WHERE id = '5' với INT → cast → mất index
Dùng đúng kiểu: WHERE id = 5
NOT IN trên subquery NULL-able
Bug tập rỗng (đã học tuần 2)
NOT EXISTS
COUNT để check tồn tại
COUNT(*) > 0 phải scan hết
EXISTS — dừng ở dòng đầu match
Cursor / loop trong stored proc
Row-by-row, chậm hơn set-based 100×
Refactor thành single UPDATE/INSERT...SELECT
OFFSET lớn cho pagination
OFFSET 100000 phải scan và bỏ 100K dòng
Keyset pagination: WHERE id > last_seen_id LIMIT N
Keyset pagination — pattern senior
-- Anti-pattern: OFFSET (chậm trên trang xa)
SELECT * FROM orders ORDER BY id LIMIT 50 OFFSET 100000;
-- Pattern: keyset (luôn nhanh)
SELECT * FROM orders WHERE id > 100000 ORDER BY id LIMIT 50;
-- Cần track id cuối từ trang trước
Đề cập keyset pagination trong mock interview là tín hiệu candidate có production experience.
5. Phương ngữ — Tổng kết
5.1
5.1 String và Date functions
Tác vụ
MySQL
PostgreSQL
SQL Server
Concat
CONCAT(a,b)
a || b hoặc CONCAT
CONCAT(a,b) hoặc a + b
Length (chars)
CHAR_LENGTH
LENGTH hoặc CHAR_LENGTH
LEN
Substring
SUBSTRING(s,start,len)
SUBSTRING(s FROM start FOR len)
SUBSTRING(s,start,len)
Position
INSTR / LOCATE
POSITION(x IN s)
CHARINDEX(x,s)
Now (datetime)
NOW()
NOW()
GETDATE()
Today (date)
CURDATE()
CURRENT_DATE
CAST(GETDATE() AS DATE)
Add days
DATE_ADD(d, INTERVAL 7 DAY)
d + INTERVAL '7 days'
DATEADD(day, 7, d)
Diff days
DATEDIFF(b, a)
b - a
DATEDIFF(day, a, b)
Year part
YEAR(d)
EXTRACT(YEAR FROM d)
YEAR(d)
Format date
DATE_FORMAT
TO_CHAR
FORMAT (2012+)
Strategy interview
LeetCode mặc định MySQL — tập trung MySQL syntax. StrataScratch / DataLemur dùng PostgreSQL — biết equivalent. Chuẩn ANSI (EXTRACT, SUBSTRING ... FROM ... FOR) chạy 2/3 phương ngữ.
5.2
5.2 Pagination, NULL handling
Pagination — LIMIT/TOP/FETCH
Phương ngữ
Cú pháp
MySQL, PostgreSQL, SQLite
SELECT ... LIMIT 10 OFFSET 20
SQL Server (truyền thống)
SELECT TOP 10 ... (không có offset)
SQL Server 2012+ / Oracle 12c+
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY (chuẩn ANSI)
LeetCode dùng MySQL 8+; mọi feature hiện đại đều dùng được. StrataScratch/DataLemur dùng PostgreSQL. Trong production, phải biết platform team đang dùng.
5.4
5.4 Strategy phương ngữ trong interview
Hỏi platform trước. "Code này tôi viết cho dialect nào?" — câu hỏi đầu tiên trong mock interview.
Default đa dialect. Khi không biết platform: viết theo chuẩn ANSI nhiều nhất có thể (COALESCE, EXTRACT, CASE WHEN thay vì IF).
Đề cập alternative. Khi viết MySQL-specific (DATE_FORMAT), có thể nói: "Nếu trên PostgreSQL, sẽ dùng TO_CHAR."
Biết "must-have" mỗi platform.
MySQL: LIMIT n OFFSET m, CONCAT, DATE_ADD ... INTERVAL, DATE_FORMAT.
SQL Server: TOP, DATEADD/DATEDIFF, FORMAT, CHARINDEX.
Tín hiệu candidate strong
Khi viết code, comment dòng dialect-specific: -- MySQL syntax. Cho thấy bạn biết code không universal — và có thể adapt khi cần. Senior interviewer thấy đây là production sensibility.
6. Tối ưu — Worked Examples
6.1
6.1 Case study — query chậm và cách fix
Query gốc
SELECT *
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE YEAR(o.order_date) = 2024
AND c.country = 'Vietnam'
ORDER BY o.order_date DESC
LIMIT 100;
EXPLAIN report
Sort (cost=12345.67..12356.78 rows=4444 width=128)
Sort Key: o.order_date DESC
-> Hash Join (cost=...)
Hash Cond: (c.id = o.customer_id)
-> Seq Scan on orders o (cost=...) ← BAD: full scan!
Filter: (date_part('year', order_date) = 2024)
-> Hash (cost=...)
-> Seq Scan on customers c
Filter: (country = 'Vietnam')
Vấn đề và fix
YEAR(order_date) ngăn dùng index trên order_date → fix: nửa-mở interval.
Customers seq scan nếu country index thiếu → tạo index.
SELECT * kéo nhiều cột không cần — chọn cột cụ thể.
-- Fix:
CREATE INDEX idx_customer_country ON customers(country);
CREATE INDEX idx_orders_date_customer ON orders(order_date, customer_id);
SELECT o.order_id, o.order_date, o.total, c.name
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.order_date >= '2024-01-01'
AND o.order_date < '2025-01-01'
AND c.country = 'Vietnam'
ORDER BY o.order_date DESC
LIMIT 100;
Sau fix: index scan trên orders, hash join nhỏ, sort tránh được nếu LIMIT đủ chặt → có thể nhanh 100×.
6.2
6.2 Index strategy cho 7 pattern (tuần 8)
Pattern
Index đề xuất
Top-N per group
(group_key, rank_key DESC) — covering nếu có thể
Median per group
(group_key, value) — sort cho ROW_NUMBER ngại
Gaps and Islands
(group_key, date) — order cho ROW_NUMBER
Consecutive Sequences
(group_key, id) — LAG cần sort
Pivot
Index trên cột GROUP BY và cột CASE WHEN
Cumulative
(partition_key, time_key)
Date arithmetic
Index trên cột date — KHÔNG bọc function
Quy tắc thiết kế
Cho query:
SELECT customer_id, name, total,
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY total DESC) rn
FROM orders WHERE status = 'paid';
Index tốt: (status, customer_id, total DESC)
status = 'paid' equality → cột đầu
customer_id partition → cột thứ hai (sort phụ)
total DESC → cột cuối, match window ORDER BY
Nếu cần covering index, thêm INCLUDE (name) (PostgreSQL 11+, SQL Server) hoặc thêm name vào index list (MySQL).
7. Knowledge Check
7.Q
Knowledge Check — Tuần 9
Q1: Cho query SELECT * FROM orders WHERE YEAR(order_date) = 2024. EXPLAIN trả về Seq Scan dù có index trên order_date. Vì sao?
B đúng. Function trên cột → index không match được pattern. Fix: viết predicate dạng nửa-mở order_date >= '2024-01-01' AND order_date < '2025-01-01'. Hoặc dùng functional index CREATE INDEX ON orders(YEAR(order_date)) (MySQL 8+, PostgreSQL).
Q2: Composite index (country, city, signup_date). Query nào KHÔNG dùng được index?
Quy tắc leftmost prefix: index dùng được khi predicate phủ từ cột đầu tiên. C bỏ qua country (cột đầu), nên không match prefix nào → seq scan. D dùng được phần country, nhưng vì city không có, signup_date dùng kém hiệu quả hơn so với khi có city.
Q3: Pagination trang xa: SELECT * FROM orders ORDER BY id LIMIT 50 OFFSET 1000000. Vấn đề và fix?
B đúng. OFFSET không skip ở storage level — engine vẫn phải đọc 1M dòng đầu rồi vứt đi. Keyset pagination: track id cuối trang trước, query WHERE id > last_id ORDER BY id LIMIT 50. Mỗi trang cùng độ phức tạp, không phụ thuộc trang xa hay gần.
8. Bài tập về nhà
8.1
8.1 Hands-on — local PostgreSQL hoặc DB Fiddle
Tuần này không phải LeetCode — luyện tools thật. Cài PostgreSQL local hoặc dùng DB Fiddle (db-fiddle.com).
Bài tập
Setup data sinh tổng hợp. Tạo bảng orders với 100K dòng (dùng generate_series trên PostgreSQL hoặc recursive CTE trên MySQL — đã học tuần 7).
Query không index. Chạy EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 42. Note thời gian.
Tạo index single-col.CREATE INDEX idx_cust ON orders(customer_id). Chạy lại EXPLAIN ANALYZE. So sánh.
Composite index. Query WHERE customer_id = 42 AND status = 'paid' ORDER BY order_date. Thử 3 thứ tự cột khác nhau cho composite index. EXPLAIN cho từng cái. Quan sát.
Anti-pattern test. Chạy WHERE YEAR(order_date) = 2024 trước và sau khi viết lại bằng nửa-mở. Note Seq Scan vs Index Scan.
Pagination test. Chạy LIMIT 50 OFFSET 50000. Refactor sang keyset. So tốc độ.
Bonus — cross-dialect
Lấy 3 query bạn đã làm tuần 6-8. Viết lại trên cả MySQL syntax VÀ PostgreSQL syntax. Note tất cả các điểm khác (function, syntax window, COALESCE/IFNULL, ...).
8.2
8.2 Đọc thêm
Đọc thiết yếu
SQL Performance Explained (Markus Winand) — toàn bộ. Cuốn sách quan trọng nhất về SQL performance; ngắn (200 trang) và practical.
Bạn vẽ được B-tree đơn giản và giải thích cách lookup không nhìn slide?
Bạn nhớ quy tắc leftmost prefix cho composite index?
Bạn liệt kê được 5 anti-pattern và cách fix mỗi cái?
Bạn nhớ 3 algorithm join và khi nào optimizer chọn cái nào?
Bạn dịch được 5 function MySQL ↔ PostgreSQL ↔ SQL Server không tra docs?
Tuần sau — Mock Interview và Capstone
Tuần 10: quy trình mock interview 45 phút, edge case checklist, capstone integrating tất cả 9 tuần. Đây là tuần áp lực thật — kiểm tra reflexes pattern recognition, code clarity, communication trong điều kiện time pressure.