PostgreSQL Partitioning — Chia bảng to, query nhanh như chớp
Bạn có bao giờ rơi vô cảnh một cái bảng chứa cả trăm triệu dòng, query ngày càng chậm, xóa dữ liệu cũ thì đơ cả server?
Đừng vội chạy đi thêm index. Có một kỹ thuật mà dân database hay dùng để giải quyết bài toán này tận gốc: table partitioning. Hôm nay mình viết cho anh em xem nó là gì và áp dụng thực chiến ra sao.
Partitioning là gì?
Nói nôm na, partitioning là chia một cái bảng vật lý to bự thành nhiều phần nhỏ hơn (partition), nhưng bên ngoài vẫn nhìn nó như một bảng duy nhất. PostgreSQL (từ bản 10 trở lên) hỗ trợ sẵn declarative partitioning — tức là mình khai báo, nó tự lo phần chia.
Khi query, PostgreSQL gọi là partition pruning: nó chỉ quét đúng những partition cần thiết, không đụng tới phần tá lả còn lại. Kết quả là query nhanh hơn hẳn, đặc biệt với bảng có data theo thời gian.
Trường hợp nào nên partition?
Không phải bảng nào cũng cần. Mình hay khuyên anh em chỉ partition khi:
- Bảng rất lớn (vài chục triệu dòng trở lên) và chủ yếu ghi theo thời gian.
- Có luồng công việc xóa/archive dữ liệu cũ định kỳ.
- Query luôn đi kèm điều kiện lọc theo cột partition (ví dụ: theo ngày, theo tháng).
Nếu bảng bạn chỉ độ vài trăm nghìn dòng thì thêm index tốt hơn, đừng partition làm gì cho phức.
Thực chiến: Partition theo tháng
Giả sử mình có bảng orders — đơn hàng, data cứ mỗi tháng một chồng. Mình chia theo tháng:
CREATE TABLE orders (
id bigserial,
created_at timestamptz NOT NULL,
customer_id bigint,
total_amount numeric(12,2)
) PARTITION BY RANGE (created_at);
-- Tạo partition cho từng tháng
CREATE TABLE orders_2026_01 PARTITION OF orders
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE orders_2026_02 PARTITION OF orders
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
Xong, mình INSERT vô bảng orders như bình thường, PostgreSQL tự ném dòng vô đúng partition. Người dùng chả cần biết chuyện gì đang xảy ra.
Query theo tháng giờ đây chỉ quét đúng partition thôi:
EXPLAIN ANALYZE
SELECT count(*) FROM orders
WHERE created_at >= '2026-02-01'
AND created_at < '2026-03-01';
-- Chỉ quét orders_2026_02, không đụng mấy partition khác
Xóa dữ liệu cũ — DROP nhanh hơn DELETE gấp triệu lần
Đây mới là chỗ tuyệt vời nhất. Trước đây muốn dọn dữ liệu 3 tháng trước, mình phải:
DELETE FROM orders WHERE created_at < '2025-12-01';
Câu này mà bảng trăm triệu dòng thì… tối hôm đó đừng hòng sleep ngon. DELETE từng dòng, viết WAL mệt nghỉ, chậm và còn phình index.
Còn với partitioning, chỉ cần:
DROP TABLE orders_2025_11;
-- hoặc DETACH để giữ lại sau đó archive riêng
ALTER TABLE orders DETACH PARTITION orders_2025_11;
DROP là thao tác metadata, xong trong tích tắc, không phải đụng tới từng dòng dữ liệu. Dọn dữ liệu cũ giờ thành chuyện nhỏ.
Mấy cái bẫy cần né
Mình từng dính vài cái, ghi ra cho anh em khỏi dẫm:
-
Không có default partition nếu chắc chắn data không nằm ngoài dải. Có default thì lỡ insert trúng ngoài dải nó vẫn nuốt — nhưng dễ thành "thùng rác" chứa data lộn xộn, quên tạo partition mới.
-
Khóa chính / unique phải bao gồm cột partition.
idđơn lẻ không chạy được vì PK phải unique trên toàn bảng, mà partition rời nhau không kiểm tra được lẫn nhau. Thường phải thêmcreated_atvô. -
Index phải tạo cho từng partition, PostgreSQL không tự 'kế thừa' index của bảng cha. May là bạn có thể tạo index có
ONLYriêng, hoặc dùngCREATE INDEX ... ON orders (col)— nó tự tạo cho tất cả partition. -
Quên tạo partition mới khi tháng mới tới — lúc đó bất kỳ insert nào cũng tạch. Giải pháp: viết cron hoặc dùng pg_partman tự động tạo partition sẵn.
Lời kết
Partitioning không phải viên đạn bạc, nhưng với bảng to theo thời gian thì nó là công cụ cực kỳ đáng giá: query nhanh hơn, dọn dữ liệu nhẹ hều, chi phí thấp. Cứ thử tạo một bản partition theo tháng rồi đo EXPLAIN ANALYZE trước sau so sánh, anh em sẽ thấy sự khác biệt rõ rệt.
Có anh em nào áp dụng partitioning vô hệ thống của mình chưa? Chia sẻ thêm kinh nghiệm ở dưới nha!