B-tree và chỉ mục cơ sở dữ liệu
(planetscale.com)B-tree là gì?
-
B-tree đóng vai trò nền tảng trong nhiều phần mềm, đặc biệt là các hệ quản trị cơ sở dữ liệu (DBMS)
-
MySQL, Postgres, MongoDB, Dynamo... dựa vào B-tree để truy xuất dữ liệu hiệu quả thông qua chỉ mục
-
Bài viết sẽ tìm hiểu cách B-tree và B+tree hoạt động, vì sao cơ sở dữ liệu dùng chúng cho chỉ mục, và vì sao dùng UUID làm khóa chính có thể là một ý tưởng tồi
-
B-tree lưu các cặp dữ liệu được gọi là khóa và giá trị trong một cấu trúc mà lập trình viên gọi là giống cây
-
B-tree gồm các nút (hình chữ nhật) và con trỏ con (các đường nối giữa các nút)
-
Nút trên cùng được gọi là nút gốc, các nút ở tầng dưới cùng là nút lá, còn lại là nút bên trong
-
Dưới đây là định nghĩa của B-tree:
- Mỗi nút lưu N cặp khóa/giá trị (trong đó N lớn hơn 1 và nhỏ hơn hoặc bằng K)
- Mỗi nút bên trong có ít nhất N/2 cặp khóa/giá trị (nút bên trong là nút không phải lá hay gốc)
- Mỗi nút có N+1 nút con
- Nút gốc có ít nhất một giá trị và hai nút con, trừ khi nó là nút duy nhất
- Mọi nút lá đều ở cùng một mức
-
Một đặc điểm cốt lõi khác của B-tree là tính sắp xếp. Các phần tử trong mỗi nút luôn được giữ theo thứ tự
-
Nhờ đó có thể tìm kiếm khóa rất hiệu quả. Quá trình tìm kiếm bắt đầu từ nút gốc và:
- Kiểm tra xem khóa cần tìm có trong nút hay không
- Nếu không, xác định vị trí khóa sẽ được chèn vào nút nếu cần thêm mới
- Từ đó, đi theo con trỏ con xuống tầng tiếp theo và lặp lại quy trình
-
Theo cách này, để tìm một khóa chỉ cần truy cập một nút ở mỗi tầng của cây
-
B-tree được thiết kế đặc biệt để hoạt động tốt khi có rất nhiều dữ liệu và vẫn phải được lưu bền vững trên bộ nhớ dài hạn (đĩa). Lý do là mỗi nút dùng một số byte cố định
-
Số lượng giá trị mỗi nút có thể lưu trong B-tree phụ thuộc vào số byte được cấp cho nút đó và số byte mà mỗi cặp khóa/giá trị tiêu thụ
B+tree (được tối ưu cho cơ sở dữ liệu)
- B+tree tương tự B-tree, nhưng có các thay đổi về quy tắc như sau:
- Các cặp khóa/giá trị chỉ được lưu ở nút lá
- Các nút không phải lá chỉ lưu khóa và con trỏ con liên quan
- Cách MySQL triển khai B+tree trong chỉ mục còn có thêm hai quy tắc riêng:
- Các nút không phải lá lưu N con trỏ con thay vì N+1
- Mọi nút còn bao gồm cả con trỏ "next" và "previous", để mỗi tầng của cây cũng có thể hoạt động như một danh sách liên kết kép
- Có hai lý do khiến B+tree phù hợp hơn với cơ sở dữ liệu:
- Nút bên trong không cần lưu giá trị nên có thể chứa nhiều khóa hơn trên mỗi nút. Điều này giúp giảm độ sâu của cây
- Mọi giá trị đều được lưu ở cùng một mức và có thể được duyệt theo thứ tự qua danh sách liên kết ở tầng dưới cùng
Cách MySQL dùng B+tree
- MySQL hỗ trợ nhiều storage engine, và engine được dùng phổ biến nhất là InnoDB, vốn phụ thuộc rất nhiều vào B+tree
- Trên thực tế, InnoDB phụ thuộc đến mức lưu toàn bộ dữ liệu bảng trong B+tree bằng cách dùng khóa chính của bảng làm khóa của cây
- Mỗi khi tạo một bảng InnoDB mới, bạn cần chỉ định khóa chính
- MySQL sẽ tạo một B+tree cho mỗi bảng mới; giá trị được đặt làm khóa chính sẽ là khóa của cây. Giá trị là các cột còn lại của từng hàng và chỉ được lưu ở các nút lá
- Kích thước mỗi nút trong B+tree này mặc định được đặt là 16k
- Mỗi khi MySQL cần truy cập một mẩu dữ liệu (khóa, giá trị...), nó sẽ nạp toàn bộ trang liên quan (nút B+tree) từ đĩa, ngay cả khi không cần các khóa hay giá trị khác trong trang đó
- Việc tạo chỉ mục phụ trên bảng InnoDB cũng rất phổ biến. Với mỗi chỉ mục phụ, một B+tree bền vững bổ sung sẽ được tạo ra; khóa là cột mà bạn chọn để lập chỉ mục, còn giá trị là khóa chính của hàng tương ứng
Chọn khóa chính: chèn dữ liệu
- Vì cách dữ liệu bảng được sắp xếp trong B+tree phụ thuộc vào khóa bạn chọn, nên việc chọn
PRIMARY KEYảnh hưởng đến bố cục đĩa của toàn bộ dữ liệu trong bảng - Hai lựa chọn thường gặp cho khóa chính là:
- dãy số nguyên (
BIGINT UNSIGNED AUTO_INCREMENT...) UUID(có nhiều phiên bản)
- dãy số nguyên (
- Nếu dùng khóa chính UUIDv4, khi chèn dữ liệu sẽ có các hệ quả sau:
- Không thể dự đoán trước nút nào sẽ được truy cập để chèn
- Không thể dự đoán trước nút lá đích cho việc chèn
- Giá trị trong các lá sẽ không được sắp theo thứ tự
- Vấn đề số 1 và 2 xảy ra vì trong quá trình chèn nhiều lần, phải truy cập nhiều nút (trang) khác nhau trong cây. Việc đọc và ghi quá mức này dẫn đến suy giảm hiệu năng
- Nếu dùng
BIGINT UNSIGNED AUTO_INCREMENTlàm khóa chính:- Khi chèn giá trị mới, luôn đi theo nhánh ngoài cùng bên phải
- Lá chỉ được thêm ở phía bên phải của cây
- Ở mức lá, dữ liệu được sắp theo đúng thứ tự đã chèn
- Nhờ 1 và 2, nhiều thao tác chèn diễn ra tuần tự sẽ lặp lại cùng một đường đi qua các trang, nên giảm số lượng yêu cầu I/O khi chèn nhiều cặp khóa/giá trị
Chọn khóa chính: đọc dữ liệu theo thứ tự
- Việc truy xuất dữ liệu theo thứ tự thời gian là rất phổ biến trong cơ sở dữ liệu
- Nếu dùng UUIDv4 làm khóa chính, chuỗi giá trị trong kết quả truy vấn sẽ bị phân tán trên nhiều nút lá không liền kề
- Khi tìm các giá trị được chèn tuần tự, mọi trang chứa kết quả tìm kiếm sẽ nằm sát nhau. Khi truy xuất nhiều hàng, thậm chí tất cả có thể cùng nằm trên một trang duy nhất và liền kề nhau
- Với kiểu truy vấn như vậy, dùng khóa chính tuần tự có thể làm giảm số trang cần đọc
Chọn khóa chính: kích thước
- Kích thước khóa chính cũng là một yếu tố quan trọng. Khóa chính luôn phải:
- đủ lớn để không bị cạn kiệt
- đủ nhỏ để không tiêu tốn quá nhiều dung lượng lưu trữ
- Với dãy số nguyên, các bảng nhỏ hơn có thể dùng
MEDIUMINT(16 triệu giá trị duy nhất) hoặcINT(4 tỷ giá trị duy nhất) - Với bảng lớn, có thể dùng
BIGINTđể an toàn (khoảng 18 tỷ tỷ giá trị khả dụng).BIGINTlà 64-bit (8 byte) - UUID thường là 128-bit (16 byte), tức gấp đôi kiểu số nguyên lớn nhất của MySQL
- Vì các nút B+tree có kích thước cố định, dùng
BIGINTsẽ chứa được nhiều khóa trên mỗi nút hơn so với UUID. Điều này dẫn đến cây nông hơn và tra cứu nhanh hơn
B+tree, page và InnoDB
- Một trong những ưu điểm lớn của B+tree là có thể đặt kích thước nút theo ý muốn
- Trong InnoDB, các nút B+tree thường được đặt bằng kích thước page của InnoDB, tức 16k
- Trong quá trình xử lý truy vấn (và vì thế là duyệt B+tree), InnoDB không đọc từng hàng và cột riêng lẻ từ đĩa. Mỗi khi cần truy cập một mẩu dữ liệu, nó sẽ nạp toàn bộ page liên quan từ đĩa
- Để giảm tác động này, InnoDB có một số kỹ thuật, trong đó quan trọng nhất là buffer pool. Buffer pool là bộ nhớ đệm trong RAM cho các page của InnoDB, nằm giữa các page trên đĩa và quá trình thực thi truy vấn của MySQL
- Khi MySQL cần đọc một page, nó sẽ kiểm tra trước xem page đó đã có trong buffer pool chưa. Nếu có, nó đọc từ đó và bỏ qua thao tác I/O đĩa. Nếu chưa có, nó sẽ tìm page trên đĩa, thêm vào buffer pool rồi tiếp tục thực thi truy vấn
- Buffer pool cải thiện hiệu năng truy vấn một cách rất lớn. Nếu không có buffer pool, hệ thống sẽ phải thực hiện nhiều thao tác I/O đĩa hơn rất nhiều để xử lý workload truy vấn
Một số tình huống khác
- Ở đây chủ yếu tập trung so sánh khóa tuần tự với khóa ngẫu nhiên/UUID, nhưng các nguyên tắc này vẫn là những điều hữu ích cần ghi nhớ bất kể bạn đang cân nhắc loại khóa chính hay khóa phụ nào
- Ví dụ, bạn có thể cân nhắc dùng dấu thời gian
user.created_atlàm khóa chỉ mục; nó có các đặc tính tương tự số nguyên tuần tự. Trừ khi chèn dữ liệu lịch sử, việc chèn thường sẽ luôn đi theo nhánh ngoài cùng bên phải - Ngược lại, một chuỗi như
user.email_addresscó đặc tính giống khóa ngẫu nhiên hơn. Người dùng không tạo tài khoản theo thứ tự chữ cái của email, nên việc chèn sẽ diễn ra rải khắp B+tree
Kết luận
- Bài viết đã đề cập khá nhiều về B+tree, chỉ mục và việc chọn khóa chính
- Nhìn bề ngoài thì có vẻ đơn giản, nhưng để khai thác tối đa hiệu năng của cơ sở dữ liệu, vẫn có những khác biệt tinh tế cần cân nhắc
- Để thử nghiệm thêm, hãy ghé thăm trang web B+tree tương tác
1 bình luận
Ý kiến Hacker News
Duy trì wiki hữu ích bằng cách áp dụng chiến lược quản lý như B-Tree
Tôi đã tìm thứ như thế này từ rất lâu rồi, một bài viết tuyệt vời
Cảm ơn vì các hình minh họa tuyệt vời
Trên Firefox di động, cửa sổ cookie không hoạt động
Không nên dùng cột UUID làm khóa chính
Đây là tài liệu giảng dạy rất tốt
Nếu khối đĩa và nút B-Tree đều là 16k, còn khóa, giá trị và con trỏ con đều là 8 bit, thì có thể lưu 682 cặp khóa/giá trị và 683 con trỏ con trên mỗi nút
Bài viết rất hay
Có người hỏi v0, v1, ...v10 trong biểu đồ có nghĩa là gì
Một hình dung tương tác rất đẹp