+4

Bảng Dữ Liệu 50 Triệu Dòng: Thuật Toán Phân Vùng (Partitioning) & Nghệ Thuật "Chia Để Trị"

Chúc mừng hệ thống của bạn đã "sống sót" qua giai đoạn startup!

Nhưng khi công ty làm ăn phát đạt, hệ thống của bạn sẽ đối mặt với một "căn bệnh nhà giàu": Bảng orders (Đơn hàng) hoặc logs (Lịch sử) phình to lên tới 50 triệu dòng, dung lượng chiếm hàng chục Gigabyte. Lúc này, bạn tự tin gắn Index (Chỉ mục) vào cột created_at. Nhưng đời không như mơ, câu query lấy báo cáo tháng này vẫn làm ổ cứng SSD "bốc khói", CPU của Database dựng đứng lên 100%, và API trả về sau... 10 giây. Tệ hơn nữa, Sếp bảo: "Xóa bớt dữ liệu từ 3 năm trước đi cho nhẹ". Bạn chạy lệnh DELETE, và toàn bộ Database bị khóa cứng đơ (Lock) trong 2 tiếng đồng hồ! Làm sao để giải cứu hệ thống? Đây là lúc Vibe Coder phải tung ra cấm thuật chia để trị của Database: Table Partitioning (Phân vùng dữ liệu).

PHẦN 1: TẠI SAO INDEX LẠI "BẤT LỰC" TRƯỚC BẢNG KHỔNG LỒ?

Nhiều anh em nghĩ: "Bảng to thì cứ táng Index vào là nhanh hết!" Sai lầm chí mạng.

Bản chất của Index là một cây thư mục (B-Tree). Khi bảng có 50 triệu dòng, cái cây B-Tree đó cũng phình to khổng lồ. RAM của Server thì có hạn (giả sử 16GB). Cây B-Tree quá to không thể nhét vừa vào RAM. Thế là MySQL phải liên tục tráo đổi dữ liệu giữa RAM và Ổ cứng (Swapping). Mỗi lần ổ cứng phải quay để tìm nhánh cây, tốc độ của bạn bị trừ đi một nửa.

Cuối cùng, dù có Index, thao tác tìm kiếm vẫn chậm như rùa bò vì bộ nhớ đệm (Buffer Pool) đã bị "bức tử".

PHẦN 2: TABLE PARTITIONING LÀ GÌ?

Ý tưởng cực kỳ đơn giản: Về mặt Logic (Code), nó vẫn là 1 Bảng. Nhưng về mặt Vật lý (Ổ cứng), nó bị băm ra thành nhiều file nhỏ.

Giả sử bạn có 50 triệu đơn hàng trải dài từ năm 2022 đến 2026. Thay vì nhét tất cả vào 1 file orders.ibd khổng lồ nặng 50GB. Bạn cấu hình Partitioning theo THÁNG. MySQL sẽ âm thầm tạo ra khoảng 60 cái file nhỏ trên ổ cứng, mỗi file chỉ chứa dữ liệu của đúng 1 tháng (VD: p2022_01, p2022_02...). Mỗi file chỉ nặng chưa tới 1GB.

Phép màu 1: Partition Pruning (Cắt tỉa phân vùng)

Khi bạn code Node.js và chạy câu query:

SELECT * FROM orders WHERE created_at BETWEEN '2026-12-01' AND '2026-12-31'

MySQL Engine sẽ làm một việc vi diệu: Nó nhìn vào điều kiện thời gian, nó biết chắc chắn dữ liệu này CHỈ NẰM ở file p2026_12. Ngay lập tức, nó BỎ QUA (Prune) 59 cái file của các tháng trước, và chỉ chui vào đúng 1 file p2026_12 để quét. Thay vì phải lục lọi 50 triệu dòng, nó chỉ cần quét 500.000 dòng. Tốc độ query lại mượt mà như thuở ban đầu!

PHẦN 3: MA THUẬT "XÓA KHÔNG DẤU VẾT" VÀ ĐỊNH LUẬT BẢO TOÀN TÀI NGUYÊN

Nhớ bài toán Sếp bắt xóa dữ liệu 3 năm trước chứ? Nếu bạn chạy lệnh: DELETE FROM orders WHERE created_at < '2024-01-01'

Thảm họa: Lệnh DELETE trong SQL là lệnh DML (Data Manipulation Language). Nó xóa từng dòng một, và ghi Lịch sử xóa (Undo Log) cho từng dòng để lỡ có lỗi thì còn Rollback. Xóa 20 triệu dòng sẽ mất 3 tiếng đồng hồ, làm phình to file Log, và treo cứng toàn bộ Database.

Nhưng với Table Partitioning, các dữ liệu của năm 2023 đang nằm gọn trong 12 cái phân vùng (từ p2023_01 đến p2023_12). Bạn chỉ cần chạy lệnh DDL (Data Definition Language) siêu cấp:

ALTER TABLE orders DROP PARTITION p2023_01, p2023_02, ...;

BÙM! MySQL không thèm đọc từng dòng để xóa. Nó xuống thẳng hệ điều hành (OS), Xóa thẳng tay cái file vật lý đó khỏi ổ cứng. Thời gian thực thi: 0.1 giây! Không sinh ra rác, không treo Database, giải phóng ngay lập tức hàng chục GB ổ cứng. Sếp nhìn bạn với ánh mắt ngưỡng mộ!

PHẦN 4: CÚ LỪA CỦA KHÓA CHÍNH (PRIMARY KEY) - NỖI ĐAU CỦA THỢ GÕ

Nghe thần thánh là vậy, nhưng 90% anh em khi áp dụng Partitioning lần đầu đều dính cú lừa này và báo lỗi đỏ choét: A PRIMARY KEY must include all columns in the table's partitioning function.

Nguyên lý ép buộc của MySQL: Nếu bạn muốn chia bảng theo cột created_at, thì cột created_at ĐÓ BẮT BUỘC PHẢI LÀ MỘT PHẦN CỦA PRIMARY KEY (Khóa chính).

  • Bảng truyền thống: Khóa chính thường chỉ là cột id (Auto Increment).
  • Bảng Partitioned: Khóa chính phải là cụm (id, created_at).
-- Cú pháp chuẩn tạo bảng Partition theo Range (Khoảng thời gian)
CREATE TABLE orders (
    id INT NOT NULL AUTO_INCREMENT,
    user_id INT NOT NULL,
    total DECIMAL(10,2),
    created_at DATETIME NOT NULL,
    -- ÉP BUỘC: Khóa chính phải chứa cột dùng để phân vùng
    PRIMARY KEY (id, created_at) 
)
PARTITION BY RANGE (YEAR(created_at) * 100 + MONTH(created_at)) (
    PARTITION p2026_01 VALUES LESS THAN (202602),
    PARTITION p2026_02 VALUES LESS THAN (202603),
    -- Cột cuối cùng để hứng các dữ liệu chưa được định nghĩa
    PARTITION p_max VALUES LESS THAN MAXVALUE 
);

Tại sao phải như vậy? Vì MySQL cần đảm bảo tính UNIQUE (Duy nhất) của Khóa chính. Nếu phân vùng theo tháng, nó chỉ cần check tính duy nhất TRONG NỘI BỘ cái tháng đó, chứ không cần phải bới tung cả 59 tháng còn lại lên để check (điều đó sẽ phá vỡ mục đích của Partition).

Lời kết

Partitioning không làm câu query INSERT của bạn nhanh hơn, cũng không giúp kết nối mạng lẹ hơn. Nó là thuật toán quy hoạch kho bãi, giúp những gã khổng lồ mang trên mình hàng trăm triệu dòng dữ liệu vẫn có thể "xoay xở" linh hoạt, dọn dẹp dễ dàng và phản hồi cực tốc độ.

🔥 Chủ đề tiếp theo: Khi "Cá mập" Cắn Cáp - Kiến Trúc Master-Slave Replication

Phân vùng giúp bảng 50 triệu dòng bớt nặng. Nhưng Server MySQL của bạn vẫn chỉ là MỘT cỗ máy vật lý. Chuyện gì xảy ra nếu 100.000 user CÙNG LÚC vào xem sản phẩm (truy vấn ĐỌC), trong khi hàng ngàn đơn hàng đang liên tục được tạo ra (truy vấn GHI)? 1 con Server sẽ bốc cháy vì phải làm 2 việc trái ngược nhau!

Làm sao để chúng ta có 1 Server CHỈ ĐỂ GHI (Master), và 3 Server CHỈ ĐỂ ĐỌC (Slaves)? Khi ta Ghi vào Master, làm thế nào 3 con Slaves kia tự động cập nhật dữ liệu theo ngay lập tức? Ở bài tới, chúng ta sẽ mở khóa thuật phân thân đỉnh cao của Database: Master-Slave Replication & Read/Write Splitting. Anh em chuẩn bị tinh thần nhé!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí