Nỗi ám ảnh mang tên dữ liệu phân cấp trên MySQL cũ
Xử lý cấu trúc cha – con (parent-child) luôn là bài toán gây đau đầu cho các lập trình viên làm việc với MySQL 5.7 trở về trước. Bạn có thể dễ dàng bắt gặp dạng dữ liệu này ở các tính năng như danh mục sản phẩm nhiều cấp, hệ thống bình luận lồng nhau hoặc sơ đồ tổ chức công ty.
Khi chưa có CTE, giải pháp phổ biến nhất là dùng Self-join lồng nhau. Nếu dữ liệu có 3 cấp, bạn join 3 lần; 10 cấp, bạn join 10 lần. Cách làm này biến câu lệnh SQL thành một “mớ hỗn độn” dài cả trăm dòng, cực kỳ khó đọc và bảo trì. Nhiều người chọn cách kéo toàn bộ dữ liệu về code (PHP, Node.js) để xử lý đệ quy. Tuy nhiên, phương pháp này thường làm tăng độ trễ (latency) thêm 200-500ms khi tập dữ liệu lớn dần.
MySQL 8.0 ra đời đã thay đổi cuộc chơi với Common Table Expressions (CTE). Đây không chỉ là cú pháp mới mà còn là cách tiếp cận hiện đại để xử lý cấu trúc dạng cây (Tree structure) một cách gọn gàng nhất.
CTE là gì?
Hãy tưởng tượng CTE như một biến tạm thời lưu trữ kết quả truy vấn. Nó chỉ tồn tại trong suốt quá trình thực thi của một câu lệnh duy nhất như SELECT, INSERT hay UPDATE. Thay vì viết subquery phức tạp, bạn tách chúng ra thành các bảng ảo có tên gọi riêng để tái sử dụng.
Hệ thống chia CTE thành hai loại chính:
- Non-Recursive CTE: Thay thế cho các truy vấn con, giúp cấu trúc code mạch lạc, dễ hiểu hơn.
- Recursive CTE: Công cụ chuyên dụng để xử lý dữ liệu có tính chất lặp lại hoặc phân cấp, nơi một bản ghi trỏ đến một bản ghi khác trong cùng bảng.
Cú pháp cơ bản với từ khóa WITH
Để bắt đầu một CTE, bạn luôn sử dụng từ khóa WITH. Cách viết này giúp tách biệt phần lấy dữ liệu thô và phần xử lý logic cuối cùng.
WITH category_summary AS (
-- Định nghĩa bảng tạm
SELECT id, name FROM categories WHERE is_active = 1
)
SELECT * FROM category_summary; -- Truy vấn trực tiếp từ bảng tạm trên
Việc đặt tên rõ ràng cho CTE giúp các thành viên khác trong team hiểu ngay mục đích của đoạn code mà không cần đọc hết logic bên trong.
Xử lý đệ quy với Recursive CTE
Đây là kỹ thuật quan trọng nhất để xử lý các tầng dữ liệu không xác định độ sâu. Một câu lệnh Recursive CTE chuẩn luôn gồm ba thành phần:
- Anchor Member (Phần neo): Lấy ra điểm khởi đầu, ví dụ như cấp cao nhất của công ty.
- UNION ALL: Kết nối kết quả của phần neo với các vòng lặp tiếp theo.
- Recursive Member (Phần đệ quy): Truy vấn tham chiếu ngược lại chính CTE để tìm các cấp con trực thuộc.
Ứng dụng: Xây dựng sơ đồ tổ chức
Giả sử chúng ta có bảng employees với cấu trúc đơn giản:
CREATE TABLE employees (
id INT PRIMARY KEY,
name VARCHAR(100),
manager_id INT
);
INSERT INTO employees VALUES
(1, 'CEO - Nguyễn Văn A', NULL),
(2, 'Giám đốc IT - Trần Văn B', 1),
(3, 'Giám đốc Sales - Lê Thị C', 1),
(4, 'Trưởng phòng Dev - Phạm Văn D', 2),
(5, 'Senior Dev - Hoàng Văn E', 4);
Để hiển thị cấp bậc của từng nhân viên, chúng ta sử dụng Recursive CTE như sau:
WITH RECURSIVE employee_hierarchy AS (
-- BƯỚC 1: Lấy ông CEO làm gốc (Level 1)
SELECT id, name, manager_id, 1 AS level
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- BƯỚC 2: Tìm nhân viên cấp dưới bằng cách Join bảng với chính CTE
SELECT e.id, e.name, e.manager_id, eh.level + 1
FROM employees e
INNER JOIN employee_hierarchy eh ON e.manager_id = eh.id
)
SELECT * FROM employee_hierarchy ORDER BY level;
Cơ chế vận hành bên dưới:
Hệ thống bắt đầu bằng việc xác định CEO (ID 1). Sau đó, nó dùng ID này để tìm các nhân viên có manager_id = 1 (Giám đốc IT và Sales). Quá trình này lặp lại liên tục cho đến khi chạm đến cấp thấp nhất thì dừng lại.
Lưu ý quan trọng về hiệu năng thực tế
Áp dụng CTE vào môi trường sản xuất (production) đòi hỏi sự cẩn trọng về tài nguyên hệ thống:
1. Index là bắt buộc
Cột liên kết (như manager_id) phải được đánh Index. Với bảng có 1 triệu bản ghi, việc thiếu index sẽ khiến mỗi vòng lặp trở thành một đợt quét toàn bảng (Full Table Scan). Điều này đẩy CPU lên 100% và có thể làm treo database chỉ trong vài giây.
2. Kiểm soát độ sâu vòng lặp
MySQL mặc định giới hạn 1000 lần lặp thông qua biến cte_max_recursion_depth. Nếu dữ liệu bị lỗi vòng lặp vô tận (A quản lý B, B quản lý A), giới hạn này sẽ bảo vệ server của bạn. Bạn có thể tăng giới hạn này nếu cấu trúc cây thực tế sâu hơn:
SET SESSION cte_max_recursion_depth = 2000;
3. Sử dụng đúng lúc, đúng chỗ
CTE tiêu tốn thêm bộ nhớ để khởi tạo bảng tạm. Với các truy vấn phẳng đơn giản, một câu SELECT thông thường sẽ nhanh hơn khoảng 5-10% do không mất chi phí quản lý vùng nhớ cho CTE.
Tổng kết
CTE trong MySQL 8 không đơn thuần là một tính năng mới mà là kỹ năng thiết yếu của Backend Developer hiện đại. Nó giúp giải quyết triệt để bài toán dữ liệu phân cấp, làm sạch mã nguồn và dễ dàng debug hơn nhiều so với phương pháp Self-join truyền thống.
Nếu dự án của bạn có các tính năng như menu đa cấp hoặc hệ thống phân quyền, hãy thử áp dụng Recursive CTE ngay. Bạn sẽ thấy hiệu quả rõ rệt trong việc bảo trì và mở rộng hệ thống sau này.

