Trực quan hóa chỉ mục SQLite
(mrsuh.com)- Phân tích cấu trúc B-Tree để xem chỉ mục SQLite thực sự được bố trí trên đĩa và trong bộ nhớ như thế nào, rồi dump dữ liệu chỉ mục để trực quan hóa
- Chỉ mục được cấu thành theo đơn vị Page và Cell; Page có liên kết tới con bên phải và dữ liệu Cell, còn Cell có dữ liệu chỉ mục, rowId và liên kết tới con bên trái
- Chỉ các thông tin về kích thước Page, số entry, độ sâu B-tree, số Page sử dụng do
sqlite3_analyzercung cấp là chưa đủ, nên đã thêm hàm debug vào mã nguồn SQLite - Thử nghiệm so sánh số lượng record, ASC/DESC, chỉ mục dựa trên biểu thức, UNIQUE có NULL, Partial Index, nhiều cột, văn bản, REAL và tổ hợp số nguyên + văn bản
- Với 1.000.000 record, nếu tạo chỉ mục trước khi chèn thì có 3.342 Pages; nếu tạo sau khi chèn thì có 2.930 Pages, và sau VACUUM hoặc REINDEX cũng giảm xuống 2.930 Pages
Vì sao tự mình xem bên trong chỉ mục SQLite
- Đây là một thử nghiệm nhằm vượt ra ngoài cấu trúc cơ bản của chỉ mục để kiểm tra cấu trúc dữ liệu, thuật toán và cách lưu trữ trên đĩa thực tế
- Mục tiêu là xem DBMS lưu chỉ mục trên đĩa và trong bộ nhớ như thế nào, cũng như truy cập chúng ra sao trong quá trình tìm kiếm
- Lý do chọn SQLite làm đối tượng thử nghiệm như sau
- Là DBMS được dùng rộng rãi trong trình duyệt, ứng dụng di động và hệ điều hành
- Dễ debug chỉ bằng ứng dụng client, không cần server riêng
- Codebase nhỏ hơn MySQL hay PostgreSQL nhưng sử dụng cấu trúc dữ liệu tương tự cho chỉ mục
- Là mã nguồn mở
B-Tree được tạo từ Page và Cell
- Theo tài liệu của SQLite, chỉ mục được lưu dưới dạng cấu trúc B-Tree
- Trong SQLite, đơn vị tương ứng với Node là Page
- Page lưu dữ liệu Cell
- Page có liên kết tới Page con bên phải
- Cell bao gồm dữ liệu chỉ mục, rowId và liên kết tới Page con bên trái
- Mỗi hàng trong bảng SQLite mặc định có một rowId duy nhất, và khi không có khóa chính tường minh thì nó hoạt động như khóa chính
- Mỗi Page có kích thước cố định, trong khoảng 512~65.536 bytes
- Header của Page và Cell dùng 4 bytes để lưu liên kết con
- Để biết số Page con, cần đọc riêng header bằng hàm
get4byte(...)
- Để biết số Page con, cần đọc riêng header bằng hàm
- Ví dụ về các struct nội bộ của SQLite như sau
MemPage: bao gồm số Pagepgno, số CellnCell, vùng chỉ mục CellaCellIdx, con trỏ tới ảnh đĩa của dữ liệu PageaData, v.v.CellInfo: bao gồmpPayloadtrỏ tới vị trí bắt đầu của payload, v.v.
Giới hạn của sqlite3_analyzer và hàm debug
- Có thể xem thông tin chung về chỉ mục bằng sqlite3_analyzer
- Ví dụ output bao gồm kích thước Page
4096, số entry1000, độ sâu B-tree2, số Page sử dụng4, v.v.
- Ví dụ output bao gồm kích thước Page
- Tuy nhiên, công cụ này chỉ dừng ở thông tin tổng quan, chưa đủ để trực tiếp xem Cell và payload bên trong chỉ mục
- Sau vài tuần thử nghiệm, đã viết các hàm để phân tích chỉ mục
- Mã:
sqlite.patch sqlite3DebugGetMemoryPayload(Mem *mem)sqlite3DebugGetCellPayloadAndRowId(BtCursor *pCur, MemPage * pPage, int cellIndex)sqlite3DebugBtreeIndexDump(BtCursor *pCur, int pageNumber)
- Mã:
- Các hàm này đọc nội dung chỉ mục được chọn và xuất ra STDOUT
- Luồng là
SQL query -> selected index -> stdout - Output bao gồm số Page, số Page con bên phải, số Cell, số Page con bên trái, payload và rowId
- Luồng là
- Có thể chạy môi trường thử nghiệm bằng Docker
docker run -it --rm -v "$PWD":/app/data --platform linux/x86_64 mrsuh/sqlite-index bashsh bin/dump-index.sh database.sqlite "SELECT * FROM table INDEXED BY index WHERE column=1" dump.txt
Sự thay đổi trong cách trực quan hóa
- Ban đầu sử dụng d3-org-tree để trực quan hóa cấu trúc chỉ mục
- Khi cây sâu hơn và số Page ở mỗi level tăng lên, việc điều chỉnh khoảng cách giữa các Page trở nên khó khăn, khiến hình ảnh quá lớn và khó đọc
- Đã thử điều chỉnh bằng JavaScript và CSS nhưng không phù hợp, nên từng chuyển sang hiển thị cấu trúc dạng văn bản
- Output dạng văn bản hiển thị tổng số Page, tổng số Cell, số Page/Cell theo từng level, thông tin Page, thông tin Cell và payload
- Sau đó phát triển thành output hình ảnh với khả năng kiểm soát thiết kế và khoảng cách chi tiết hơn bằng extension ImageMagick của PHP
- Hình ảnh cuối cùng bao gồm các thông tin sau
- Hiển thị thông tin chung của chỉ mục ở góc trên bên trái
- Hiển thị tổng số Page và Cell ở mỗi level
- Hiển thị số Page, liên kết con bên phải, thông tin Cell đầu tiên và Cell cuối cùng cho từng Page
- Chỉ hiển thị một số Page ở mỗi level, bao gồm Page đầu tiên và Page cuối cùng
- Root Page nằm ở level đầu tiên
- Lệnh tạo hình ảnh từ dump như sau
php bin/console app:render-index --dumpIndexPath=dump.txt --outputImagePath=image.webp
Số lượng record làm thay đổi hình dạng chỉ mục
- Tạo chỉ mục
column1 ASCtrên bảngcolumn1 INT NOT NULLvà thay đổi số lượng record để kiểm tra cấu trúc - Chỉ mục với 1 record gồm 1 level, 1 Page, 1 Cell
- Chỉ mục với 1.000 record cũng được tạo và trực quan hóa theo cách tương tự
- Chỉ mục với 1.000.000 record có cấu trúc như sau
- 3 level
- 2.930 Pages
- 1.000.000 Cells
- Vì dữ liệu được thêm theo thứ tự, khi
rowId = 1thìcolumn1 = 1
Hướng sắp xếp và chỉ mục biểu thức
- Tạo
idx_ascvàidx_desctrên cùng dữ liệu để so sánh chỉ mục ASC/DESC - Chỉ mục ASC giống với chỉ mục trước đó vì thứ tự mặc định là ASC
- Entry có
rowId=1.000.000,column1=1.000.000,payload=1.000.000nằm ở Cell cuối của Page ngoài cùng bên phải - Entry có
rowId=1,column1=1,payload=1nằm ở Cell đầu của Page ngoài cùng bên trái
- Entry có
- Chỉ mục DESC được bố trí ngược lại
- Entry có
rowId=1,column1=1,payload=1nằm ở Cell cuối của Page ngoài cùng bên phải - Entry có
rowId=1.000.000,column1=1.000.000,payload=1.000.000nằm ở Cell đầu của Page ngoài cùng bên trái
- Entry có
- Chỉ mục dựa trên biểu thức lưu chuỗi do biểu thức tạo ra
- Ví dụ trích xuất
$.timestamptừ văn bản JSON, rồi chuyển đổi bằngstrftime('%Y-%m-%d %H:%M:%S', ..., 'unixepoch')để tạo chỉ mục ASC - Cũng có thể dùng các biểu thức phức tạp hơn; chỉ kết quả của chúng được lưu trong chỉ mục
- Ví dụ trích xuất
NULL, Partial Index, nhiều cột
- SQLite hỗ trợ chỉ mục UNIQUE có chứa giá trị NULL
- Ví dụ chèn các giá trị
1, nhiềuNULL,1000000rồi chạyCREATE UNIQUE INDEX idx ON table_test (column1 ASC) - Chỉ mục được trực quan hóa trông như chỉ lưu các giá trị không phải NULL
- Ví dụ chèn các giá trị
- Partial Index có điều kiện
WHERE column1 IS NOT NULLlọc bỏ các giá trị NULL- Chỉ mục này chỉ gồm một Page
- Dẫn tới tìm kiếm nhanh hơn so với ví dụ UNIQUE trước đó
- Chỉ mục nhiều cột lưu tuần tự dữ liệu của tất cả field trong Cell
- Ví dụ là chỉ mục
(column1 ASC, column2 ASC) - Trong phần trực quan hóa, các field được phân tách bằng dấu hai chấm
:
- Ví dụ là chỉ mục
Thời điểm tạo chỉ mục và hiệu quả tái cấu trúc
- So sánh trường hợp tạo chỉ mục trước khi đưa dữ liệu vào và trường hợp tạo chỉ mục sau khi đã đưa toàn bộ dữ liệu vào
- Khi dữ liệu mới được thêm, cây phải tự tái cân bằng
- Cách tạo chỉ mục một lần trên dữ liệu hiện có có thể hiệu quả hơn nhiều
- Hai chỉ mục trông tương tự nhau, nhưng chỉ mục thứ hai có số Page ít hơn có thể nhanh hơn
- Kết quả so sánh với 1.000.000 Cells như sau
| Phân loại | Total Pages | Total Cells |
|---|---|---|
| Tạo trước khi chèn | 3342 | 1000000 |
| Tạo sau khi chèn | 2930 | 1000000 |
- Có thể thực hiện tối ưu hóa tương tự bằng VACUUM hoặc REINDEX
VACUUMtạo lại chỉ mục và bảng cùng với dữ liệuREINDEX idxchỉ tạo lại chỉ mục
- Trong ví dụ, cả hai lệnh đều giảm số Page từ 3342 xuống 2930
Lưu trữ chỉ mục theo kiểu dữ liệu
- Dữ liệu văn bản: chuỗi ngắn được lưu trực tiếp trong Cell của chỉ mục, nhưng văn bản dài phải được lưu riêng
- Ví dụ đưa vào các giá trị từ
text-1đếntext-1000000rồi tạo chỉ mụccolumn1 ASC - Có thể xác nhận rằng chuỗi thực tế được lưu trực tiếp trong chỉ mục
- Ví dụ đưa vào các giá trị từ
- Dữ liệu REAL cũng được lưu trong chỉ mục và trực quan hóa
- Ví dụ sử dụng các giá trị
1.14,2.14, ...,1000000.14
- Ví dụ sử dụng các giá trị
- Cũng kiểm tra chỉ mục phức hợp dùng đồng thời số nguyên và văn bản
- Ví dụ tạo chỉ mục
(column1 ASC, column2 ASC)trên bảng(column1 INT, column2 TEXT) - Số nguyên và chuỗi được lưu cùng nhau trong cùng một Cell theo đúng thứ tự chỉ định khi tạo chỉ mục
- Ví dụ tạo chỉ mục
Cách tái hiện và công việc tiếp theo
- Thử nghiệm cho thấy chỉ mục SQLite được cấu trúc ra sao, dữ liệu record được lưu trong bộ nhớ như thế nào, và B-Tree tổ chức cũng như truy cập dữ liệu ra sao
- Phần trực quan hóa được dùng để phân tích và so sánh các chỉ mục khác nhau
- Có thể tái hiện mọi ví dụ bằng các lệnh sau
docker run -it --rm -v "$PWD":/app/data --platform linux/x86_64 mrsuh/sqlite-index bashsh bin/test-index.sh
- Mã và ví dụ nằm tại
mrsuh/sqlite-index - Công việc tiếp theo là trực quan hóa tìm kiếm dựa trên chỉ mục và khám phá một số SQL query
1 bình luận
Ý kiến trên Hacker News
Mỗi hàng trong bảng SQLite về cơ bản có một rowId duy nhất, và nếu không có khóa chính tường minh thì nó hoạt động như khóa chính; nhưng thực tế là ngay cả khi có khóa chính, SQLite vẫn dùng rowid
Sẽ hay nếu trực quan hóa chỉ mục khóa chính của bảng
WITHOUT ROWID. Những chỉ mục như vậy đặc biệt thú vịDù hai chỉ mục trông có vẻ giống nhau, việc chỉ mục thứ hai có ít trang hơn không đồng nghĩa ngay là nó nhanh hơn. Điều quan trọng là chiều cao của cây, sau đó là sau khi tìm được giá trị trong chỉ mục, có phải đọc phần dữ liệu còn lại từ một bảng riêng (rowid) hay không, hay dữ liệu đã nằm ngay đó như với
WITHOUT ROWID. Khác biệt đặc biệt lớn trong các truy vấn phạm vi nhưwhere 50 <= col <= 100INTEGER PRIMARY KEY, SQLite sẽ dùng nó thay thế [1][1]: https://sqlite.org/rowidtable.html
SQLite khá đặc biệt trong hầu như mọi cách xử lý, và tôi cho là đặc biệt hơn nữa ở xử lý truy vấn
SQLite có xu hướng ưu tiên sự đơn giản hơn hiệu năng, nên thường triển khai theo cách khác với các cơ sở dữ liệu khác mà tôi từng làm việc. SQLite cạnh tranh với tệp JSON/XML dùng cho lưu trữ bền vững hơn là cạnh tranh với các cơ sở dữ liệu khác. Vì vậy, nhìn vào cách triển khai của SQLite không hẳn cho bạn biết nhiều về việc một cơ sở dữ liệu thực thụ sẽ làm cùng việc đó như thế nào
Điều đó đúng là có nghĩa yêu cầu rất khác nhau, nhưng phạm vi sử dụng của nó không chỉ dừng ở việc thay thế tệp JSON/XML
Trang web dễ đọc đến mức thực sự khiến tôi muốn đọc
“indexes” vừa là ngôi thứ ba số ít hiện tại của động từ “to index”, vừa là danh từ số nhiều của “index”. Trong khi đó “indices” là dạng số nhiều truyền thống, đặc biệt hay dùng trong ngữ cảnh toán học và khoa học
Trong tiếng Anh thông dụng, “indexes” phổ biến hơn, nhưng trong lĩnh vực kỹ thuật đôi khi người ta thích dùng indices để chính xác hơn về mặt ngôn ngữ. Trong ngữ cảnh này, dùng “indices” giúp phân biệt thao tác lập chỉ mục với dạng số nhiều của chỉ mục, qua đó rõ ràng hơn
Ở Phần Lan, tôi từng thấy người ta dùng “time series” cho số nhiều và “time serie” cho số ít
Tất cả các hệ quản trị cơ sở dữ liệu quan hệ lớn đều dùng thuật ngữ indexes
Sẽ hay nếu được xem PostgreSQL làm cùng việc này như thế nào. Có lẽ sẽ học được nhiều điều khi so sánh
Để xem nhiều bố cục khác nhau với ít công sức hơn, cũng có thể cho xuất TGF dành cho yEd