Lập trình 22/09/2026 6 phút

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.

Lập trình 22/09/2026 6 phút

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.

Lập trình 22/09/2026 6 phút

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.

Lập trình 22/09/2026 6 phút

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.

Lập trình 22/09/2026 6 phút

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ờ.

Lập trình 22/09/2026 6 phút

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.