PostgreSQL Nâng Cao: Locks, Execution Plan và Partitioning
Hướng dẫn chi tiết về locks, execution plan và partitioning trong PostgreSQL cho senior developers
Nếu đã nắm PostgreSQL cơ bản, thì Advanced PostgreSQL có thể hiểu qua 3 mảng lớn:
- Locks & concurrency — nhiều transaction cùng truy cập dữ liệu thì PostgreSQL xử lý thế nào?
- Execution plan — PostgreSQL quyết định chạy câu SQL bằng cách nào, và làm sao biết query chậm ở đâu?
- Partitioning — làm sao chia một bảng rất lớn thành nhiều phần để truy vấn/quản lý hiệu quả hơn?
1. Locks — PostgreSQL khóa dữ liệu như thế nào?#
Hãy tưởng tượng có hai transaction:
-- Transaction A
BEGIN;
UPDATE accounts
SET balance = balance - 100
WHERE id = 1;sqlTrong lúc A chưa COMMIT, transaction B chạy:
BEGIN;
UPDATE accounts
SET balance = balance + 50
WHERE id = 1;sqlB sẽ phải chờ A xử lý xong.
Đây là một dạng row-level locking.
SELECT ... FOR UPDATE#
Một pattern rất phổ biến:
BEGIN;
SELECT *
FROM accounts
WHERE id = 1
FOR UPDATE;
-- xử lý logic
UPDATE accounts
SET balance = balance - 100
WHERE id = 1;
COMMIT;sqlFOR UPDATE nói với PostgreSQL:
“Tôi chuẩn bị sửa row này, đừng để transaction khác sửa nó đồng thời.”
Rất hữu ích khi có logic kiểu:
đọc → kiểm tra → thay đổitextVí dụ:
SELECT stock
FROM products
WHERE id = 10
FOR UPDATE;sqlSau đó:
UPDATE products
SET stock = stock - 1
WHERE id = 10;sqlNếu không lock đúng cách, hai request đồng thời có thể cùng thấy:
stock = 1textvà cả hai đều tưởng rằng sản phẩm còn hàng.
Các loại lock cần biết#
Không cần học thuộc tất cả ngay. Quan trọng nhất:
Row lock#
SELECT ...
FOR UPDATE;sqlhoặc:
SELECT ...
FOR SHARE;sqlDùng khi concurrency ở mức record.
Table lock#
Ví dụ:
LOCK TABLE accounts;sqlKhóa ở cấp bảng.
Thông thường application code không nên tùy tiện dùng table lock, vì dễ làm giảm concurrency.
Deadlock#
Đây là phần rất quan trọng.
Transaction A:
lock row 1
↓
đợi row 2textTransaction B:
lock row 2
↓
đợi row 1textTa có:
A ──locks──> Row 1
A ──waits──> Row 2
B ──locks──> Row 2
B ──waits──> Row 1textKhông ai tiến được.
PostgreSQL sẽ phát hiện deadlock và terminate một transaction.
Cách phòng tránh#
Đảm bảo mọi transaction lấy lock theo cùng một thứ tự.
Ví dụ luôn:
lock account nhỏ hơn trước
→ lock account lớn hơntextthay vì transaction này:
account 1 → account 2textvà transaction kia:
account 2 → account 1text2. Execution Plan — PostgreSQL thực sự chạy query thế nào?#
Đây là kỹ năng cực kỳ quan trọng khi tối ưu PostgreSQL.
Bạn có query:
SELECT *
FROM users
WHERE email = 'alice@example.com';sqlĐừng chỉ hỏi:
“Có index chưa?”
Hãy hỏi:
PostgreSQL đang thực sự thực hiện query này như thế nào?
Dùng:
EXPLAIN
SELECT *
FROM users
WHERE email = 'alice@example.com';sqlVí dụ PostgreSQL có thể trả:
Index Scan using users_email_idx on users
Index Cond: (email = 'alice@example.com')textĐiều đó nghĩa là PostgreSQL dùng index.
EXPLAIN ANALYZE#
Quan trọng hơn:
EXPLAIN ANALYZE
SELECT *
FROM users
WHERE email = 'alice@example.com';sqlEXPLAIN cho bạn kế hoạch dự kiến.
EXPLAIN ANALYZE sẽ thực thi query và đo thực tế.
Ví dụ:
Index Scan using users_email_idx on users
(cost=0.42..8.44 rows=1 width=100)
(actual time=0.035..0.037 rows=1 loops=1)textBạn nên tập trung vào:
costrowsactual timeloops- loại scan
Sequential Scan vs Index Scan#
Giả sử bảng có:
10,000,000 userstextQuery:
SELECT *
FROM users
WHERE id = 123;sqlPostgreSQL có thể dùng:
Index Scantextvì chỉ cần tìm một row.
Nhưng query:
SELECT *
FROM users
WHERE country = 'Vietnam';sqlNếu 70% bảng là Vietnam thì PostgreSQL có thể chọn:
Seq Scantextdù country có index.
Đây không nhất thiết là vấn đề.
Một sai lầm phổ biến:
“Seq Scan = query sai.”
Không đúng.
Nếu cần đọc phần lớn bảng, đọc tuần tự đôi khi rẻ hơn dùng index.
Những execution node nên biết#
Ít nhất hãy hiểu:
Seq Scan
Index Scan
Index Only Scan
Bitmap Index Scan
Nested Loop
Hash Join
Merge Join
Sort
AggregatetextVí dụ:
SELECT *
FROM orders o
JOIN users u ON u.id = o.user_id;sqlPostgreSQL có thể chọn:
Nested Looptexthoặc:
Hash Jointexthoặc:
Merge JointextTùy dữ liệu, statistics và cost model.
Một ví dụ tối ưu query#
Giả sử:
SELECT *
FROM orders
WHERE customer_id = 100
AND created_at >= '2026-01-01';sqlIndex:
CREATE INDEX idx_orders_customer
ON orders(customer_id);sqlCó thể tốt hơn nếu workload thường xuyên query cả hai điều kiện:
CREATE INDEX idx_orders_customer_created
ON orders(customer_id, created_at);sqlSau đó:
EXPLAIN ANALYZE
SELECT *
FROM orders
WHERE customer_id = 100
AND created_at >= '2026-01-01';sqlBạn muốn nhìn xem planner có tận dụng index mới hay không.
3. Partitioning — chia bảng lớn#
Giả sử:
orderstextcó:
2 billion rowstextvà dữ liệu có:
created_attextBạn có thể partition theo tháng:
orders
├── orders_2026_01
├── orders_2026_02
├── orders_2026_03
├── ...
└── orders_2026_08textThay vì một bảng khổng lồ.
Range partitioning#
Ví dụ:
CREATE TABLE orders (
id bigint,
created_at timestamp,
customer_id bigint,
amount numeric
) PARTITION BY RANGE (created_at);sqlTạo partition:
CREATE TABLE orders_2026_08
PARTITION OF orders
FOR VALUES FROM ('2026-08-01')
TO ('2026-09-01');sqlPartition tháng tiếp theo:
CREATE TABLE orders_2026_09
PARTITION OF orders
FOR VALUES FROM ('2026-09-01')
TO ('2026-10-01');sqlPartition pruning#
Đây là lý do partitioning hữu ích.
Query:
SELECT *
FROM orders
WHERE created_at >= '2026-08-01'
AND created_at < '2026-09-01';sqlPostgreSQL có thể chỉ đọc:
orders_2026_08textthay vì:
orders_2026_01
orders_2026_02
orders_2026_03
...
orders_2026_08textĐó gọi là partition pruning.
Nhưng partitioning không phải “magic performance button”#
Ví dụ:
SELECT *
FROM orders
WHERE customer_id = 123;sqlNếu partition theo:
created_attextthì PostgreSQL có thể vẫn phải tìm qua nhiều partition.
Do đó partition key phải phù hợp với workload.
Một câu hỏi quan trọng trước khi partition:
“Các query lớn nhất của tôi thường filter theo column nào?”
4. Ba chủ đề này liên quan với nhau thế nào?#
Đây mới là phần quan trọng.
Hãy tưởng tượng một hệ thống e-commerce:
orders: 2 billion rowstextBạn có:
SELECT *
FROM orders
WHERE customer_id = 123
AND created_at >= '2026-01-01';sqlBạn có thể cần:
Partitioning#
Chia:
orders
→ theo created_attextIndex#
Trong từng partition:
(customer_id, created_at)sqlExecution plan#
Kiểm tra:
EXPLAIN ANALYZEsqlxem PostgreSQL có:
Partition pruning
↓
Index Scan
↓
ít rowstexthay không.
5. Locks + Execution Plan cũng liên quan#
Một query chậm không chỉ gây khó chịu cho user.
Ví dụ:
Transaction A
↓
UPDATE
↓
giữ lock
↓
query chạy 30 giâytextTrong lúc đó:
Transaction B
↓
UPDATE cùng row
↓
WAITtextMột query không tối ưu có thể khiến lock được giữ lâu hơn, từ đó tạo ra:
slow query
↓
long transaction
↓
lock contention
↓
more waiting
↓
more slow requeststextĐây là lý do khi debug production PostgreSQL, không nên chỉ nhìn:
“Query này mất 5 giây.”
Mà phải hỏi thêm:
“Nó đang giữ lock gì trong 5 giây đó?”
6. Một roadmap học Advanced PostgreSQL#
Nếu mục tiêu là senior/backend engineer, mình sẽ học theo thứ tự:
flowchart TD
A[PostgreSQL] --> B[Transactions]
A --> C[Query Planning]
A --> D[Storage]
B --> E[Isolation]
B --> F[Locks]
B --> G[Deadlock]
C --> H[EXPLAIN]
C --> I[Indexes]
D --> J[MVCC]
D --> K[VACUUM]
D --> L[Bloat]
F --> M[Concurrency]
I --> N[Performance]
M --> O[Partitioning]
N --> O
O --> P[Large datasets]
Sau 3 chủ đề bạn hỏi, các chủ đề tiếp theo rất đáng học là:
- MVCC
- VACUUM / autovacuum
- Index internals — B-tree, GIN, GiST, BRIN
- Transaction isolation
- Deadlock debugging
- CTE / materialized CTE
- Window functions
- Query planner & statistics
- Table/index bloat
- Connection pooling
- Replication
- Read replicas
- Partition maintenance
Nếu học theo kiểu thực chiến, bộ ba EXPLAIN ANALYZE + locks + MVCC nên được ưu tiên trước, vì chúng giúp bạn debug phần lớn các vấn đề PostgreSQL production.