Đặc thù PostgreSQL và Performance
1. Index — đa dạng nhất trong các RDBMS
Postgres có 6 loại index, mỗi loại tối ưu cho usecase khác nhau:
| Loại | Khi nào |
|---|---|
| B-tree (default) | Cột scalar, range query, equality |
| Hash | Equality thuần (= chỉ thôi) |
| GIN | Array, JSONB, full-text — multi-value indexing |
| GiST | Geometric, range, full-text |
| SP-GiST | Space-partitioned (quadtree, k-d tree) |
| BRIN | Bảng cực lớn, dữ liệu sắp xếp tự nhiên (log time-series) |
-- B-tree (default)
CREATE INDEX idx_orders_user ON orders (user_id);
-- GIN cho JSONB
CREATE INDEX idx_events_payload ON events USING GIN (payload);
-- Hoặc compact hơn nếu chỉ dùng @>
CREATE INDEX idx_events_payload ON events USING GIN (payload jsonb_path_ops);
-- GIN cho array
CREATE INDEX idx_posts_tags ON posts USING GIN (tags);
-- GIST cho geometry / range
CREATE INDEX idx_locations_geom ON locations USING GIST (geom);
-- BRIN cho time-series (cực gọn)
CREATE INDEX idx_logs_created ON logs USING BRIN (created_at);
Partial index
-- Chỉ index khách hàng active — index nhỏ + nhanh hơn
CREATE INDEX idx_users_active_email ON users (email) WHERE status = 'active';
Expression index
-- Index trên expression — giải bài "non-sargable" hay gặp
CREATE INDEX idx_users_lower_email ON users (LOWER(email));
-- Query có thể dùng index này:
SELECT * FROM users WHERE LOWER(email) = 'a@b.com';
WHERE LOWER(email) = ? chậm?""Tạo expression index
CREATE INDEX ... ON users (LOWER(email)). Postgres dùng được index trên expression — SQL Server cần computed column PERSISTED, MySQL cần computed column."
B-tree Skip Scan (Postgres 18)
Postgres 18+ planner hỗ trợ B-tree Skip Scan — dùng composite index hiệu quả ngay cả khi filter thiếu left-most column:
CREATE INDEX idx_a_b ON t (a, b);
-- Pre-18: không dùng được idx_a_b vì thiếu điều kiện trên 'a'
-- PG 18+: planner có thể Skip Scan — duyệt distinct 'a', tìm 'b' trong từng group
SELECT * FROM t WHERE b = 10;
Cơ chế: planner duyệt qua các distinct value của cột a trong index, với mỗi value tìm b = 10 bên trong. Hiệu quả khi số distinct value của a ít.
b. Chỉ hiệu quả khi cardinality của left-most column thấp. Nếu query WHERE b = 10 thường xuyên → vẫn nên tạo index riêng ON t (b).CONCURRENTLY — tạo index không lock
-- Production: dùng CONCURRENTLY để không block write
CREATE INDEX CONCURRENTLY idx_orders_status ON orders(status);
Chậm hơn nhưng không lock bảng. Cần thiết cho live DB.
2. JSONB — sức mạnh Postgres
Index strategy
-- Toàn bộ JSONB
CREATE INDEX idx_payload ON events USING GIN (payload);
-- Chỉ key cụ thể (nhỏ hơn)
CREATE INDEX idx_payload_type ON events USING BTREE ((payload->>'type'));
-- Query path
CREATE INDEX idx_payload_user_id
ON events USING BTREE ((payload->'user'->>'id'));
Operator quan trọng
| Operator | Ý nghĩa | Index |
|---|---|---|
-> | Get JSONB | – |
->> | Get TEXT | B-tree |
@> | Contains | GIN |
<@ | Contained by | GIN |
? | Key exists | GIN |
| `? | ` | Any key |
?& | All keys | GIN |
@? | JSON path matches | GIN |
Update JSONB
-- jsonb_set
UPDATE events
SET payload = jsonb_set(payload, '{processed}', 'true', true)
WHERE id = 1;
-- Merge (||)
UPDATE events
SET payload = payload || '{"reviewed": true}'::jsonb;
-- Remove key (-)
UPDATE events SET payload = payload - 'temp_field';
JSON COPY TO — Postgres 19
Trước PG 19, export JSON cần row_to_json() + json_agg() — build toàn bộ JSON array trong memory rồi mới trả về. Với bảng hàng triệu row, query dễ OOM.
PG 19 hỗ trợ native JSON export qua COPY TO:
-- NDJSON output (default) — streaming từng row, memory-efficient
COPY users TO STDOUT WITH (FORMAT JSON);
-- JSON array output — [] bao toàn bộ
COPY users TO STDOUT WITH (FORMAT JSON, FORCE_ARRAY);
-- Có thể pipe thẳng vào file
COPY users TO '/tmp/users.json' WITH (FORMAT JSON);
Ưu điểm:
- Streaming: không build toàn bộ JSON trong memory — an toàn cho bảng lớn.
- Nhanh hơn
json_agg()nhiều lần do không cần aggregate. - PG 19 còn cho phép
COPY TOtrực tiếp trên partitioned table (trước đây phải COPY từng partition).
"Dùng
COPY TO STDOUT WITH (FORMAT JSON)(PG 19+) — streaming từng row ra NDJSON, không build toàn bộ JSON trong memory nhưjson_agg()cũ. Có thể pipe thẳng ra file hoặc HTTP response. HoặcFORCE_ARRAYnếu cần JSON array."
3. Array — không cần bảng phụ
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
tags TEXT[] DEFAULT '{}'
);
-- Operator
SELECT * FROM posts WHERE tags @> ARRAY['vue']; -- contains
SELECT * FROM posts WHERE tags && ARRAY['vue','react']; -- overlap
SELECT * FROM posts WHERE 'vue' = ANY(tags);
SELECT * FROM posts WHERE NOT ('react' = ALL(tags));
-- unnest — biến array thành rows
SELECT unnest(tags) AS tag FROM posts WHERE id = 1;
-- Aggregate ngược lại
SELECT array_agg(name ORDER BY name) FROM users;
- Array OK khi: số phần tử ít (< 100), không cần JOIN/index từng giá trị riêng, atomic.
- Bảng phụ khi: cần FK, query phức tạp theo từng phần tử, set lớn.
4. UUID — Postgres 18 mới có uuidv7()
CREATE TABLE accounts (
id UUID PRIMARY KEY DEFAULT uuidv7(), -- 18+
-- DEFAULT gen_random_uuid() cho pre-18 (v4 random)
email TEXT
);
uuidv7().5. Extension — feature qua plugin
-- Top extension dev thường dùng
CREATE EXTENSION IF NOT EXISTS pg_trgm; -- fuzzy search, similarity
CREATE EXTENSION IF NOT EXISTS pgcrypto; -- crypto, hash
CREATE EXTENSION IF NOT EXISTS postgis; -- GIS, geometry
CREATE EXTENSION IF NOT EXISTS pg_stat_statements; -- query monitoring
CREATE EXTENSION IF NOT EXISTS vector; -- AI embedding (pgvector)
CREATE EXTENSION IF NOT EXISTS timescaledb; -- time-series
CREATE EXTENSION IF NOT EXISTS unaccent; -- bỏ dấu tiếng Việt
pg_trgm — fuzzy search
CREATE EXTENSION pg_trgm;
CREATE INDEX idx_products_name_trgm ON products USING GIN (name gin_trgm_ops);
-- Tìm gần đúng
SELECT name, similarity(name, 'iPone') AS sim
FROM products
WHERE name % 'iPone' -- threshold default 0.3
ORDER BY sim DESC;
pgvector — AI embedding
CREATE EXTENSION vector;
CREATE TABLE docs (
id BIGSERIAL PRIMARY KEY,
content TEXT,
embedding vector(1536) -- OpenAI text-embedding-3-small
);
-- IVFFlat hoặc HNSW index
CREATE INDEX ON docs USING hnsw (embedding vector_cosine_ops);
-- Semantic search
SELECT id, content, embedding <=> '[...]'::vector AS distance
FROM docs
ORDER BY embedding <=> '[...]'::vector
LIMIT 5;
6. Temporal table (mô phỏng) + Postgres 18 temporal constraints
-- 18+: PK temporal — không trùng theo time range
CREATE TABLE room_reservations (
room_id INT,
valid_during TSRANGE,
PRIMARY KEY (room_id, valid_during WITHOUT OVERLAPS)
);
-- 18+: FK temporal
CREATE TABLE bookings (
... ,
FOREIGN KEY (room_id, period) REFERENCES room_reservations
(room_id, valid_during) PERIOD (period)
);
7. EXPLAIN — đọc plan
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT * FROM orders
WHERE user_id = 1 AND status = 'paid'
ORDER BY created_at DESC LIMIT 10;
Output example:
Limit (cost=0.42..8.45 rows=10 width=120) (actual time=0.05..0.10 rows=10 loops=1)
-> Index Scan using idx_orders_user_created on orders
(cost=0.42..234.50 rows=300 width=120)
Index Cond: (user_id = 1)
Filter: (status = 'paid')
Rows Removed by Filter: 50
Buffers: shared hit=15
Planning Time: 0.15 ms
Execution Time: 0.13 ms
Đọc top-down: outer node nhận row từ inner node.
Tools để visualize
- explain.dalibo.com — paste plan, được tree đẹp.
- pgAdmin — built-in Plan Explorer.
8. pg_plan_advice — Query Plan Hints (Postgres 19)
Đây là một trong những tính năng major nhất của PG 19: official plan hints qua contrib module pg_plan_advice. Không giống Oracle/MySQL nhúng hint vào SQL comment — Postgres để hint trong GUC setting, SQL code vẫn sạch.
-- Bước 1: Generate advice từ plan hiện tại
EXPLAIN (COSTS OFF, PLAN_ADVICE)
SELECT * FROM orders o JOIN customers c ON o.cust_id = c.id;
-- Output example: JOIN_ORDER(o c) HASH_JOIN(c) SEQ_SCAN(o c)
-- Bước 2: Lock plan theo advice
SET pg_plan_advice.advice = 'JOIN_ORDER(o c) HASH_JOIN(c) SEQ_SCAN(o c)';
Cơ chế
- Planner tự generate "advice string" từ plan nó chọn → bạn có thể review rồi lock.
- Module có feedback mechanism: log cho biết hint có được honor không và vì sao bị bỏ qua.
- Hint nằm ngoài SQL → đổi hint không cần deploy code, SQL portable giữa các môi trường.
So sánh với Oracle/MySQL hints
| pg_plan_advice (PG 19) | Oracle hints | MySQL hints | |
|---|---|---|---|
| Vị trí hint | GUC setting | SQL comment /*+ */ | SQL comment /*+ */ |
| SQL có sạch không | Có | Không | Không |
| Đổi hint cần deploy | Không | Có | Có |
| Feedback nếu bị bỏ qua | Có (log) | Có (note in plan) | Có (warning) |
| Generate tự động | Có (PLAN_ADVICE) | Không | Không |
Usecase thực tế
-- Pin join order khi planner chọn sai trên dữ liệu skewed
SET pg_plan_advice.advice = 'JOIN_ORDER(orders customers) NESTED_LOOP(customers)';
-- Force hash join cho batch query nightly
SET pg_plan_advice.advice = 'HASH_JOIN(events)';
-- Theo dõi hint có được honor không
-- Log: "pg_plan_advice: hint 'SEQ_SCAN(o c)' not honored because index scan is cheaper"
"Postgres 19 có
pg_plan_advice— hint nằm trong GUC setting thay vì SQL comment. Planner tự generate advice string từ plan nó chọn → đọc hiểu được. Lợi thế: SQL sạch, đổi hint không deploy, có feedback mechanism nếu hint bị planner bỏ qua. Oracle/MySQL nhúng hint vào SQL comment → SQL bẩn, đổi hint phải deploy."
9. REPACK — Online Table Rebuilding (Postgres 19)
REPACK gộp chức năng của VACUUM FULL (thu hồi disk space) và CLUSTER (sắp xếp lại row theo index) thành một lệnh duy nhất, với chế độ CONCURRENTLY không lock bảng.
-- Reclaim space (thay VACUUM FULL)
REPACK orders;
-- Reorder theo index (thay CLUSTER)
REPACK orders USING INDEX orders_created_at_idx;
-- Online mode — chỉ lock ACCESS EXCLUSIVE vài ms lúc swap file cuối
REPACK (CONCURRENTLY) orders USING INDEX orders_created_at_idx;
So sánh với VACUUM FULL / CLUSTER
| VACUUM FULL | CLUSTER | REPACK | REPACK CONCURRENTLY | |
|---|---|---|---|---|
| Reclaim space | Có | Không | Có | Có |
| Reorder row | Không | Có | Có (USING INDEX) | Có (USING INDEX) |
| Lock bảng | ACCESS EXCLUSIVE (suốt) | ACCESS EXCLUSIVE (suốt) | ACCESS EXCLUSIVE (suốt) | ACCESS EXCLUSIVE (vài ms cuối) |
| Đọc/ghi trong lúc chạy | Không | Không | Không | Có |
Cơ chế CONCURRENTLY
- Tạo bảng mới, copy row theo thứ tự index.
- Trigger ghi log vào queue mọi thay đổi trên bảng cũ trong lúc copy.
- Khi copy xong, apply queue (replay thay đổi).
- Swap file — ACCESS EXCLUSIVE lock chỉ vài ms.
"Dùng
REPACK (CONCURRENTLY)— PG 19 gộp VACUUM FULL + CLUSTER. Bảng vẫn đọc/ghi được trong suốt quá trình. Chỉ lock ACCESS EXCLUSIVE vài ms lúc swap file cuối. Trước PG 19:pg_repackextension; PG 19: built-in."
10. Query optimization checklist
- ✅ EXPLAIN ANALYZE để xem plan thật.
- ✅ Tránh Seq Scan trên bảng lớn — check index.
- ✅ Statistics fresh —
ANALYZEđịnh kỳ (auto-vacuum lo, nhưng manual khi bulk load). - ✅ JOIN cùng kiểu dữ liệu — tránh implicit cast.
- ✅ Pagination dùng keyset (
WHERE id > last_id) cho bảng cực lớn — OFFSET chậm khi lệch sâu. - ✅ Composite index theo left-most prefix.
- ✅ Partial index nếu filter cố định.
- ✅
LIMITđể dừng sớm. - ✅ Connection pool (PgBouncer) cho production.
- ✅ Postgres 18/19 — bật AIO config (
io_method = io_uringtrên Linux). - ✅ PG 19 — dùng
pg_plan_adviceđể lock plan khi cần ổn định performance. - ✅ PG 19 — dùng
REPACK CONCURRENTLYthayVACUUM FULLhoặcCLUSTERđể tránh lock bảng. - ✅ PG 19 —
autovacuum_max_parallel_workers > 0cho bảng nhiều index. - ✅ PG 19 — enable
data_checksumsonline cho production. - ✅ PG 19 —
COPY TO FORMAT JSONthayjson_agg()khi export bảng lớn.
11. Performance tuning configuration
Tham số quan trọng (postgresql.conf)
# Memory
shared_buffers = 25% RAM # cache buffer
effective_cache_size = 75% RAM # hint planner
work_mem = 32MB # mỗi sort/hash op
maintenance_work_mem = 512MB # VACUUM, CREATE INDEX
# Checkpoint
checkpoint_timeout = 10min
max_wal_size = 4GB
# Postgres 18/19 — AIO
io_method = io_uring # Linux 5.10+ only
io_max_concurrency = 256
# Logging
log_min_duration_statement = 1000ms # log query > 1s
# Autovacuum — PG 19 parallel
autovacuum_max_parallel_workers = 4
Parallel Autovacuum — Postgres 19
Postgres 19 cho phép autovacuum chạy song song nhiều worker để vacuum index:
-- Cluster-wide default
ALTER SYSTEM SET autovacuum_max_parallel_workers = 4;
-- Per-table override (ưu tiên hơn)
ALTER TABLE events SET (autovacuum_parallel_workers = 6);
- Mỗi worker xử lý một index — bảng nhiều index vacuum nhanh hơn rất nhiều.
- Disabled by default (
autovacuum_max_parallel_workers = 0). autovacuum_parallel_workerslà per-table storage parameter.- Đặc biệt hiệu quả trên bảng heavily-indexed (10+ index) — thời gian vacuum giảm tuyến tính theo số worker.
"Postgres 19 hỗ trợ parallel autovacuum. Set
autovacuum_parallel_workers = 4(hoặc 6) trên bảng đó — mỗi worker vacuum một index song song. Kết hợp vớimaintenance_work_memđủ lớn."
Online Data Checksums — Postgres 19
Checksum giúp phát hiện data corruption ở storage level. Trước PG 19: bắt buộc initdb với --data-checksums hoặc shutdown cluster để enable → không thay đổi được trên running system.
PG 19 cho phép enable/disable checksum online, không cần restart:
-- Enable checksum với rate limit (không ảnh hưởng production)
SELECT pg_enable_data_checksums(cost_delay := 10, cost_limit := 1000);
-- Theo dõi tiến trình
SHOW data_checksums;
-- Giá trị: 'off' → 'inprogress-on' → 'on'
-- Disable nếu cần (cẩn thận!)
-- SELECT pg_disable_data_checksums();
cost_delay/cost_limit: throttle giống VACUUM — không làm quá tải I/O.- Tiến trình chạy background, có thể pause/resume.
- Có thể disable ngược lại nếu cần performance tuyệt đối.
Connection pooling
Postgres mỗi connection = 1 OS process (~10MB RAM). Production bắt buộc dùng pooler:
- PgBouncer — phổ biến nhất, transaction-level pooling.
- Pgpool-II — feature đầy đủ hơn.
- Supavisor (Supabase) — pgbouncer compatible.
12. Replication & HA
| Loại | Mô tả |
|---|---|
| Streaming replication | Async/sync, byte-level WAL stream tới replica |
| Logical replication | Theo bảng/row, dùng cho cross-version, partial sync |
| Synchronous commit | Master chờ replica confirm trước khi return |
Logical replication — cải tiến PG 19
-- Sequence synchronization: replicate sequence values cùng table
CREATE PUBLICATION my_pub FOR ALL TABLES, ALL SEQUENCES;
-- EXCEPT TABLE: loại trừ bảng không muốn replicate
CREATE PUBLICATION prod_pub FOR ALL TABLES
EXCEPT (TABLE audit_log, temp_imports);
-- Filter row trong publication (PG 15+ có WHERE clause)
CREATE PUBLICATION us_customers FOR TABLE customers
WHERE (country = 'US');
Dynamic WAL level (PG 19): effective_wal_level tự động tăng lên logical khi cần, không cần restart — giảm rủi ro quên set WAL level cho logical replication.
FOR ALL TABLES, ALL SEQUENCES— lần đầu tiên replicate sequence value, critical khi failover.EXCEPTclause — không cần liệt kê từng bảng muốn replicate, chỉ exclude những bảng không muốn.- Dynamic WAL level — tránh quên config, tự động nâng cấp khi tạo publication.
13. Postgres đặc thù — khác SQL Server / MySQL
| Postgres | SQL Server | MySQL | |
|---|---|---|---|
| Identifier | "Name" | [Name] | backtick `Name` |
| Case sensitive | identifier yes nếu quote | no | tuỳ table_name_case |
| String | 'text' (UTF-8) | N'text' | 'text' |
| Concat | ` | ` | |
| Auto-increment | GENERATED AS IDENTITY / SERIAL | IDENTITY | AUTO_INCREMENT |
| Boolean | BOOLEAN thật | BIT 0/1 | TINYINT(1) 0/1 |
| Now | NOW(), CURRENT_TIMESTAMP | SYSUTCDATETIME() | NOW() |
| Limit | LIMIT n OFFSET m | OFFSET .. FETCH | LIMIT n OFFSET m |
| UPSERT | ON CONFLICT DO UPDATE | MERGE | ON DUPLICATE KEY UPDATE |
| Schema vs DB | DB chứa nhiều schema | DB chứa nhiều schema | DB = schema |
| Concurrency | MVCC native | Lock-based default | InnoDB MVCC |