時序資料庫的秘密 —— 快速檢索
Elasticsearch 是透過 Lucene 的倒排索引技術實現比關係型資料庫更快的過濾。特別是它對多條件的過濾支援非常好,比如年齡在 18 和 30 之間,性別為女性這樣的組合查詢。倒排索引很多地方都有介紹,但是其比關係型資料庫的 b-tree 索引快在哪裡?到底為什麼快呢?
籠統的來說,b-tree 索引是為寫入最佳化的索引結構。當我們不需要支援快速的更新的時候,可以用預先排序等方式換取更小的儲存空間,更快的檢索速度等好處,其代價就是更新慢。要進一步深入的化,還是要看一下 Lucene 的倒排索引是怎麼構成的。
這裡有好幾個概念。我們來看一個實際的例子,假設有如下的資料:
docid | 年齡 | 性別 |
1 | 18 | 女 |
2 | 20 | 女 |
3 | 18 | 男 |
這裡每一行是一個 document。每個 document 都有一個 docid。那麼給這些 document 建立的倒排索引就是:
年齡
性別
可以看到,倒排索引是 per field 的,一個欄位有一個自己的倒排索引。18,20 這些叫做 term,而 [1,3] 就是 posting list。Posting list 就是一個 int 的陣列,儲存了所有符合某個 term 的文件 id。那麼什麼是 term dictionary 和 term index?
假設我們有很多個 term,比如:
Carla,Sara,Elin,Ada,Patty,Kate,Selena
如果按照這樣的順序排列,找出某個特定的 term 一定很慢,因為 term 沒有排序,需要全部過濾一遍才能找出特定的 term。排序之後就變成了:
Ada,Carla,Elin,Kate,Patty,Sara,Selena
這樣我們可以用二分查詢的方式,比全遍歷更快地找出目標的 term。這個就是 term dictionary。有了 term dictionary 之後,可以用 logN 次磁碟查詢得到目標。但是磁碟的隨機讀操作仍然是非常昂貴的(一次 random access 大概需要 10ms 的時間)。所以儘量少的讀磁碟,有必要把一些資料快取到記憶體裡。但是整個 term dictionary 本身又太大了,無法完整地放到記憶體裡。於是就有了 term index。term index 有點像一本字典的大的章節表。比如:
A 開頭的 term ……………. Xxx 頁
C 開頭的 term ……………. Xxx 頁
E 開頭的 term ……………. Xxx 頁
如果所有的 term 都是英文字元的話,可能這個 term index 就真的是 26 個英文字元表構成的了。但是實際的情況是,term 未必都是英文字元,term 可以是任意的 byte 陣列。而且 26 個英文字元也未必是每一個字元都有均等的 term,比如 x 字元開頭的 term 可能一個都沒有,而 s 開頭的 term 又特別多。實際的 term index 是一棵 trie 樹:
例子是一個包含 "A", "to", "tea", "ted", "ten", "i", "in", 和 "inn" 的 trie 樹。這棵樹不會包含所有的 term,它包含的是 term 的一些字首。透過 term index 可以快速地定位到 term dictionary 的某個 offset,然後從這個位置再往後順序查詢。再加上一些壓縮技術(搜尋 Lucene Finite State Transducers) term index 的尺寸可以只有所有 term 的尺寸的幾十分之一,使得用記憶體快取整個 term index 變成可能。整體上來說就是這樣的效果。
現在我們可以回答“為什麼 Elasticsearch/Lucene 檢索可以比 mysql 快了。Mysql 只有 term dictionary 這一層,是以 b-tree 排序的方式儲存在磁碟上的。檢索一個 term 需要若干次的 random access 的磁碟操作。而 Lucene 在 term dictionary 的基礎上新增了 term index 來加速檢索,term index 以樹的形式快取在記憶體中。從 term index 查到對應的 term dictionary 的 block 位置之後,再去磁碟上找 term,大大減少了磁碟的 random access 次數。
額外值得一提的兩點是:term index 在記憶體中是以 FST(finite state transducers)的形式儲存的,其特點是非常節省記憶體。Term dictionary 在磁碟上是以分 block 的方式儲存的,一個 block 內部利用公共字首壓縮,比如都是 Ab 開頭的單詞就可以把 Ab 省去。這樣 term dictionary 可以比 b-tree 更節約磁碟空間。
如何聯合索引查詢?
所以給定查詢過濾條件 age=18 的過程就是先從 term index 找到 18 在 term dictionary 的大概位置,然後再從 term dictionary 裡精確地找到 18 這個 term,然後得到一個 posting list 或者一個指向 posting list 位置的指標。然後再查詢 gender= 女 的過程也是類似的。最後得出 age=18 AND gender= 女 就是把兩個 posting list 做一個“與”的合併。
這個理論上的“與”合併的操作可不容易。對於 mysql 來說,如果你給 age 和 gender 兩個欄位都建立了索引,查詢的時候只會選擇其中最 selective 的來用,然後另外一個條件是在遍歷行的過程中在記憶體中計算之後過濾掉。那麼要如何才能聯合使用兩個索引呢?有兩種辦法:
使用 skip list 資料結構。同時遍歷 gender 和 age 的 posting list,互相 skip;
使用 bitset 資料結構,對 gender 和 age 兩個 filter 分別求出 bitset,對兩個 bitset 做 AN 操作。
PostgreSQL 從 8.4 版本開始支援透過 bitmap 聯合使用兩個索引,就是利用了 bitset 資料結構來做到的。當然一些商業的關係型資料庫也支援類似的聯合索引的功能。Elasticsearch 支援以上兩種的聯合索引方式,如果查詢的 filter 快取到了記憶體中(以 bitset 的形式),那麼合併就是兩個 bitset 的 AND。如果查詢的 filter 沒有快取,那麼就用 skip list 的方式去遍歷兩個 on disk 的 posting list。
利用 Skip List 合併
以上是三個 posting list。我們現在需要把它們用 AND 的關係合併,得出 posting list 的交集。首先選擇最短的 posting list,然後從小到大遍歷。遍歷的過程可以跳過一些元素,比如我們遍歷到綠色的 13 的時候,就可以跳過藍色的 3 了,因為 3 比 13 要小。
整個過程如下
最後得出的交集是 [13,98],所需的時間比完整遍歷三個 posting list 要快得多。但是前提是每個 list 需要指出 Advance 這個操作,快速移動指向的位置。什麼樣的 list 可以這樣 Advance 往前做蛙跳?skip list:
從概念上來說,對於一個很長的 posting list,比如:
[1,3,13,101,105,108,255,256,257]
我們可以把這個 list 分成三個 block:
[1,3,13] [101,105,108] [255,256,257]
然後可以構建出 skip list 的第二層:
[1,101,255]
1,101,255 分別指向自己對應的 block。這樣就可以很快地跨 block 的移動指向位置了。
Lucene 自然會對這個 block 再次進行壓縮。其壓縮方式叫做 Frame Of Reference 編碼。
示例如下:考慮到頻繁出現的 term(所謂 low cardinality 的值),比如 gender 裡的男或者女。如果有 1 百萬個文件,那麼性別為男的 posting list 裡就會有 50 萬個 int 值。用 Frame of Reference 編碼進行壓縮可以極大減少磁碟佔用。這個最佳化對於減少索引尺寸有非常重要的意義。當然 mysql b-tree 裡也有一個類似的 posting list 的東西,是未經過這樣壓縮的。
因為這個 Frame of Reference 的編碼是有解壓縮成本的。利用 skip list,除了跳過了遍歷的成本,也跳過了解壓縮這些壓縮過的 block 的過程,從而節省了 cpu。
利用 bitset 合併
Bitset 是一種很直觀的資料結構,對應 posting list 如:
[1,3,4,7,10]
對應的 bitset 就是:
[1,0,1,1,0,0,1,0,0,1]
每個文件按照文件 id 排序對應其中的一個 bit。Bitset 自身就有壓縮的特點,其用一個 byte 就可以代表 8 個文件。所以 100 萬個文件只需要 12.5 萬個 byte。但是考慮到文件可能有數十億之多,在記憶體裡儲存 bitset 仍然是很奢侈的事情。而且對於個每一個 filter 都要消耗一個 bitset,比如 age=18 快取起來的話是一個 bitset,18<=age<25 是另外一個 filter 快取起來也要一個 bitset。
所以秘訣就在於需要有一個資料結構:
可以很壓縮地儲存上億個 bit 代表對應的文件是否匹配 filter;
這個壓縮的 bitset 仍然可以很快地進行 AND 和 OR 的邏輯操作。
Lucene 使用的這個資料結構叫做 Roaring Bitmap。
其壓縮的思路其實很簡單。與其儲存 100 個 0,佔用 100 個 bit。還不如儲存 0 一次,然後宣告這個 0 重複了 100 遍。
這兩種合併使用索引的方式都有其用途。Elasticsearch 對其效能有詳細的對比( https://www.elastic.co/blog/frame-of-reference-and-roaring-bitmaps )。簡單的結論是:因為 Frame of Reference 編碼是如此 高效,對於簡單的相等條件的過濾快取成純記憶體的 bitset 還不如需要訪問磁碟的 skip list 的方式要快。
如何減少文件數?
一種常見的壓縮儲存時間序列的方式是把多個資料點合併成一行。Opentsdb 支援海量資料的一個絕招就是定期把很多行資料合併成一行,這個過程叫 compaction。類似的 vivdcortext 使用 mysql 儲存的時候,也把一分鐘的很多資料點合併儲存到 mysql 的一行裡以減少行數。
這個過程可以示例如下:
12:05:00 | 10 |
12:05:01 | 15 |
12:05:02 | 14 |
12:05:03 | 16 |
合併之後就變成了:
可以看到,行變成了列了。每一列可以代表這一分鐘內一秒的資料。
Elasticsearch 有一個功能可以實現類似的最佳化效果,那就是 Nested Document。我們可以把一段時間的很多個資料點打包儲存到一個父文件裡,變成其巢狀的子文件。示例如下:
可以打包成:
這樣可以把資料點公共的維度欄位上移到父文件裡,而不用在每個子文件裡重複儲存,從而減少索引的尺寸。
在儲存的時候,無論父文件還是子文件,對於 Lucene 來說都是文件,都會有文件 Id。但是對於巢狀文件來說,可以儲存起子文件和父文件的文件 id 是連續的,而且父文件總是最後一個。有這樣一個排序性作為保障,那麼有一個所有父文件的 posting list 就可以跟蹤所有的父子關係。也可以很容易地在父子文件 id 之間做轉換。把父子關係也理解為一個 filter,那麼查詢時檢索的時候不過是又 AND 了另外一個 filter 而已。前面我們已經看到了 Elasticsearch 可以非常高效地處理多 filter 的情況,充分利用底層的索引。
使用了巢狀文件之後,對於 term 的 posting list 只需要儲存父文件的 doc id 就可以了,可以比儲存所有的資料點的 doc id 要少很多。如果我們可以在一個父文件裡塞入 50 個巢狀文件,那麼 posting list 可以變成之前的 1/50。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31561269/viewspace-2660854/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 資料庫的檢索語句資料庫
- 時序資料庫資料庫
- 實時資料庫與時序資料庫資料庫
- 時序資料庫influxdb資料庫UX
- 學術檢索資料庫總結資料庫
- 資料檢索
- Prometheus時序資料庫-資料的查詢Prometheus資料庫
- 初識時序資料庫資料庫
- 時序資料庫的叢集方案?資料庫
- 時序資料庫-01-時序資料庫有哪些?為什麼要使用資料庫
- QuestDB時序資料庫介紹資料庫
- 時序資料庫之InfluxDB的基本操作資料庫UX
- 時序資料庫InfluxDB的基本語法資料庫UX
- 時間序列化資料庫選型?時序資料庫的選擇?資料庫
- 時序資料庫連載系列:當SQL遇到時序TimescaleDB資料庫SQL
- MySQL-檢索資料MySql
- Prometheus時序資料庫-報警的計算Prometheus資料庫
- Python_json資料檢索與定位之jsonPath類庫PythonJSON
- 全文檢索庫 bluge
- 聊一聊時序資料庫和TimescaleDB資料庫
- 聊聊時序資料庫發展情況資料庫
- Spring Boot中使用時序資料庫InfluxDBSpring Boot資料庫UX
- 在ef core中使用postgres資料庫的全文檢索功能實戰資料庫
- 從實時資料庫轉戰時序資料庫,他陪伴 TDengine 從 1.0 走到 3.0資料庫
- 資料檢索擴充套件包套件
- Prometheus時序資料庫-磁碟中的儲存結構Prometheus資料庫
- 時序資料庫破局開放探討資料庫
- 深入淺出:瞭解時序資料庫 InfluxDB資料庫UX
- 時序資料庫 TDengine 3.0.2.0 版本正式釋出資料庫
- 快速檢查資料庫一致性資料庫
- 一種基於概率檢索模型的大資料專利檢索方法與流程模型大資料
- 資訊檢索
- MYSQL——mysql檢索不包含字母U的資料MySql
- 百度時序資料庫——儲存的省錢之道資料庫
- 分散式時序資料庫QTSDB的設計與實現分散式資料庫QT
- 投票|選出你心中最優秀的時序資料庫資料庫
- KaiwuDB 多模資料庫-時序效能最佳化AI資料庫
- .NET使用TDengine時序資料庫和SqlSugar操作TDengine資料庫SqlSugar