PostgreSQL 1 — Tổng quan & kiến trúc tiến trình

13 thg 7, 2026 3 lượt xem
#postgresql
#sql
#kien-truc
#mvcc
#wal

PostgreSQL 1 — Tổng quan & kiến trúc tiến trình

Trước khi tối ưu một truy vấn, thiết kế một schema hay debug một sự cố "database chậm", có một câu hỏi cần trả lời trước: bên trong PostgreSQL thực sự có gì đang chạy? Rất nhiều hiểu lầm tai hại — "vì sao mở nhiều kết nối lại làm sập server", "vì sao dữ liệu vẫn an toàn khi mất điện", "vì sao DELETE không làm nhỏ file", "vì sao hai giao dịch đọc cùng bảng mà không chặn nhau" — đều bắt nguồn từ việc không hiểu kiến trúc.

Bài này là nền móng của cả series. Nó không dạy bạn viết SQL nhanh hơn ngay, mà dựng một mô hình tinh thần đúng về cách PostgreSQL tổ chức tiến trình, bộ nhớ và lưu trữ. Có mô hình đó rồi, các bài sau — kiểu dữ liệu, JSONB, MVCC, EXPLAIN, VACUUM — sẽ ăn khớp thay vì rời rạc.

PostgreSQL là gì

PostgreSQL là một hệ quản trị cơ sở dữ liệu quan hệ (RDBMS) mã nguồn mở, phát hành dưới giấy phép PostgreSQL License (kiểu BSD/MIT — rất thoáng, dùng thương mại thoải mái). Ba tính từ mô tả đúng bản chất của nó:

  • Chuẩn SQL cao (standards-compliant): PostgreSQL bám sát chuẩn SQL nghiêm túc bậc nhất trong các RDBMS phổ biến. Điều này quan trọng vì kiến thức bạn học ít bị "khoá" vào một phương ngữ riêng.
  • Đáng tin cậy (reliable): ACID đầy đủ, ghi trước log (WAL), phục hồi sau sự cố. Đây là database bạn giao trọng trách lưu tiền, đơn hàng, hồ sơ.
  • Mở rộng được (extensible): đây mới là điểm khiến nó khác biệt. Bạn có thể thêm kiểu dữ liệu, toán tử, hàm, chỉ mục, thậm chí ngôn ngữ thủ tục mà không sửa lõi, thông qua cơ chế extension (CREATE EXTENSION). PostGIS (dữ liệu địa lý), pg_trgm (tìm gần đúng), pgvector (tìm vector cho AI)... đều là extension.

Lịch sử ngắn và triết lý "object-relational"

Gốc rễ là dự án POSTGRES ở UC Berkeley (giữa thập niên 1980, do Michael Stonebraker dẫn dắt) — cái tên nghĩa là "post-Ingres", đời sau của database Ingres. Khi dự án thêm bộ thông dịch SQL vào đầu thập niên 1990, nó được đổi tên thành PostgreSQL ("Postgres" + "SQL"). Vì vậy nhiều người vẫn gọi tắt thân mật là "Postgres".

Triết lý cốt lõi từ thời POSTGRES là object-relational: giữ nền tảng quan hệ (bảng, dòng, cột, SQL) nhưng cho phép kiểu dữ liệu và hành vi phong phú như hướng đối tượng — kiểu tự định nghĩa, kế thừa bảng, kiểu mảng, kiểu phức hợp. Đó là lý do PostgreSQL có sẵn những kiểu mà nhiều database khác thiếu: jsonb, array, range, inet, uuid, hstore... Chúng ta sẽ đào sâu ở bài Kiểu dữ liệuJSONB.

Điểm mạnh gói gọn: đúng chuẩn, kiểu dữ liệu phong phú, hệ sinh thái extension, MVCC cho đọc-ghi song song không chặn nhau, và độ tin cậy đã được kiểm chứng nhiều thập kỷ.

Kiến trúc tiến trình: nhiều process, không phải nhiều thread

Đây là quyết định kiến trúc quan trọng nhất cần nhớ: PostgreSQL dùng mô hình đa tiến trình (process-based), không phải đa luồng (thread-based).

Nghĩa là mỗi kết nối client được phục vụ bởi một tiến trình hệ điều hành riêng biệt, chứ không phải một thread trong một tiến trình chung (khác với mô hình của một số database khác). Lựa chọn này đánh đổi: cách ly lỗi tốt (một backend crash không kéo sập cả server) và ổn định, đổi lấy chi phí tạo/duy trì tiến trình cao hơn thread.

Postmaster — tiến trình cha

Khi bạn khởi động PostgreSQL, tiến trình đầu tiên chạy lên là postmaster (chính là binary postgres ở vai trò supervisor). Nó:

  • Lắng nghe trên cổng (mặc định 5432) chờ kết nối đến.
  • Với mỗi kết nối mới, fork ra một backend process để phục vụ riêng client đó.
  • Sinh ra và giám sát toàn bộ background process (mô tả bên dưới).
  • Khởi tạo và quản lý shared memory dùng chung cho mọi tiến trình con.

Postmaster không tự xử lý truy vấn — nó là "người điều phối", không phải "người làm việc".

Backend process — mỗi kết nối một tiến trình

Mỗi client (một ứng dụng, một phiên psql) khi kết nối sẽ được phục vụ bởi đúng một backend process. Backend này thực thi mọi câu lệnh của kết nối đó: parse, plan, execute, trả kết quả. Khi client ngắt, backend kết thúc.

Đây là chi tiết có hệ quả vận hành lớn: mỗi kết nối tốn một tiến trình + vùng bộ nhớ riêng của nó. Vài trăm kết nối rảnh rỗi cũng ngốn RAM và làm nặng bộ lập lịch của OS. Vì thế:

  • Đừng để ứng dụng mở hàng nghìn kết nối trực tiếp.
  • Dùng connection pooling — hoặc pool phía ứng dụng (HikariCP, psycopg_pool...), hoặc proxy chuyên dụng như PgBouncer/pgcat đặt trước database. Pool giữ một số ít kết nối "nóng" và tái sử dụng, thay vì fork backend mới liên tục.

Ghi nhớ nguyên tắc: số backend đồng thời tối ưu thường chỉ vài chục đến trăm, giới hạn bởi max_connections. Nhiều hơn không đồng nghĩa nhanh hơn.

Background processes — đội hậu cần

Ngoài các backend phục vụ client, postmaster còn duy trì một loạt tiến trình nền lo việc "hậu trường":

  • background writer — dần dần ghi các trang "bẩn" (dirty page) trong shared_buffers xuống đĩa, để giảm gánh cho lúc checkpoint.
  • checkpointer — định kỳ thực hiện checkpoint: buộc mọi dirty page tính đến một mốc phải nằm an toàn trên đĩa, đánh dấu điểm phục hồi trong WAL.
  • WAL writer — ghi bản ghi Write-Ahead Log từ WAL buffers xuống đĩa.
  • autovacuum launcher và các autovacuum workers — tự động chạy VACUUM/ANALYZE dọn dòng chết và cập nhật thống kê (rất quan trọng với MVCC — xem bài Bảo trì/VACUUM).
  • archiver — sao lưu các file WAL đã hoàn tất (khi bật archiving), phục vụ backup và point-in-time recovery.
  • logical replication workers — áp dụng thay đổi khi dùng logical replication.

Một điểm phiên bản đáng nhớ: trước PostgreSQL 15 từng có tiến trình riêng gọi là stats collector thu thập số liệu thống kê thời gian chạy. Từ PostgreSQL 15 trở đi, tiến trình này bị loại bỏ — thống kê tích luỹ nay được giữ trong shared memory, giảm chi phí và độ trễ. Nếu bạn đọc tài liệu cũ nhắc tới "stats collector process", hãy biết rằng nó không còn tồn tại trên bản mới.

Kiến trúc bộ nhớ

Bộ nhớ trong PostgreSQL chia thành hai nhóm: dùng chung và riêng từng backend.

Shared memory — dùng chung mọi tiến trình

  • shared_buffers — vùng cache trang lớn nhất và quan trọng nhất. Khi cần đọc một trang dữ liệu (page 8KB), PostgreSQL tìm trong shared_buffers trước; chỉ khi trượt (miss) mới đọc từ đĩa. Ghi cũng đi qua đây (ghi vào buffer, đánh dấu bẩn, sau đó mới flush). Giá trị khởi điểm thường gợi ý ~25% RAM máy, rồi tinh chỉnh theo tải.
  • WAL buffers — vùng đệm nhỏ giữ các bản ghi WAL trước khi ghi xuống đĩa.
  • Ngoài ra còn các cấu trúc dùng chung khác: lock table, danh sách tiến trình, vùng thống kê tích luỹ...

Per-backend memory — riêng từng kết nối

  • work_mem — bộ nhớ mỗi thao tác sort hoặc hash trong một truy vấn được dùng trước khi phải tràn ra đĩa. Đây là tham số per-operation, không phải per-connection: một truy vấn phức tạp có thể dùng work_mem nhiều lần. Đặt quá cao × nhiều kết nối × nhiều node sort = nguy cơ hết RAM.
  • maintenance_work_mem — bộ nhớ cho các thao tác bảo trì (VACUUM, CREATE INDEX); thường đặt lớn hơn work_mem vì các việc này ít khi chạy song song đông.

OS page cache — tầng đệm thứ hai

Bên dưới, hệ điều hành còn có page cache riêng. Một trang PostgreSQL đọc từ "đĩa" thực ra thường lấy từ page cache của OS (RAM). Vì vậy tổng bộ nhớ hữu ích để cache dữ liệu = shared_buffers + OS page cache — lý do người ta không đặt shared_buffers gần 100% RAM (sẽ tranh RAM với page cache, phản tác dụng).

Lưu trữ trên đĩa

Cách PostgreSQL tổ chức dữ liệu, từ lớn đến nhỏ:

  • Cluster — một cài đặt PostgreSQL (một data directory, một postmaster). Chú ý: "cluster" ở đây nghĩa là cụm database trên một máy chủ, không phải cụm nhiều máy.
  • Database — bên trong một cluster có nhiều database, cách ly nhau. Kết nối phải chọn một database.
  • Schema — namespace bên trong một database (mặc định là public), gom nhóm bảng/view/hàm.
  • Table và các object khác nằm trong schema.

Về vật lý, mỗi bảng lưu trong các heap file. Dữ liệu được chia thành page (block) kích thước 8KB — đơn vị đọc/ghi cơ bản. Một dòng (tuple) không thể lớn hơn một page ở dạng thô; giá trị lớn (text/jsonb/bytea dài) được xử lý bởi cơ chế TOAST (The Oversized-Attribute Storage Technique): tự động nén và/hoặc tách giá trị lớn ra bảng phụ, để dòng chính vẫn gọn trong page.

WAL — Write-Ahead Log

Đây là trái tim của độ bền (durability). Nguyên tắc write-ahead logging: trước khi một thay đổi được coi là đã commit, bản ghi mô tả thay đổi đó phải được ghi xuống WAL trên đĩa trước, trước cả khi trang dữ liệu tương ứng được ghi xuống file bảng.

Vì sao thiết kế vậy? Vì ghi tuần tự vào một file log nhanh hơn nhiều so với ghi ngẫu nhiên rải rác vào các trang dữ liệu. WAL cho phép:

  • Durability: commit chỉ cần chắc WAL đã xuống đĩa (fsync). Nếu mất điện ngay sau đó, khi khởi động lại PostgreSQL replay WAL từ checkpoint gần nhất để tái tạo mọi thay đổi đã commit — không mất dữ liệu.
  • Hiệu năng: gom nhiều thay đổi, flush trang dữ liệu thật sự về sau (qua background writer/checkpointer) một cách hiệu quả.
  • Nền tảng cho replication và backup: stream WAL sang máy khác chính là cách replica đồng bộ, và là cơ sở của point-in-time recovery.

Vòng đời một truy vấn

Khi một câu SQL đi vào backend của bạn, nó đi qua bốn giai đoạn:

  1. Parse — kiểm tra cú pháp, dựng parse tree. Sai chính tả từ khoá dừng ở đây.
  2. Rewrite — áp dụng rule và view: nếu bạn SELECT từ một view, câu lệnh được viết lại theo định nghĩa view thành query trên bảng gốc.
  3. Plan / Optimizeplanner sinh nhiều cách thực thi vật lý cho cùng một query, ước lượng chi phí từng cách dựa trên thống kê (pg_statistic), rồi chọn plan rẻ nhất. Đây là nơi quyết định "quét bảng hay dùng index", "join kiểu gì".
  4. Executeexecutor chạy cây plan đã chọn, kéo dữ liệu từ shared_buffers/đĩa, trả về các dòng.

Nhìn được planner đang định làm gì chính là kỹ năng đọc EXPLAIN — chủ đề trọng tâm của bài EXPLAIN. Ở đây chỉ cần nhớ: cùng một câu SQL có thể có nhiều plan chênh nhau hàng nghìn lần về tốc độ, và planner chọn dựa trên thống kê chứ không nhìn dữ liệu thật.

MVCC — tổng quan

Câu hỏi kinh điển: nếu giao dịch A đang cập nhật một dòng, giao dịch B đọc dòng đó có phải chờ không? Trong PostgreSQL, câu trả lời thường là không — nhờ MVCC (Multi-Version Concurrency Control).

Ý tưởng cốt lõi: thay vì khoá dòng để đọc, PostgreSQL giữ nhiều phiên bản của một dòng cùng lúc. Mỗi giao dịch nhìn thấy một snapshot nhất quán của dữ liệu tại thời điểm nó bắt đầu (tuỳ isolation level). Người đọc không chặn người ghi, người ghi không chặn người đọc.

Cơ chế thực hiện: mỗi tuple mang hai cột hệ thống ẩn — xmin (id giao dịch đã tạo ra phiên bản này) và xmax (id giao dịch đã xoá/thay thế nó). Một UPDATE không sửa tại chỗ mà tạo phiên bản mới và đánh dấu phiên bản cũ hết hạn. Nhờ so xmin/xmax với snapshot, mỗi giao dịch tự biết phiên bản nào "hiển thị" với mình.

Hệ quả có hai mặt:

  • Mặt lợi: đọc-ghi song song mượt mà, không chặn nhau — điểm mạnh lớn của PostgreSQL.
  • Mặt phí: những phiên bản cũ đã "chết" (dead tuple) vẫn nằm trong file cho tới khi được dọn. Đó là lý do DELETE không làm file nhỏ ngay, và vì sao VACUUM (đặc biệt autovacuum) là bắt buộc để thu hồi không gian và tránh phình bảng (bloat).

Toàn bộ chi tiết — isolation levels, snapshot, freeze, transaction ID wraparound — nằm ở bài MVCC & concurrency. Việc dọn dẹp thực tế nằm ở bài Bảo trì/VACUUM.

Sơ đồ tổng thể kiến trúc

Ví dụ chạy được: xem phiên bản

Bài này thiên về khái niệm nên phần lớn là minh hoạ, không chạy. Nhưng có một câu vô hại, hữu ích và chạy được để bạn biết mình đang trên phiên bản nào:

▶ Chạy được trong SQL Builder

SELECT version();

Kết quả trả về chuỗi kiểu PostgreSQL 17.x on x86_64-.... Con số major (17) quyết định tài liệu bạn nên đọc và tính năng bạn có.

Vài truy vấn chỉ đọc catalog khác (minh hoạ — tuỳ quyền và cấu hình sandbox có thể không chạy):

-- (minh hoạ) xem vài tham số bộ nhớ quan trọng
SELECT name, setting, unit
FROM pg_settings
WHERE name IN ('shared_buffers', 'work_mem', 'max_connections');
-- (minh hoạ) đếm số backend đang kết nối
SELECT count(*) AS active_backends
FROM pg_stat_activity
WHERE backend_type = 'client backend';

Phiên bản & vòng đời phát hành

PostgreSQL theo lịch phát hành rất kỷ luật:

  • Một bản major mỗi năm, thường vào khoảng tháng 9. Ví dụ PostgreSQL 17 phát hành 26/09/2024; đánh số major là một số nguyên tăng dần (14, 15, 16, 17...).
  • Bản minor (ví dụ 17.1, 17.2...) ra định kỳ vài tháng một lần, chỉ chứa sửa lỗi và vá bảo mật, không đổi định dạng file. Nâng minor là thao tác nhẹ: thay binary, khởi động lại.
  • Mỗi major được hỗ trợ khoảng 5 năm kể từ khi ra mắt, sau đó end-of-life (ngừng vá).

Khi nào nâng cấp?

  • Minor: nâng sớm, gần như bắt buộc — đây là các bản vá an toàn, ít rủi ro.
  • Major: cân nhắc kỹ hơn. Nâng major có thể cần đổi định dạng lưu trữ, kiểm thử tương thích extension, và dùng pg_upgrade hoặc dump/restore. Đừng chạy bản đã end-of-life trên production (không còn vá bảo mật). Nguyên tắc thực dụng: đừng vội nhảy lên bản .0 cho hệ thống trọng yếu, nhưng cũng đừng để tụt lại quá xa.

Use case thực tế

Tình huống 1 — "Database chậm, chắc thiếu index". Team thấy ứng dụng chậm giờ cao điểm, đổ lỗi cho query. Nhìn pg_stat_activity thì thấy hơn 2.000 kết nối, phần lớn idle. Mỗi kết nối là một backend + bộ nhớ riêng → OS thrash, database bò. Nguyên nhân thật là không có connection pooling: ứng dụng mở kết nối mới cho mỗi request. Giải pháp là đặt PgBouncer phía trước và giới hạn pool — không phải thêm index. Hiểu "mỗi kết nối một tiến trình" giúp chẩn đoán đúng.

Tình huống 2 — "Xoá 10 triệu dòng mà đĩa không giảm". Sau khi DELETE một lượng lớn dữ liệu cũ, dung lượng file bảng vẫn nguyên. Đây không phải bug: MVCC chỉ đánh dấu các dòng là dead, chưa thu hồi. Không gian được VACUUM tái sử dụng (autovacuum sẽ dọn), nhưng để trả lại đĩa cho OS cần VACUUM FULL hoặc pg_repack (khoá/khối lượng cân nhắc). Biết MVCC lưu nhiều phiên bản giúp bạn không hoảng.

Tình huống 3 — "Mất điện, dữ liệu có sao không?" Máy chủ tắt đột ngột. Khi bật lại, PostgreSQL tự replay WAL từ checkpoint gần nhất và khôi phục mọi giao dịch đã commit trước sự cố; các giao dịch chưa commit bị bỏ. Không cần thao tác thủ công. Đó chính là durability nhờ write-ahead logging.

Ghi nhớ

  • PostgreSQL là RDBMS mã nguồn mở, chuẩn SQL cao, object-relational, extensible (extension), MVCC và độ tin cậy đã kiểm chứng.
  • Kiến trúc đa tiến trình: postmaster lắng nghe và fork; mỗi kết nối = một backend process riêng → tốn tài nguyên → cần connection pooling (PgBouncer, pool phía app).
  • Background processes: background writer, checkpointer, WAL writer, autovacuum, archiver, logical replication workers. (Từ PG15, không còn tiến trình stats collector riêng — thống kê ở shared memory.)
  • Bộ nhớ: shared memory (shared_buffers cache trang, WAL buffers) dùng chung; work_mem/maintenance_work_mem riêng backend. Bên dưới còn OS page cache — đừng đặt shared_buffers chiếm hết RAM.
  • Lưu trữ: cluster → database → schema → table; heap file, page 8KB, TOAST cho giá trị lớn.
  • WAL (write-ahead log): ghi log thay đổi xuống đĩa trước → durability (replay sau sự cố), hiệu năng ghi tuần tự, nền tảng replication/backup.
  • Vòng đời truy vấn: parse → rewrite → plan/optimize (dựa thống kê) → execute. Đọc plan bằng EXPLAIN.
  • MVCC: nhiều phiên bản dòng (xmin/xmax), mỗi giao dịch thấy snapshot; đọc không chặn ghi. Cái giá là dead tuple → cần VACUUM/autovacuum.
  • Phát hành: một major mỗi năm (~tháng 9), minor là bản vá; nâng minor sớm, nâng major có kiểm thử; đừng chạy bản end-of-life.

Có mô hình tinh thần này rồi, bước tiếp theo là làm quen với "chất liệu" bạn thao tác hằng ngày: Kiểu dữ liệu, rồi JSONB và cơ chế đa phiên bản chi tiết ở MVCC & concurrency.

Nguồn tham khảo

Bài viết liên quan

Cách index hoạt động (B-Tree), đọc EXPLAIN, seq scan vs index scan và mẫu tối ưu truy vấn.

13 thg 7, 2026 4

Khoá, ràng buộc, quan hệ và chuẩn hoá 1NF/2NF/3NF — thiết kế lược đồ đúng từ đầu.

13 thg 7, 2026 4

Kết nhiều bảng đúng cách: các loại JOIN, bẫy thường gặp và phép hợp tập.

13 thg 7, 2026 3

Truy vấn lồng, CTE (WITH), CTE đệ quy và hàm cửa sổ — vũ khí cho phân tích nâng cao.

13 thg 7, 2026 3