Khi Bảng MySQL Của Bạn Trở Thành Nút Thắt Cổ Chai
Đến một lúc nào đó, hầu như mọi ứng dụng có database đều gặp bức tường. Các truy vấn từng chạy trong mili-giây giờ mất cả giây. Bảng orders của bạn có 50 triệu hàng, index đã có đầy đủ, nhưng MySQL vẫn phải quét qua núi dữ liệu để tìm thứ bạn cần.
Thủ phạm hầu như lúc nào cũng giống nhau: toàn bộ dữ liệu nằm trong một bảng phẳng, nguyên khối. Dù có index, query planner vẫn phải làm việc nặng hơn khi dataset ngày càng lớn. Phân vùng bảng (table partitioning) cho MySQL cách chia nhỏ dữ liệu đó thành các mảnh nhỏ hơn — rồi chỉ truy vấn mảnh liên quan thay vì toàn bộ bảng.
Đây không phải sharding qua nhiều server. Phân vùng diễn ra trong một MySQL instance đơn, và code ứng dụng của bạn không cần thay đổi. Bạn truy vấn bảng theo cách thông thường; MySQL tự tìm ra partition nào cần xem.
Bối Cảnh & Lý Do: Phân Vùng Thực Sự Làm Gì
MySQL lưu mỗi partition như một segment riêng trên đĩa. Khi bạn chạy truy vấn với bộ lọc trên partition key, MySQL áp dụng partition pruning — bỏ qua các partition không thể chứa dữ liệu bạn cần. Nếu bạn có 12 partition theo tháng và truy vấn bản ghi tháng Giêng, MySQL chỉ đọc partition tháng Giêng thay vì quét cả 12 partition.
Đây là lúc con số trở nên thú vị. Trên bảng 500 triệu hàng được chia thành 12 partition theo tháng, một truy vấn theo khoảng ngày trước đây phải quét 500M hàng nay chỉ cần đọc khoảng ~42M. Cùng index, cùng cú pháp truy vấn, hiệu suất cải thiện đáng kể.
Việc dọn dẹp dữ liệu mới thực sự là lúc phân vùng phát huy sức mạnh. Muốn xóa dữ liệu năm ngoái? Thay vì DELETE FROM orders WHERE year = 2022 chậm chạp, khóa bảng hàng giờ, bạn chạy ALTER TABLE orders DROP PARTITION p2022 — tức thời, vì chỉ đơn giản là xóa một file trên đĩa.
Ba loại partition bao phủ hầu hết các trường hợp sử dụng:
- RANGE — gán hàng dựa trên cột nằm trong một khoảng liên tiếp (phổ biến nhất cho dữ liệu time-series)
- LIST — gán hàng dựa trên cột khớp với tập giá trị rời rạc (tốt cho vùng địa lý, mã trạng thái, danh mục)
- HASH — phân phối hàng đều qua N partition bằng hàm hash (tốt cho phân phối tải khi không có ranh giới khoảng tự nhiên)
Cài Đặt: Kiểm Tra Hỗ Trợ Phân Vùng
Phân vùng được tích hợp sẵn trong MySQL 5.6 trở lên — có trên tất cả phiên bản hiện tại bao gồm 8.0 và 8.4. Không cần cài đặt thêm. Kiểm tra xem nó đang hoạt động:
SHOW PLUGINS WHERE Name = 'partition';
Bạn sẽ thấy partition | ACTIVE | STORAGE ENGINE. Trên MySQL 8.0+, phân vùng luôn được biên dịch sẵn.
Một ràng buộc cần biết trước: mọi unique index (kể cả primary key) phải bao gồm cột partition. Đây là yêu cầu bắt buộc của MySQL. Đang có primary key auto-increment đơn giản? Bạn cần gộp cột partition vào primary key tổ hợp, hoặc thiết kế lại key trước khi bắt đầu.
Cũng kiểm tra phiên bản MySQL vì có sự khác biệt cú pháp giữa 5.7 và 8.0:
SELECT VERSION();
Cấu Hình: Triển Khai Ba Loại Partition
Phân Vùng RANGE — Phương Pháp Phổ Biến cho Dữ Liệu Chuỗi Thời Gian
Hầu hết các team bắt đầu với RANGE partitioning, đặc biệt cho các bảng time-series như logs, orders hay events. Bạn định nghĩa các khoảng giá trị liên tiếp, và MySQL gán từng hàng vào partition phù hợp.
CREATE TABLE orders (
order_id INT NOT NULL,
customer_id INT NOT NULL,
order_date DATE NOT NULL,
amount DECIMAL(10,2),
PRIMARY KEY (order_id, order_date)
)
PARTITION BY RANGE (YEAR(order_date)) (
PARTITION p2022 VALUES LESS THAN (2023),
PARTITION p2023 VALUES LESS THAN (2024),
PARTITION p2024 VALUES LESS THAN (2025),
PARTITION p2025 VALUES LESS THAN (2026),
PARTITION p_future VALUES LESS THAN MAXVALUE
);
Partition MAXVALUE dùng để bắt tất cả là bắt buộc, không phải tùy chọn. Nếu không có, MySQL từ chối mọi hàng nằm ngoài khoảng đã định nghĩa — nghĩa là lần insert đầu tiên vào ngày 1 tháng 1 của năm mới sẽ báo lỗi.
Thêm partition mới trước khi sang năm:
ALTER TABLE orders ADD PARTITION (
PARTITION p2026 VALUES LESS THAN (2027)
);
Xóa dữ liệu cũ là tính năng nổi bật nhất của RANGE partitioning:
-- Xóa toàn bộ dữ liệu 2022 ngay lập tức, không quét bảng, không khóa
ALTER TABLE orders DROP PARTITION p2022;
Khi migrate bảng hiện có sang cấu trúc phân vùng, quy trình thông thường là export dữ liệu dạng CSV, rồi import lại vào schema mới. Tôi thường mở sẵn tab trình duyệt với toolcraft.app/vi/tools/data/csv-to-json để kiểm tra nhanh dữ liệu đã export — công cụ tự phát hiện dấu phân cách và chuyển CSV sang JSON ngay trên trình duyệt, dữ liệu không rời khỏi máy bạn. Điều này rất quan trọng khi export chứa bản ghi đơn hàng hoặc dữ liệu khách hàng.
Phân Vùng LIST — Khi Giá Trị Đến Từ Tập Cố Định
LIST partitioning gán hàng dựa trên khớp giá trị chính xác với danh sách định sẵn. Điển hình: hệ thống đa vùng nơi bạn muốn gom dữ liệu theo địa lý hoặc trạng thái.
CREATE TABLE customers (
customer_id INT NOT NULL,
region VARCHAR(10) NOT NULL,
name VARCHAR(100),
email VARCHAR(255),
PRIMARY KEY (customer_id, region)
)
PARTITION BY LIST COLUMNS (region) (
PARTITION p_asia VALUES IN ('JP', 'KR', 'CN', 'SG', 'TH'),
PARTITION p_europe VALUES IN ('DE', 'FR', 'GB', 'NL', 'IT'),
PARTITION p_americas VALUES IN ('US', 'CA', 'BR', 'MX')
);
Lưu ý LIST COLUMNS thay vì LIST thông thường. Biến thể COLUMNS chấp nhận giá trị chuỗi trực tiếp. LIST thường chỉ hoạt động với biểu thức số nguyên, khá bất tiện cho mã quốc gia ISO.
Một cạm bẫy cứng: insert hàng có giá trị region không thuộc bất kỳ danh sách partition nào, MySQL sẽ báo lỗi. Lên kế hoạch bao phủ giá trị từ đầu. Nếu tập giá trị không được kiểm soát hoàn toàn bởi ứng dụng, hãy thêm partition bắt-tất-cả để tránh bất ngờ trên production.
Có thể tổ chức lại partition sau mà không mất dữ liệu:
-- Tách p_americas thành hai partition riêng biệt
ALTER TABLE customers REORGANIZE PARTITION p_americas INTO (
PARTITION p_north_america VALUES IN ('US', 'CA', 'MX'),
PARTITION p_south_america VALUES IN ('BR', 'AR', 'CL', 'CO')
);
Phân Vùng HASH — Phân Phối Đều Không Cần Logic Nghiệp Vụ
HASH partitioning không yêu cầu bạn định nghĩa khoảng hay tập giá trị. Cho MySQL một cột và số lượng partition, nó phân phối hàng đều bằng phép toán modulo. Tốt nhất cho các bảng cần phân phối tải đều nhưng không có ranh giới khoảng hay danh mục tự nhiên.
CREATE TABLE user_sessions (
session_id VARCHAR(64) NOT NULL,
user_id INT NOT NULL,
created_at DATETIME NOT NULL,
payload JSON,
PRIMARY KEY (session_id, user_id)
)
PARTITION BY HASH (user_id)
PARTITIONS 8;
Mỗi hàng rơi vào partition số user_id MOD 8. Phân phối duy trì đều miễn là user ID được trải rộng hợp lý — ID auto-increment tuần tự hoạt động tốt ở đây.
Tuy nhiên cần lưu ý: HASH partitioning không hỗ trợ query pruning như RANGE. Trừ khi bạn lọc theo đúng giá trị hash key, MySQL quét tất cả partition. Dùng HASH khi mục tiêu là I/O song song và phân phối lưu trữ, không phải quét chọn lọc.
Với workload động hơn, MySQL cũng cung cấp LINEAR HASH, sử dụng thuật toán lũy thừa hai. Thêm hoặc bớt partition nhanh hơn, nhưng phân phối kém đều hơn một chút.
Kiểm Tra & Giám Sát: Đảm Bảo Phân Vùng Thực Sự Có Ích
Kiểm Tra Cấu Trúc Partition và Phân Phối Hàng
Điểm dừng đầu tiên sau khi tạo bảng phân vùng là information_schema.PARTITIONS:
SELECT
PARTITION_NAME,
TABLE_ROWS,
FORMAT(DATA_LENGTH / 1024 / 1024, 2) AS data_mb,
FORMAT(INDEX_LENGTH / 1024 / 1024, 2) AS index_mb
FROM information_schema.PARTITIONS
WHERE TABLE_SCHEMA = 'your_db'
AND TABLE_NAME = 'orders'
ORDER BY PARTITION_ORDINAL_POSITION;
Lệnh này hiển thị số hàng và kích thước lưu trữ của từng partition. Dùng để phát hiện phân phối không đều trong partition HASH/LIST, hoặc xem mỗi khoảng thời gian chứa bao nhiêu dữ liệu khi bảng phát triển.
Xác Minh Partition Pruning bằng EXPLAIN
Toàn bộ lợi ích hiệu suất của RANGE partitioning phụ thuộc vào pruning hoạt động đúng. Hãy xác minh:
EXPLAIN
SELECT * FROM orders
WHERE order_date BETWEEN '2024-01-01' AND '2024-12-31';
Kiểm tra cột partitions trong output EXPLAIN. Nếu chỉ thấy p2024, pruning đang hoạt động. Nếu thấy tất cả partition được liệt kê, có gì đó đang chặn — thường là hàm bao bọc cột partition.
Đây là cạm bẫy pruning phổ biến nhất:
-- XẤU: bọc cột trong hàm làm hỏng pruning — MySQL quét tất cả partition
WHERE DATE_FORMAT(order_date, '%Y') = '2024'
-- TỐT: so sánh trực tiếp với cột cho phép pruning — MySQL chỉ đọc p2024
WHERE order_date >= '2024-01-01' AND order_date < '2025-01-01'
Làm Việc với Timestamp trong Lên Kế Hoạch Partition
Partition RANGE dựa trên epoch phổ biến cho các bảng log. Khi cần tính giá trị VALUES LESS THAN chính xác theo định dạng Unix timestamp, tôi dùng toolcraft.app/vi/tools/datetime/timestamp-converter để chuyển đổi giữa ngày tháng dạng đọc được và Unix timestamp. Công cụ hỗ trợ nhiều múi giờ, quan trọng khi dữ liệu trải qua nhiều vùng và bạn cần ranh giới partition căn chỉnh theo UTC nửa đêm thay vì giờ địa phương.
-- Phân vùng RANGE trên cột Unix timestamp
CREATE TABLE access_logs (
log_id BIGINT NOT NULL AUTO_INCREMENT,
user_id INT NOT NULL,
logged_at INT UNSIGNED NOT NULL, -- Unix timestamp
path VARCHAR(500),
PRIMARY KEY (log_id, logged_at)
)
PARTITION BY RANGE (logged_at) (
PARTITION p_2024_q1 VALUES LESS THAN (1711929600), -- 2024-04-01 00:00:00 UTC
PARTITION p_2024_q2 VALUES LESS THAN (1719792000), -- 2024-07-01 00:00:00 UTC
PARTITION p_2024_q3 VALUES LESS THAN (1727740800), -- 2024-10-01 00:00:00 UTC
PARTITION p_2024_q4 VALUES LESS THAN (1735689600), -- 2025-01-01 00:00:00 UTC
PARTITION p_future VALUES LESS THAN MAXVALUE
);
Khi Nào Không Nên Phân Vùng
Phân vùng không phải lúc nào cũng là câu trả lời đúng. Các bảng dưới vài triệu hàng thường không thấy lợi ích — overhead của metadata partition và planning thực ra có thể làm chậm truy vấn. Full-text index, ràng buộc foreign key, và một số loại spatial index không hoạt động với bảng phân vùng. Các truy vấn không thể được pruning (vì lọc trên cột không phải partition key) cũng có thể chạy chậm hơn đôi chút so với bảng không phân vùng.
Điểm ngọt cho phân vùng: bảng của bạn đang đẩy lên hàng chục triệu hàng hoặc hơn, các truy vấn phổ biến nhất lọc trên cột tạo thành partition key tự nhiên — thường là ngày tháng hoặc cột danh mục cố định — và bạn có nhu cầu lifecycle liên tục như lưu trữ hoặc xóa bản ghi cũ. Cả ba? Phân vùng gần như chắc chắn sẽ vượt trội so với việc thêm index. Và bạn không cần động đến một dòng code ứng dụng nào để đạt được điều đó.

