SQL sâu cho lập trình viên: đọc query plan và tối ưu đo thật
Loạt bài nâng cao về SQL và PostgreSQL cho lập trình viên backend: đọc EXPLAIN ANALYZE, index, JOIN, transaction/isolation, MVCC, window function, phân trang, planner. Mỗi bài demo THẬT bằng PostgreSQL trong container, đo thời gian và query plan thật.
11/12 phần đã đăng
Lập trình
1
Đọc EXPLAIN ANALYZE: query plan là ống nghe của bác sĩ database, và cách nghe cho đúng
Query chậm mà không biết vì sao? EXPLAIN ANALYZE cho biết database THỰC SỰ làm gì với query của bạn. Bài này đo thật trên bảng 1 triệu dòng trong PostgreSQL: cùng một query, chưa index chạy Seq Scan mất 10.764ms (quét cả bảng, đọc 7353 block); thêm index thành Bitmap Index Scan còn 0.116ms (14 block) — nhanh 93 lần. Kèm cách đọc từng con số: cost không phải ms, actual time mới thật, và rows dự đoán vs thật lộ khi planner đoán sai.
22/09/2026
· 6 phút đọc
2
Index B-tree: vì sao tạo index rồi vẫn Seq Scan — bẫy non-sargable và cách viết query cho đúng
Bạn tạo index nhưng query vẫn chậm và EXPLAIN vẫn báo Seq Scan? Vì predicate không sargable — chỉ cần bọc cột trong một hàm là index nằm im vô dụng. Bài này đo thật trên bảng 1 triệu dòng: WHERE email=x dùng index (0.045ms), nhưng lower(email)=x quét cả bảng (54.857ms); age+1=31 chậm 12ms còn age=30 chỉ 1ms; và một bất ngờ — LIKE 'x%' với index thường vẫn Seq Scan, phải dùng text_pattern_ops.
22/09/2026
· 6 phút đọc
3
Index tổ hợp: thứ tự cột quyết định query nào dùng được, và covering index xoá luôn việc đọc bảng
Một index nhiều cột không phải muốn dùng sao cũng được — quy tắc leftmost prefix quyết định query nào tận dụng được nó. Bài này đo thật trên bảng 2 triệu dòng: index (user_id, created_at) giúp query lọc user_id (0.037ms) nhưng bó tay với query chỉ lọc created_at (Seq Scan 54ms); đổi thứ tự cột cho cùng query nhanh 14 lần; và covering index INCLUDE(payload) cho Index Only Scan với Heap Fetches=0 — trả lời trọn query mà không chạm bảng.
22/09/2026
· 6 phút đọc
4
Ba thuật toán JOIN của PostgreSQL: nested loop, hash, merge — và vì sao planner chọn cái nào
JOIN không phải một phép — PostgreSQL có ba thuật toán để nối bảng, mỗi cái hợp một tình huống, và planner ước lượng để chọn. Bài này đo thật trên customers 1000 dòng ⋈ orders 2 triệu dòng: lọc một khách hàng thì Nested Loop chỉ 2.962ms; nối toàn bộ thì Hash Join (planner chọn) 133ms nhanh nhất, ép Nested Loop 187ms, ép Merge Join 301ms vì phải sắp xếp cả hai bên. Hiểu ba thuật toán để đọc plan và biết khi nào planner chọn sai.
22/09/2026
· 6 phút đọc
5
N+1 query: ORM giấu 101 lần đấm xuống DB sau một vòng lặp — và cách gộp về 1 query
Bạn viết một vòng lặp vô hại trong ORM, và nó lặng lẽ bắn 101 query xuống database. Đó là N+1 — cái bẫy hiệu năng phổ biến nhất của backend. Bài này đo thật trong PostgreSQL: lấy 100 tác giả rồi lặp lấy sách từng người. Trên cùng một connection, N+1 chậm 2.7 lần; nhưng khi mỗi query là một round-trip thật (như app gọi qua mạng), N+1 mất 1110ms so với 17ms của một JOIN — chậm 64 lần. Chi phí không ở database, mà ở 101 lần đi-về cộng dồn.
22/09/2026
· 6 phút đọc
6
Isolation level: tái hiện thật non-repeatable read và write skew, và vì sao READ COMMITTED không đủ
Transaction chạy đồng thời sinh ra các hiện tượng bất thường mà một mình bạn khó hình dung. Bài này tái hiện THẬT trong PostgreSQL bằng hai session: ở READ COMMITTED, cùng một transaction đọc balance hai lần ra 1000 rồi 500 (non-repeatable read); REPEATABLE READ giữ snapshot ổn định nên đọc lại vẫn 1000; và write skew — hai bác sĩ cùng xin nghỉ trực khiến còn 0 người trực ở REPEATABLE READ, nhưng SERIALIZABLE bắt được và abort một transaction.
22/09/2026
· 6 phút đọc
7
MVCC và VACUUM: vì sao UPDATE làm bảng phình 9 lần dù số dòng không đổi
PostgreSQL không sửa dòng tại chỗ — mỗi UPDATE tạo một phiên bản mới và để lại 'xác chết' (dead tuple). Bài này đo thật: một bảng 100.000 dòng sau 8 lần UPDATE toàn bộ phình từ 3.5MB lên 31MB với 799.810 dead tuple, dù vẫn đúng 100.000 dòng sống. VACUUM dọn dead tuple (dead về 0) nhưng không trả đĩa; chỉ VACUUM FULL trả bảng về 3.5MB — nhưng khoá cả đọc. Hiểu MVCC để không bị bloat bất ngờ.
22/09/2026
· 6 phút đọc
8
Window function: tính theo nhóm mà vẫn giữ từng dòng — và nhanh hơn self-join 124 lần
GROUP BY gộp nhóm lại làm mất chi tiết từng dòng. Window function tính toán theo nhóm (xếp hạng, tổng luỹ kế, so kỳ trước) mà VẪN giữ mọi dòng — thứ mà nhiều người viết bằng self-join phức tạp và chậm. Bài này đo thật trong PostgreSQL: ROW_NUMBER/RANK/DENSE_RANK khác nhau khi có tie, running total + LAG, và một benchmark đắt giá — window làm running total trên 30.000 dòng mất 15.9ms, còn self-join tương quan chỉ 3.000 dòng đã mất 1971ms.
22/09/2026
· 6 phút đọc
9
CTE và recursive CTE: viết query dễ đọc, duyệt cây — và bẫy MATERIALIZED chậm 1000 lần
CTE (WITH) chia query rối rắm thành các bước có tên, dễ đọc; recursive CTE duyệt cây/đồ thị chỉ bằng một câu SQL. Nhưng có một bẫy hiệu năng: bài này đo thật trong PostgreSQL — cùng một query lấy WHERE id=500000 trên bảng 1 triệu dòng, CTE thường (inline) dùng Index Scan chỉ 0.098ms, còn thêm MATERIALIZED buộc quét cả 1 triệu dòng mất 99.756ms — chậm hơn 1000 lần. Kèm recursive CTE dựng cây tổ chức từ CEO xuống.
22/09/2026
· 6 phút đọc
10
Phân trang: vì sao OFFSET 1 triệu chậm 3000 lần, và keyset pagination giữ tốc độ phẳng
OFFSET LIMIT là cách phân trang ai cũng viết, và nó hoạt động hoàn hảo... cho tới trang thứ 50.000. Bài này đo thật trên bảng 2 triệu dòng: OFFSET chậm dần tuyến tính — trang đầu 0.029ms nhưng trang cuối 144.941ms, vì nó phải đọc qua 1.000.020 dòng chỉ để lấy 20. Keyset pagination (WHERE id > last_id) giữ ~0.04ms phẳng ở mọi trang. Kèm plan chứng minh OFFSET quét rồi vứt, keyset nhảy thẳng.
22/09/2026
· 6 phút đọc
11
Statistics và planner: vì sao query 'đột nhiên chậm' — khi database đoán sai số dòng
Planner PostgreSQL không biết dữ liệu của bạn — nó ƯỚC LƯỢNG từ thống kê rồi chọn plan. Thống kê sai thì chọn plan tệ. Bài này đo thật: một bảng chèn 500.000 dòng nhưng chưa ANALYZE khiến planner ước lượng 1 dòng trong khi thực tế 5000 (chọn plan chậm 15.5ms); ANALYZE đưa về đúng, plan nhanh gấp đôi. Và cột tương quan (city suy ra country) khiến planner ước lượng thấp 4 lần — CREATE STATISTICS sửa từ ~23.000 về 99.500 sát thực tế 100.000.
22/09/2026
· 6 phút đọc
Còn 1 phần nữa sẽ lần lượt được đăng.