PostgreSQL
PostgreSQL là lựa chọn mặc định an toàn cho phần lớn hệ thống: ACID, index phong phú, replication, partitioning và hệ extension mạnh. Bài này giải thích MVCC, các loại index, mức isolation, replication, connection pooling, bảng sizing theo cấu hình và dấu hiệu nên rời một node Postgres.
Mẹo: So Postgres với Oracle, SQL Server, MySQL ở bài So sánh database quan hệ; khi một node không đủ, xem Database scale.
PostgreSQL là gì và vì sao là lựa chọn mặc định
PostgreSQL là database quan hệ mã nguồn mở, có lịch sử từ dự án Postgres ở Berkeley. Nó cho bạn:
- ACID đầy đủ: transaction, foreign key, unique constraint, check constraint. Seven Databases định nghĩa ACID là Atomic, Consistent, Isolated, Durable và lưu ý "consistency" của ACID khác consistency của CAP.1
- SQL mạnh: join, window function, full-text search (
tsvector/tsquery), thêm CTE vàJSONB.2 - Index đa dạng: B-tree, hash, GiST, GIN và nhiều loại khác.3
- Extension: thêm kiểu dữ liệu, hàm và index mới thông qua các module đóng góp (contrib) như
fuzzystrmatch,pg_trgm,cube;4 Microservices Patterns cũng nhắc các extension geospatial của Postgres.5 Các extension khác (pgvector, TimescaleDB, Citus) là kiến thức sản phẩm chưa có sách kiểm chứng.6
Trong phỏng vấn, câu "tôi bắt đầu với PostgreSQL" gần như luôn được chấp nhận, miễn là bạn chứng minh được nó chịu được tải. Một node Postgres thường đủ cho tải vừa; mốc tham khảo trong sách là khoảng 1.000 QPS cho một server MySQL (xem phần sizing).7 Các con số cao hơn trong bài là ước lượng của khoá học. Rất nhiều sản phẩm không bao giờ vượt qua ngưỡng một node.
Điều quan trọng là biết giới hạn của nó: ghi chỉ đi qua một primary (mô hình primary-secondary của Grokking),8 connection tốn bộ nhớ, và bảng cập nhật nhiều cần VACUUM (hai ý sau là kiến thức sản phẩm, chưa có sách).6 Phần còn lại của bài đi qua từng điểm. Xem thêm Chọn database và Mô hình dữ liệu.
Kiến trúc: process, WAL và replication
- Mỗi connection là một process của hệ điều hành và tốn bộ nhớ, nên Postgres không chịu được hàng nghìn connection trực tiếp (kiến thức sản phẩm, chưa có sách).6
- Mọi thay đổi được ghi vào WAL (write-ahead log) trước, rồi mới ghi vào file dữ liệu. Grokking mô tả primary lưu câu lệnh vào file WAL trước khi thực thi và dùng log đó để copy dữ liệu sang secondary; kỹ thuật này dùng trong PostgreSQL.9 Microservices Patterns cũng nhắc Postgres WAL là nguồn để đọc thay đổi cho outbox/CDC.10
- Shared buffers là cache trang dữ liệu của Postgres; phần RAM còn lại để OS page cache làm tầng cache thứ hai (khuyến nghị cấu hình của khoá học, cần tra tài liệu).6
Replication
| Kiểu | Cách hoạt động | Dùng cho |
|---|---|---|
| WAL shipping, bất đồng bộ | Primary không chờ secondary xác nhận, báo thành công sau khi tự cập nhật | Read replica, standby; nếu primary chết, các ghi chưa kịp copy bị mất |
| WAL shipping, đồng bộ | Primary chờ secondary xác nhận rồi mới báo thành công | Secondary luôn mới; một secondary lỗi làm tăng độ trễ phản hồi |
| Logical (row-based) | Secondary lặp lại thay đổi thực tế ở mức row | Ít gắn với cấu trúc nội bộ của engine nên nâng cấp dễ hơn, hợp CDC |
Ba kiểu trên khớp phân loại sync/async và WAL shipping / logical log của Grokking.89 Với replication bất đồng bộ, đọc từ secondary có thể thấy dữ liệu cũ (replication lag), nên luồng "ghi xong đọc lại ngay" (read-your-writes) nên đọc từ primary.8 Công cụ failover tự động và cách đặt tên cấu hình cụ thể là kiến thức sản phẩm, chưa có sách.6 Xem Scale đọc.
MVCC và VACUUM
Postgres dùng MVCC (multi-version concurrency control): người đọc không chặn người ghi, người ghi không chặn người đọc. Thư viện chưa có sách mô tả MVCC, VACUUM hay mức isolation của Postgres, nên phần này chỉ nêu khái niệm cơ bản.6
- Mỗi row (tuple) mang thông tin transaction tạo ra và transaction xoá nó.
UPDATEkhông sửa tại chỗ. Nó đánh dấu bản cũ là đã chết và ghi một bản mới.- Mỗi transaction có một snapshot và chỉ thấy các phiên bản hợp lệ với snapshot đó.
Hệ quả thực tế:
- Bản cũ (dead tuple) chiếm chỗ cho tới khi VACUUM dọn. Autovacuum chạy nền, nhưng bảng cập nhật rất nhiều (bộ đếm, trạng thái job) có thể phình to (bloat) và chậm dần.
- Một transaction mở quá lâu (ví dụ một job báo cáo chạy 2 giờ, hoặc session quên commit) chặn VACUUM dọn mọi bản cũ hơn nó.
- Nếu VACUUM bị chặn quá lâu, Postgres có nguy cơ phải dừng ghi để tránh lỗi tràn ID transaction (wraparound).
- Một số kiểu update chỉ đổi cột không có index được tối ưu để rẻ hơn (HOT update); chi tiết cấu hình tra tài liệu.
Sai lầm hay gặp: Dùng một row Postgres làm bộ đếm lượt xem được cập nhật hàng nghìn lần mỗi giây. Mỗi lần tăng tạo một tuple mới, khoá row, và tạo bloat. Đếm trong Redis hoặc gom lại rồi ghi định kỳ (khuyến nghị của khoá học; Grokking ch.24 cũng chia counter nóng thành nhiều shard).11
Transaction và mức isolation
| Mức | Hành vi trong Postgres | Khi nào dùng |
|---|---|---|
| Read Committed (mặc định) | Mỗi câu lệnh thấy dữ liệu đã commit | Phần lớn CRUD |
| Repeatable Read | Cả transaction dùng một snapshot | Báo cáo nhất quán, đọc nhiều bảng cùng thời điểm |
| Serializable | Kết quả tương đương chạy tuần tự | Bất biến phức tạp, chấp nhận retry |
Bảng này là mô tả khái quát của khoá học; Seven Databases chỉ nêu thuộc tính Isolated của ACID (transaction không can thiệp lẫn nhau), chưa có sách mô tả từng mức của Postgres.16
Với các bài toán tranh chấp tài nguyên như đặt vé, bạn có ba công cụ chính:
-- 1. Khoá bi quan: giữ khoá row tới khi commit
BEGIN;
SELECT status FROM seats WHERE id = $1 FOR UPDATE;
UPDATE seats SET status = 'HELD', held_by = $2, held_until = now() + interval '10 minutes'
WHERE id = $1 AND status = 'AVAILABLE';
COMMIT;
-- 2. Cập nhật có điều kiện (atomic, không cần SELECT trước)
UPDATE inventory SET stock = stock - $2
WHERE sku = $1 AND stock >= $2
RETURNING stock;
-- 3. Hàng đợi job: nhiều worker lấy job không đụng nhau
SELECT id FROM jobs WHERE state = 'queued'
ORDER BY run_at LIMIT 10
FOR UPDATE SKIP LOCKED;
Ở mức Serializable hoặc Repeatable Read, ứng dụng phải retry khi gặp lỗi serialization failure (kiến thức sản phẩm).6 Ba đoạn SQL trên là ví dụ của khoá học cho các kỹ thuật khoá bi quan, cập nhật có điều kiện và hàng đợi job; Microservices Patterns cũng dùng optimistic locking kiểu kiểm tra điều kiện khi ghi.12 Xem chi tiết các lựa chọn ở Tranh chấp tài nguyên.
Index, partitioning và extension
Các loại index
| Index | Hợp với | Ví dụ |
|---|---|---|
| B-tree (mặc định) | =, khoảng < >, ORDER BY | (user_id, created_at DESC) cho feed |
| GIN | Giá trị chứa nhiều phần tử, full-text | to_tsvector(...) cho tìm theo từ khoá |
| GiST | Index API tổng quát: trigram, dữ liệu hình học | Tìm tên gõ sai bằng trigram |
| Hash | Chỉ so sánh bằng | Hiếm khi tốt hơn B-tree |
B-tree lưu giá trị dạng cây cân bằng, khớp được truy vấn theo khoảng; hash index chỉ khớp bằng; GiST là API index tổng quát; GIN dùng cho full-text trên tsvector.313 Các loại khác (BRIN cho bảng rất lớn theo thời gian, index trên JSONB/mảng) là kiến thức sản phẩm chưa có sách.6
Thêm vài biến thể rất hữu ích:
- Expression index:
ON users (lower(email)), Seven Databases dùng index trênlower(title)cho tìm không phân biệt hoa thường.14 - Partial index (
CREATE INDEX ... WHERE state = 'queued') và covering index (INCLUDE) là tính năng sản phẩm, nhỏ và nhanh theo kinh nghiệm của khoá học.6 - Dùng
CREATE INDEX CONCURRENTLYtrên bảng đang chạy production để không khoá ghi: tuỳ chọnCONCURRENTLYcó trong cú phápCREATE INDEXmà Seven Databases in ra.15
Mỗi index làm chậm ghi và tốn bộ nhớ (nguyên tắc chung của khoá học). Xem Database index.
Partitioning
Partitioning chia một bảng logic thành nhiều bảng con theo khoảng, danh sách hoặc hash. Ý tưởng chia theo khoảng khoá và theo hash được Grokking mô tả ở phần sharding (key-range, hash-based).16 Cú pháp declarative của Postgres dưới đây là kiến thức sản phẩm.6
CREATE TABLE events (
id bigint GENERATED ALWAYS AS IDENTITY,
user_id bigint NOT NULL,
created_at timestamptz NOT NULL,
payload jsonb
) PARTITION BY RANGE (created_at);
CREATE TABLE events_2026_11 PARTITION OF events
FOR VALUES FROM ('2026-11-01') TO ('2026-12-01');
Lợi ích: query có điều kiện thời gian chỉ chạm vào partition liên quan, và xoá dữ liệu cũ bằng DROP TABLE events_2025_01 thay vì DELETE hàng loạt. Partitioning vẫn nằm trên một node, nó giúp quản lý dữ liệu, không tăng sức ghi.6
Extension đáng nhớ
Seven Databases dùng bộ contrib fuzzystrmatch, pg_trgm và cube để làm hệ tìm phim: tìm tên gõ sai bằng trigram, so khớp gần đúng, tìm theo vector đặc trưng.4 Microservices Patterns nhắc Postgres và MySQL có extension geospatial để lưu dữ liệu vị trí.5 Các extension còn lại chưa có sách trong thư viện:6
- PostGIS: kiểu
geography, query "trong bán kính"; đủ cho Yelp và nhiều bài tìm kiếm gần ở quy mô vừa. - pgvector: lưu embedding cho tìm kiếm tương đồng, trước khi cần vector database riêng.
- pg_stat_statements: thống kê query chậm nhất.
- TimescaleDB, Citus: biến Postgres thành time-series store hoặc database phân tán.
Connection pooling và sizing theo cấu hình
Vì sao cần connection pooler
Ứng dụng nhiều instance, mỗi instance một pool connection, cộng thêm worker, cron job, serverless function, tổng số connection tới database tăng rất nhanh. Vì mỗi connection tốn tài nguyên trong Postgres, số connection thật tới database nên nhỏ và được chia sẻ qua một tầng pooling (kiến thức sản phẩm, chưa có sách).6
Tầng pooling (ví dụ PgBouncer) có ba chế độ phổ biến: theo phiên, theo transaction (phổ biến nhất, cần cẩn thận với trạng thái theo session) và theo câu lệnh.6 Số connection thật tới Postgres nên được chọn theo số core và đo bằng load test, không có con số trong sách.
Bảng sizing (giả định của khoá học)
Các con số dưới đây là giả định của khoá học (ước lượng, cần load test) cho query đơn giản có index, dữ liệu nóng nằm trong RAM. Query nặng, join lớn, hoặc dữ liệu không vừa RAM sẽ thấp hơn nhiều. Mốc từ sách: Grokking dùng khoảng 1.000 QPS cho MySQL, 10.000 QPS cho key-value store, và 100.000 đến 1 triệu QPS cho cache server.7
| Cấu hình | Đọc đơn giản/giây | Ghi đơn giản/giây | Hợp với |
|---|---|---|---|
| Nhỏ (vài vCPU) | Vài trăm đến vài nghìn | Vài trăm | MVP, dự án nội bộ |
| Vừa | Vài nghìn đến chục nghìn | Một đến vài nghìn | Sản phẩm vừa |
| Lớn | Hàng chục nghìn | Vài nghìn đến chục nghìn | Gần trần của một node |
Hãy coi 1.000 QPS của Grokking là mức an toàn cho query thực tế có join, còn bảng trên là mức tốt nhất cho tra cứu theo khoá.
Ví dụ (giả định): hệ thống 1 triệu DAU, mỗi người 50 request/ngày, 30% request chạm DB với trung bình 2 query → 30 triệu query/ngày ≈ 350 QPS trung bình, peak ×5 ≈ 1.750 QPS. Chia theo ngày/giây theo Grokking ch.5;17 hệ số peak ×5 là giả định. Với mốc 1.000 QPS mỗi server, một primary cùng một replica đủ dùng. Xem thêm Con số cần thuộc.
Khi nào dùng, khi nào rời Postgres
| Tình huống | PostgreSQL | Lựa chọn khác |
|---|---|---|
| Dữ liệu quan hệ, cần transaction, constraint | Rất hợp: ACID1 | |
| Thanh toán, tồn kho, đặt chỗ | Rất hợp (cần ACID) | |
| Query ad-hoc, báo cáo vừa | Hợp (chạy trên replica) | Warehouse khi dữ liệu lớn |
| Tìm kiếm full-text đơn giản | Hợp với tsvector + GIN | Elasticsearch khi cần ranking, facet, nhiều ngôn ngữ |
| Ghi rất lớn kéo dài, dữ liệu vượt một node | Khó | Cassandra, DynamoDB (xem sharding và NoSQL ở Grokking ch.9)16 |
| Bộ đếm cực nóng, leaderboard realtime | Không hợp (giả định của khoá học) | Redis |
| Event log, metrics tỉ lệ lớn | Hợp tới mức vừa với partitioning hoặc TimescaleDB | Time-series database, Kafka |
Dấu hiệu nên tách hoặc shard
- Ghi vượt sức một primary: mọi ghi đi qua một primary nên đây là trần của mô hình này;8 CPU primary cao kéo dài (ngưỡng là giả định của khoá học).
- Working set không vừa RAM của máy lớn nhất bạn chấp nhận trả tiền (giả định của khoá học).
- Thời gian vận hành quá dài: backup, restore, tạo replica mới mất quá lâu (giả định của khoá học).
- Multi-region ghi: người dùng nhiều châu lục cần ghi với độ trễ thấp.
Trước khi shard, hãy làm các bước rẻ hơn: thêm index đúng, cache đọc, chuyển đọc sang replica, tách bảng ít liên quan sang database riêng theo chức năng, partition và xoá dữ liệu cũ, nâng cấp phần cứng. Grokking nêu sharding theo chiều dọc (tách bảng/cột) và theo chiều ngang (tách row) là hai hướng, kèm nhược điểm của sharding.16 Building Microservices cũng khuyên: khi chạm giới hạn ghi, mua máy to hơn thường là cách nhanh nhất, rồi mới tính sharding.18
Khi thật sự cần shard:
- Extension phân tán (ví dụ Citus): bảng phân tán theo một cột như
tenant_id; query theo cột đó đi thẳng tới một node (chưa có sách trong thư viện).6 - Shard ở tầng ứng dụng: tự định tuyến theo
user_idtới N cluster Postgres. Đơn giản về công nghệ, phức tạp về vận hành (resharding, query xuyên shard), đúng các nhược điểm Grokking nêu cho hash-based sharding.16 - Database phân tán tương thích Postgres hoặc tầng proxy shard: đổi lại độ trễ ghi cao hơn và một số tính năng không có (nhận định của khoá học).6
Vận hành ở quy mô nhỏ và lớn, và điểm nói trong phỏng vấn
Quy mô nhỏ. Dùng dịch vụ managed để khỏi tự vận hành, bật backup tự động và point-in-time recovery, dùng migration có version. Chi phí và danh sách nhà cung cấp là thông tin thay đổi theo thời gian, thư viện không có sách nên bài không liệt kê.6
Quy mô lớn. Một primary lớn, vài replica, tầng pooling, công cụ failover. Theo dõi: replication lag, số connection, tỉ lệ cache hit, dead tuple và tuổi transaction cũ nhất, query chậm nhất (danh sách theo kinh nghiệm của khoá học).6 Đổi schema trên bảng lớn phải làm theo kiểu không khoá: thêm cột nullable, backfill theo lô, rồi mới thêm constraint.
Mẹo: Phần lớn sự cố Postgres ở production đến từ ba nguồn: quá nhiều connection, transaction mở quá lâu, và migration khoá bảng. Nhắc tới ba điều này khi nói về vận hành cho thấy bạn có kinh nghiệm thật.
Trong phỏng vấn: Hãy nói theo trình tự: "Một primary Postgres với read replica phục vụ được tải X theo ước lượng (mốc Grokking cho một server DB).7 Khi ghi vượt mức đó, tôi sẽ partition theo thời gian, rồi shard theo user_id bằng Citus hoặc ở tầng ứng dụng." Đưa ra con số, rồi đưa ra lộ trình.
Các bài toán dùng PostgreSQL làm nguồn sự thật (gợi ý của khoá học):
- Đặt vé sự kiện, Flash sale:
FOR UPDATE, update có điều kiện. - Payment system, Stock trading: transaction, idempotency key unique.
- Job scheduler:
FOR UPDATE SKIP LOCKEDcho hàng đợi. - Yelp, Local delivery: PostGIS cho tìm theo vị trí.
- URL shortener, LeetCode, Online auction: dữ liệu quan hệ, tải vừa phải.
Nguồn trích dẫn
-
Seven Databases in Seven Weeks (file .zip, tải về giải nén) · ch.2 PostgreSQL · Day 2, mục về transaction ("PostgreSQL transactions follow ACID compliance", tr. 25). ↩ ↩2 ↩3
-
Seven Databases in Seven Weeks (file .zip, tải về giải nén) · ch.2 PostgreSQL · Day 2 (window function, tr. 22–24) và Day 3 (TSVector and TSQuery). ↩
-
Seven Databases in Seven Weeks (file .zip, tải về giải nén) · ch.2 PostgreSQL · Day 1, mục về index (hash index, B-tree, bảng thuật ngữ Index/B-tree, tr. 20–21). ↩ ↩2
-
Seven Databases in Seven Weeks (file .zip, tải về giải nén) · ch.2 PostgreSQL · Day 1 (gói contrib: tablefunc, dict_xsyn, fuzzystrmatch, pg_trgm, cube) và Day 3, mục về fuzzy matching bằng trigram. ↩ ↩2
-
Microservices Patterns (file .zip, tải về giải nén) · ch.7 · §7.3.1 Choosing a view datastore, đoạn nhắc MongoDB và extension geospatial của Postgres và MySQL. ↩ ↩2
-
Chưa có nguồn trong thư viện: thư viện không có sách riêng về nội bộ PostgreSQL (MVCC, VACUUM, mức isolation, partitioning declarative, connection pooling, extension hiện đại); phần này chỉ nêu khái niệm cơ bản, không đưa số liệu hay phiên bản của sản phẩm. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17 ↩18 ↩19
-
Grokking Modern System Design · ch.5 Back-of-the-envelope Calculations · mục "Standard numbers to remember" (bảng Important Rates). ↩ ↩2 ↩3
-
Grokking Modern System Design · ch.9 Databases · mục "Replication", "Synchronous versus asynchronous", "Single leader/primary-secondary replication". ↩ ↩2 ↩3 ↩4
-
Grokking Modern System Design · ch.9 Databases · mục "Primary-secondary replication methods", "Write-ahead log (WAL) shipping" và "Logical (row-based) log replication". ↩ ↩2
-
Microservices Patterns (file .zip, tải về giải nén) · ch.3 · §3.3.7 Transactional messaging, mục "Transaction log tailing" (Eventuate Tram dùng Postgres WAL, tr. 99–100). ↩
-
Grokking Modern System Design · ch.24 Sharded Counters · mục "High-level solution sketch". ↩
-
Microservices Patterns (file .zip, tải về giải nén) · ch.7 · §7.4.2 Data modeling and query design with DynamoDB, mục "Updating orders" (optimistic locking, conditional update, tr. 247–248). ↩
-
Seven Databases in Seven Weeks (file .zip, tải về giải nén) · ch.2 PostgreSQL · Day 3, mục "TSVector and TSQuery" và phần tạo index GIN cho full-text. ↩
-
Seven Databases in Seven Weeks (file .zip, tải về giải nén) · ch.2 PostgreSQL · Day 3, tạo index
movies_title_patterntrênlower(title). ↩ -
Seven Databases in Seven Weeks (file .zip, tải về giải nén) · ch.2 PostgreSQL · Day 1, mục trợ giúp
\h CREATE INDEX(cú pháp có tuỳ chọn CONCURRENTLY). ↩ -
Grokking Modern System Design · ch.9 Databases · mục "Sharding", "Key-range based sharding", "Hash-based sharding", "Advantages" và "Disadvantages". ↩ ↩2 ↩3 ↩4
-
Grokking Modern System Design · ch.5 Back-of-the-envelope Calculations · mục "Requests estimation" và "Number of servers required". ↩
-
Building Microservices (file .zip, tải về giải nén) · ch.11 Microservices at Scale · mục "Scaling for Writes" (tr. 223). ↩