在這一章,我們將了解LiteDB里面幾個基本數據結構包括索引結構和數據塊結構,我也會試着說明前輩數據之巔在博客中遇到的問題,最后對比mysql進一步深入了解LiteDB的索引原理。
1.LiteDB的五種基本數據結構
在LiteDB的Structures中定義了五個基本數據結構,分別為PageAddress、CollectionIndex、DataBlock、IndexNode和IndexKey。他們各自說明如下:
1.1 PageAddress
頁地址,代表一個數據在頁的位置。其中的PageID就是頁的ID,Index是數據頁或者索引頁中字典的key值。根據頁的ID和字典key值,就可以找到相應的IndexNode或者DataBlock。將PageID=uint.MaxValue和Index= ushort.MaxValue的頁地址作為空頁。
1.2 CollectionIndex
表索引,類似於mysql將表的字段設置為一個索引。屬性Field表示存入的字段名稱,屬性Unique用於標識是否是非重復字段。
1.3 DataBlock
數據塊,這個類中Position就是存放在DataPage中的字典索引,比如一個Customer{ID=1,Name=“Jim1”,age=100},將這條記錄處理成byte數組也就是屬性Data,將該數據塊存進DataPage中的字典中,在放入字典的同時,將生成一個key值。DataPage的PageID和字典的key值就作為這個數據塊的Position。
IndexRef是一個頁地址數組,存儲的是索引頁的位置。ExtendPageID是指擴展頁的ID。Key是這條記錄的主鍵。
1.4 IndexKey
索引鍵值,這個結構體實現了IComparable接口,主要完成的作用就是將常見的數據類型比如int、byte、string、DateTime轉換為統一一個數據類型,然后進行比較。
1.5 IndexNode
索引節點,用來存儲一條記錄的索引。索引節點之間用跳表來組成數據結構。
注意:屬性Position是存放在IndexPage中的字典索引,而DataBlock是存放在DataPage頁中字典索。屬性Prev和Next分別是指向上一個和下一個節點的IndexPage頁地址數組。
2. 舉例說明
下面用一個示例來說明索引頁、數據頁和數據塊之間的關系,我創建一個"customer"的表,字段為{“Id”,"Age","Name",},然后插入10記錄:{Id=1,Age=1,Name="Jim_1"},{Id=2,Age=2,Name="Jim_2"}.....{Id=10,Age=10,Name="Jim_10"}。大家可以看一下當前的數據展示如下:
從上面可以清楚看到IndexPage里面的IndexNode是有序排列。我們學過排序算法肯定都知道,要想實現排序,必須有比較對象。所以Key就要實現一個IComparable接口,這樣所有的IndexNode就可以通過他們的Key值進行排序。對IndexNode的增刪改查是用跳表這種數據結構,后面有機會我會專門結合LiteDB講一下跳表這種數據結構。同時要注意的是IndexNode的鏈接有Next和Prev,我這里為了簡略,只繪制出Next的鏈接。
我們能看到目前只有一頁DataPage,這頁DataPage的ItemCount是10,正好對應了10條數據。我再用PPT的模式將IndexNode,DataBlock的關系描述如下:
上面這張圖就將IndexNode和DataBlock之間關系描述出來了,兩個不同的IndexPage中的索引節點指向的是同一個DataPage中的DataBlock。同時請各位注意的是,由於插入的Age是數值,Name是字符串,所以他們各自在索引頁里面的排列順序肯定是不一樣的。
3.針對博主數據之巔的疑問
博主數據之巔在分頁問題上做了一些嘗試,嘗試過程中遇到了查詢出來的數據ID並不是按順序排列的問題,我這里截圖如下:
為什么查詢出來的ID沒有按序排列,這是因為執行n.Name.StartWith("Jim1")的linq語句時,LiteDB就從Field為Name的IndexPage中進行查詢,這個IndexPage中的Key裝的是字符串變量,那么進行比較的也是字符串。做個簡單實驗我們就能知道“Jim_1”<"Jim_11"<"Jim_2",這樣索引對應的數據查詢出來的id就是1,11,2這種順序了。
4.對比Mysql
如果大家對mysql的索引稍微有些了解的話,應該知道mysql的索引數據結構使用的是B+樹,mysql有兩種主要的存儲引擎叫做InnoDB和MyISM(下面內容轉自博客https://www.cnblogs.com/shijingxiang/articles/4743324.html)。
InnoDB使用的是聚簇索引,將主鍵組織到一棵B+樹中,而行數據就儲存在葉子節點上,若使用"where id = 14"這樣的條件查找主鍵,則按照B+樹的檢索算法即可查找到對應的葉節點,之后獲得行數據。若對Name列進行條件搜索,則需要兩個步驟:第一步在輔助索引B+樹中檢索Name,到達其葉子節點獲取對應的主鍵。第二步使用主鍵在主索引B+樹種再執行一次B+樹檢索操作,最終到達葉子節點即可獲取整行數據。
MyISAM使用的是非聚簇索引,非聚簇索引的兩棵B+樹看上去沒什么不同,節點的結構完全一致只是存儲的內容不同而已,主鍵索引B+樹的節點存儲了主鍵,輔助鍵索引B+樹存儲了輔助鍵。表數據存儲在獨立的地方,這兩顆B+樹的葉子節點都使用一個地址指向真正的表數據,對於表數據來說,這兩個鍵沒有任何差別。由於索引樹是獨立的,通過輔助鍵檢索無需訪問主鍵的索引樹。
一個表如下圖存儲了4行數據。其中Id作為主索引,Name作為輔助索引。圖示清晰的顯示了聚簇索引和非聚簇索引的差異。
1 由於行數據和葉子節點存儲在一起,這樣主鍵和行數據是一起被載入內存的,找到葉子節點就可以立刻將行數據返回了,如果按照主鍵Id來組織數據,獲得數據更快。
2 輔助索引使用主鍵作為"指針" 而不是使用地址值作為指針的好處是,減少了當出現行移動或者數據頁分裂時輔助索引的維護工作,使用主鍵值當作指針會讓輔助索引占用更多的空間,換來的好處是InnoDB在移動行時無須更新輔助索引中的這個"指針"。也就是說行的位置(實現中通過16K的Page來定位,后面會涉及)會隨着數據庫里數據的修改而發生變化(前面的B+樹節點分裂以及Page的分裂),使用聚簇索引就可以保證不管這個主鍵B+樹的節點如何變化,輔助索引樹都不受影響。
最后我們可以看到LiteDB的索引方式和MyISAM類似,不管是主鍵索引還是輔助索引指向的是數據的地址,只不過LiteDB索引內部是用跳表,而mysql用的是B+樹。