← PostgreSQL DBA← PostgreSQL DBA
PostgreSQL DBAPostgreSQL DBA19 Th7, 2026Jul 19, 202630 phút đọc23 min read

Lập kế hoạch tài nguyên & Cộng đồngResource Planning & Community

Thuộc bộ kiến thức PostgreSQL DBA Roadmap.

Tổng quan

Mọi chủ đề trong bộ kiến thức này cho đến giờ đều trả lời câu hỏi “làm thế nào”: làm thế nào để model dữ liệu, làm thế nào để index, làm thế nào để replicate, làm thế nào để backup. Chủ đề cuối cùng này trả lời hai câu hỏi khác: “bao nhiêu là đủ” và “còn ai khác đang làm việc này.” Câu hỏi đầu — resource và capacity planning — là thứ biến một hệ thống PostgreSQL đang chạy tốt thành một hệ thống tiếp tục chạy tốt khi dữ liệu và traffic tăng lên, mà không phải để DBA đoán mò lúc 2 giờ sáng tại sao shared_buffers hay max_connections lại được set như vậy. Câu hỏi thứ hai — cộng đồng — chính là lý do PostgreSQL đủ đáng tin cậy để người ta lập kế hoạch tài nguyên xoay quanh nó ngay từ đầu: một quy trình phát triển kéo dài hàng chục năm, xoay quanh mailing list, cởi mở một cách bất thường ngay cả so với chuẩn mực của các dự án mã nguồn mở khác, và vẫn là nguồn tri thức troubleshooting sâu nhất mà một DBA đang làm việc có thể tìm đến, lâu sau khi tài liệu chính thức đã cạn.

Sai lầm kinh điển mà chủ đề này tồn tại để sửa chữa là provisioning theo mặc định thay vì theo đo lường: lấy bất kỳ kích thước instance nào mà cloud console gợi ý, để max_connections ở một con số tròn “nghe có vẻ an toàn,” rồi chỉ phát hiện ra resource profile thực sự của workload trong lúc sự cố xảy ra. Capacity planning làm đúng cách đi theo chiều ngược lại — đo baseline, hiểu tại sao PostgreSQL dùng memory, connection, và disk I/O theo cách nó vẫn dùng, rồi mới size cấu hình theo workload đã đo được thay vì theo phỏng đoán. Kỷ luật đó là nội dung nửa đầu bài viết này. Nửa sau hướng ra ngoài: các mailing list, quy trình review patch, và những con đường — cả code lẫn không phải code — dẫn tới chuyên môn PostgreSQL sâu hơn. Bài viết kết thúc bằng một bảng tổng hợp nối mọi chủ đề trong bộ kiến thức này lại với một điều duy nhất DBA cần nhớ về nó, và một ghi chú ngắn về việc chiều sâu PostgreSQL dẫn tới đâu trong sự nghiệp.

Kiến thức nền tảng

Đo baseline trước khi lập kế hoạch

Capacity planning vô nghĩa nếu không có baseline, và baseline là một mô tả được đo, không phải giả định, về cách hệ thống hiện tại thực sự hoạt động dưới tải hiện tại. Trước khi quyết định database cần lớn hơn bao nhiêu, DBA cần có số liệu cho ít nhất các chiều sau, thu thập trong một khoảng thời gian đại diện (không phải một tối Chủ nhật vắng vẻ, cũng không chỉ giờ cao điểm nhất của Black Friday — lý tưởng là cả hai, để biết cả đỉnh lẫn đáy):

Chiều đoĐo cái gìLấy ở đâu
CPUUtilization trung bình và đỉnh, thời gian dành cho planner so với executorCông cụ OS (top, vmstat), pg_stat_statements để tìm pattern query tốn CPU
MemoryTỷ lệ hit của shared_buffers, kích thước OS page cache, mức dùng work_mem trên mỗi connection khi sort/hash đồng thờipg_stat_database (blks_hit / blks_read), công cụ OS free/vm_stat
Disk I/OIOPS đọc/ghi, độ trễ, độ sâu queue, tách riêng I/O của WAL và data filepg_stat_io (PostgreSQL 16+), công cụ OS (iostat), metric của cloud provider
ConnectionSố connection active vs. idle đồng thời, tốc độ churn connectionpg_stat_activity, SHOW POOLS của PgBouncer nếu đã có pooling
Tăng trưởng storageTốc độ tăng kích thước bảng/index, khối lượng WAL mỗi ngày, tỷ lệ bloatpg_stat_user_tables, pg_total_relation_size(), pgstattuple

Lý do điều này quan trọng với PostgreSQL hơn so với, chẳng hạn, một application server không lưu state, là vì các bên tiêu thụ tài nguyên của PostgreSQL tương tác với nhau theo những cách không hiển nhiên từ bất kỳ một metric riêng lẻ nào. Một cấu hình shared_buffers nhìn riêng lẻ có vẻ “hào phóng” có thể làm chết đói OS page cache mà PostgreSQL cũng phụ thuộc vào. Một giới hạn max_connections nhìn có vẻ bảo thủ vẫn có thể làm cạn RAM nếu work_mem được set cao và mỗi query đều thực hiện nhiều lần sort. Capacity planning cho PostgreSQL là lập kế hoạch cho sự tương tác giữa các cấu hình này, không phải cho riêng từng cái một — đó chính xác là lý do các phần tiếp theo sẽ đi qua từng chiều lớn một trước khi quay lại xem chúng kết hợp với nhau ra sao.

Sizing shared_buffers

shared_buffers là khối shared memory mà PostgreSQL dùng làm buffer cache của riêng nó — các trang dữ liệu bảng và index mà nó giữ thường trú trong bộ nhớ thay vì đọc lại từ đĩa mỗi lần truy cập. Cám dỗ, nhất là trên một server database chuyên dụng với 64 hay 128 GB RAM, là giao cho PostgreSQL càng nhiều bộ nhớ đó càng tốt, với lý thuyết rằng cache nhiều hơn chỉ có lợi. Trong thực tế điều này gần như ngược lại, và điểm khởi đầu được lặp lại rộng rãi là 25% RAM hệ thống cho shared_buffers tồn tại vì một lý do cụ thể: PostgreSQL dựa vào page cache của chính hệ điều hành như một lớp cache thứ hai bên dưới lớp cache của riêng nó, và sự phụ thuộc đó mang tính kiến trúc, không phải ngẫu nhiên.

Khi PostgreSQL đọc một trang không có trong shared_buffers, nó gọi system call read() bình thường. Nếu trang đó tình cờ đang nằm trong OS page cache, việc đọc gần như miễn phí — không có I/O đĩa thật sự, chỉ là copy bộ nhớ từ kernel space. Nếu shared_buffers được set quá lớn, hai vấn đề cộng dồn: thứ nhất, OS page cache co lại để nhường chỗ, nên miss trong shared_buffers nhiều khả năng là đọc đĩa thật thay vì hit rẻ tiền từ OS cache; thứ hai, PostgreSQL kết cục lưu nhiều trang hai lần — một lần trong shared_buffers, một lần trong OS cache giữ cùng trang đó vì lý do riêng của nó (ví dụ trong lúc checkpoint writeback) — đây là lãng phí RAM thuần túy, RAM đó lẽ ra có thể mở rộng kích thước cache hiệu dụng. Có những workload mà đẩy shared_buffers cao hơn 25% (tới 40% trên instance RAM rất lớn, đọc nhiều) đem lại lợi ích đo được, nhưng đó là một quyết định tuning được đưa ra sau khi đo tỷ lệ hit buffer và áp lực OS cache, không phải một mặc định để với tới. Provisioning theo mặc định của console — ví dụ một dịch vụ PostgreSQL managed âm thầm set shared_buffers bằng một tỷ lệ RAM instance mà không tính đến tỷ lệ đọc/ghi thực sự của workload — chính là sai lầm mà phần này tồn tại để ngăn chặn.

max_connections và tại sao tăng nó thường là bản năng sai

Mỗi connection PostgreSQL là một tiến trình OS đầy đủ (PostgreSQL dùng mô hình process-per-connection, không phải thread), và mỗi tiến trình mang overhead bộ nhớ thực sự, không nhỏ: bộ nhớ cục bộ của process cho trạng thái thực thi query, cộng với một phần áp lực bộ nhớ từ work_mem nếu connection đó chạy query nặng sort hoặc hash. Một cluster với max_connections = 100work_mem = 64MB có thể, trong trường hợp xấu nhất (mọi connection đều chạy query có nhiều node sort/hash), commit vài gigabyte bộ nhớ chỉ riêng cho work_mem, chưa kể shared_buffers và overhead per-process đã tiêu tốn từ trước. Tăng max_connections để “giải quyết” vấn đề cạn connection nhân bản trường hợp xấu nhất đó lên mà không giải quyết lý do connection bị cạn ngay từ đầu — thường là vì ứng dụng (hoặc ORM của nó, hoặc một đội application server mỗi cái đều giữ mở một connection pool riêng) đang giữ mở nhiều connection hơn hẳn số query đồng thời thực sự đang chạy.

Câu trả lời tốt hơn, gần như trong mọi trường hợp, là connection pooling — PgBouncer là lựa chọn chuẩn trong hệ sinh thái PostgreSQL (được trình bày sâu trong Replication & High Availability, vì pooling và HA routing thường được triển khai cùng nhau trong cùng một tầng quản lý connection). PgBouncer nằm giữa ứng dụng và PostgreSQL, giữ một pool nhỏ các connection backend thật và multiplex nhiều connection client hơn lên trên chúng. Ở chế độ pooling transaction, một client chỉ chiếm một connection backend thật trong suốt thời gian của một transaction, nên hàng trăm connection ứng dụng đang idle có thể được phục vụ bởi một pool chỉ vài chục backend PostgreSQL thật. Điều này giải quyết đúng vấn đề thực sự — quá nhiều connection idle đồng thời chiếm overhead process và bộ nhớ mà không làm việc gì — thay vì che đậy nó bằng cách khiến chính PostgreSQL sẵn sàng sinh thêm nhiều tiến trình backend đắt đỏ. max_connections vẫn cần được set thành một con số thực tế, có chủ đích (đủ cho pool backend của pooler, cộng connection quản trị, cộng dư địa), nhưng nó nên được coi là một trần cứng, size theo những gì phần cứng thực sự chịu được, chứ không phải một núm vặn để tăng vô hạn định mỗi khi ứng dụng chạm trần.

Provisioning disk I/O và dự phóng tăng trưởng storage

Năng lực disk I/O phải khớp với hình dạng của workload, không chỉ tổng khối lượng của nó. Một workload chi phối bởi ghi ngẫu nhiên nhỏ (nhiều UPDATE đồng thời vào các hàng khác nhau trên một bảng lớn) gây áp lực lên IOPS và cần storage xử lý tốt thông lượng ghi ngẫu nhiên cao ở độ trễ thấp; một workload chi phối bởi quét tuần tự lớn (query phân tích, COPY hàng loạt) lại gây áp lực lên thông lượng bền vững thay vì IOPS. WAL, đặc biệt, luôn là một luồng ghi tuần tự, chỉ append, và hiệu năng của nó phụ thuộc nặng vào độ trễ ghi — đây là lý do pg_wal được lợi khi đặt trên storage riêng, độ trễ thấp, tách khỏi data directory chính khi khối lượng đủ lớn để biện minh cho việc đó, và là lý do các tier disk cloud được tối ưu cho thông lượng tuần tự thường là tier sai cho một data directory OLTP bận rộn (vốn bị chi phối bởi I/O ngẫu nhiên từ index và trang heap) dù chúng trông hấp dẫn trên giấy.

Dự phóng tăng trưởng storage là chỗ một DBA thiếu kinh nghiệm hay đánh giá thấp nghiêm trọng, vì “dữ liệu sẽ lớn cỡ nào” không phải cùng câu hỏi với “storage cần lớn cỡ nào.” Ba loại overhead cộng dồn lên trên dữ liệu hàng thô:

Một dự phóng storage thực tế nhân tăng trưởng dữ liệu thô với một hệ số tính đến index và dư địa cho bloat cùng việc giữ WAL — thường là 1.5–3 lần dữ liệu hàng thô, tùy mật độ index và pattern update — thay vì giả định việc dùng đĩa bám theo số lượng hàng theo tỷ lệ 1:1.

Khái niệm chính

Cấu hình theo từng role và database: ALTER ROLE ... SET / ALTER DATABASE ... SET

Một cluster PostgreSQL duy nhất thường host các workload có nhu cầu tài nguyên thực sự khác nhau — một ứng dụng OLTP chạy hàng nghìn query ngắn mỗi giây, song song với một role reporting chạy một vài query phân tích dài, tốn bộ nhớ, trên cùng dữ liệu đó. Set một work_mem chung cho toàn server để phục vụ cả hai là một thỏa hiệp không phục vụ tốt bên nào: đủ thấp để giữ số connection của workload OLTP an toàn thì làm query reporting chết đói phải spill ra đĩa; đủ cao để cho query reporting đủ chỗ thì có nguy cơ số connection của workload OLTP nhân áp lực bộ nhớ thành sự kiện out-of-memory.

PostgreSQL giải quyết bằng cấu hình có phạm vi dưới mức server. ALTER ROLE ... SET gắn một cấu hình vào một role cụ thể, tự động áp dụng bất cứ khi nào role đó kết nối; ALTER DATABASE ... SET làm điều tương tự cho một database cụ thể, bất kể ai kết nối tới nó. Cả hai đều override giá trị postgresql.conf toàn server cho những session mà chúng áp dụng, và cả hai đều có thể xếp lớp lên nhau — một cấu hình theo database override mặc định server, và một cấu hình theo role override cả hai (thứ tự ưu tiên là kết hợp role-và-database trước, rồi role, rồi database, rồi mặc định server).

-- Cho role reporting nhiều chỗ hơn để sort/hash trong bộ nhớ, và cho phép
-- query của nó chạy lâu hơn mà không bị kill — không đụng vào mặc định server.
ALTER ROLE reporting_user SET work_mem = '256MB';
ALTER ROLE reporting_user SET statement_timeout = '30min';

-- Giữ role ứng dụng OLTP chặt chẽ, để một query chạy sổng không thể giữ
-- một connection (và bộ nhớ của nó) vô thời hạn.
ALTER ROLE app_user SET statement_timeout = '5s';

-- Ví dụ theo database: một database staging/reporting thỉnh thoảng chạy
-- batch job nặng có thể được cấp work_mem nhiều hơn database OLTP chính
-- trên cùng cluster.
ALTER DATABASE analytics_db SET work_mem = '512MB';

Các cấu hình này có hiệu lực từ lần kết nối tiếp theo của role/database đó (session hiện có giữ nguyên cấu hình cũ cho đến khi kết nối lại), và có thể kiểm tra bằng \drds trong psql hoặc query bảng pg_db_role_setting. Đây là cơ chế cho phép một cluster duy nhất phục vụ trung thực nhiều profile workload khác nhau thay vì ép mọi connection qua một cấu hình thỏa hiệp — cùng nguyên lý khiến các cấu hình pool theo từng database của PgBouncer hữu ích, chỉ khác là được áp dụng ở tầng cấu hình PostgreSQL thay vì tầng pooler.

Storage parameter: fillfactor và tuning autovacuum theo từng bảng

Bảng và index chấp nhận các storage parameter riêng, được set qua WITH (...) khi CREATE TABLE/CREATE INDEX hoặc thay đổi sau đó bằng ALTER TABLE ... SET (...), cho phép DBA override mặc định toàn cluster cho một đối tượng cụ thể có pattern truy cập không khớp với trường hợp chung.

fillfactor kiểm soát PostgreSQL nén dữ liệu chặt tới đâu khi ghi dữ liệu mới vào mỗi trang — mặc định 100 nén trang chặt hết mức có thể, không chừa chỗ trống. Với các bảng thường xuyên bị UPDATE, việc chủ động hạ fillfactor (xuống mức 90 hay thậm chí 70) dành sẵn không gian trống trong mỗi trang cụ thể để các hàng được update có chỗ để đi ngay trong cùng trang đó. Điều này quan trọng vì Heap-Only Tuple (HOT) update, tối ưu MVCC của PostgreSQL cho trường hợp phổ biến khi một UPDATE không thay đổi cột nào đang được index: nếu phiên bản hàng mới vừa vặn trong cùng trang với hàng cũ, PostgreSQL có thể tạo nó dưới dạng HOT update, hoàn toàn không cần entry index mới nào — mọi index vẫn trỏ vào cùng trang đó, và một lookup đi theo một chuỗi ngắn trong trang để tìm phiên bản sống hiện tại. Một HOT update bỏ qua chi phí cập nhật mọi index trên bảng, thường chiếm phần lớn chi phí của một UPDATE trên bảng có nhiều index. Không có chỗ trống trên trang (fillfactor = 100 và một trang gần đầy), hàng được update thường không vừa trên trang gốc của nó, buộc phải thực hiện update không-HOT phải chạm mọi index. Cơ chế đầy đủ của HOT và cấu trúc trang heap được trình bày trong Storage Internals & Vacuum; góc nhìn capacity-planning ở đây là việc chủ động chừa chỗ trống là một sự đánh đổi trực tiếp: một lượng nhỏ dung lượng đĩa dư để lấy update rẻ hơn đáng kể và ít bloat index hơn trên các bảng bị update dày đặc.

autovacuum_vacuum_scale_factor (và người anh em autovacuum_analyze_scale_factor) kiểm soát tỷ lệ hàng của một bảng phải thay đổi bao nhiêu trước khi autovacuum kích hoạt vacuum (hoặc analyze) trên bảng đó — mặc định toàn cluster 0.2 nghĩa là autovacuum chờ cho đến khi khoảng 20% hàng của bảng chết mới vacuum nó. Mặc định đó là thỏa hiệp hợp lý cho một bảng trung bình, nhưng nó gãy nghiêm trọng ở hai cực của kích thước bảng và pattern ghi:

Override theo từng bảng giải quyết cả hai:

-- Một bảng cực lớn: vacuum tích cực hơn nhiều theo tỷ lệ tương đối, vì
-- 20% của 500 triệu hàng là một lượng bloat tuyệt đối rất lớn để mang theo.
ALTER TABLE events SET (autovacuum_vacuum_scale_factor = 0.02);

-- Một bảng queue nhỏ, nóng: vacuum ở ngưỡng thấp hơn nhiều (hoặc dựa vào
-- autovacuum_vacuum_threshold cố định thấp cùng với scale factor thấp),
-- vì số lượng hàng của bảng này gần như không quan trọng — tốc độ xoay
-- vòng của nó mới quan trọng.
ALTER TABLE job_queue SET (
  autovacuum_vacuum_scale_factor = 0.01,
  autovacuum_vacuum_threshold = 50
);

Đây là cùng một ý tưởng nền tảng như ALTER ROLE/DATABASE ... SET, áp dụng cho hành vi storage và bảo trì thay vì hành vi session: mặc định toàn cluster là một điểm khởi đầu, không phải một mệnh lệnh, và các bảng (hay role, hay database) có hành vi thực tế khác biệt so với trung bình xứng đáng có cấu hình khớp với thực tế của chúng thay vì một thỏa hiệp một-kích-cỡ-cho-tất-cả.

Cộng đồng PostgreSQL và văn hóa mailing list

Quy trình phát triển của PostgreSQL có trước GitHub hơn một thập kỷ, và không giống nhiều dự án đã chuyển hoàn toàn sang workflow xoay quanh issue tracker và pull request khi những công cụ đó xuất hiện, phần lõi phát triển của PostgreSQL vẫn chủ yếu chạy qua mailing list, được điều phối tại postgresql.org. Đây không phải hoài niệm hay quán tính — mà là sự tiếp tục có chủ đích của một quy trình mà cộng đồng cho rằng hoạt động tốt: patch được nộp và thảo luận dưới dạng luồng email, bất đồng về thiết kế được giải quyết trong những cuộc trò chuyện dài, được lưu trữ, tìm kiếm được, và quyết định được đưa ra công khai bằng đồng thuận tương đối giữa các committer thay vì đằng sau nút merge của một maintainer duy nhất.

Có hai mailing list quan trọng nhất mà một DBA đang làm việc cần biết, ngay cả một người hoàn toàn không có ý định đóng góp code:

ListMục đíchTại sao DBA quan tâm
pgsql-generalCâu hỏi người dùng chung — cấu hình, troubleshooting, “tại sao query của tôi làm thế này”Kho lưu trữ sâu về các vấn đề production thực tế và cách core developer chẩn đoán chúng; thường tìm ra câu trả lời mà không blog post nào đề cập
pgsql-hackersThảo luận phát triển — patch, đề xuất thiết kế, kế hoạch releaseNơi lý do đằng sau một tính năng hay một mặc định tồn tại; hiểu tại sao wal_level hay synchronous_commit hành xử như vậy thường có nghĩa là đọc luồng thảo luận trên hackers đã đưa nó vào
pgsql-bugsBáo cáo lỗiXác nhận liệu một hành vi bất ngờ có phải là vấn đề đã biết hay không, và thường có workaround trước khi fix được release
pgsql-announceThông báo release và bảo mậtKênh lưu lượng thấp, tín hiệu cao để biết khi nào một bản vá bảo mật đòi hỏi phải cập nhật

Lý do điều này quan trọng với một “DBA thuần túy” sẽ không bao giờ nộp patch là vì kho lưu trữ mailing list, gộp lại, là nguồn troubleshooting sâu và đáng tin cậy nhất tồn tại cho PostgreSQL — sâu hơn cả tài liệu chính thức, vì tài liệu mô tả hành vi dự định, còn mailing list ghi lại những trường hợp biên lộn xộn, lịch sử “chúng tôi đổi cái này vì X,” và câu trả lời trực tiếp từ những người đã viết code đang được hỏi tới. Một DBA biết cách tìm kiếm trong kho lưu trữ (kho lưu trữ tìm kiếm được chính thức hay các bản mirror do cộng đồng vận hành) cho một thông báo lỗi hay tên GUC cụ thể thường xuyên tìm ra câu trả lời mà không tìm kiếm web tổng quát nào tìm ra được, vì cuộc trò chuyện đã diễn ra trên list cả chục năm trước và chưa bao giờ được tóm tắt ở đâu khác.

Best Practices

Đóng góp cho PostgreSQL — và tại sao các con đường không phải code cũng quan trọng

Chu kỳ phát triển của PostgreSQL chạy theo nhịp hàng năm, và một phần lớn công việc trong mỗi chu kỳ là review patch, không phải viết patch. Bất kỳ ai cũng có thể review một patch đã nộp — kiểm tra xem nó có apply sạch hay không, có làm đúng như nó tuyên bố hay không, có gây regression hay không, tài liệu có khớp với hành vi hay không — và các review được theo dõi công khai qua quy trình Commitfest của mỗi chu kỳ. Đây là một quá trình đáng chú ý là không bị gác cổng: review không đòi hỏi phải là committer, phải thuộc một công ty nào đó, hay phải có nhiều năm tham gia trước đó, và đóng góp đầu tiên cho PostgreSQL rất thường xuyên là một review, không phải một dòng code C. Với việc phần lớn chuyên môn của một DBA đang làm việc đã đến từ việc đọc SQL, config, và query plan của người khác một cách phê phán, review patch là một sự mở rộng tự nhiên của kỹ năng mà DBA đã có sẵn, chỉ áp dụng vào chính công cụ thay vì vào một ứng dụng xây dựng bên trên nó.

Ngoài review, các con đường đóng góp thực tế khác bao gồm:

Điểm chung xuyên suốt tất cả những điều này là nền tảng người đóng góp của PostgreSQL luôn thu hút mạnh mẽ từ những người bắt đầu như người dùng và DBA, không chỉ riêng từ các kỹ sư chuyên nghiệp về compiler và nội bộ database — con đường vào là thực sự mở, và review/tài liệu/testing không phải là những đóng góp an ủi mà là phần đa số theo nghĩa đen của những gì giữ mỗi chu kỳ release tiến lên.

Chủ đề nâng cao như một chân trời mở, không phải vạch đích

Chính roadmap PostgreSQL DBA của roadmap.sh kết thúc bằng một mục “advanced topics” gộp chung, rõ ràng — một sự thừa nhận có chủ đích rằng năng lực của một DBA đang làm việc, dù sâu đến đâu, vẫn có ranh giới, và bên ngoài ranh giới đó là việc học tiếp thực sự mở: đóng góp code C cho core PostgreSQL, làm việc sâu về nội bộ executor và planner, xây dựng extension tùy chỉnh bằng C, hay công việc mang tính nghiên cứu về các chủ đề như đồng thuận phân tán trong thiết lập multi-master. Cách đóng khung trung thực cho bài viết này (và bộ kiến thức này) kết thúc ở chủ đề 20 không phải là “làm chủ PostgreSQL, hoàn tất” — không có tập tài liệu cố định nào đạt tới đó — mà đúng hơn là những kiến thức nền tảng được trình bày qua các chủ đề 01–19 là những gì cho phép một DBA nhận ra, khi họ chạm phải một trong những vùng nâng cao này trong thực tế, rằng họ đã chạm tới một ranh giới thực sự đáng để nghiên cứu có chủ đích thay vì đoán mò.

Tổng hợp kết thúc: nhớ mỗi chủ đề vì điều gì

#Chủ đềTại sao quan trọng với một DBA đang làm việc
01Giới thiệu & Khái niệm RDBMSMô hình quan hệ và ACID là vốn từ vựng mà mọi thứ khác được xây dựng dựa trên — không thể lý luận về isolation level hay đảm bảo replication mà thiếu chúng
02Cài đặt, Thiết lập & Kết nốiCó một instance được cấu hình đúng, kết nối được qua mạng, xác thực được là điều kiện tiên quyết cho mọi chủ đề khác
03Kiểu dữ liệu & Đối tượng SchemaChọn đúng kiểu dữ liệu từ đầu tránh những cuộc migration tốn kém sau này và mở khóa index/constraint chuyên biệt theo kiểu
04Truy vấn dữ liệu — SQL cơ bảnNăng lực nền tảng để đọc và viết SQL đúng, hiệu quả trên bất kỳ schema nào
05Truy vấn nâng caoWindow function, CTE, và phép toán tập hợp biến logic ứng dụng nhiều query thành một câu lệnh duy nhất, có thể được planner tối ưu
06Sửa đổi & Nạp dữ liệu hàng loạtThao tác hàng loạt (COPY, INSERT theo batch) có đặc tính hiệu năng và locking hoàn toàn khác so với DML từng hàng một
07Procedure, Function & TriggerLogic phía server có thể ép buộc các bất biến mà tầng ứng dụng không thể được tin tưởng để ép buộc nhất quán
08Chiến lược IndexingĐòn bẩy hiệu năng có tác động lớn nhất mà một DBA kiểm soát, và cũng là thứ hay bị lạm dụng sai nhất (quá ít, quá nhiều, sai loại)
09Transaction & Concurrency ControlMVCC và isolation level giải thích gần như mọi sự cố “tại sao read của tôi thấy dữ liệu cũ/không nhất quán”
10Query Planning & Performance TuningĐọc output EXPLAIN là kỹ năng chẩn đoán cốt lõi để biến một query chậm thành nhanh
11Storage Internals & VacuumBloat, transaction ID wraparound, và HOT update vô hình cho đến khi chúng gây ra sự cố — hiểu nội bộ storage là thứ giúp DBA thấy trước chúng
12Partitioning & ShardingCơ chế giữ hiệu năng query và thao tác bảo trì khả thi một khi bảng vượt quá một bộ index duy nhất
13Replication & High AvailabilityBiến một điểm lỗi duy nhất thành một cluster tự chữa lành, và giảm tải traffic đọc khỏi primary
14Backup & RecoverySự bảo vệ thực sự duy nhất chống mất dữ liệu — replication bảo vệ chống downtime, backup bảo vệ chống sai lầm và hỏng dữ liệu
15Bảo mật & Access ControlRole, pg_hba.conf, row-level security, và mã hóa là những gì giữ một database đang hoạt động không trở thành một vụ rò rỉ dữ liệu chờ xảy ra
16Monitoring, Logging & Chẩn đoánKhông thể lập kế hoạch capacity, tuning, hay debug thứ mà bạn không đo — đây là tầng đo lường mà mọi thứ khác phụ thuộc vào
17Schema Design Pattern & Anti-patternĐúc kết những bài học đắt giá về hình dạng schema nào bền theo thời gian và hình dạng nào âm thầm trở nên khó bảo trì
18Extension & Hệ sinh tháiKhả năng mở rộng của PostgreSQL (PostGIS, pg_stat_statements, pgvector, và hàng chục cái khác) là một thế mạnh định hình — biết hệ sinh thái tránh việc phát minh lại thứ đã tồn tại sẵn
19Automation & Infrastructure as CodeBiến kiến thức DBA thành quy trình có thể lặp lại, quản lý phiên bản thay vì tri thức truyền miệng và runbook thủ công
20Lập kế hoạch tài nguyên & Cộng đồngKết nối capacity planning được đo lường với cộng đồng và nguồn tri thức giữ cho chuyên môn của DBA luôn cập nhật sau khi roadmap này kết thúc

Chiều sâu PostgreSQL dẫn tới đâu tiếp theo

Sự quen thuộc sâu, thực sự với nội bộ PostgreSQL — cấu trúc storage, WAL, replication, query planning, và kỷ luật vận hành của capacity planning được trình bày trong bài viết này — chồng lấp đáng kể với bộ kỹ năng được trình bày trong Data Engineer. Một DBA hiểu tại sao một query plan trông như vậy, cách replication và WAL thực sự di chuyển dữ liệu, và cần gì để giữ một cluster khỏe mạnh ở quy mô lớn thường chỉ cách một bước để trở nên hiệu quả trong việc xây dựng pipeline, warehouse, và công cụ platform mà các vai trò data engineering và platform engineering đòi hỏi. Chuyên môn PostgreSQL DBA không phải là ngõ cụt sự nghiệp — nó thường xuyên là nền tảng sâu nhất có thể để phát triển sang những vai trò liền kề đó, chính xác là vì phần lớn data engineering, bên dưới lớp công cụ, vẫn là về việc hiểu cách một engine quan hệ thực sự hành xử dưới tải.

Tài liệu tham khảo

Part of the PostgreSQL DBA Roadmap knowledge base.

Overview

Every topic in this knowledge base so far has answered a “how” question: how to model data, how to index it, how to replicate it, how to back it up. This final topic answers two different questions instead: “how much” and “who else is doing this.” The first — resource and capacity planning — is what turns a working PostgreSQL deployment into one that stays working as data and traffic grow, without the DBA having to guess at 2 a.m. why shared_buffers or max_connections was set the way it was. The second — the community — is the reason PostgreSQL got to be trustworthy enough to plan capacity around in the first place: a multi-decade, mailing-list-driven development process that is unusually open even by open-source standards, and that remains a working DBA’s deepest well of troubleshooting knowledge long after the official docs run out.

The classic mistake this topic exists to correct is provisioning by default rather than by measurement: taking whatever instance size a cloud console suggests, leaving max_connections at a round number that “feels safe,” and only discovering the actual resource profile of the workload during an incident. Capacity planning done properly runs the other direction — measure the baseline, understand why PostgreSQL uses memory, connections, and disk I/O the way it does, and then size configuration to the measured workload rather than to a guess. That discipline is what the first half of this note covers. The second half turns outward: the mailing lists, the patch review process, and the paths — code and non-code alike — into deeper PostgreSQL expertise. It closes with a recap table tying every topic in this knowledge base back to the one thing a working DBA needs to remember it for, and a brief note on where PostgreSQL depth leads next in a career.

Fundamentals

Baseline before you plan

Capacity planning is meaningless without a baseline, and a baseline is a measured, not assumed, description of how the current system actually behaves under its current load. Before deciding how much bigger a database needs to get, a DBA needs numbers for at least these dimensions, gathered over a representative period (not a quiet Sunday night, not the single busiest hour of Black Friday — ideally both, so peak and trough are both known):

DimensionWhat to measureWhere to get it
CPUAverage and peak utilization, time spent in planner vs. executorOS tools (top, vmstat), pg_stat_statements for per-query CPU-heavy patterns
Memoryshared_buffers hit ratio, OS page cache size, per-connection work_mem usage under concurrent sorts/hashespg_stat_database (blks_hit / blks_read), OS free/vm_stat
Disk I/ORead/write IOPS, latency, queue depth, split between WAL and data file I/Opg_stat_io (PostgreSQL 16+), OS tools (iostat), cloud provider metrics
ConnectionsConcurrent active vs. idle connections, connection churn ratepg_stat_activity, PgBouncer’s SHOW POOLS if pooling is already in place
Storage growthTable/index size growth rate, WAL volume per day, bloat ratiopg_stat_user_tables, pg_total_relation_size(), pgstattuple

The reason this matters more for PostgreSQL specifically than it might for a stateless application server is that PostgreSQL’s resource consumers interact with each other in ways that aren’t obvious from any single metric. A shared_buffers setting that looks generous in isolation can starve the OS page cache that PostgreSQL also depends on. A max_connections limit that looks conservative can still exhaust RAM if work_mem is set high and queries do multiple sorts each. Capacity planning for PostgreSQL is planning for the interaction between these settings, not any one of them alone — which is exactly why the next few sections work through the major dimensions one at a time before returning to how they compose.

Sizing shared_buffers

shared_buffers is the block of shared memory PostgreSQL uses as its own buffer cache — the pages of table and index data it keeps resident in memory rather than re-reading from disk on every access. The temptation, especially on a dedicated database server with 64 or 128 GB of RAM, is to hand PostgreSQL as much of that memory as possible on the theory that more cache can only help. In practice this is close to backwards, and the widely repeated starting point of 25% of system RAM for shared_buffers exists for a specific reason: PostgreSQL relies on the operating system’s own page cache as a second layer of caching underneath its own, and that reliance is architectural, not incidental.

When PostgreSQL reads a page that isn’t in shared_buffers, it issues a normal read() system call. If that page happens to be in the OS page cache, the read is nearly free — no actual disk I/O, just a memory copy from kernel space. If shared_buffers is set too large, two problems compound: first, the OS page cache shrinks to make room, so misses in shared_buffers are more likely to be genuine disk reads instead of cheap OS-cache hits; second, PostgreSQL ends up storing many pages twice — once in shared_buffers, once in the OS cache holding the same page for its own reasons (e.g., during checkpoint writeback) — which is pure waste of RAM that could otherwise extend the effective cache size. There are workloads where pushing shared_buffers higher than 25% (up to 40% on very large-RAM, read-heavy dedicated instances) measurably helps, but that’s a tuning decision made after measuring buffer hit ratios and OS cache pressure, not a default to reach for. Provisioning by console default — for example, a managed PostgreSQL offering that silently sets shared_buffers to some fraction of instance RAM without accounting for the actual workload’s read/write mix — is precisely the mistake this section exists to prevent.

max_connections and why raising it is usually the wrong instinct

Every PostgreSQL connection is a full OS process (PostgreSQL uses a process-per-connection model, not threads), and each one carries real, non-trivial memory overhead: process-local memory for query execution state, plus a share of memory pressure from work_mem if that connection runs sort- or hash-heavy queries. A cluster with max_connections = 100 and work_mem = 64MB can, in the worst case (every connection running a query with multiple sort/hash nodes), commit several gigabytes of memory to work_mem alone, on top of whatever shared_buffers and per-process overhead already consumes. Raising max_connections to “solve” a connection-exhaustion problem multiplies that worst case without addressing why connections were exhausted in the first place — usually because the application (or its ORM, or a fleet of application servers each holding open a connection pool) is holding far more connections open than it has concurrent queries actually in flight.

The better answer, in almost every case, is connection pooling — PgBouncer being the standard choice in the PostgreSQL ecosystem (covered in depth in Replication & High Availability, since pooling and HA routing are often deployed together in the same connection-management layer). PgBouncer sits between the application and PostgreSQL, holding a small pool of real backend connections and multiplexing many more client connections onto them. In transaction pooling mode, a client only occupies a real backend connection for the duration of a single transaction, so hundreds of idle application connections can be served by a pool of a few dozen real PostgreSQL backends. This solves the actual problem — too many concurrent idle connections eating process and memory overhead for no work being done — instead of papering over it by making PostgreSQL itself willing to spawn more expensive backend processes. max_connections still needs to be set to a real, deliberate number (enough for the pooler’s backend pool, plus administrative connections, plus headroom), but it should be treated as a hard ceiling sized to what the hardware can actually sustain, not a knob to raise indefinitely whenever an application hits it.

Disk I/O provisioning and storage growth

Disk I/O capacity has to match the shape of the workload, not just its total volume. A workload dominated by small, random writes (many concurrent UPDATEs against different rows across a large table) stresses IOPS and needs storage that handles high random-write throughput at low latency; a workload dominated by large sequential scans (analytical queries, bulk COPY) stresses sustained throughput instead. WAL, in particular, is always a sequential, append-only write stream, and its performance depends heavily on write latency — this is why pg_wal benefits from being on separate, low-latency storage from the main data directory when volume justifies it, and why cloud disk tiers optimized for sequential throughput are usually the wrong tier for a busy OLTP data directory (which is dominated by random I/O from indexes and heap pages) even though they look attractive on paper.

Storage growth projection is where a naive DBA underestimates badly, because “how big will the data get” is not the same question as “how big will the storage need to be.” Three overheads compound on top of raw row data:

A realistic storage projection multiplies raw data growth by a factor accounting for indexes and headroom for bloat and WAL retention — commonly 1.5–3x raw row data, depending on index density and update patterns — rather than assuming disk usage tracks row count 1:1.

Key Concepts

Per-role and per-database configuration: ALTER ROLE ... SET / ALTER DATABASE ... SET

A single PostgreSQL cluster often hosts workloads with genuinely different resource needs — an OLTP application that runs thousands of short queries a second alongside a reporting role that runs a handful of long, memory-hungry analytical queries against the same data. Setting a single server-wide work_mem to serve both is a compromise that serves neither well: low enough to keep the OLTP workload’s connection count safe, it starves the reporting queries into spilling to disk; high enough to give reporting queries room, it risks the OLTP workload’s connection count multiplying memory pressure into an out-of-memory event.

PostgreSQL solves this with configuration scoped below the server level. ALTER ROLE ... SET attaches a setting to a specific role, applied automatically whenever that role connects; ALTER DATABASE ... SET does the same for a specific database, regardless of who connects to it. Both override the server-wide postgresql.conf value for sessions they apply to, and both can be layered — a per-database setting overrides the server default, and a per-role setting overrides both (the precedence is role-and-database combination first, then role, then database, then server default).

-- Give the reporting role more room to sort/hash in memory, and let its
-- queries run longer without being killed — without touching the server default.
ALTER ROLE reporting_user SET work_mem = '256MB';
ALTER ROLE reporting_user SET statement_timeout = '30min';

-- Keep the OLTP application role tight, so a runaway query can't hold a
-- connection (and its memory) indefinitely.
ALTER ROLE app_user SET statement_timeout = '5s';

-- A per-database example: a staging/reporting database that runs occasional
-- heavy batch jobs can get more work_mem than the primary OLTP database on
-- the same cluster.
ALTER DATABASE analytics_db SET work_mem = '512MB';

These take effect on the next connection for that role/database (existing sessions keep their settings until they reconnect), and they can be inspected with \drds in psql or by querying pg_db_role_setting. This is the mechanism that lets a single cluster serve several distinct workload profiles honestly instead of forcing every connection through one compromise configuration — the same principle that makes PgBouncer’s per-database pool settings useful, applied at the PostgreSQL configuration layer instead of the pooler layer.

Storage parameters: fillfactor and per-table autovacuum tuning

Tables and indexes accept their own storage parameters, set via WITH (...) on CREATE TABLE/CREATE INDEX or changed later with ALTER TABLE ... SET (...), which let a DBA override cluster-wide defaults for a specific object whose access pattern doesn’t match the general case.

fillfactor controls how full PostgreSQL packs each page when writing new data — the default of 100 packs pages as tightly as possible, leaving no free space. For tables that see frequent UPDATEs, deliberately lowering fillfactor (to something like 90 or even 70) reserves free space within each page specifically so that updated rows have somewhere to go within the same page. This matters because of Heap-Only Tuple (HOT) updates, PostgreSQL’s MVCC optimization for the common case where an UPDATE doesn’t change any indexed column: if the new row version fits on the same page as the old one, PostgreSQL can create it as a HOT update, which needs no new index entries at all — every index still points at the same page, and a lookup follows a short in-page chain to find the current live version. A HOT update skips the cost of updating every index on the table, which is often the majority of the cost of an UPDATE on a heavily indexed table. Without free space on the page (fillfactor = 100 and a nearly-full page), an updated row often can’t fit on its original page, forcing a non-HOT update that touches every index. Full mechanics of HOT and heap page layout are covered in Storage Internals & Vacuum; the capacity-planning angle here is that leaving intentional free space is a direct trade of a small amount of extra disk space for meaningfully cheaper updates and less index bloat on update-heavy tables.

autovacuum_vacuum_scale_factor (and its sibling autovacuum_analyze_scale_factor) control how large a fraction of a table’s rows must change before autovacuum triggers a vacuum (or analyze) on it — the cluster-wide default of 0.2 means autovacuum waits until roughly 20% of a table’s rows are dead before vacuuming it. That default is a reasonable compromise for an average table, but it breaks down badly at the extremes of table size and write pattern:

Per-table overrides address both:

-- A huge table: vacuum much more aggressively in relative terms, since 20%
-- of 500 million rows is a lot of absolute bloat to carry around.
ALTER TABLE events SET (autovacuum_vacuum_scale_factor = 0.02);

-- A small, hot queue table: vacuum on a much lower threshold (or lean on
-- a low fixed autovacuum_vacuum_threshold alongside a low scale factor),
-- since this table's row count barely matters — its churn rate does.
ALTER TABLE job_queue SET (
  autovacuum_vacuum_scale_factor = 0.01,
  autovacuum_vacuum_threshold = 50
);

This is the same underlying idea as ALTER ROLE/DATABASE ... SET, applied to storage and maintenance behavior instead of session behavior: a cluster-wide default is a starting point, not a mandate, and tables (or roles, or databases) whose actual behavior diverges from the average deserve configuration that matches their reality rather than a one-size-fits-all compromise.

The PostgreSQL community and its mailing-list culture

PostgreSQL’s development process predates GitHub by well over a decade, and unlike many projects that migrated wholesale to issue-tracker- and pull-request-centric workflows once those tools existed, PostgreSQL’s core development still runs primarily through mailing lists, coordinated at postgresql.org. This isn’t nostalgia or inertia — it’s a deliberate continuation of a process the community considers to work well: patches are submitted and discussed as email threads, design disagreements are hashed out in long, archived, searchable conversations, and decisions are made in the open by rough consensus among committers rather than behind a single maintainer’s merge button.

Two lists matter most for a working DBA to know about, even one with zero interest in contributing code:

ListPurposeWhy a DBA cares
pgsql-generalGeneral user questions — configuration, troubleshooting, “why does my query do this”Deep archive of real production problems and how core developers diagnosed them; often turns up answers no blog post covers
pgsql-hackersDevelopment discussion — patches, design proposals, release planningWhere the reasoning behind a feature or default lives; understanding why wal_level or synchronous_commit behaves the way it does often means reading the hackers thread that introduced it
pgsql-bugsBug reportsConfirms whether a surprising behavior is a known issue, and often has a workaround before a fix ships
pgsql-announceRelease and security announcementsLow-traffic, high-signal channel for knowing when a security patch demands an update

The reason this matters for a “pure DBA” who will never submit a patch is that the mailing list archives are, cumulatively, the deepest and most authoritative troubleshooting resource that exists for PostgreSQL — deeper than the official documentation, because the documentation describes intended behavior while the mailing lists capture the messy edge cases, the “we changed this because of X” history, and direct answers from the people who wrote the code being asked about. A DBA who knows how to search the archives (the official searchable archives or the community-run postgresql.org mailing list archives mirrors) for a specific error message or GUC name routinely finds answers that no amount of general web searching turns up, because the conversation happened on-list a decade ago and never got summarized anywhere else.

Best Practices

Contributing to PostgreSQL — and why non-code paths count

PostgreSQL’s development cycle runs on an annual cadence, and a large share of the work in every cycle is patch review, not patch writing. Anyone can review a submitted patch — checking whether it applies cleanly, whether it does what it claims, whether it introduces regressions, whether the documentation matches the behavior — and reviews are tracked publicly through each cycle’s Commitfest process. This is notably non-gatekept: reviewing does not require being a committer, a company affiliation, or years of prior involvement, and a first contribution to PostgreSQL is very often a review, not a line of C code. Given how much of a working DBA’s expertise already comes from reading other people’s SQL, configs, and query plans critically, patch review is a natural extension of skills a DBA already has, applied to the tool itself instead of to an application built on top of it.

Beyond review, realistic paths into contribution include:

The through-line across all of these is that PostgreSQL’s contributor base has always drawn heavily from people who started as users and DBAs, not exclusively from professional compiler-and-database-internals engineers — the on-ramp is genuinely open, and review/documentation/testing are not consolation-prize contributions but the literal majority of what keeps each release cycle moving.

Advanced topics as an open horizon, not a finish line

roadmap.sh’s own PostgreSQL DBA roadmap ends with an explicit “advanced topics” catch-all — a deliberate acknowledgment that a working DBA’s competence, however deep, has a boundary, and beyond that boundary lies genuinely open-ended further learning: contributing C code to PostgreSQL core, deep executor and planner internals work, building custom extensions in C, or research-level work on topics like distributed consensus in multi-master setups. The honest framing for this note (and this knowledge base) closing at topic 20 is not “PostgreSQL mastery, complete” — no fixed set of documents gets there — but rather that the fundamentals covered across topics 01–19 are what let a DBA recognize, when they hit one of these advanced areas in practice, that they’ve hit a real edge worth researching deliberately rather than guessing at.

Closing synthesis: what to remember each topic for

#TopicWhy it matters for a working DBA
01Introduction & RDBMS ConceptsThe relational model and ACID are the vocabulary everything else is built on — you can’t reason about isolation levels or replication guarantees without them
02Installation, Setup & ConnectingGetting a correctly configured, network-reachable, authenticated instance running is the prerequisite for every other topic
03Data Types & Schema ObjectsChoosing the right type up front avoids expensive migrations later and unlocks type-specific indexing and constraints
04Querying Data — SQL FundamentalsThe baseline competency for reading and writing correct, efficient SQL against any schema
05Advanced QueryingWindow functions, CTEs, and set operations turn multi-query application logic into single, plannable statements
06Modifying & Bulk-Loading DataBulk operations (COPY, batched INSERT) have entirely different performance and locking characteristics than row-at-a-time DML
07Procedures, Functions & TriggersServer-side logic can enforce invariants the application layer can’t be trusted to enforce consistently
08Indexing StrategiesThe single highest-leverage performance lever a DBA controls, and the one most often misused (too few, too many, wrong type)
09Transactions & Concurrency ControlMVCC and isolation levels explain almost every “why did my read see stale/inconsistent data” incident
10Query Planning & Performance TuningReading EXPLAIN output is the core diagnostic skill for turning a slow query into a fast one
11Storage Internals & VacuumBloat, transaction ID wraparound, and HOT updates are invisible until they cause an outage — understanding storage internals is what lets a DBA see them coming
12Partitioning & ShardingThe mechanism for keeping query performance and maintenance operations tractable once a table outgrows a single set of indexes
13Replication & High AvailabilityTurns a single point of failure into a self-healing cluster, and offloads read traffic from the primary
14Backup & RecoveryThe only real protection against data loss — replication protects against downtime, backups protect against mistakes and corruption
15Security & Access ControlRoles, pg_hba.conf, row-level security, and encryption are what keep a working database from being a data breach waiting to happen
16Monitoring, Logging & DiagnosticsYou cannot capacity-plan, tune, or debug what you don’t measure — this is the instrumentation layer everything else depends on
17Schema Design Patterns & Anti-PatternsEncodes hard-won lessons about what schema shapes age well and which ones quietly become unmaintainable
18Extensions & EcosystemPostgreSQL’s extensibility (PostGIS, pg_stat_statements, pgvector, and dozens more) is a defining strength — knowing the ecosystem avoids reinventing what already exists
19Automation & Infrastructure as CodeTurns DBA knowledge into repeatable, version-controlled process instead of tribal knowledge and manual runbooks
20Resource Planning & CommunityTies measured capacity planning to the community and knowledge sources that keep a DBA’s expertise current after this roadmap ends

Where PostgreSQL depth leads next

Deep, genuine familiarity with PostgreSQL internals — storage layout, WAL, replication, query planning, and the operational discipline of capacity planning covered in this note — overlaps substantially with the skill set laid out in Data Engineer. A DBA who understands why a query plan looks the way it does, how replication and WAL actually move data, and what it takes to keep a cluster healthy at scale is very often one step away from being effective at building the pipelines, warehouses, and platform tooling that data engineering and platform engineering roles require. PostgreSQL DBA expertise is not a career cul-de-sac — it’s frequently the deepest possible foundation for growing into those adjacent roles, precisely because so much of data engineering is, underneath the tooling, still about understanding how a relational engine actually behaves under load.

References