Quay lại
Công Nghệ

What is Partitioning?

20 phút đọc11 thg 7, 2026
H

hoanggg2110

Tác giả

What is Partitioning?

"Không phải cứ nâng CPU hay thêm RAM là database sẽ nhanh hơn."

Vấn đề bắt đầu từ đâu?

Ban đầu, bảng orders chỉ có vài trăm nghìn bản ghi. Mọi thứ đều rất ổn — query dưới vài chục milliseconds, backup nhanh, VACUUM chạy trong vài phút là xong.

Hai năm sau, bức tranh đã hoàn toàn khác.

orders
──────────────────────────────
Rows      : 180,000,000
Size      : 90 GB
Indexes   : 18 GB

Ứng dụng vẫn chủ yếu truy vấn đơn hàng của 30 ngày gần nhất, nhưng mỗi lần mở dashboard nội bộ lại mất vài giây. Job dọn dữ liệu cũ chạy hàng giờ. VACUUM ngày càng lâu. Backup ngày càng nặng.

Trong khi đó:

CPU Usage      : 28%
RAM Usage      : 42%
Disk Usage     : 55%

Máy vẫn còn rất nhiều tài nguyên.

Nếu phản ứng đầu tiên của bạn là "tạo thêm index" hoặc "chắc PostgreSQL không đủ mạnh", thì rất có thể bạn đang nhìn nhầm chỗ. Vấn đề thật sự không nằm ở PostgreSQL, cũng không nằm ở phần cứng. Nó nằm ở cách chúng ta đang tổ chức dữ liệu.

Đó chính là lúc Partitioning phát huy tác dụng.

      API Server        Dashboard        Batch Job
           \                |               /
            \               |              /
             ▼              ▼             ▼
      ┌──────────────────────────────────────┐
      │           orders (180M rows)         │
      │              90 GB                   │
      │     Mọi query đều đi qua đây        │
      └──────────────────────────────────────┘

Một chút kiến thức trước khi bắt đầu

Trong bài này sẽ xuất hiện một số thuật ngữ của PostgreSQL. Nếu bạn chưa quen, đừng lo — chúng ta chỉ cần hiểu ý tưởng cơ bản là đủ.

OLTP — Hệ thống giao dịch trực tuyến

Ví điện tử (MoMo, ZaloPay...), internet banking, thương mại điện tử, đặt xe, đặt đồ ăn — tất cả đều là OLTP. Đặc điểm chung: rất nhiều request mỗi giây, mỗi request chỉ đọc hoặc ghi một lượng dữ liệu nhỏ, và yêu cầu thời gian phản hồi rất thấp (vài chục đến vài trăm milliseconds).

Đa số backend chúng ta xây dựng hằng ngày đều thuộc nhóm này. Đây cũng là loại hệ thống mà Partitioning được sử dụng nhiều nhất.

MVCC và Dead Tuple

Đây là một trong những điểm khiến PostgreSQL khác với nhiều database khác.

Khi bạn chạy UPDATE, PostgreSQL không ghi đè trực tiếp lên dòng cũ. Nó tạo ra một phiên bản mới của dòng dữ liệu. Phiên bản cũ không bị xóa ngay — nó trở thành một Dead Tuple.

Old Version (PENDING)
        │
        │   UPDATE
        ▼
New Version (COMPLETED)

Dead Tuple giống như một "xác" của bản ghi cũ. Nó không còn được các transaction mới nhìn thấy, nhưng vẫn đang chiếm dung lượng trên đĩa.

Đó là lý do vì sao sau hàng triệu lần UPDATE hoặc DELETE, kích thước bảng vẫn cứ tăng lên dù số lượng bản ghi gần như không đổi.

VACUUM

Để dọn các Dead Tuple, PostgreSQL sử dụng một tiến trình gọi là VACUUM.

UPDATE  →  Sinh ra Dead Tuple  →  VACUUM  →  Thu hồi không gian

Nếu bảng chỉ vài triệu dòng thì VACUUM diễn ra khá nhanh. Nhưng nếu bảng đã lên tới vài trăm triệu dòng thì mọi chuyện khác hẳn. VACUUM phải đọc qua một lượng dữ liệu khổng lồ, I/O tăng mạnh, Autovacuum chạy thường xuyên hơn, và database dễ xảy ra hiện tượng "bloat".

💡 VACUUM không phải là "tối ưu query". Nhiệm vụ chính của nó là dọn dẹp các phiên bản dữ liệu cũ để PostgreSQL có thể tái sử dụng không gian lưu trữ và duy trì hiệu năng ổn định.

Query Planner

Khi bạn viết một câu SQL, PostgreSQL không chạy ngay. Trước tiên nó sẽ phân tích câu lệnh và lập ra một Execution Plan: dùng Index Scan hay Sequential Scan, join kiểu gì, cần sort không, đọc partition nào...

Toàn bộ những quyết định đó được đưa ra trước khi query thực sự bắt đầu. Đó cũng là lý do chúng ta thường dùng EXPLAIN ANALYZE để xem PostgreSQL đang "nghĩ" gì.


Khi bảng càng lớn, bottleneck không còn nằm ở CPU

Giả sử bảng orders có 500 triệu bản ghi. Trong đó, 450 triệu là dữ liệu cũ hơn một năm. Chỉ khoảng 50 triệu thuộc 3 tháng gần nhất.

Nhưng khoảng 90% request đều giống thế này:

SELECT *
FROM orders
WHERE created_at >= NOW() - INTERVAL '30 days';

Bảng chứa 500 triệu dòng, nhưng phần lớn request chỉ quan tâm đến một phần rất nhỏ.

┌──────────────────────────────────────────────────────────┐
│                      orders (500M rows)                  │
│                                                          │
│ ████████████████████████████████████████████████████████ │
│ ████████████████████████████████████████████████████████ │
│ ████████████████████████████████████████████████████████ │
│ ████████████████████████████████████████████████████████ │
│                                                ████      │
│                                                ████      │
└──────────────────────────────────────────────────────────┘
                                                 ▲
                                      Query chỉ cần phần này

Vấn đề là đối với PostgreSQL, tất cả vẫn chỉ là một bảng duy nhất. Planner vẫn quản lý metadata toàn bộ bảng. Index vẫn xây trên toàn bộ dữ liệu. VACUUM vẫn đi qua toàn bộ. Backup vẫn backup toàn bộ.

Bạn không thực sự cần đến 95% dữ liệu còn lại trong hầu hết các request.

Đó chính là bài toán mà Partitioning được thiết kế để giải quyết.


"Nhưng tôi đã tạo Index rồi mà?"

Đây là hiểu lầm phổ biến nhất khi nói về Partitioning.

Index giúp PostgreSQL tìm vị trí dữ liệu nhanh hơn. Nhưng nó không làm dữ liệu nhỏ đi.

Giả sử tháng 1 có 18 triệu đơn hàng. Dù có B-Tree index trên created_at, PostgreSQL vẫn phải đọc 18 triệu dòng sau khi xác định vị trí bắt đầu.

Index chỉ trả lời câu hỏi: "Dữ liệu bắt đầu ở đâu?"

Nó không trả lời câu hỏi: "Những phần dữ liệu nào hoàn toàn không cần đụng tới?"

Đó mới là khác biệt lớn nhất giữa Index và Partitioning.


Partitioning là gì?

Nói đơn giản nhất: Partitioning là kỹ thuật chia một bảng lớn thành nhiều bảng nhỏ hơn, nhưng với ứng dụng, chúng vẫn được nhìn như một bảng duy nhất.

Đây là điểm rất nhiều người mới học dễ nhầm với Sharding (mình sẽ nói rõ ở cuối bài).

Nếu không partition, toàn bộ dữ liệu nằm trong một table vật lý. Khi partition theo created_at, PostgreSQL chia thành nhiều bảng vật lý:

                   orders (Logical Table)
                           │
        ┌──────────────────┼──────────────────┐
        │                  │                  │
        ▼                  ▼                  ▼
 ┌────────────┐     ┌────────────┐     ┌────────────┐
 │orders_2023 │     │orders_2024 │     │orders_2025 │
 └────────────┘     └────────────┘     └────────────┘

Điều quan trọng là ứng dụng không cần biết điều này. Bạn vẫn viết SELECT * FROM orders chứ không phải SELECT * FROM orders_2025. Toàn bộ việc định tuyến dữ liệu do PostgreSQL xử lý. Đây là điểm khiến Partitioning gần như trong suốt (transparent) đối với application.

PostgreSQL biết dữ liệu thuộc partition nào?

Khi bạn chạy:

INSERT INTO orders (...) VALUES (..., '2024-03-18');

PostgreSQL nhìn vào giá trị created_at, xác định nó nằm trong khoảng [2024-03-01, 2024-04-01), rồi tự động ghi vào orders_2024_03.

'2024-03-18'  →  nằm trong [03-01, 04-01)  →  ghi vào orders_2024_03

Ứng dụng hoàn toàn không cần biết partition nào tồn tại. Điều này đúng với cả UPDATE, DELETE và SELECT.


Partition Pruning — "Vũ khí bí mật" của Partitioning

Nếu chỉ chia bảng thành nhiều bảng nhỏ thì chưa đủ. Sức mạnh thật sự nằm ở một cơ chế gọi là Partition Pruning — quá trình PostgreSQL loại bỏ những partition không liên quan trước khi query bắt đầu.

Ví dụ. Bạn có 24 partition. Query:

SELECT *
FROM orders
WHERE created_at >= '2024-03-01'
  AND created_at < '2024-04-01';

Planner nhìn thấy điều kiện created_at → March 2024 và lập tức kết luận: "Chỉ có partition orders_2024_03 mới có khả năng chứa dữ liệu."

orders_2023_01      ✖ bỏ qua
orders_2023_02      ✖ bỏ qua
...
orders_2024_02      ✖ bỏ qua
orders_2024_03      ✔ SCAN
orders_2024_04      ✖ bỏ qua
...
orders_2024_12      ✖ bỏ qua

Thay vì phải quản lý toàn bộ dữ liệu, PostgreSQL chỉ cần quan tâm đến đúng một partition.

Không đọc. Không scan. Không kiểm tra. Không filter 23 partition còn lại. Ngay từ giai đoạn lập kế hoạch thực thi, PostgreSQL đã loại chúng khỏi execution plan.

Đây chính là lý do query trên bảng partition có thể nhanh hơn rất nhiều. Không phải vì PostgreSQL scan nhanh hơn, mà vì nó gần như không phải scan những dữ liệu không liên quan.

💡 "Pruning" không có nghĩa là xóa dữ liệu. Nó chỉ đơn giản là loại bỏ các partition không cần thiết khỏi execution plan.


Hãy tự nhìn PostgreSQL làm việc

Lý thuyết đến đây là đủ. Bây giờ chúng ta sẽ dùng đúng bộ script demo để quan sát trực tiếp.

Bước 1 — Tạo bảng Partition

CREATE TABLE orders (
    id            BIGSERIAL,
    user_id       BIGINT NOT NULL,
    created_at    TIMESTAMP NOT NULL,
    total_amount  NUMERIC(12,2) NOT NULL,
    status        VARCHAR(20) NOT NULL DEFAULT 'completed',
    PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);

Đây là Parent Table — gần như không chứa dữ liệu. Nó chỉ đóng vai trò "điểm vào" để PostgreSQL biết phải chuyển dữ liệu sang partition nào.

Tại sao created_at phải nằm trong Primary Key?

PostgreSQL yêu cầu Partition Key phải nằm trong Primary Key hoặc Unique Key. Nếu chỉ dùng (id), mỗi lần INSERT PostgreSQL sẽ phải kiểm tra tất cả partition để đảm bảo không trùng — điều này phá vỡ toàn bộ lợi ích của Partitioning.

Bước 2 — Tạo 12 partition + Default

Thay vì viết tay 12 câu CREATE TABLE, dùng vòng lặp:

DO $$
DECLARE
    start_date DATE;
    end_date DATE;
    partition_name TEXT;
BEGIN
    FOR i IN 0..11 LOOP
        start_date := '2024-01-01'::DATE + (i || ' months')::INTERVAL;
        end_date := start_date + '1 month'::INTERVAL;
        partition_name := 'orders_' || to_char(start_date, 'YYYY_MM');

        EXECUTE format(
            'CREATE TABLE %I PARTITION OF orders FOR VALUES FROM (%L) TO (%L);',
            partition_name, start_date, end_date
        );
    END LOOP;
END $$;

-- Luôn tạo Default Partition
CREATE TABLE orders_default PARTITION OF orders DEFAULT;

Mỗi partition quản lý một khoảng thời gian. Ví dụ:

orders_2024_03: [2024-03-01 00:00:00, 2024-04-01 00:00:00)

Cận trên là exclusive (TO). Các partition nối tiếp nhau mà không bị chồng lấn.

Tại sao cần DEFAULT PARTITION? Nếu quên tạo partition cho tháng mới mà không có Default, toàn bộ INSERT sẽ fail với lỗi no partition found for row. Default Partition là "lưới an toàn" — giống túi khí trên ô tô, hy vọng không bao giờ phải dùng nhưng có thì yên tâm hơn rất nhiều.

Bước 3 — Seed 5 triệu bản ghi

INSERT INTO orders (user_id, total_amount, status, created_at)
SELECT
    (1 + floor(random() * random() * 100000))::BIGINT,
    (random() * 1000 + 10)::NUMERIC(12,2),
    CASE WHEN random() > 0.9 THEN 'failed' ELSE 'completed' END,
    '2024-01-01'::TIMESTAMP + (random() * 366 * 24 * 60) * INTERVAL '1 minute'
FROM generate_series(1, 5000000);

ANALYZE orders;

Điểm thú vị nằm ở random() * random(). Nếu chỉ dùng random() thì mọi user_id có xác suất xuất hiện bằng nhau. Nhưng thực tế hiếm khi như vậy — ở ứng dụng ví điện tử hay thương mại điện tử, một nhóm nhỏ người dùng tạo ra phần lớn giao dịch (Power Law / Long Tail Distribution).

Nhân hai số ngẫu nhiên với nhau tạo phân bố nghiêng về giá trị nhỏ:

User #1        ████████████████████████
User #20       ███████████████
User #500      ███████
User #10000    ███
User #50000    ██
User #99000    █

Dữ liệu test gần với production hơn rất nhiều so với random đều.

Tại sao phải chạy ANALYZE? Sau khi insert 5 triệu dòng, Planner chưa biết dữ liệu phân bố ra sao. ANALYZE thu thập thống kê (số dòng, giá trị min/max, phân bố...) để Planner ước lượng chi phí chính xác. Nếu thiếu bước này, Planner có thể nghĩ bảng chỉ có 100 rows trong khi thực tế là 8 triệu — dẫn đến chọn sai execution plan.


Benchmark — Quan sát Partition Pruning bằng EXPLAIN ANALYZE

Kịch bản 1: Pruning hoạt động (query theo Partition Key)

EXPLAIN (ANALYZE, BUFFERS, TIMING)
SELECT count(*)
FROM orders
WHERE created_at >= '2024-03-01'
  AND created_at < '2024-03-15';

Execution Plan kỳ vọng:

Aggregate (...)
  -> Append (...)
       -> Seq Scan on orders_2024_03 (...)
            Filter: ((created_at >= '2024-03-01'...))

Subplans Removed: 12
Execution Time: ~15 ms

Dù bảng có 5 triệu dòng, Planner chỉ scan ~200k dòng của orders_2024_03. Dòng Subplans Removed: 12 chính là bằng chứng Partition Pruning đã xảy ra.

Kịch bản 2: Không pruning được (query không có Partition Key)

EXPLAIN (ANALYZE, BUFFERS, TIMING)
SELECT * FROM orders
WHERE user_id = 42;

Execution Plan kỳ vọng:

Append (...)
  -> Seq Scan on orders_2024_01 (...)   Filter: (user_id = 42)
  -> Seq Scan on orders_2024_02 (...)   Filter: (user_id = 42)
  -> Seq Scan on orders_2024_03 (...)   Filter: (user_id = 42)
  ...
  -> Seq Scan on orders_2024_12 (...)   Filter: (user_id = 42)
  -> Seq Scan on orders_default (...)   Filter: (user_id = 42)

Execution Time: ~180 ms

Planner không biết user 42 giao dịch vào tháng nào → buộc phải scan toàn bộ 13 partition.

Kịch bản 3: Gỡ rối — thêm Partition Key vào query

EXPLAIN (ANALYZE, BUFFERS, TIMING)
SELECT * FROM orders
WHERE user_id = 42
  AND created_at >= '2024-11-01'
  AND created_at < '2025-01-01';

Execution Plan kỳ vọng:

Append (...)
  -> Seq Scan on orders_2024_11 (...)
       Filter: ((user_id = 42) AND (created_at >= ...))
  -> Seq Scan on orders_2024_12 (...)
       Filter: ((user_id = 42) AND (created_at >= ...))

Subplans Removed: 11
Execution Time: ~25 ms

Nhờ có created_at, Planner gạt bỏ 10 tháng đầu năm. Chỉ tìm user_id trên 2 partition.

Bảng so sánh

Bài học: Partitioning không tăng tốc mọi query. Nó chỉ tăng tốc những query có thể Partition Pruning. Hiệu quả phụ thuộc gần như hoàn toàn vào việc bạn chọn Partition Key có đúng với workload hay không.


Runtime Partition Pruning — Prepared Statement vẫn được hưởng lợi

Trong production, gần như không ai hard-code giá trị trong SQL. Hibernate, Spring Data JPA, MyBatis — tất cả đều dùng Prepared Statement với parameter.

Vậy Partition Pruning có còn hoạt động không?

Có. Từ PostgreSQL 12 trở lên, PostgreSQL bổ sung Runtime Partition Pruning. Khi câu SQL được PREPARE, planner chưa biết giá trị. Nhưng đến lúc EXECUTE, nó mới biết tháng nào — và prune ngay lúc runtime.

PREPARE order_by_month(date, date) AS
SELECT * FROM orders
WHERE created_at >= $1 AND created_at < $2;

EXPLAIN ANALYZE
EXECUTE order_by_month('2024-07-01', '2024-08-01');
Prepare  →  Chưa biết giá trị  →  Execute  →  Biết là July  →  Prune ngay

💡 Nếu bạn đang dùng PostgreSQL 12+, Partition Pruning hoạt động rất tốt với Hibernate, Spring Data JPA hay MyBatis. Không cần hard-code giá trị thời gian trong SQL.

Query theo nhiều điều kiện thì sao?

SELECT *
FROM orders
WHERE created_at >= '2024-03-01'
  AND created_at < '2024-04-01'
  AND user_id = 84213;

Planner luôn cố gắng prune trước, sau đó mới filter:

created_at  →  Partition Pruning  →  orders_2024_03  →  Filter user_id  →  Result

Không có prune: 24 × 200k = 4.8 triệu rows phải xử lý.
Có prune: 1 partition × 200k rows, sau đó filter user_id.

Chi phí giảm đi rất nhiều. Đó là lý do Partition Key nên xuất hiện trong hầu hết các câu query.


Chọn Partition Key như thế nào?

Đây có lẽ là quyết định quan trọng nhất.

Mình từng thấy nhiều hệ thống partition xong... chậm hơn trước. Không phải vì PostgreSQL, mà vì chọn sai Partition Key.

Nguyên tắc rất đơn giản:

Partition theo cách dữ liệu được truy vấn, chứ không phải theo cách dữ liệu được lưu.

Ví dụ:

Đừng cố áp dụng một công thức cho mọi hệ thống. Workload quyết định tất cả.


Ba chiến lược Partitioning phổ biến

Hơn 95% hệ thống thực tế sẽ rơi vào ba loại sau.

1. Range Partitioning

PARTITION BY RANGE(created_at)

Gần như là lựa chọn mặc định cho mọi dữ liệu có yếu tố thời gian: Orders, Payments, Transactions, Audit Logs, Events, Sensor Data, Time Series...

Ưu điểm lớn nhất không phải query, mà là Retention.

Giả sử công ty chỉ giữ log trong hai năm. Nếu không partition:

DELETE FROM application_logs
WHERE created_at < NOW() - INTERVAL '2 years';

-- Database phải: tìm từng row → delete → ghi WAL → sinh Dead Tuple → VACUUM
-- Nếu vài trăm triệu dòng, job này có thể chạy hàng giờ.

Với Range Partitioning:

DROP TABLE application_logs_2023_06;

-- Toàn bộ dữ liệu biến mất gần như ngay lập tức.
-- Không quét. Không delete. Không Dead Tuple. Không VACUUM.

Nhược điểm: Nếu workload chủ yếu WHERE user_id = ? thì lợi ích giảm rất nhiều.

2. List Partitioning

PARTITION BY LIST(region)

Chia theo từng nhóm giá trị cụ thể. Phù hợp khi dashboard luôn lọc theo quốc gia, hoặc hệ thống SaaS nơi mọi request chỉ làm việc với đúng một tenant.

Nhược điểm: Dễ bị skew. VN chiếm 70%, SG chiếm 20%, TH chiếm 10% — partition VN sẽ lớn hơn rất nhiều, lợi ích giảm đi đáng kể.

3. Hash Partitioning

PARTITION BY HASH(user_id)

Không quan tâm giá trị cụ thể, chỉ dùng hash(value) % N để phân bố đều. Tốt khi không có yếu tố thời gian hay region, chỉ muốn chia đều. Đổi lại, bạn mất khả năng Partition Pruning theo khoảng thời gian.

So sánh nhanh


Partitioning không phải "viên đạn bạc"

Giống như Index, Cache hay Read Replica — Partitioning là một công cụ tối ưu. Nó chỉ phát huy hiệu quả khi đúng với workload. Dùng sai, bạn có thể khiến hệ thống chậm hơn trước.

Trade-off 1: Query không dùng Partition Key

Bảng partition theo created_at, nhưng app thường chạy WHERE user_id = ?. PostgreSQL buộc phải đọc toàn bộ 24 partition. Partitioning lúc này gần như không mang lại lợi ích gì.

Trade-off 2: Quá nhiều partition

Partition theo ngày cho 5 năm dữ liệu = 1.825 partition. Trước mỗi query, Planner phải đọc metadata, kiểm tra constraint, xây execution plan... Bạn sẽ thấy trong EXPLAIN ANALYZE:

Planning Time : 120 ms
Execution Time : 18 ms

Database mất nhiều thời gian lập kế hoạch hơn cả thực thi query. Đây là dấu hiệu điển hình của over-partitioning.

Kinh nghiệm thực tế:

Loại dữ liệu

Gợi ý partition

Transaction, Orders, Payment

Theo tháng

Audit Log

Theo tháng hoặc tuần

IoT / Time-series cực lớn

Theo ngày

Nếu dữ liệu một ngày chỉ vài nghìn dòng thì gần như không có lý do gì để tạo partition riêng.

Trade-off 3: Mỗi partition là một bảng thật

Trong PostgreSQL, orders_2024_03 không phải là một "folder". Nó là một table thật — có heap riêng, index riêng, statistics riêng, file riêng trên disk, autovacuum riêng.

120 partition × 3 index = PostgreSQL đang quản lý 120 tables + 360 indexes thay vì 1 table + 3 indexes. Ảnh hưởng đến Backup, Restore, Monitoring, Reindex, Migration, Autovacuum.

Trade-off 4: Không có Global Index

Khi bạn CREATE INDEX ON orders(user_id), PostgreSQL tạo index riêng cho mỗi partition. Không có một Global Index duy nhất như Oracle. Query theo non-partition-key có thể phải kiểm tra index của nhiều partition.

Trade-off 5: Migrate bảng thường sang Partition Table rất phức tạp

Không thể chỉ chạy ALTER TABLE là xong. Phải: tạo bảng mới → tạo partition → copy dữ liệu → đồng bộ phát sinh → swap tên → xóa bảng cũ. Nếu dữ liệu vài TB, đây là một dự án migration thực sự.


Những sai lầm thường gặp

Sai lầm #1: Partition vì "nghe nói nhanh". Bảng 2 triệu rows, query dưới 50ms — vẫn partition. Kết quả: phức tạp hơn, hiệu năng không đổi. Hãy viết query tốt hơn, tạo index đúng, kiểm tra execution plan trước.

Sai lầm #2: Chọn sai Partition Key. Partition theo status trong khi 99% query theo created_at. Pruning gần như không bao giờ xảy ra. Toàn bộ lợi ích biến mất.

Sai lầm #3: Quên tạo partition mới. Đầu tháng mới, partition chưa tồn tại, không có DEFAULT PARTITION → toàn bộ INSERT fail. Rất nhiều team tạo sẵn partition cho 6–12 tháng tiếp theo bằng CronJob hoặc migration tự động.

Sai lầm #4: Không monitor. Partition nào quá lớn? Partition nào không còn dùng? Partition nào chưa được tạo? Partition nào chứa dữ liệu lẽ ra phải archive? Đừng tạo partition rồi quên luôn.


Best Practices

Metric nên theo dõi sau khi triển khai


PostgreSQL làm gì phía sau?

Phần này dành cho ai muốn đào sâu hơn một chút.

Parent Table gần như không chứa dữ liệu

Khi tạo CREATE TABLE orders (...) PARTITION BY RANGE(created_at), nhiều người nghĩ dữ liệu sẽ nằm trong bảng này. Thực tế không. Parent Table chỉ đóng vai trò Router — giống receptionist trong khách sạn, không giữ khách, chỉ biết khách phải đi phòng nào.

Metadata nằm ở đâu?

PostgreSQL lưu thông tin partition vào hai catalog chính:

Planner quyết định prune như thế nào?

Planner đọc metadata → kiểm tra partition bound → so sánh với điều kiện WHERE → loại bỏ partition không khớp → tạo execution plan chỉ gồm partition cần thiết.

Với Prepared Statement (Runtime Pruning): planner tạo plan "mở" lúc prepare → đến lúc execute mới biết giá trị → prune ngay lúc runtime.


Partitioning vs Sharding — Khác nhau như thế nào?

Hai khái niệm thường đi cùng nhau nhưng giải quyết hai bài toán hoàn toàn khác.

Partitioning — Tổ chức dữ liệu tốt hơn trên cùng một máy:

              1 Database Server
                     │
       ┌─────────────┼─────────────┐
       │             │             │
 orders_2024_01  orders_2024_02  orders_2024_03
       │             │             │
       └── Cùng một PostgreSQL Instance ──┘

Một server. Một database. Một connection. Một transaction.

Sharding — Phân tán dữ liệu sang nhiều máy:

                Application
                     │
        ┌────────────┼────────────┐
        ▼            ▼            ▼
     Shard 1      Shard 2      Shard 3
   PostgreSQL   PostgreSQL   PostgreSQL
     Server A     Server B     Server C

Mỗi server giữ một phần dữ liệu, có CPU, RAM, Disk riêng.

Partitioning = Vertical Optimization (tối ưu trên một máy).
Sharding = Horizontal Scaling (mở rộng sang nhiều máy).

Điều thú vị: hai kỹ thuật không loại trừ nhau. Rất nhiều hệ thống lớn sẽ shard trước, rồi mỗi shard lại tiếp tục partition theo tháng:

Shard 1
├── orders_2025_01
├── orders_2025_02
└── ...

Shard 2
├── orders_2025_01
├── orders_2025_02
└── ...

Đây là kiến trúc phổ biến ở các hệ thống có dữ liệu hàng chục hoặc hàng trăm TB.


Tổng kết

Khi mới học database, chúng ta thường nghĩ hiệu năng phụ thuộc vào CPU, RAM hay SSD. Nhưng khi hệ thống phát triển đến hàng trăm triệu bản ghi, bottleneck thường chuyển sang cách dữ liệu được tổ chức.

Partitioning không làm PostgreSQL "đọc nhanh hơn". Nó giúp PostgreSQL không phải đọc những gì không liên quan. Đó là lý do Partition Pruning có thể tạo ra sự khác biệt rất lớn với những workload truy vấn theo thời gian.

Tuy nhiên, Partitioning chỉ hiệu quả khi:

Nếu bạn đang quản lý một bảng có vài trăm triệu dòng, thường xuyên truy vấn theo thời gian, có chính sách retention, và bắt đầu thấy VACUUM, backup hay DELETE trở thành gánh nặng — thì Partitioning là một trong những kỹ thuật đáng cân nhắc nhất trước khi nghĩ đến việc mở rộng sang nhiều server.

Partition theo cách dữ liệu được truy vấn, chứ không phải theo cách dữ liệu được lưu. Workload quyết định tất cả.

Thích bài viết này?

Nội dung trên Vết Mực luôn được chia sẻ miễn phí. Nếu bài viết mang lại giá trị cho bạn, hãy cân nhắc ủng hộ để chúng mình có thể duy trì máy chủ, phát triển thêm tính năng mới và tiếp tục xây dựng một không gian dành cho những người yêu viết lách. ✨

Các cách ủng hộ:

  • Viết và đăng bài trên Vết Mực
  • Chia sẻ bài viết với bạn bè
  • Góp ý để chúng mình cải thiện sản phẩm qua email: nsikhoa@gmail.com

Dù bạn chọn ủng hộ hay chỉ đơn giản là tiếp tục đọc và chia sẻ bài viết, đó đều là nguồn động lực rất lớn với chúng mình. ❤️

Bình luận

Đăng nhập để để lại bình luận.