SydexaSydexa

SQL Nâng Cao

Index và B+TreeCái giá của IndexEquality & EXPLAIN PlanConcatenated Index & Thứ tự cột3 chiến thuật ScanKhi Function phá IndexPlan Caching & SQL InjectionRange, LIKE & Bitmap CombinePartial Index & Bẫy NULLJoin - Nested Loop & IndexJoin - Hash Join & Sort MergeClustering Data - Index Filter & Index Only ScanClustering Data - Index Organized TablesSorting & Grouping với IndexPartial Result, Top N, Pagination & Window FunctionsInsert, Update, Delete - tác động của Index lên DML
Trang chủKhoá họcSQL Nâng Cao
Index và B+Tree
Từ Full Table Scan đến B+Tree, hiểu tại sao disk I/O là nút thắt, cách xây dựng cấu trúc index từng bước, và sự khác biệt giữa B-Tree thuần và B+Tree mà database thực sự dùng.
0:00/0:00

Quick Quiz

1.Ở ví dụ mở đầu, chỉ thêm 1 dòng CREATE INDEX mà query nhanh hơn 4000 lần. Theo bạn, điều gì đã thay đổi?
1/1

Lộ trình bài học

Câu hỏi vừa rồi chạm đến bản chất của Index, một cấu trúc dữ liệu giúp database nhảy thẳng đến kết quả thay vì duyệt từng dòng. Bài học này sẽ đưa bạn từ bài toán gốc đến cấu trúc hoàn chỉnh, chia thành 3 chặng:

  • Chặng 1: Bài toán gốc - Ổ cứng chậm kinh khủng, Full Table Scan là ác mộng. Hiểu tại sao database CẦN một cấu trúc dữ liệu đặc biệt.
  • Chặng 2: Ý tưởng & thất bại - Index là gì, Binary Search Tree hoạt động ra sao, và tại sao nó thất bại trên ổ cứng.
  • Chặng 3: Xây dựng B+Tree - Leaf Node, Doubly Linked List, B-Tree, và cuối cùng B+Tree, cấu trúc dữ liệu đứng sau mọi database hiện đại.

Phần 1: Bài toán gốc: Ổ cứng chậm kinh khủng

0:00/0:00

Và có một chi tiết quan trọng nữa: khi ổ cứng đọc, nó không đọc một byte, nó đọc nguyên cả một khối (block), thường là 4KB hoặc 8KB hoặc 16KB. Giống như bạn mở tủ hồ sơ, bạn không lôi ra một tờ giấy, bạn lôi nguyên cả ngăn kéo.

Database cũng vậy, nó đọc dữ liệu theo đơn vị gọi là page (trang), mỗi page thường 4KB đến 16KB. Và đây chính là manh mối quan trọng nhất mà chúng ta sẽ cần để hiểu B-Tree.

Ghi nhớ 2 điều này, sẽ cần ở phần sau:

1. Đọc disk rất đắt → cần giảm số lần đọc

2. Mỗi lần đọc = đọc nguyên một page → cần tận dụng tối đa mỗi lần đọc

Quick Quiz

1.HDD chậm hơn RAM khoảng bao nhiêu lần?
1/1

Phần 2: Không có Index: Full Table Scan

0:00/0:00

Quick Quiz

1.Khi chạy SELECT * FROM users WHERE email = 'abc@mail.com' mà KHÔNG có index, database phải làm gì?
1/1

Phần 3: Index là gì? Khái niệm nền tảng

Tấm bản đồ chỉ đường

Hãy tưởng tượng bảng dữ liệu của bạn là một cuốn từ điển khổng lồ nhưng các trang lại bị xáo trộn ngẫu nhiên. Để tìm một từ, bạn buộc phải lật từng trang từ đầu đến cuối (Full Table Scan). Đó là ác mộng về hiệu năng.

Đây là lúc Index xuất hiện như một “vị cứu tinh”.

Về bản chất, khi bạn chạy lệnh CREATE INDEX, database không sắp xếp lại bảng gốc. Thay vào đó, nó âm thầm tạo ra một cấu trúc dữ liệu riêng biệt, một bảng chỉ mục phụ trợ, chỉ chứa cột bạn cần tìm kiếm đã được sắp xếp trật tự, kèm theo một tấm vé chỉ đường (gọi là ROWID trong Oracle database hoặc ctid trong PostgreSQL) trỏ thẳng về dữ liệu gốc.

Hãy nghĩ về Index giống hệt phần Mục lục (Index) ở cuối một cuốn sách giáo khoa:

  • Nằm tách biệt - Nó nằm tách biệt ở những trang cuối (chiếm disk space riêng).
  • Tra cứu nhanh - Nó liệt kê từ khóa theo thứ tự A-Z để bạn tra cứu nhanh, kèm số trang (ROWID) để bạn lật ngay đến nơi.
  • Dư thừa có chủ đích - Nó là sự "Dư thừa có chủ đích": Nếu xé bỏ phần mục lục, nội dung sách không mất đi, nhưng tốc độ tìm kiếm của bạn sẽ giảm đi hàng nghìn lần.

Tuy nhiên, đừng để phép so sánh trên đánh lừa bạn rằng Index đơn giản.

Sách in là dữ liệu chết: Bạn chỉ làm mục lục một lần duy nhất khi xuất bản.

Database là cơ thể sống: Dữ liệu liên tục biến đổi.

Đây chính là điểm mà bạn cần lưu ý: Mỗi khi bạn INSERT, UPDATE, hay DELETE một dòng trong bảng, database buộc phải lao đi cập nhật lại toàn bộ các Index liên quan ngay lập tức. Bạn nhận được tốc độ đọc siêu nhanh, nhưng phải trả giá bằng công sức bảo trì hệ thống mỗi khi ghi dữ liệu.

Điều rút ra: Index là một cuộc đánh đổi: Chúng ta chấp nhận tốn thêm dung lượng lưu trữ (Disk Space) và giảm tốc độ ghi (Write Speed) để đổi lấy tốc độ đọc (Read Speed) cực nhanh.

Quick Quiz

1.Khi bạn tạo một Index, điều nào sau đây là ĐÚNG?
1/1

Chúng ta đã biết Index lưu gì: key đã sắp xếp + pointer về bảng gốc. Nhưng đó mới chỉ là một nửa câu chuyện. Nửa còn lại, cũng là nửa quyết định hiệu năng, là: dùng cấu trúc dữ liệu nào để tổ chức những key ấy trên disk?

Phần 4: Binary Search Tree: Và tại sao nó thất bại

Chúng ta cần một cấu trúc dữ liệu có thể tìm kiếm nhanh trong hàng triệu key đã sắp xếp. Nếu bạn từng học qua môn Cấu trúc dữ liệu & Giải thuật, câu trả lời đầu tiên hiện lên trong đầu gần như chắc chắn là Binary Search Tree (BST), cây nhị phân tìm kiếm. Chia đôi, so sánh, loại nửa còn lại, đơn giản và hiệu quả với độ phức tạp O(log₂n).

Trên lý thuyết, BST là ứng viên hoàn hảo. Nhưng khi đặt nó lên disk thay vì RAM, mọi thứ sụp đổ.

0:00/0:00

Quick Quiz

1.BST có O(log₂n) nhưng vẫn chậm trên disk. Tại sao?
1/1

Phần 5: Từ Sorted Array đến B-Tree: Bước tiến hóa

BST thất bại vì lãng phí 99.9% mỗi page. Ý tưởng đã rõ: nhét cho đầy mỗi page với data. Nhưng nhét bằng cách nào?

Phương án hiển nhiên: Sorted Array

Cách đơn giản nhất ai cũng nghĩ đến: xếp tất cả key vào một mảng đã sắp xếp trên disk. Nhưng mỗi lần INSERT, toàn bộ phần tử phía sau phải dịch chuyển giống như chèn một cái tên mới vào giữa cuốn danh bạ điện thoại đã in. Chi phí: O(n) cho mỗi insert.

Sorted Array, Chi phí Insert O(n)

[0]
[1]
[2]
[3]
[4]
[5]
[6]
[7]
5
10
15
20
25
30
35
18

Bấm “Insert 18” để xem các phần tử bị dịch chuyển

Sorted array không phải hoàn toàn vô dụng, nó cho phép tìm kiếm cực nhanh nhờ Binary Search (O(log n)). Vấn đề duy nhất là ghi quá chậm (O(n)) vì phải dịch chuyển dữ liệu. Vậy có cách nào vừa tìm nhanh, vừa không phải dịch chuyển hàng loạt khi ghi?

Database giải quyết bằng một ý tưởng then chốt: thay vì ép tất cả vào một mảng khổng lồ, hãy chia nhỏ sorted array thành nhiều page (gọi là leaf node), nối chúng bằng Doubly Linked List để duyệt tuần tự, rồi xây một cây phân cấp (tree) phía trên để tìm nhanh. Kết quả chính là B-Tree, cấu trúc dữ liệu mà hầu hết database hiện đại đều sử dụng.

  • Leaf Node (tầng lá) - Mỗi page chứa hàng trăm key đã sắp xếp, tận dụng tối đa mỗi lần đọc disk.
  • Doubly Linked List (cầu nối) - Nối các leaf node lại với nhau, cho phép duyệt tuần tự mà không cần quay lại root.
  • Tree (tầng trên) - Cây phân cấp đóng vai trò bản đồ, cho phép nhảy thẳng đến đúng leaf node cần thiết, O(log n).

Ba thành phần này kết hợp lại tạo nên B-Tree. Trong 3 phần tiếp theo, chúng ta sẽ lần lượt “mổ xẻ” từng thành phần để hiểu rõ cách chúng hoạt động.

Phần 6: Giải phẫu một Leaf Node

Ở phần trước, chúng ta đã biết database chia dữ liệu index thành các page nhỏ gọi là leaf node, nơi dữ liệu index thực sự được lưu trữ. Bây giờ hãy “mở nắp” một leaf node ra xem bên trong có gì.

0:00/0:00

Quick Quiz

1.Trong InnoDB (MySQL), mỗi Leaf Node (page) có kích thước bao nhiêu?
1/1

Phần 7: Doubly Linked List: Cầu nối giữa các Leaf Node

Thứ tự trên 2 cấp độ

Index duy trì thứ tự trên 2 cấp độ đồng thời:

  • Cấp 1: Bên trong mỗi leaf node - Các entries sắp xếp tăng dần theo key value. Ví dụ: [11, 13, 18] luôn có thứ tự.
  • Cấp 2: Giữa các leaf node - Doubly linked list nối các nodes lại. Node chứa [11,13,18] → Node chứa [21,27,27] → Node chứa [34,35,39].

Tiếp tục câu chuyện về những “chiếc hộp” Leaf Node. Nếu Leaf Node là những hòn đảo chứa kho báu, thì Doubly Linked List (Danh sách liên kết đôi) chính là hệ thống cầu nối giúp chúng ta di chuyển giữa các đảo mà không cần quay lại đất liền.

Tại sao database lại chọn kiến trúc này? Dưới đây là 3 lý do “sống còn”:

Doubly Linked List, Insert O(1)

Node 111, 13, 18Node 221, 27, 27Node 334, 35, 39New Node19, 20, 21← Doubly Linked List: mỗi node có con trỏ prev và next →

Bấm “Insert Node” để xem cách chèn node mới vào Linked List

1. Duyệt tuần tự: Range Query cực nhanh

Đây là lý do quan trọng nhất. Khi bạn viết WHERE price BETWEEN 100 AND 500, database dùng tree để nhảy thẳng tới leaf node chứa giá trị 100. Sau đó, thay vì phải leo ngược lên root để tìm giá trị tiếp theo, nó chỉ cần đi theo con trỏ “next” sang leaf node kế tiếp, cứ thế chạy dọc cho đến khi vượt quá 500.

Không có linked list, mỗi lần muốn sang leaf node kế tiếp, database phải leo ngược lên parent rồi đi xuống lại, có thể tốn nhiều lần disk I/O. Linked list biến thao tác này thành một bước duy nhất.

  • Không có linked list - Mỗi lần sang leaf node kế tiếp phải leo lên parent → đi xuống lại. Trong cây 4 tầng, mỗi bước nhảy có thể tốn 6+ lần đọc disk.
  • Có linked list - Theo con trỏ next/previous, chỉ 1 lần đọc disk. Hiệu quả gấp bội cho range query và sequential scan.
0:00/0:00

Quick Quiz

1.Tại sao bạn KHÔNG cần tạo 2 index riêng biệt cho ORDER BY ASC và ORDER BY DESC?
1/1
0:00/0:00

Quick Quiz

1.Không có linked list giữa các leaf node, database phải làm gì khi range query cần dữ liệu ở leaf node kế tiếp?
1/1

Phần 8: B-Tree: Tấm bản đồ kho báu

0:00/0:00

Sức mạnh của con số

Trong InnoDB (MySQL), mỗi page là 16KB. Mỗi node B-Tree chiếm đúng 16KB, vừa khít một page. Một node internal có thể chứa khoảng 500 đến 1000 key tuỳ kích thước key.

Giả sử mỗi node chứa được khoảng 500 key. Một cây B-Tree 3 tầng có thể chứa:

500 × 500 × 500 = 125 TRIỆU bản ghi: Bạn đọc lại đi: BA lần đọc disk, chỉ BA, để tìm ra một bản ghi trong 125 triệu dòng. So với BST cần 27 lần đọc (log₂ của 125 triệu).

Và thực tế còn ngon hơn thế: node gốc (root) gần như luôn nằm sẵn trong RAM vì nó được truy cập liên tục. Nên thực tế thường chỉ cần 2 lần đọc disk, đôi khi chỉ 1 lần.

Cấu trúc 3 tầng

B-Tree gồm 3 loại node:

  • Root - Đỉnh cây, chỉ 1 node
  • Branch - Tầng trung gian, chứa key + pointer dẫn xuống tầng dưới
  • Leaf - Tầng cuối, chứa key + ROWID (pointer về bảng gốc)

Quy tắc xây dựng B-Tree

Mỗi entry trong branch node chứa giá trị lớn nhất (max key) của leaf node tương ứng bên dưới, kèm con trỏ đến leaf node đó. Ví dụ: leaf node chứa [46, 53, 53] → branch entry = 53.

Tầng branch được xây từ dưới lên: gom tất cả max keys của leaf nodes → tạo branch layer. Nếu branch layer vẫn quá lớn → tạo thêm tầng branch nữa, cho đến khi toàn bộ fit vào 1 node duy nhất = Root Node.

Xây dựng B-Tree từ dưới lên

Leaf111318Leaf2127Leaf303539Leaf46Leaf485153Leaf555757Leaf5862Leaf758083Leaf8590Leaf9295Leaf9798Leaf105Leaf110120← Doubly Linked List giữa các Leaf Nodes →Branch182739Branch46535783Branch909598Branch105120Root39839818273946535783909598105120398398

Bấm “Xây B-Tree” để xem cách cây được xây từ dưới lên

B-Tree Traversal, Tìm key = 57

Root39839839, 83, 98Branch18, 27, 39Branch4653578346, 53, 57, 83Branch90, 95, 98Branch105, 120Leaf11, 13, 18Leaf21, 27Leaf30, 35, 39Leaf4646Leaf48515348, 51, 53Leaf55575755, 57, 57Leaf586258, 62Leaf75808375, 80, 83Leaf85, 90Leaf92, 95Leaf97, 98Leaf105Leaf110, 12057Chỉ các node trên đường đi được highlight, các node khác mờ đi

Bấm “Search 57” để xem đường đi từ Root xuống Leaf

B-Tree Traversal: Ví dụ tìm key = 57

Bước 1: Root Node: [39, 83, 98]

Duyệt: 39 < 57 → tiếp. 83 ≥ 57 → đi theo pointer của 83 xuống branch node giữa.

Bước 2: Branch Node: [46, 53, 57, 83]

Duyệt: 46 < 57 → tiếp. 53 < 57 → tiếp. 57 ≥ 57 → đi theo pointer của 57 xuống leaf node.

Bước 3: Leaf Node: [55, 57, 57]

Tìm thấy 2 entries có key=57! Tổng cộng: chỉ đọc 3 blocks (1 root + 1 branch + 1 leaf).

Trên SSD? Khoảng 0.3ms. Đó là sức mạnh của B-Tree.

Quick Quiz

1.Một B-Tree 3 tầng, mỗi node chứa 500 key, có thể chứa tối đa bao nhiêu bản ghi?
1/2

Ghép tất cả lại: B-Tree Insert

Qua 3 phần vừa rồi, bạn đã hiểu từng thành phần: Leaf Node chứa data, Doubly Linked List nối chúng lại, và Tree dẫn đường từ trên xuống. Bây giờ hãy xem chúng phối hợp với nhau như thế nào khi INSERT một key mới vào B-Tree:

B-Tree Insert, Duyệt Root → Branch → Leaf

Root20, 40Branch10Branch25, 30Branch50Leaf3, 5, 8Leaf10, 12, 15Leaf20, 22Leaf25, 27, 28Leaf30, 33, 37Leaf40, 42, 45Leaf50, 55, 6018← Doubly Linked List giữa các Leaf Nodes →

Bấm “Insert 18” để xem đường đi từ Root xuống Leaf

Phần 9: B+Tree: Cái tên thật của thứ bạn đã học

0:00/0:00

Tóm lại, B+Tree cải tiến B-Tree thuần ở 2 điểm:

B-Tree thuầnB+Tree (cái bạn đã học)
Linked List giữa leaf?Không có, phải quay lại parentCó, Doubly Linked List nối các leaf
Data ở đâu?Mọi node (root, branch, leaf)Chỉ leaf node
Internal node chứa gì?Key + data → ít key/nodeChỉ key + pointer → nhiều key/node
Chiều cao cây?Cao hơn (ít key → nhiều tầng)Lùn hơn (nhiều key → ít tầng)
Range queryChậm, leo lên leo xuống treeNhanh, lướt theo linked list
Database nào dùng?Hầu như không còn dùngMySQL, PostgreSQL, SQLite, Oracle...
Tại sao mọi người vẫn gọi là “B-Tree”?: Trong tài liệu PostgreSQL, MySQL docs, và hầu hết sách thực hành, thuật ngữ “B-Tree index” thực chất đang nói về B+Tree. Đây là quy ước ngành, giống như mọi người gọi “xe hơi” dù engine bên trong đã thay đổi hoàn toàn so với thế kỷ trước.

Range Query: nơi B+Tree tỏa sáng

Khi bạn viết:

Database tìm leaf node chứa giá trị 100 (chỉ cần 2-3 lần đọc disk nhờ cây lùn), rồi lướt theo linked list qua các leaf liên tiếp cho đến khi vượt quá 500. Toàn bộ quá trình là sequential I/O (tuần tự), nhanh gấp bội so với phải leo lên leo xuống cây như B-Tree thuần.

Range Query, B+Tree vs B-Tree

B+Tree250,500100,250400,50070050,80100,150200,250300,350400,450500,550600,700800Disk reads: 0B-Tree250,500100,250400,50070050,80100,150200,250300,350400,450500,550600,700800Disk reads: 0

Bấm để so sánh range query trên B+Tree vs B-Tree

Quick Quiz

1.Cấu trúc index bạn học ở các phần trước (leaf nodes + linked list + tree) thực ra là gì?
1/2

Tổng kết

Hành trình của bài này:Từ câu hỏi “tại sao query chậm?” → hiểu disk I/O là nút thắt → Full Table Scan đọc hết mọi page → BST lãng phí 99.9% mỗi page → xây dựng cấu trúc index từng bước (Leaf Node → Linked List → Tree) → và cuối cùng nhận ra: cái chúng ta xây chính là B+Tree, cấu trúc mà mọi database hiện đại đều dùng.

Disk rất chậm: chậm bao nhiêu?

Nhấp vào thẻ để lật

HDD chậm hơn RAM 100.000 lần. Bài toán cốt lõi: giảm số lần đọc disk.

Nhấp vào thẻ để lật lại
1/7
Ở bài tiếp theo, chúng ta sẽ đi vào thực tế: 3 bước của Index Lookup, khi nào index phản tác dụng, và trade-offs khi đánh index.
Bài tiếp theo
range_query.sql
SQL
1
SELECT * FROM orders WHERE price BETWEEN 100 AND 500;