Bài 1 chúng ta học cây AVL tự cân bằng — hoàn hảo khi dữ liệu nằm trong RAM. Nhưng nếu dữ liệu nằm trên ổ đĩa (như trong mọi hệ quản trị cơ sở dữ liệu), AVL lại không phải lựa chọn tốt nhất. B-Tree mới là cấu trúc đứng sau chỉ mục (index) của MySQL, PostgreSQL, và hầu hết hệ thống file.

🕳️ Cạm bẫy thường gặp: "B" không phải viết tắt của "Binary"!
Tên gọi B-Tree khiến nhiều người lầm tưởng nó là một dạng cây nhị phân. Thực ra "B" không có nghĩa chính thức được thống nhất (có thể là "Balanced", "Bayer" theo tên người phát minh Rudolf Bayer, hoặc "Broad"). Điểm khác biệt cốt lõi: mỗi node B-Tree chứa nhiều khóanhiều con, không phải tối đa 2 con như cây nhị phân.

1. Vì sao AVL không tối ưu cho lưu trữ trên ổ đĩa?

Truy cập RAM mất vài nano-giây, nhưng một lần đọc ổ đĩa (disk seek) — kể cả SSD — có thể chậm hơn hàng nghìn lần. Với cây nhị phân như AVL, mỗi node là một lần đọc đĩa riêng biệt; tra cứu 1 triệu bản ghi cần khoảng $\log_2(10^6) \approx 20$ lần đọc đĩa. B-Tree giải quyết vấn đề bằng cách cho mỗi node chứa hàng trăm khóa (vừa đúng 1 trang đĩa — thường 4KB–16KB), khiến cây THẤP hơn rất nhiều dù chứa cùng số lượng dữ liệu.

2. Cấu trúc B-Tree: Bậc (Order) và ràng buộc số khóa

Một B-Tree bậc $m$ (order $m$) tuân theo các ràng buộc: mỗi node có tối đa $m$ con và tối đa $m{-}1$ khóa; mọi node (trừ gốc) có tối thiểu $\lceil m/2 \rceil$ con. Mọi lá nằm ở cùng một độ sâu — đây là điều kiện cân bằng của B-Tree, khác với AVL cân bằng theo chiều cao từng nhánh:

btree-node.js
class BTreeNode {
  constructor(leaf) {
    this.keys = [];     // tối đa m-1 khóa, luôn giữ SẮP XẾP
    this.children = [];  // tối đa m con — children[i] chứa khóa GIỮA keys[i-1] và keys[i]
    this.leaf = leaf;    // true nếu là node lá (không có con)
  }
}

3. Chèn node & Tách trang (Insert & Split)

Khi chèn khiến một node có đủ $m{-}1$ khóa (đầy), node phải tách đôi: khóa ở giữa được đẩy lên node cha, 2 nửa còn lại trở thành 2 node con riêng biệt. Nếu node gốc bị tách, cây tăng thêm 1 tầng — đây là cách DUY NHẤT B-Tree tăng chiều cao, luôn từ gốc lên chứ không từ lá xuống như AVL:

btree-split.js
// T = bậc tối thiểu (min degree); bậc tối đa m = 2T
// splitChild tách children[i] của parent (đang đầy 2T-1 khóa) làm 2 node,
// đẩy khóa giữa (vị trí T-1) lên parent
function splitChild(parent, i, T) {
  const fullChild = parent.children[i];
  const newChild = new BTreeNode(fullChild.leaf);
  const midKey = fullChild.keys[T - 1];

  newChild.keys = fullChild.keys.slice(T);        // nửa phải -> node mới
  fullChild.keys = fullChild.keys.slice(0, T - 1); // nửa trái -> node cũ

  if (!fullChild.leaf) {
    newChild.children = fullChild.children.slice(T);
    fullChild.children = fullChild.children.slice(0, T);
  }

  parent.children.splice(i + 1, 0, newChild); // gắn node mới làm con của parent
  parent.keys.splice(i, 0, midKey);           // đẩy khóa giữa lên parent
}

B-Tree tách chủ động (proactive splitting): khi đi từ gốc xuống lá để chèn, hễ gặp node đầy trên đường đi là tách ngay trước khi đi tiếp — đảm bảo node cha luôn còn chỗ trống để nhận khóa đẩy lên.

4. Xoá node & Gộp trang (Delete & Merge)

Xoá phức tạp hơn chèn: sau khi xoá, một node có thể có ít hơn $\lceil m/2 \rceil{-}1$ khóa (vi phạm ràng buộc tối thiểu). Khi đó node phải "mượn" (borrow) một khóa từ node anh em liền kề (nếu anh em đó dư khóa), hoặc gộp (merge) với node anh em nếu cả 2 đều thiếu khóa:

btree-delete-fix.js
// Sau khi xoá 1 khóa khỏi child[i] khiến nó thiếu khóa (còn < T khóa):
function fill(node, i, T) {
  if (i > 0 && node.children[i - 1].keys.length >= T) {
    borrowFromPrev(node, i);   // mượn khóa từ anh em BÊN TRÁI (nếu dư)
  } else if (i < node.keys.length && node.children[i + 1].keys.length >= T) {
    borrowFromNext(node, i);   // mượn khóa từ anh em BÊN PHẢI (nếu dư)
  } else {
    // Cả 2 bên đều không dư — GỘP với 1 anh em + khóa cha ở giữa
    if (i < node.keys.length) merge(node, i);
    else merge(node, i - 1);
  }
}
ℹ️ Lưu ý: Gộp trang là cách DUY NHẤT B-Tree giảm chiều cao
Nếu gộp diễn ra ngay tại node gốc (root chỉ còn 0 khóa sau khi gộp 2 con làm 1), node gốc cũ bị loại bỏ và node con duy nhất trở thành gốc mới — cây giảm 1 tầng. Đây là điểm đối xứng hoàn hảo với việc tách trang ở node gốc khi chèn (tăng chiều cao) — cả hai đều xảy ra ở gốc, không bao giờ ở lá.

5. So sánh B-Tree với AVL/Red-Black cho lưu trữ trên đĩa

Tiêu chí B-Tree AVL / Red-Black
Số con mỗi node Hàng chục tới hàng trăm (bậc $m$) Tối đa 2
Chiều cao cây với 1 tỷ bản ghi $O(\log_m n)$ — chỉ ~4-5 tầng với $m$=100 $O(\log_2 n)$ — khoảng 30 tầng
Số lần đọc đĩa mỗi tra cứu Rất ít (mỗi node = 1 trang đĩa) Nhiều (mỗi node truy cập riêng lẻ)
Ứng dụng điển hình Chỉ mục CSDL (MySQL, PostgreSQL), hệ thống file Cấu trúc trong RAM (std::map, TreeMap)
🔬 Đào sâu: B+Tree — biến thể thực tế các CSDL hay dùng
Hầu hết CSDL thực tế (InnoDB của MySQL, PostgreSQL) dùng B+Tree chứ không phải B-Tree thuần: mọi dữ liệu thực sự chỉ lưu ở node lá (node trong chỉ lưu khóa để định tuyến), và các lá được liên kết thành danh sách — giúp quét theo khoảng (range scan, ví dụ WHERE age BETWEEN 20 AND 30) cực nhanh mà không cần đi lên xuống cây liên tục.
💡 Mẹo: Vì sao MySQL/PostgreSQL không dùng AVL cho chỉ mục?
Nếu dùng AVL cho chỉ mục 1 tỷ dòng, mỗi lần tra cứu cần ~30 lần đọc đĩa ngẫu nhiên (random I/O) — cực chậm dù ổ SSD hiện đại. B-Tree bậc cao gói gọn tra cứu trong 4-5 lần đọc, mỗi lần đọc trọn 1 trang đĩa chứa hàng trăm khóa cùng lúc — tận dụng tối đa băng thông đĩa thay vì tối ưu số lần so sánh trong RAM như AVL.

Sân chơi tương tác: B-Tree Visualizer

Chèn hoặc xoá giá trị để xem cây B-Tree bậc 4 tự tách/gộp trang — quan sát khóa được đẩy lên node cha khi tách, và node anh em cho mượn/gộp khóa khi xoá.

🗄️ Sân chơi tương tác: B-Tree Visualizer

Điều khiển (bậc m=4)

Số khóa0
Chiều cao cây0

Nhật ký

btree-live.js

Trắc nghiệm ôn tập

Câu 1: Vì sao B-Tree cho phép mỗi node chứa hàng trăm khóa thay vì chỉ 1 khóa như cây nhị phân?

Trắc nghiệm ôn tập

Câu 2: Khi nào B-Tree tăng thêm 1 tầng chiều cao?

Trắc nghiệm ôn tập

Câu 3: Vì sao hầu hết CSDL thực tế (MySQL InnoDB, PostgreSQL) dùng B+Tree thay vì B-Tree thuần cho chỉ mục?

📖 Tài liệu tham khảo / References

Bài viết liên quan trong series

Bài 9: Memory Allocator Visualizer Bài 7: Quy Hoạch Động Trực Quan Quay lại Lộ trình Series DSA Bài 1: Xoay Cây AVL & Red-Black (cây tự cân bằng trong RAM)