Skip to content

PostgreSQL Advanced Roadmap

Lộ trình học PostgreSQL nâng cao, tối ưu performance và xử lý dữ liệu lớn


Tổng quan

PhaseChủ đề
1PostgreSQL Internals
2Indexing chuyên sâu
3Query Planner & Optimization
4Xử lý dữ liệu lớn
5Scalability & Operations

Phase 1 — PostgreSQL Internals

Nền tảng bắt buộc. Bỏ qua phần này thì các phần sau sẽ rất mơ hồ.

1.1 MVCC & Transactions

Multi-Version Concurrency Control — lý do PG có thể đọc/ghi đồng thời mà không block nhau.

Cần hiểu:

  • Snapshot isolation hoạt động như thế nào
  • xmin, xmax trên mỗi tuple là gì
  • Transaction ID (XID) và vấn đề wraparound
  • Các isolation levels: READ COMMITTED, REPEATABLE READ, SERIALIZABLE
  • pg_stat_activity, pg_stat_user_tables

Thực hành:

sql
-- Xem transaction id hiện tại
SELECT txid_current();

-- Xem snapshot hiện tại
SELECT txid_current_snapshot();

-- Kiểm tra tuples bị ảnh hưởng bởi MVCC
SELECT xmin, xmax, ctid, * FROM your_table LIMIT 10;

1.2 Locking & Concurrency

Cần hiểu:

  • Row-level locks: FOR UPDATE, FOR SHARE, FOR NO KEY UPDATE, FOR KEY SHARE
  • Table-level locks: ACCESS SHAREACCESS EXCLUSIVE (8 mức)
  • Advisory locks — app tự quản lý
  • Deadlock: nguyên nhân, cách phát hiện, cách phòng tránh
  • Lock queuing — tại sao 1 ALTER TABLE có thể block cả hệ thống

Thực hành:

sql
-- Xem các lock đang active
SELECT pid, locktype, relation::regclass, mode, granted
FROM pg_locks
WHERE relation IS NOT NULL;

-- Xem ai đang block ai
SELECT blocked.pid, blocked.query,
       blocking.pid AS blocking_pid,
       blocking.query AS blocking_query
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking
  ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
WHERE cardinality(pg_blocking_pids(blocked.pid)) > 0;

Phòng tránh deadlock: Luôn lock theo cùng thứ tự. Dùng SELECT FOR UPDATE sớm ngay đầu transaction.


1.3 WAL — Write-Ahead Log

WAL là lý do PostgreSQL không mất data khi server crash.

Flow cơ bản:

app UPDATE row

ghi vào WAL trước  ← sequential write, rất nhanh

ghi vào data page  ← random write, chậm hơn

crash ở đây? → restart → replay lại WAL → data OK

Checkpoint là điểm PostgreSQL flush hết data pages xuống disk. WAL trước checkpoint có thể xóa được → giữ disk không đầy.

Khi nào BE dev cần quan tâm:

Triệu chứngNguyên nhân WALHướng xử lý
App chậm từng đợtCheckpoint đang flushTăng checkpoint_completion_target
Disk đầy bất thườngWAL tích lũy quá nhiềuTăng max_wal_size, kiểm tra long transaction
Setup replication / backupCần cấu hình WAL levelĐọc chi tiết WAL.md

full_page_writes, wal_compression để default — không cần quan tâm.


1.4 VACUUM & Autovacuum

VACUUM là garbage collector của PostgreSQL — dọn dead rows mà MVCC để lại sau mỗi UPDATE/DELETE.

Chạy tự động qua autovacuum, bình thường không cần động vào. Chỉ cần đọc thêm vacuum.md khi gặp bảng write-heavy bị chậm dần không rõ nguyên nhân (bloat) hoặc autovacuum có vấn đề.


1.5 Storage & Buffer Pool

  • shared_bufferspage 8KB → đã cover trong WAL.md
  • TOASTOS page cache → xem chi tiết storage.md

Phase 2 — Indexing chuyên sâu

2.1 B-tree internals

Index mặc định của PG. Phù hợp cho equality và range queries.

Cần hiểu:

  • Cấu trúc B-tree, page split
  • Deduplication (PG 13+): index entries trùng được gộp
  • Index-only scan: khi nào query không cần đọc heap
  • Correlation: ảnh hưởng đến cost của index scan
  • Khi nào planner bỏ qua index (low selectivity, small table)
sql
-- Xem correlation của column (càng gần 1 hoặc -1 càng tốt)
SELECT tablename, attname, correlation
FROM pg_stats
WHERE tablename = 'your_table'
ORDER BY abs(correlation) DESC;

2.2 GIN / GiST / BRIN

IndexDùng choTrade-off
GINjsonb, array, full-text searchBuild chậm, lookup nhanh
GiSTGeo (PostGIS), range types, nearest neighborLossy — cần recheck
BRINTime-series, insert-ordered dataCực nhỏ, chỉ hiệu quả với sorted data
HashEquality onlyKhông WAL-safe trước PG10
sql
-- GIN cho jsonb
CREATE INDEX idx_data_gin ON events USING GIN (payload);
-- Query: SELECT * FROM events WHERE payload @> '{"type": "click"}';

-- BRIN cho time-series
CREATE INDEX idx_created_brin ON logs USING BRIN (created_at);
  
-- GiST cho range
CREATE INDEX idx_range ON bookings USING GIST (during);

2.3 Partial & Expression index

sql
-- Partial index: chỉ index những row cần thiết
CREATE INDEX idx_active_users ON users (email)
WHERE is_active = true;

-- Expression index: index trên function
CREATE INDEX idx_email_lower ON users (lower(email));
-- Query phải dùng: WHERE lower(email) = 'user@example.com'

-- Covering index (INCLUDE): tránh heap lookup
CREATE INDEX idx_orders_covering ON orders (customer_id)
INCLUDE (status, total_amount, created_at);

2.4 Index maintenance

sql
-- Xem unused indexes (tốn disk mà không được dùng)
SELECT schemaname, tablename, indexname,
       idx_scan, idx_tup_read, idx_tup_fetch,
       pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;

-- Rebuild index không lock
REINDEX INDEX CONCURRENTLY idx_name;

-- Xem index bloat
SELECT indexrelname,
       round(bloat_ratio::numeric, 2) AS bloat_pct,
       pg_size_pretty(real_size) AS real_size
FROM pgstattuple_approx('your_index');

2.5 pg_trgm — Similarity & LIKE optimization

Rất hay gặp trong thực tế: search theo tên, autocomplete, fuzzy match.

sql
CREATE EXTENSION pg_trgm;

-- GIN index cho LIKE/ILIKE và similarity search
CREATE INDEX idx_name_trgm ON products USING GIN (name gin_trgm_ops);

-- Giờ query này dùng index thay vì Seq Scan
SELECT * FROM products WHERE name ILIKE '%điện thoại%';

-- Similarity score
SELECT name, similarity(name, 'iphone') AS score
FROM products
WHERE name % 'iphone'          -- toán tử % = similarity > threshold (mặc định 0.3)
ORDER BY score DESC
LIMIT 10;

-- Điều chỉnh threshold
SET pg_trgm.similarity_threshold = 0.4;

-- GiST thay thế khi cần nearest-neighbor với ORDER BY ... <-> (distance operator)
CREATE INDEX idx_name_gist ON products USING GIST (name gist_trgm_ops);
SELECT name FROM products ORDER BY name <-> 'iphone' LIMIT 5;

Khi nào dùng GIN vs GiST với trgm:

GINGiST
Build timeChậm hơnNhanh hơn
LookupNhanh hơnChậm hơn
ORDER BY <->Không hỗ trợHỗ trợ
RecommendedLIKE/ILIKE, %Nearest-neighbor

Phase 3 — Query Planner & Optimization

Đây là tầng quan trọng nhất. Kỹ năng đọc EXPLAIN ANALYZE tốt giải quyết được 80% vấn đề performance.

3.1 EXPLAIN ANALYZE

sql
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT ...;

Cách đọc output:

Seq Scan on orders  (cost=0.00..1842.00 rows=50000 width=64)
                     ^^^^^^^^^^^^^^^^^^^
                     cost=startup..total
                     rows=estimated rows

                    (actual time=0.123..45.678 rows=49821 loops=1)
                                                ^^^^^^^^^^^
                                                actual rows — nếu lệch nhiều với estimated → planner sai
                    Buffers: shared hit=1230 read=342
                                          ^^^  ^^^
                                          cache hit  disk read

Các node quan trọng:

NodeÝ nghĩa
Seq ScanĐọc toàn bộ bảng — thường là vấn đề
Index ScanDùng index + heap lookup
Index Only ScanDùng index, không cần heap (tốt nhất)
Bitmap Heap ScanKết hợp nhiều index conditions
Hash JoinJoin bằng hash table — tốt với large datasets
Nested LoopJoin lồng nhau — tốt khi outer nhỏ
Merge JoinJoin trên sorted data

3.2 Join strategies

Khi planner chọn Hash Join:

  • Một trong hai bảng vừa fit vào work_mem
  • Không có index hữu ích
  • Cardinality lớn

Khi planner chọn Nested Loop:

  • Outer set nhỏ
  • Inner có index
  • OLTP queries điển hình

Khi planner chọn Merge Join:

  • Cả hai bảng đã sorted theo join key
  • Thường thấy với window functions
sql
-- Force join strategy để test (đừng để trong production)
SET enable_hashjoin = off;
SET enable_nestloop = off;

3.3 Planner statistics

Planner dựa vào statistics để ước lượng rows. Nếu ước lượng sai → plan sai.

sql
-- Xem statistics của một column
SELECT * FROM pg_stats
WHERE tablename = 'orders' AND attname = 'status';

-- Tăng statistics target cho column có nhiều giá trị distinct
ALTER TABLE orders ALTER COLUMN customer_id SET STATISTICS 500;
ANALYZE orders;

-- Extended statistics cho correlated columns
CREATE STATISTICS stat_region_product ON region, product_id FROM sales;
ANALYZE sales;

3.4 CTE & Subquery optimization

sql
-- PG 12+: CTE mặc định được inline (không còn là optimization fence)
-- Dùng MATERIALIZED để force materialize
WITH expensive_cte AS MATERIALIZED (
  SELECT * FROM large_table WHERE complex_condition
)
SELECT * FROM expensive_cte JOIN other_table USING (id);

-- LATERAL join: subquery tham chiếu outer row
SELECT u.id, last_orders.*
FROM users u
CROSS JOIN LATERAL (
  SELECT * FROM orders WHERE user_id = u.id
  ORDER BY created_at DESC LIMIT 3
) last_orders;

3.5 Parallel query

sql
-- Config cơ bản
max_parallel_workers_per_gather = 4
max_parallel_workers = 8
parallel_setup_cost = 1000
parallel_tuple_cost = 0.1

-- Xem query có dùng parallel không
EXPLAIN SELECT count(*) FROM large_table;
-- Tìm "Gather" node trong plan

-- Force parallel cho session (test)
SET max_parallel_workers_per_gather = 8;
-- PG 15 trở xuống:
SET force_parallel_mode = on;
-- PG 16+: force_parallel_mode bị deprecated, dùng:
SET debug_parallel_query = on;

Phase 4 — Xử lý dữ liệu lớn

4.1 Table Partitioning

Khi nào nên dùng: Bảng > 50–100 triệu dòng, query chậm dù đã index tốt.

sql
-- Range partitioning theo thời gian
CREATE TABLE events (
  id bigint,
  created_at timestamptz NOT NULL,
  payload jsonb
) PARTITION BY RANGE (created_at);

CREATE TABLE events_2024_01 PARTITION OF events
  FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');

CREATE TABLE events_2024_02 PARTITION OF events
  FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');

-- Index tự động apply cho tất cả partitions
CREATE INDEX ON events (created_at);

-- Xem partition pruning có hoạt động không
EXPLAIN SELECT * FROM events WHERE created_at >= '2024-01-15';
-- Phải thấy chỉ scan partition tương ứng, không phải tất cả

Detach partition để archive:

sql
-- Zero-downtime, không lock lâu
ALTER TABLE events DETACH PARTITION events_2023 CONCURRENTLY;

pg_partman — tự động hóa partition management:

pg_partman là extension standard cho production — tự động tạo partition mới, detach partition cũ, chạy qua background worker.

sql
CREATE EXTENSION pg_partman;

-- Tạo partitioned table và setup pg_partman quản lý
SELECT partman.create_parent(
  p_parent_table => 'public.events',
  p_control      => 'created_at',
  p_type         => 'range',
  p_interval     => 'monthly',
  p_premake      => 3           -- tạo trước 3 partition tương lai
);

-- Cập nhật config
UPDATE partman.part_config
SET retention            = '12 months',
    retention_keep_table = false,   -- drop partition cũ thay vì detach
    infinite_time_partitions = true
WHERE parent_table = 'public.events';

-- Chạy maintenance (thường đặt trong cron hoặc pg_partman background worker)
CALL partman.run_maintenance_proc();

4.2 Bulk Loading

sql
-- COPY nhanh hơn INSERT nhiều lần
COPY orders FROM '/path/to/orders.csv' WITH (FORMAT csv, HEADER true);

-- Tối ưu bulk insert:
-- 1. Drop indexes trước, rebuild sau
-- 2. Dùng UNLOGGED table (không ghi WAL)
-- 3. Tăng maintenance_work_mem
-- 4. Tắt autovacuum tạm thời

BEGIN;
SET maintenance_work_mem = '1GB';
ALTER TABLE staging DISABLE TRIGGER ALL;
COPY staging FROM '/data/file.csv' CSV;
ALTER TABLE staging ENABLE TRIGGER ALL;
COMMIT;

-- Rebuild indexes sau bulk load
REINDEX TABLE CONCURRENTLY staging;
ANALYZE staging;

4.3 Parallel Query cho analytics

sql
-- Parallel aggregation
SET max_parallel_workers_per_gather = 8;

SELECT region, sum(revenue), avg(order_value)
FROM sales
WHERE created_at >= '2024-01-01'
GROUP BY region;
-- Với plan tốt: Finalize GroupAggregate ← Gather ← Partial GroupAggregate

-- Parallel index scan (PG 14+)
SET enable_parallel_hash = on;

4.4 Time-series patterns

sql
-- BRIN index — cực nhỏ, hiệu quả với insert-ordered data
CREATE INDEX idx_ts_brin ON metrics USING BRIN (recorded_at)
WITH (pages_per_range = 128);

-- Partitioning theo thời gian + BRIN = combo mạnh nhất cho time-series

-- Continuous aggregation với materialized view
CREATE MATERIALIZED VIEW hourly_metrics AS
SELECT
  date_trunc('hour', recorded_at) AS hour,
  sensor_id,
  avg(value) AS avg_value,
  max(value) AS max_value
FROM metrics
GROUP BY 1, 2;

-- Refresh incremental
REFRESH MATERIALIZED VIEW CONCURRENTLY hourly_metrics;

TimescaleDB nếu cần: hypertable tự động chunk theo thời gian, continuous aggregates real-time.


4.5 Sharding với Citus

Khi single-node PG không đủ — horizontal scaling.

sql
-- Tạo distributed table
SELECT create_distributed_table('orders', 'customer_id');

-- Colocation: đặt related tables cùng shard
SELECT create_distributed_table('order_items', 'customer_id',
  colocate_with => 'orders');

-- Query tự động route đến đúng shard
SELECT count(*) FROM orders WHERE customer_id = 12345;

Phase 5 — Scalability & Operations

5.1 Replication

sql
-- Streaming replication: monitor lag
SELECT client_addr,
       state,
       sent_lsn - write_lsn AS write_lag,
       write_lsn - flush_lsn AS flush_lag,
       flush_lsn - replay_lsn AS replay_lag
FROM pg_stat_replication;

-- Logical replication: chỉ replicate specific tables
CREATE PUBLICATION my_pub FOR TABLE orders, users;
-- Trên subscriber:
CREATE SUBSCRIPTION my_sub
  CONNECTION 'host=primary dbname=mydb'
  PUBLICATION my_pub;

Failover tự động: Dùng Patroni + etcd/Consul.


5.2 Connection Pooling — PgBouncer

PgBouncer là must-have khi có nhiều connections.

Modes:

  • session: 1 PG connection / 1 client session — ít lợi nhất
  • transaction: connection trả về pool sau mỗi transaction — recommended
  • statement: connection trả về sau mỗi statement — không hỗ trợ transactions

Transaction mode — incompatibilities cần biết:

Transaction mode trả connection về pool sau mỗi transaction, nên các tính năng cần connection persist sẽ không hoạt động đúng:

Tính năngVấn đề
PREPARE / EXECUTE (prepared statements)Statement chuẩn bị ở connection A, execute ở connection B → lỗi
LISTEN / NOTIFYLISTEN mất khi connection trả về pool
Advisory locks per-sessionLock gắn với connection, không phải session
SET config (e.g. SET search_path)Reset sau mỗi transaction
Temporary tablesDrop khi connection trả về pool

Workaround: Dùng DISCARD ALL hoặc cấu hình server_reset_query = DISCARD ALL trong pgbouncer.ini. Với prepared statements: dùng prepared_statements = off trong driver hoặc dùng protocol-level prepared statements (PgBouncer 1.21+ hỗ trợ).

Pool sizing formula:

pool_size = (num_cores * 2) + effective_spindle_count

pgbouncer.ini:

ini
[databases]
mydb = host=localhost port=5432 dbname=mydb

[pgbouncer]
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 25
server_idle_timeout = 600

5.3 Config tuning — postgresql.conf

ini
# Memory
shared_buffers = 4GB                    # 25% RAM
effective_cache_size = 12GB             # 75% RAM (hint cho planner)
work_mem = 16MB                         # per sort/hash operation, PER CONNECTION — tổng RAM = work_mem × connections × sorts_per_query → OOM nếu set quá cao
maintenance_work_mem = 1GB              # VACUUM, CREATE INDEX

# WAL & Checkpoint
wal_buffers = 64MB
checkpoint_completion_target = 0.9
max_wal_size = 4GB
min_wal_size = 1GB

# Parallel
max_parallel_workers = 8
max_parallel_workers_per_gather = 4
max_parallel_maintenance_workers = 4

# Planner
random_page_cost = 1.1                  # SSD: set 1.1, HDD: giữ 4.0
effective_io_concurrency = 200          # SSD: 200, HDD: 2

# Logging
log_min_duration_statement = 1000       # Log queries chậm hơn 1s
log_autovacuum_min_duration = 500

5.4 Monitoring & Profiling

Extensions cần cài:

sql
CREATE EXTENSION pg_stat_statements;
CREATE EXTENSION auto_explain;

Queries hữu ích hàng ngày:

sql
-- Top 10 queries chậm nhất
SELECT query,
       calls,
       round(total_exec_time::numeric, 2) AS total_ms,
       round(mean_exec_time::numeric, 2) AS mean_ms,
       round(stddev_exec_time::numeric, 2) AS stddev_ms,
       rows
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;

-- Cache hit ratio (nên > 99%)
SELECT
  sum(heap_blks_hit) / nullif(sum(heap_blks_hit) + sum(heap_blks_read), 0) AS ratio
FROM pg_statio_user_tables;

-- Bảng lớn nhất
SELECT tablename,
       pg_size_pretty(pg_total_relation_size(tablename::regclass)) AS total_size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(tablename::regclass) DESC
LIMIT 20;

-- Long running queries
SELECT pid, now() - query_start AS duration, state, query
FROM pg_stat_activity
WHERE state != 'idle'
  AND query_start < now() - interval '5 minutes'
ORDER BY duration DESC;

Tools:

  • pgBadger: phân tích log file → HTML report
  • pganalyze: SaaS monitoring, giải thích plan tự động
  • Prometheus + postgres_exporter: metrics cho Grafana
  • pspg: pager đẹp hơn cho psql

5.5 Zero-downtime Migrations

sql
-- Thêm column — safe (PG 11+ với DEFAULT)
ALTER TABLE orders ADD COLUMN notes text DEFAULT '';

-- Thêm index — PHẢI dùng CONCURRENTLY
CREATE INDEX CONCURRENTLY idx_orders_status ON orders (status);
-- Không dùng CONCURRENTLY → lock cả bảng!

-- Thêm NOT NULL constraint — cẩn thận
-- Cách safe: CHECK constraint trước, sau đó validate
ALTER TABLE orders ADD CONSTRAINT orders_user_id_not_null
  CHECK (user_id IS NOT NULL) NOT VALID;
-- Sau đó:
ALTER TABLE orders VALIDATE CONSTRAINT orders_user_id_not_null;

-- Rename column — cần code deploy phối hợp
-- 1. Thêm column mới, sync data
-- 2. Deploy code đọc cả hai column
-- 3. Migrate data hoàn toàn
-- 4. Drop column cũ

-- Lock timeout — tránh queue dài
SET lock_timeout = '2s';
SET statement_timeout = '30s';

5.6 Zero-downtime Major Version Upgrade (Logical Replication)

pg_upgrade nhanh nhưng yêu cầu downtime. Với production không thể dừng, dùng logical replication để upgrade không downtime:

[PG 14 - primary]  →  logical replication  →  [PG 17 - standby]

                                               khi đồng bộ xong → failover
sql
-- Trên PG 14 (source): tạo publication cho tất cả tables
CREATE PUBLICATION upgrade_pub FOR ALL TABLES;

-- Trên PG 17 (target): tạo schema giống hệt source (pg_dump --schema-only)
-- Sau đó subscribe:
CREATE SUBSCRIPTION upgrade_sub
  CONNECTION 'host=pg14-host port=5432 dbname=mydb user=replicator'
  PUBLICATION upgrade_pub;

-- Monitor replication lag
SELECT subname, received_lsn, latest_end_lsn,
       (latest_end_lsn - received_lsn) AS lag_bytes
FROM pg_stat_subscription;

-- Khi lag = 0: switch app connection sang PG 17, drop subscription
DROP SUBSCRIPTION upgrade_sub;

Lưu ý:

  • Sequences không replicate tự động → cần sync thủ công trước khi failover
  • DDL changes trong thời gian replication phải apply cả hai nơi
  • Logical replication không support: large objects, DDL, unlogged tables

Tài nguyên tham khảo

NguồnNội dung
use-the-index-luke.comIndexing từ gốc rễ, ngôn ngữ dễ hiểu
pganalyze.com/docsQuery optimization, planner
postgresqlco.nfGiải thích từng config parameter
pgpedia.infoCatalog tables, system views
PostgreSQL docs — src/backend/optimizer/Source code planner nếu muốn hiểu sâu nhất
Book: "PostgreSQL 14 Internals" — Egor RogovInternals toàn diện nhất
Book: "High Performance PostgreSQL"Practical performance tuning

Thứ tự học gợi ý

EXPLAIN ANALYZE (đọc được output)

MVCC + Dead tuples + VACUUM

Index types (B-tree → GIN/GiST → Partial/Expression)

Locking & Deadlock

Planner statistics + Join strategies

CTE / Window functions optimization

Config tuning (postgresql.conf)

Partitioning (khi data đủ lớn)

Replication + Connection pooling

Monitoring + Zero-downtime migrations

Nguyên tắc vàng: Đo trước, optimize sau. Không bao giờ tối ưu thứ chưa đo được.

Personal notes by thanhlt