Bài shared_buffers nói về vùng nhớ PostgreSQL thực sự cấp phát để cache trang. effective_cache_size nghe rất giống — nhưng nó là một con thú hoàn toàn khác, và hiểu nhầm nó là chuyện rất phổ biến. Nó không cấp phát một byte bộ nhớ nào. Nó chỉ là một con số ước lượng mà bạn khai để planner đoán xem: khi một truy vấn đọc đi đọc lại các trang index, những lần đọc đó là cache hit (rẻ) hay đọc đĩa (đắt)? Và đo thật cho thấy con số này đủ sức làm một truy vấn nhanh gấp 26 lần — hoặc chậm gấp 26 lần.

effective_cache_size là một gợi ý, không phải vùng nhớ

Đây là điểm dễ nhầm nhất. shared_buffers và work_mem cấp phát RAM thật. effective_cache_size thì không — nó chỉ nói với planner một điều: "cả hệ thống có khoảng ngần này cache dùng được cho dữ liệu" (gồm shared_buffers cộng page cache của hệ điều hành).

SHOW effective_cache_size;   -- mặc định 4GB
-- KHÔNG cấp phát 4GB. Chỉ nói với planner:
-- "hệ thống có ~4GB cache (shared_buffers + OS page cache)"

Planner dùng con số này trong công thức Mackert-Lohman để ước lượng: với một Index Scan chạy lặp lại nhiều lần (ví dụ vế trong của Nested Loop), có bao nhiêu phần trăm lần đọc trang là cache hit. effective_cache_size càng lớn, planner càng tin các trang index nằm sẵn trong cache, nên nó đánh giá Index Scan lặp lại là rẻ và dám chọn Nested Loop. effective_cache_size nhỏ thì ngược lại — planner sợ mỗi lần tra index là một cú đọc đĩa, nên né Nested Loop và thà quét tuần tự cả bảng một lần.

Ảnh chụp đoạn mã SQL nền tối minh hoạ effective_cache_size gợi ý cache cho planner không cấp bộ nhớ, SHOW effective_cache_size mặc định 4GB không cấp phát 4GB chỉ nói với planner hệ thống có khoảng 4GB cache shared_buffers cộng OS page cache planner dùng con số này đoán lần đọc lại trang index là cache hit rẻ hay đọc đĩa đắt, ảnh hưởng chi phí Index Scan lặp lại công thức Mackert-Lohman SELECT count b.payload FROM ecs_dim d JOIN ecs_big b ON b.dim_id bằng d.id WHERE d.id nhỏ hơn 1300 khoảng 1300 lần tra index vào bảng 5 triệu dòng, ECS thấp planner nghĩ cache nhỏ đọc lại index bằng đọc đĩa Nested Loop đắt chọn Hash Join cộng Seq Scan quét cả bảng, ECS cao planner nghĩ trang index nằm sẵn trong cache Index Scan lặp lại rẻ chọn Nested Loop, đặt sai quá thấp làm planner chọn nhầm SET effective_cache_size 4MB Hash Join Seq Scan 5 triệu dòng 408 ms SET effective_cache_size 32GB Nested Loop Index Scan 15,8 ms nhanh hơn khoảng 26 lần cùng dữ liệu cùng truy vấn chỉ đổi một gợi ý, đặt bao nhiêu ALTER SYSTEM SET effective_cache_size 12GB khoảng 50-75 phần trăm RAM vì cộng gộp shared_buffers và OS page cache không cấp phát nên đặt rộng tay an toàn không tốn RAM thật mặc định 4GB thường quá thấp cho server hiện đại planner ngại Index Scan vô cớ

Hình 1: effective_cache_size là gợi ý cho planner (gồm shared_buffers + OS page cache), không cấp phát RAM. Nó điều chỉnh chi phí ước lượng của Index Scan lặp lại, từ đó ảnh hưởng lựa chọn Nested Loop hay Hash Join.

Đo thật: cùng truy vấn, 408 ms hay 15,8 ms

Để thấy tác động, tôi dựng bảng ecs_big 5 triệu dòng (mỗi giá trị dim_id có ~25 dòng, có index trên dim_id) và ecs_dim 200.000 dòng. Truy vấn JOIN một dải nhỏ (~1.300 dòng của ecs_dim) vào bảng lớn — đúng cái ngưỡng mà planner phân vân giữa hai kế hoạch:

SELECT count(b.payload)
FROM ecs_dim d JOIN ecs_big b ON b.dim_id = d.id
WHERE d.id < 1300;

Chỉ đổi một tham số effective_cache_size (không đụng gì khác), planner chọn hai kế hoạch khác hẳn nhau:

Ảnh chụp bảng kết quả EXPLAIN ANALYZE nền tối effective_cache_size đổi kế hoạch 408 ms so với 15,8 ms cùng truy vấn JOIN 1300 dòng vào bảng 5 triệu dòng PostgreSQL 16, effective_cache_size 4MB planner chọn Hash Join Aggregate actual time 399.8 Hash Join time 3.3 đến 398.9 rows 32475 Hash Cond b.dim_id bằng d.id Seq Scan on ecs_big b rows 5000000 quét cả 5 triệu dòng Buffers shared hit 2474 read 44310 Hash Index Only Scan on ecs_dim d rows 1299 Execution Time 408.190 ms planner tưởng cache tí hon sợ Index Scan lặp lại tốn đĩa thà quét tuần tự cả bảng một lần, effective_cache_size 32GB planner chọn Nested Loop Aggregate actual time 9.2 Nested Loop time 1.9 đến 8.4 rows 32475 Index Only Scan on ecs_dim d rows 1299 Index Scan using idx_big_dim on ecs_big b loops 1299 actual time 0.000 đến 0.004 rows 25 Buffers shared hit 36248 read 124 gần như toàn cache hit Execution Time 15.816 ms nhanh hơn khoảng 26 lần planner biết trang index nằm sẵn trong cache 1299 lần tra đều rẻ, bảng 4MB cache nhỏ Hash Join cộng Seq Scan 5M dòng 408 ms 32GB nhiều RAM Nested Loop cộng Index Scan 1299 loops 15,8 ms cùng dữ liệu cùng truy vấn chỉ đổi một gợi ý 0 byte cấp phát khoảng 26 lần, điểm cốt lõi effective_cache_size không cấp phát bộ nhớ khác work_mem shared_buffers chỉ điều chỉnh chi phí ước lượng Index Scan lặp lại mặc định 4GB thường quá thấp đặt 50-75 phần trăm RAM an toàn boot_val 524288 nhân 8kB bằng 4GB

Hình 2: effective_cache_size=4MB → planner chọn Hash Join + Seq Scan cả 5 triệu dòng, 408 ms. effective_cache_size=32GB → Nested Loop + Index Scan (1.299 loops, gần như toàn cache hit), 15,8 ms — nhanh gấp ~26 lần. Cùng dữ liệu, cùng truy vấn.

Đọc kỹ hai kế hoạch:

  • effective_cache_size = 4MB: planner tin cache tí hon nên sợ 1.299 lần tra index (mỗi lần có thể là một cú đọc đĩa). Nó kết luận thà quét tuần tự cả bảng 5 triệu dòng một lần (Seq Scan, read=44310 trang) rồi Hash Join — 408 ms.
  • effective_cache_size = 32GB: planner tin trang index nằm sẵn trong cache nên 1.299 lần tra đều rẻ. Nó chọn Nested Loop + Index Scan (loops=1299, mỗi lần chỉ đụng 25 dòng, Buffers hit=36248 gần như toàn cache hit) — 15,8 ms.

Kế hoạch Nested Loop là đúng ở đây: đọc ~32.000 dòng qua index rẻ hơn nhiều so với quét cả 5 triệu dòng. Nhưng planner chỉ dám chọn nó khi tin rằng cache đủ lớn. Con số effective_cache_size chính là thứ nói cho nó biết niềm tin đó có cơ sở hay không — và nó không tốn một byte RAM nào để "bật" niềm tin đó lên.

Đặt bao nhiêu

Vì effective_cache_size không cấp phát bộ nhớ, bạn có thể (và nên) đặt nó rộng tay. Khuyến nghị phổ biến là 50-75% RAM của máy — vì nó phản ánh tổng cache khả dụng: shared_buffers của PostgreSQL cộng với page cache của hệ điều hành (thứ này thường lớn hơn shared_buffers nhiều).

ALTER SYSTEM SET effective_cache_size = '12GB';  -- ~50-75% RAM máy 16-24GB
SELECT pg_reload_conf();

Giá trị mặc định là 4GB (boot_val = 524288 × 8kB). Với server hiện đại 32GB, 64GB RAM, con số 4GB này quá thấp — nó khiến planner ngại chọn Index Scan một cách vô cớ, đúng như ca 4MB phóng đại ở trên. Nâng nó lên là một trong những chỉnh sửa rẻ nhất và an toàn nhất: không tốn RAM, chỉ giúp planner ra quyết định sát thực tế hơn.

Đánh đổi cần cân nhắc

Đây là con dao hai lưỡi — đặt quá cao cũng sai. Nếu bạn khai effective_cache_size lớn hơn cache thật sự có, planner sẽ quá lạc quan về cache hit và chọn Nested Loop + Index Scan cả khi dữ liệu thật ra không nằm trong cache — lúc đó mỗi lần tra index là một cú đọc đĩa thật và truy vấn chậm. Con số nên phản ánh cache thật, đừng thổi phồng.

Nó không thay thế việc chạy ANALYZE. effective_cache_size chỉ điều chỉnh mô hình chi phí; planner vẫn cần thống kê đúng (số dòng ước lượng, độ chọn lọc) từ ANALYZE. Đặt effective_cache_size chuẩn mà thống kê cũ thì planner vẫn chọn sai.

Tác động chỉ rõ ở truy vấn có Index Scan lặp lại. Nếu tải của bạn toàn full-table scan hoặc truy vấn điểm đơn giản, đổi effective_cache_size gần như không thấy khác biệt. Nó phát huy đúng ở các JOIN kiểu Nested Loop trên bảng lớn — nơi lựa chọn "index nhiều lần" so với "quét một lần" là ranh giới sát nhau.

Ba ý mang về

  1. effective_cache_size là gợi ý cho planner, KHÔNG cấp phát bộ nhớ: khác hẳn shared_buffers/work_mem, nó chỉ là con số ước lượng tổng cache (shared_buffers + OS page cache) để planner đoán chi phí Index Scan lặp lại qua công thức Mackert-Lohman.
  2. Nó đổi hẳn kế hoạch: đo thật, cùng một truy vấn JOIN vào bảng 5 triệu dòng — đặt 4MB planner quét tuần tự cả bảng mất 408 ms, đặt 32GB planner chọn Nested Loop + Index Scan còn 15,8 ms, nhanh gấp ~26 lần, mà không tốn thêm một byte RAM.
  3. Đặt ~50-75% RAM, an toàn vì không tốn RAM thật: mặc định 4GB thường quá thấp cho server hiện đại khiến planner ngại Index Scan vô cớ — nhưng đừng thổi phồng quá cache thật, và nhớ nó không thay thế ANALYZE.

Phần sau ta xét một tham số cost gắn liền với phần cứng lưu trữ: Phần sau mổ xẻ random_page_cost — vì sao mặc định 4.0 hợp với ổ cứng cơ mà quá cao với SSD, và hạ nó xuống đổi lựa chọn index thế nào.