0

PostgreSQL Bài 37: Kiến trúc phân mảnh dữ liệu: Table Partitioning (Phân vùng) và Sharding

Khi hệ thống của bạn phát triển, sẽ đến một ngày bảng orders hoặc logs vượt mốc hàng trăm triệu, thậm chí hàng tỷ dòng (kích thước hàng trăm GB hoặc vài TB). Lúc này, bạn sẽ đối mặt với các vấn đề vật lý không thể vượt qua bằng Index thông thường:

  1. Index tràn RAM: Cây B-Tree quá lớn, không thể nhét vừa vào shared_buffers, dẫn đến hiện tượng trượt bộ nhớ đệm (Cache Miss) liên tục và tăng I/O đĩa.

  2. Chi phí bảo trì khổng lồ: Lệnh VACUUM, ANALYZE, hoặc CREATE INDEX có thể mất nhiều ngày để chạy xong trên một bảng TB. Việc xóa dữ liệu cũ (DELETE) trở thành cơn ác mộng làm phình to bảng (Table Bloat).

Để giải quyết, chúng ta áp dụng chiến lược "Chia để trị" (Divide and Conquer) thông qua hai cấp độ kiến trúc: Table Partitioning (Cấp độ cục bộ) và Sharding (Cấp độ phân tán).

1. Table Partitioning (Phân vùng dữ liệu cục bộ)

Table Partitioning là kỹ thuật chia một bảng logic khổng lồ (Parent Table) thành nhiều bảng vật lý nhỏ hơn (Child Tables / Partitions) nằm trên cùng một máy chủ (Database Instance).

Đối với ứng dụng backend, cấu trúc này hoàn toàn "trong suốt" (Transparent). Backend vẫn chỉ gửi câu lệnh SELECT hoặc INSERT vào bảng Parent, PostgreSQL sẽ tự động định tuyến dữ liệu xuống đúng bảng Child tương ứng.

1.1. Các phương pháp Phân vùng (Partitioning Strategies)

PostgreSQL hỗ trợ nguyên bản (Native) 3 phương pháp phân vùng:

  1. RANGE Partitioning (Phân vùng theo khoảng): Phổ biến nhất. Chia dữ liệu theo khoảng thời gian (theo tháng, theo năm) hoặc dải số (ID từ 1-1.000.000, 1.000.001-2.000.000). Lý tưởng cho dữ liệu Time-series, Logs, Hóa đơn.

  2. LIST Partitioning (Phân vùng theo danh sách): Chia dữ liệu theo các giá trị rời rạc cụ thể. Ví dụ: Phân vùng theo Quốc gia ('VN', 'US', 'JP'), hoặc theo Tenant ID trong mô hình Multi-tenant.

  3. HASH Partitioning (Phân vùng băm): Chỉ định một số lượng bảng con cố định (ví dụ 4 bảng). PostgreSQL sẽ băm (hash) khóa phân vùng và phân phối đều dữ liệu vào 4 bảng đó. Dùng để cân bằng tải I/O khi dữ liệu không có tính tuần tự rõ ràng.

2. Thực hành: Cài đặt RANGE Partitioning cho dữ liệu Đơn hàng

Giả sử bạn cần lưu trữ bảng orders chứa hàng chục triệu đơn hàng mỗi tháng. Chúng ta sẽ chia bảng này theo từng tháng.

Bước 1: Tạo Bảng Cha (Parent Table)

Bảng cha không lưu trữ dữ liệu thực tế, nó chỉ đóng vai trò làm khung logic và định tuyến. Khi khai báo, bắt buộc phải dùng mệnh đề PARTITION BY.

SQL

CREATE TABLE orders (
    id BIGINT GENERATED ALWAYS AS IDENTITY,
    customer_id INT NOT NULL,
    amount NUMERIC(15, 2) NOT NULL,
    created_at TIMESTAMPTZ NOT NULL
) PARTITION BY RANGE (created_at);

(Lưu ý: Bảng cha không thể có Khóa chính (Primary Key) hoặc Khóa ngoại trừ khi cột phân vùng (Partition Key) nằm trong Khóa chính đó).

Bước 2: Tạo các Bảng Con (Partitions)

Mỗi bảng con sẽ đảm nhiệm lưu trữ dữ liệu của một tháng cụ thể:

SQL

-- Partition cho tháng 10/2026
CREATE TABLE orders_2026_10 PARTITION OF orders
    FOR VALUES FROM ('2026-10-01 00:00:00Z') TO ('2026-11-01 00:00:00Z');

-- Partition cho tháng 11/2026
CREATE TABLE orders_2026_11 PARTITION OF orders
    FOR VALUES FROM ('2026-11-01 00:00:00Z') TO ('2026-12-01 00:00:00Z');

Lưu ý: Toán tử dải là [Bắt đầu, Kết thúc), nghĩa là bao gồm giá trị Bắt đầu nhưng KHÔNG bao gồm giá trị Kết thúc.

Bước 3: Tạo Bảng Mặc định (Default Partition)

Nếu ứng dụng INSERT một đơn hàng thuộc tháng 12/2026 mà bảng orders_2026_12 chưa được tạo, lệnh chèn sẽ báo lỗi. Để an toàn, bạn tạo một bảng Default để hứng các dữ liệu không thuộc bất kỳ dải nào:

SQL

CREATE TABLE orders_default PARTITION OF orders DEFAULT;

3. Vũ khí tối thượng: Partition Pruning (Cắt tỉa phân vùng)

Lý do lớn nhất khiến Partitioning tăng tốc độ truy vấn là tính năng Partition Pruning.

Khi bạn thực hiện một truy vấn có chứa điều kiện lọc (WHERE) trùng với cột phân vùng (created_at), Query Planner đủ thông minh để bỏ qua hoàn toàn (Prune) các bảng con không liên quan, chỉ quét đúng bảng vật lý chứa dữ liệu.

Thử nghiệm EXPLAIN:

SQL

EXPLAIN (ANALYZE, COSTS OFF)
SELECT sum(amount) FROM orders 
WHERE created_at >= '2026-10-15' AND created_at < '2026-10-20';

Kết quả Execution Plan:

Plaintext

Aggregate (actual time=0.015..0.016 rows=1 loops=1)
  ->  Seq Scan on orders_2026_10 orders (actual time=0.005..0.010 rows=15 loops=1)
        Filter: ((created_at >= '2026-10-15 00:00:00+00'::timestamptz) AND (created_at < '2026-10-20 00:00:00+00'::timestamptz))

Nhận xét: Thay vì phải quét hoặc tìm trên index của toàn bộ bảng orders (bao gồm các tháng 11, 12, default), Planner chỉ quét duy nhất bảng orders_2026_10. Chi phí I/O giảm theo cấp số nhân!

Lợi ích cực lớn trong việc Xóa dữ liệu cũ (Data Retention)

Trong bảng Heap thông thường, lệnh DELETE FROM orders WHERE created_at < '2025-01-01' sẽ tốn rất nhiều giờ, làm phình bảng (Bloat) và sinh ra hàng GB file WAL. Với Partitioning, bạn chỉ cần gỡ bỏ (Detach) và xóa (Drop) bảng vật lý đó. Quá trình này hoàn thành trong vài mili-giây, trả lại không gian ổ cứng ngay lập tức mà không dội tải I/O:

SQL

DROP TABLE orders_2024_12; -- Cực nhanh và sạch sẽ!

4. Sharding: Kiến trúc Phân mảnh Phân tán (Scale-Out)

Table Partitioning rất tuyệt vời, nhưng nó vẫn bị giới hạn bởi phần cứng của một máy chủ duy nhất (Scale-Up). Nếu CPU, RAM và IOPS của máy chủ đó đạt đỉnh, Partitioning không thể cứu bạn.

Đó là lúc Sharding (Phân mảnh dữ liệu) vào cuộc. Sharding chia dữ liệu thành nhiều mảnh và lưu trữ chúng trên nhiều máy chủ vật lý khác nhau (Nodes).

MÔ HÌNH SHARDING:
                         [ Application ]
                                │
                      [ Coordinator Node ] (Máy điều phối)
                                │
       ┌────────────────────────┼────────────────────────┐
       ▼                        ▼                        ▼
[ Worker Node 1 ]        [ Worker Node 2 ]        [ Worker Node 3 ]
(Chứa Tenant 1-100)      (Chứa Tenant 101-200)    (Chứa Tenant 201-300)

4.1. Giải pháp Sharding trong Hệ sinh thái PostgreSQL

PostgreSQL không tích hợp sẵn kiến trúc Sharding phân tán hoàn chỉnh ở lõi như Cassandra hay MongoDB, nhưng nó hỗ trợ thông qua hệ sinh thái:

  1. Foreign Data Wrappers (postgres_fdw): Cung cấp khả năng truy cập các bảng ở máy chủ PostgreSQL khác như thể chúng là bảng cục bộ. Kết hợp postgres_fdw với Partitioning, bạn có thể tự xây dựng hệ thống Sharding thủ công (nhưng khá phức tạp trong việc quản lý Transaction phân tán và Join).

  2. Citus (Đỉnh cao của PostgreSQL Sharding): Citus là một extension (hiện đã là mã nguồn mở hoàn toàn bởi Microsoft) biến PostgreSQL thành một cơ sở dữ liệu phân tán (Distributed Database).

    • Nó tự động chia nhỏ các bảng và phân phối (Hash phân tán) xuống các Worker Nodes.

    • Ứng dụng chỉ kết nối vào máy Coordinator (Máy điều phối). Coordinator tự động tách câu query SQL, gửi xuống các Worker chạy song song (Distributed Query Execution), thu thập kết quả và trả về cho Client.

    • Lý tưởng cho các mô hình SaaS Multi-tenant (nhóm dữ liệu theo tenant_id) hoặc Real-time Analytics (nhóm theo device_id).

5. Bảng đối chiếu: Partitioning vs Sharding

Tiêu chí Table Partitioning (Local) Sharding (Citus / Distributed)
Vị trí dữ liệu Trên cùng 1 máy chủ (Cùng 1 ổ đĩa/cụm đĩa). Nằm rải rác trên nhiều máy chủ vật lý khác nhau.
Mục đích chính Giải quyết giới hạn RAM cho Index, tăng tốc xóa dữ liệu (Drop Table). Tăng sức mạnh tính toán (CPU) và dung lượng tổng (Storage).
Độ phức tạp Thấp (Native, dễ cài đặt và bảo trì). Rất cao (Cần quản lý Node, Transaction phân tán).
Truy vấn JOIN Hoạt động bình thường như bảng chuẩn. Hạn chế. Phải Join trên cùng 1 phân mảnh (Co-located Joins) để tránh trễ mạng.

Khi dữ liệu phình to, luôn ưu tiên tối ưu Index →\rightarrow Sau đó áp dụng Table Partitioning →\rightarrow Chỉ khi phần cứng một máy chủ (Vertical Scaling) không thể đáp ứng, mới chuyển sang Sharding (Horizontal Scaling).


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.