Cơ sở dữ liệu cho lập trình viên
Sê-ri đo đạc: cơ sở dữ liệu thật sự làm gì với truy vấn của bạn — chỉ mục, kế hoạch truy vấn, MVCC, khóa, WAL, bộ đệm — đo thật bằng EXPLAIN ANALYZE, pg_stat và \timing trong container PostgreSQL dùng một lần.
45/45 phần đã đăng
Cơ sở dữ liệu
1
Tìm một email trong 1 triệu hàng: không index mất 14ms, có index 0,03ms — nhanh 400 lần
Tìm một email trong bảng 1 triệu hàng: không index quét 7353 trang mất 14ms; có index đọc 4 trang mất 0,034ms — nhanh 400 lần, đọc ít 1800 lần. Nhưng tôi suýt khoe 'nhanh 1600 lần' vì đọc nhầm 'cost' như thời gian. Cost là ước lượng của planner, actual time mới là đồng hồ. Bài mở đầu sê-ri Cơ sở dữ liệu.
03/09/2026
· 8 phút đọc
2
Bảng to gấp 10.000 lần mà tìm một hàng vẫn đọc 6-7 trang: phép màu nông của B-tree
Bảng to gấp 10.000 lần (1k lên 10 triệu hàng) mà cây chỉ mục chỉ sâu thêm một tầng (2 lên 3), tìm một hàng vẫn đọc 6-7 trang. Tôi tưởng bảng gấp 100 lần thì tìm phải chậm hẳn — đo ra đường phẳng. Vì fanout ~360, chi phí tra cứu tăng theo log cơ số vài trăm, gần như đứng yên. Cỡ dữ liệu và chi phí tra cứu là hai thứ khác hẳn.
03/09/2026
· 8 phút đọc
3
Planner đoán 99 hàng, thực tế 10000 — bí mật nằm ở khoảng cách giữa 'rows' và 'actual rows'
Trên hai cột trùng nhau, planner đoán 99 hàng còn thực tế 10000 — lệch 100 lần. 'rows' của EXPLAIN là ước lượng (không chạy), 'actual rows' của EXPLAIN ANALYZE mới là đo thật (có chạy). Chính khoảng cách giữa hai con số tố cáo planner đang đoán mù — và EXPLAIN ANALYZE chạy thật cả UPDATE/DELETE.
03/09/2026
· 8 phút đọc
4
Tự tính cost của một truy vấn bằng tay, khớp EXPLAIN đến từng số lẻ — rồi đổi một dòng, kế hoạch lật hẳn
Cost một seq scan = 5406 trang × 1.0 + 1 triệu hàng × 0.01 = 15406.00, khớp EXPLAIN đến từng số lẻ. Tôi tưởng cost cố định theo dữ liệu; nhưng đổi random_page_cost là planner đổi kế hoạch dù dữ liệu y nguyên. Cost là đầu ra của một mô hình với tham số phần cứng, không phải mili giây, không phải bất biến.
03/09/2026
· 9 phút đọc
5
Đánh index lên cột boolean rồi đợi truy vấn nhanh — nhưng PostgreSQL phớt lờ nó, và nó đúng
Truy vấn chọn lọc (1% hàng) dùng index nhanh 4,7 lần; nhưng trên cột boolean 50% hàng, planner bỏ index chọn seq scan. Tôi tưởng 'có index thì phải dùng index'. Index chỉ giúp khi truy vấn trả về ít hàng — và một index chết còn âm thầm làm chậm mọi lần ghi.
03/09/2026
· 8 phút đọc
6
Tạo index phủ đúng bài mà vẫn nhảy vào bảng — thủ phạm là một lệnh VACUUM bị quên
Index chỉ có cột a đọc 103 trang (phải nhảy heap lấy b); index phủ (a) INCLUDE (b) cắt còn 4 trang, Heap Fetches=0 — nhưng CHỈ sau khi VACUUM cập nhật visibility map. Tôi tưởng INCLUDE hỏng, hóa ra quên VACUUM. Vì sao index-only scan của PostgreSQL phụ thuộc VACUUM.
03/09/2026
· 9 phút đọc
7
Index (a,b) tăng tốc WHERE a nhưng bỏ rơi WHERE b — vì sao thứ tự cột quyết định tất cả
Index (a,b) tăng tốc WHERE a=? và WHERE a=? AND b=? (4 trang) — nhưng WHERE b=? một mình cho Seq Scan quét cả 5406 trang. Tôi tưởng (a,b) là hai index cho a và b; hóa ra nó là một cây sắp theo a trước. Quy tắc tiền tố trái nhất, và vì sao thứ tự cột là quyết định thiết kế.
03/09/2026
· 8 phút đọc
8
Bảng 81MB nhỏ hơn cache 128MB mà chạy 3 lần vẫn không nóng lên — bí mật của ring buffer
Lần đọc đầu là shared read (nạp trang), lần sau thành shared hit (từ RAM). Nhưng một seq scan bảng lớn KHÔNG vào cache dù bảng nhỏ hơn shared_buffers — vì ring buffer cố ý bảo vệ cache khỏi bị một lần quét lớn xóa sạch. Tôi tưởng 'chạy lại luôn nhanh hơn', hóa ra đó không phải luật tự nhiên.
03/09/2026
· 8 phút đọc
9
Hàng 8 byte tưởng nhét 1024 dòng một trang — đo ra chỉ 226, phần còn lại đi đâu?
Một hàng 8 byte dữ liệu (2 int) tưởng 1024 hàng/trang 8KB, đo thật chỉ 226 — vì mỗi tuple còn cõng 23 byte header MVCC + con trỏ + đệm = 36 byte. Tôi nhẩm phép chia mà quên chi phí cố định mỗi hàng. Mổ trang thật bằng pageinspect: gần 78% mỗi hàng hẹp là overhead, không phải dữ liệu.
03/09/2026
· 8 phút đọc
10
Một hàng UPDATE 10.000 lần phình bảng lên 360KB — vì PostgreSQL không hề sửa tại chỗ
UPDATE một cột không sửa tại chỗ: ctid đổi từ (0,1) sang (0,2), bản cũ thành dead tuple. Tôi tưởng UPDATE ghi đè giá trị, bảng giữ nguyên; đo ra một hàng UPDATE 10.000 lần làm bảng phình từ 8KB lên 360KB với 10.000 xác chết. Đây là MVCC — mỗi thay đổi để lại một phiên bản cũ, và vì sao đọc-ghi đồng thời mượt.
03/09/2026
· 8 phút đọc
11
VACUUM dọn 20000 xác về 0 mà bảng vẫn 712kB — tôi tưởng nó không chạy, hóa ra tôi đo nhầm chỉ số
VACUUM dọn 20000 dead tuple về 0 nhưng bảng vẫn đúng 712kB — nó đánh dấu chỗ trống để tái dùng chứ không trả đĩa. Tôi tưởng VACUUM hỏng vì nhìn kích thước file, hóa ra phải nhìn n_dead_tup. VACUUM FULL mới nén còn 8kB nhưng khóa bảng — hai lệnh gần tên mà khác hẳn tác động.
03/09/2026
· 8 phút đọc
12
Quên một chữ BEGIN, 500 đồng bốc hơi khỏi hệ thống — bài học nguyên tử của giao dịch
Chuyển 500 mà lỗi giữa hai UPDATE: không giao dịch làm tổng tụt từ 2000 còn 1500 (mất 500 trong không khí); bọc BEGIN...COMMIT thì ROLLBACK giữ nguyên 2000. Mặc định là autocommit — mỗi lệnh tự commit, nên hai lệnh liên tiếp KHÔNG tự thành một đơn vị. Tính nguyên tử đo bằng một bất biến: tổng tiền.
03/09/2026
· 8 phút đọc
13
Đọc cùng một hàng hai lần trong một giao dịch mà ra hai số khác nhau — chuyện gì vậy?
Đọc cùng một hàng hai lần trong một giao dịch: ở mặc định READ COMMITTED được 100 rồi 200; REPEATABLE READ giữ 100; SERIALIZABLE có thể hủy giao dịch bắt bạn retry. Tôi tưởng 'đọc lại thì y nguyên chứ' — hóa ra mặc định KHÔNG phải mức chặt nhất, và mức cô lập là biến ẩn quyết định bạn thấy gì.
03/09/2026
· 8 phút đọc
14
Hàng đang bị khóa mà vẫn đọc được ngay — vì sao khóa của CSDL chỉ chặn người ghi
A giữ SELECT FOR UPDATE trên một hàng: B ghi cùng hàng chờ 2 giây, nhưng ghi hàng khác hay đọc thuần không chờ mảy may. Tôi tưởng khóa chặn tất cả — hóa ra nó chỉ chặn người ghi cùng hàng, đọc luôn qua nhờ MVCC. Và khóa hai hàng ngược thứ tự thì deadlock, PostgreSQL tự hủy một bên.
03/09/2026
· 8 phút đọc
15
Tắt fsync chỉ nhanh gấp 2 lần? Con số đó đúng — và cũng đánh lừa tôi hoàn toàn
Tắt fsync (synchronous_commit=off) chỉ nhanh gấp 2 lần trong đo của tôi — vì đĩa container là đĩa ảo nhanh; trên ổ cứng thật nó nhanh gấp hàng chục lần. WAL là thứ làm COMMIT bền, fsync là cái giá, và cái giá đó phụ thuộc hoàn toàn vào tốc độ đĩa. Gộp 3000 hàng vào 1 giao dịch = 1 fsync, nhanh gấp trăm lần.
03/09/2026
· 9 phút đọc
16
Có index trên đúng cột mà vẫn quét cả bảng 141ms — vì bạn tạo nhầm LOẠI index
Tìm tài liệu chứa một từ trên 200k dòng: không index 141ms, một B-tree cũng 141ms (bị bỏ qua hoàn toàn), nhưng GIN chỉ 0,28ms — nhanh 500 lần. B-tree không hiểu toán tử 'chứa từ'. Loại index phải khớp phép toán truy vấn, không chỉ khớp cột — và EXPLAIN là trọng tài.
03/09/2026
· 8 phút đọc
17
0,001ms trong EXPLAIN — nhưng nó chạy 1999 lần: cái bẫy loops của nested loop join
Nested loop join với bảng ngoài nhỏ + bảng trong có index chỉ 0,027ms; cùng 2000 hàng nhưng bỏ index thì 156ms — chậm 83 lần. Và tôi suýt tưởng join miễn phí vì đọc actual time 0,001ms mà quên nhân với loops=1999. Đọc EXPLAIN phải nhân thời gian node trong với số vòng lặp.
03/09/2026
· 8 phút đọc
18
Hash join 78ms hay 223ms cho cùng một câu? Con số 'Batches' trong EXPLAIN nói tất cả
Join hai bảng lớn: hash join 78ms (bảng băm vừa RAM, Batches=1), nhanh gần 3 lần nested loop. Nhưng work_mem nhỏ làm bảng băm tràn 16 batch ra đĩa và chậm hẳn. Tôi suýt kết luận 'hash join nhanh' vô điều kiện — cho tới khi hạ work_mem và thấy Batches nhảy lên 16. Đọc Batches, không chỉ thời gian tổng.
03/09/2026
· 8 phút đọc
19
Merge join: 77ms khi dữ liệu đã sắp, 160ms khi phải tự sắp — và vì sao đừng vội chê nó thua hash
Merge join trên khóa đã sắp: 77ms không cần sort; nhưng trên cột chưa sắp phải thêm Sort tràn đĩa, 160ms (hash chỉ 78ms). Tôi suýt kết luận 'merge dở hơn hash' — cho tới khi thấy mình đo nó ở đúng điều kiện bất lợi. Đổi lại nó giữ nguyên thứ tự: ORDER BY LIMIT nhanh 0,03ms so với 72ms của hash.
03/09/2026
· 9 phút đọc
20
ORDER BY ... LIMIT 10 không sắp cả triệu hàng như bạn tưởng — nó chỉ giữ 10, tốn 25kB
ORDER BY trên 1 triệu hàng: work_mem nhỏ thì external merge tràn đĩa 208ms, lớn thì quicksort RAM 162ms. Nhưng ORDER BY LIMIT 10 dùng top-N heapsort chỉ 25kB, 53ms — không sắp hết. Đừng đoán thuật toán từ câu SQL; EXPLAIN cho biết cơ sở dữ liệu thật sự sắp thế nào, và một index có thể bỏ luôn bước sắp.
03/09/2026
· 8 phút đọc
21
Cùng 2 triệu hàng, GROUP BY này tốn 24kB còn kia tràn 62MB ra đĩa — khác nhau ở đâu?
GROUP BY 100 nhóm trên 2 triệu hàng dùng HashAggregate chỉ 24kB; nhưng GROUP BY 2 triệu nhóm phân biệt làm nó tràn 62MB ra đĩa. Tôi suýt kết luận 'GROUP BY nhẹ RAM bất kể bảng lớn'. Bộ nhớ tính theo SỐ NHÓM phân biệt, không phải số hàng — và đó cũng là cái bẫy của COUNT(DISTINCT).
03/09/2026
· 8 phút đọc
22
Planner đoán WHERE v<10 khớp 333350 hàng, thực tế 10000 — không phải nó dở, nó đang bị bịt mắt
Bảng vừa nạp 1 triệu hàng chưa ANALYZE: planner ước lượng WHERE v<10 là 333350 hàng, thực tế 10000 — lệch 33 lần. Tôi suýt trách planner ngu. Chạy ANALYZE, ước lượng về 9872, khớp. Thống kê bảng là nguồn của mọi ước lượng, và một planner mù thống kê buộc phải đoán bằng hằng số mặc định.
03/09/2026
· 8 phút đọc
23
101 truy vấn, câu nào cũng dưới 0,2ms — vì sao đo cục bộ giấu mất thảm họa N+1 qua mạng
Lấy 100 bài rồi mỗi bài một truy vấn bình luận: 101 truy vấn, mỗi câu <0,2ms, đo cục bộ chỉ chậm 3 lần một JOIN nên tôi suýt xem thường. Nhưng qua mạng, 100 round-trip tuần tự làm N+1 chậm 10-100 lần. Số round-trip mới là đại lượng quyết định, và pg_stat_statements với calls=100 là dấu vết để nhận ra.
03/09/2026
· 8 phút đọc
24
Trang 1 nhanh 0,02ms, trang cuối treo 64ms: vì sao OFFSET giết phân trang ở trang sâu
Phân trang OFFSET 0 quét 20 hàng (0,02ms), nhưng OFFSET 500000 quét 500020 hàng (34ms) và trang cuối quét cả triệu hàng (64ms). Chi phí tăng tuyến tính theo độ sâu — và tôi suýt bỏ qua vì chỉ thử trang đầu. Keyset pagination nhảy thẳng qua index, nhanh đều 0,03ms mọi trang. Vì sao OFFSET đếm còn keyset tra cứu.
03/09/2026
· 8 phút đọc
25
int hay bigint hay text — đo ra cùng 35 MB, và vì sao kết luận đó gần như sai hoàn toàn
Đo dung lượng bằng bảng một cột: int, bigint, numeric, text đều ra 35 MB y hệt — tôi suýt kết luận kiểu chẳng ảnh hưởng. Header hàng 23 byte đã trùm mất khác biệt. Cho cột thành phần lớn của hàng thì bigint 89MB so int 57MB, numeric chậm 2,7 lần, và text so số sai ('10' < '9'). Chọn kiểu là chuyện dung lượng, tốc độ, và tính đúng.
03/09/2026
· 10 phút đọc
26
WHERE diem <> 7 nuốt mất 200.000 hàng: cái bẫy NULL mà mọi lập trình viên SQL đều vấp
Viết WHERE diem <> 7 để lấy mọi hàng không phải 7, đợi 990000, đo ra 790000 — 200000 hàng NULL biến mất lặng lẽ. NULL không phải giá trị, so sánh với nó ra 'unknown' và WHERE loại sạch. NOT IN với một NULL trả 0 hàng, AVG bỏ NULL, và hàng toàn NULL còn nhỏ hơn trên đĩa. Logic ba trạng thái của SQL, đo tận nơi.
03/09/2026
· 10 phút đọc
27
Kiểm email trùng ở tầng app là đủ? Hai request song song lọt cả hai, và chỉ CSDL chặn được
Tưởng kiểm trùng email ở tầng ứng dụng là đủ — cho hai request chạy song song thì cả hai lọt, hai dòng trùng vào bảng. Chỉ UNIQUE ở CSDL mới chặn thật dưới đồng thời. Đo: vi phạm ràng buộc bị từ chối ngay với lỗi rõ, và khóa ngoại không đánh index bên con khiến xóa cha chậm 80 lần.
03/09/2026
· 9 phút đọc
28
SELECT 1 chỉ 0,028ms, sao app chỉ chạy 1499 truy vấn/giây? Chi phí ẩn của mỗi kết nối
Đo truy vấn nhẹ ra 0,028ms rồi nhẩm 'app gọi vài chục nghìn lần một giây thoải mái'. Nhưng đó là khi tái dùng kết nối — mở kết nối mới mỗi lần chỉ còn 1499 tps, chậm 23 lần. Mỗi kết nối PostgreSQL là một tiến trình phải fork; chi phí mở đường lấn át chính truy vấn. Vì sao pool kết nối là lời giải, không phải tăng max_connections.
03/09/2026
· 9 phút đọc
29
Prepared statement tưởng luôn nhanh — hóa ra có lúc quét cả bảng, chậm gấp trăm lần
Tưởng prepared statement luôn nhanh hơn nhiều — đo query nhẹ chỉ nhanh 13%. Nguy hơn: sau 5 lần chạy PostgreSQL có thể đổi sang generic plan, và trên cột lệch nó quét cả bảng 13ms thay vì index scan 0,13ms — nhanh ở khâu parse nhưng chậm gấp trăm ở khâu thực thi. Vẫn phải luôn tham số hóa, vì lý do quan trọng hơn tốc độ.
03/09/2026
· 9 phút đọc
30
Một BEGIN quên commit làm bảng phình gấp 3 — và VACUUM chạy trơn tru mà bất lực
Chạy VACUUM tưởng đã dọn sạch hàng chết, nhưng một phiên khác đang mở giao dịch dài giữ snapshot cũ, khiến VACUUM chạy không báo lỗi mà bảng vẫn chồng thêm 200k hàng chết mỗi vòng, phình lên gấp 3. Thủ phạm là một BEGIN bị quên commit. Đo bằng hai kịch bản y hệt, chỉ khác một giao dịch treo song song.
03/09/2026
· 9 phút đọc
31
Bảng đứng yên 14 MB, nhưng index âm thầm phình gấp đôi — và VACUUM không bao giờ cứu
VACUUM bảng sau mỗi vòng, thấy bảng đứng yên 14 MB nên tưởng mọi thứ gọn sạch. Nhưng index kẹt ở 8792 kB, mật độ lá chỉ 45% — phình gấp đôi, và VACUUM không bao giờ sửa nổi. Chỉ REINDEX mới nén lại cây. Vì sao phải đo cả index chứ không chỉ bảng, và HOT update giảm phình ngay từ đầu.
03/09/2026
· 9 phút đọc
32
Cột text 5KB tưởng làm chậm cả bảng — hóa ra vô hình, trừ khi bạn SELECT *
Tôi tưởng một cột text 5KB làm mọi truy vấn trên bảng chậm. Đo: SUM(tag) chỉ 1,5ms vì giá trị lớn nằm ngoài bảng chính, ở TOAST; chỉ khi truy vấn chạm cột đó mới chậm (420ms, ~280 lần). Và nén quyết định tất cả: cùng 5KB×50k hàng, text lặp còn 6,8 MB, dữ liệu ngẫu nhiên phình thành 300 MB.
03/09/2026
· 9 phút đọc
33
Index nhỏ hơn 128 lần nhờ chỉ đánh 1% dữ liệu — và vì sao WHERE status=$1 vẫn seq scan
Trên cột lệch 99% 'done', index đầy đủ 30 MB đánh cả những hàng chẳng ai tra; partial index chỉ 'active' còn 240 kB — nhỏ hơn 128 lần, ghi rẻ hơn 2 lần. Nhưng tạo xong rồi WHERE status=$1 vẫn seq scan cả bảng, vì planner không chứng minh được tham số khớp điều kiện index. Cách để index thật sự được dùng.
03/09/2026
· 9 phút đọc
34
Có index trên email mà WHERE lower(email) vẫn quét cả triệu hàng — và cú chữa hụt
Có index trên cột email nhưng WHERE lower(email)='...' vẫn quét cả bảng 7352 trang — một hàm bọc cột làm index cột trần vô dụng. Tạo index thẳng trên lower(email) thì cùng truy vấn còn 4 trang, nhanh gần 1800 lần. Nhưng đổi sang upper(email) là seq scan lại ngay, vì planner khớp biểu thức theo cú pháp chứ không theo ý nghĩa.
03/09/2026
· 9 phút đọc
35
Phân vùng bảng: prune còn 1/12 số trang, DROP vùng cũ nhanh 34 lần — nhưng chỉ khi hỏi đúng khóa
Phân vùng theo thời gian: truy vấn lọc theo khóa phân vùng chỉ quét 1 vùng (541 trang) thay vì cả bảng (6487 trang), và DROP một vùng cũ mất 1,5ms so với DELETE 51ms. Nhưng truy vấn không chứa khóa phân vùng phải quét cả 12 vùng — chẳng lợi gì. Vì sao chọn đúng khóa phân vùng quyết định tất cả.
03/09/2026
· 9 phút đọc
36
Nạp 100.000 dòng: INSERT mất 8,5 giây, COPY chỉ 62ms — cùng dữ liệu, nhanh 137 lần
Nạp 100.000 dòng bằng INSERT từng dòng mất 8,5 giây, suýt kết luận 'ghi vào CSDL vốn chậm'. Nhưng COPY nạp đúng 100.000 dòng đó chỉ 62ms — nhanh 137 lần, cùng dữ liệu, cùng độ bền. Cái chậm không phải việc ghi mà là mỗi dòng một commit và mỗi câu một lần parse. Đo bốn cách nạp, và tiền thời gian thật đi đâu.
03/09/2026
· 8 phút đọc
37
synchronous_commit=off cho throughput gấp đôi — và tôi suýt quên nó đang bí mật vứt dữ liệu
Bật synchronous_commit=off thấy tps gấp đôi, suýt kết luận 'cứ tắt cho nhanh'. Nhưng đó là đánh đổi độ bền. Đo lại: 8 luồng sync=on nhờ group commit đạt 37689 tps — còn cao hơn sync=off một luồng, mà vẫn bền nguyên. Mỗi commit là một fsync, và fsync mới là nút cổ chai thật; cách gỡ nó không phải bỏ nó.
03/09/2026
· 9 phút đọc
38
Truy vấn chậm nhất không phải thủ phạm: cái tra index 0,0075ms lại ngốn gấp 16 lần
Theo phản xạ, tôi lao vào tối ưu truy vấn chậm nhất (23ms một lần). Nhưng nó là báo cáo chạy 2 lần — tổng chỉ 47ms. Trong khi một truy vấn tra index chỉ 0,0075ms mỗi lần, gọi 100000 lần, ngốn 754ms — gấp 16 lần. pg_stat_statements xếp theo TỔNG thời gian mới lộ thủ phạm thật: chi phí = mỗi lần × tần suất.
03/09/2026
· 9 phút đọc
39
5 tiến trình cùng quét bảng, nhưng chỉ nhanh 3,2 lần — vì sao song song không nhân đủ
Tưởng 2 worker (3 tiến trình) thì quét nhanh 3 lần, đo ra chỉ 2,6 lần; 4 worker chỉ 3,2 lần. Song song không nhân tuyến tính vì bước Gather, phần tuần tự và cái đĩa dùng chung (định luật Amdahl). Và xin 6 worker chỉ được 4 — planner cấp theo cỡ bảng; bảng nhỏ thì PostgreSQL chẳng thèm song song. Khi nào một index thắng xa việc thêm worker.
03/09/2026
· 8 phút đọc
40
'CTE luôn chậm hơn subquery' — lời khuyên đúng năm 2018, sai 720 lần trên PostgreSQL đời mới
Mang niềm tin cũ 'CTE là hàng rào tối ưu, luôn chậm hơn subquery', tôi viết CTE thường rồi ngồi đợi nó chậm. Đo ra nhanh y hệt subquery — 0,134ms, index scan — vì từ PG12 CTE được inline. Chỉ khi ép AS MATERIALIZED thì hàng rào cũ mới trở lại: 95ms, chậm 720 lần cho cùng logic. Vì sao một luật hiệu năng hết hạn theo phiên bản.
03/09/2026
· 8 phút đọc
41
'Đổi subquery thành JOIN cho nhanh' — tôi làm theo và truy vấn chậm đi 10 lần
Tin lời khuyên kinh điển 'subquery chậm, viết lại thành JOIN'. Đo ngược: IN và EXISTS ra kế hoạch y hệt (semi join), và semi join đó nhanh gấp 10 lần JOIN+DISTINCT của tôi — vì DISTINCT nối cả triệu cặp rồi mới khử trùng. Nhãn 'subquery hay join' gần như không quyết định tốc độ; cái quyết định là việc truy vấn thật sự cần làm, đọc được trong EXPLAIN.
03/09/2026
· 9 phút đọc
42
'read=12760' trong EXPLAIN không có nghĩa xuống đĩa: bí mật của tầng cache thứ hai
Thấy EXPLAIN ghi read=12760, tôi đinh ninh 12760 trang đó lôi từ đĩa lên nên hẳn phải chậm. Nhưng thời gian 17ms ổn định y hệt suốt ba lần quét — chúng do OS page cache phục vụ từ RAM, không phải đĩa. 'read' chỉ nghĩa là 'không có trong shared_buffers', không phải 'từ đĩa'. Đo tận mắt hai tầng cache, và vì sao shared_buffers to hết cỡ lại chậm hơn.
03/09/2026
· 9 phút đọc
43
COMMIT xong nhưng data chưa ra đĩa: 127MB còn nằm trong RAM, và cú checkpoint dồn tất cả xuống một lúc
INSERT rồi COMMIT xong, tôi đinh ninh dữ liệu đã nằm an toàn trong file trên đĩa. Đo pg_buffercache mới ngã ngửa: 16320 trang vẫn bẩn trong shared_buffers, chưa hề chạm file data. Cái làm COMMIT bền là WAL fsync; trang data xuống đĩa muộn hơn, cả lô ở checkpoint — một cú bùng I/O 79ms. Vì sao max_wal_size nhỏ làm CSDL giật, và cách làm phẳng.
03/09/2026
· 9 phút đọc
44
Tôi suýt khoe '200 nghìn giao dịch/giây' — rồi nhận ra con số đó nói dối
pgbench cho tôi 203189 tps read-only và tôi mừng rơn. Nhưng con số đó gần như vô nghĩa cho production: không ghi, đĩa VM fsync gần như miễn phí, 150MB nằm gọn trong RAM. Đổi bất kỳ điều kiện nào về phía thật là tps sụp mười mươi. Cách đo tải đúng với pgbench, và vì sao một con số thiếu điều kiện là một con số nói dối.
03/09/2026
· 8 phút đọc
45
Truy vấn quét 2 triệu hàng để tìm 40 — một index kéo nó nhanh 113 lần
Bài kết sê-ri Cơ sở dữ liệu: quy trình tối ưu đúng là đo trước, đọc kế hoạch thật, sửa một thứ, đo lại. Một vòng trọn vẹn: WHERE khach=X quét cả 2 triệu hàng (17 ms) chỉ để tìm 40; EXPLAIN chỉ thẳng, thêm index, đo lại 0,15 ms — nhanh 113 lần. Không đoán, không cãi — đo là xong.
03/09/2026
· 8 phút đọc