Python記憶體資料庫/引擎

Prodesire發表於2016-09-07

1 初探

在平時的開發工作中,我們可能會有這樣的需求:我們希望有一個記憶體資料庫或者資料引擎,用比較Pythonic的方式進行資料庫的操作(比如說插入和查詢)。

舉個具體的例子,分別向資料庫db中插入兩條資料,”a=1, b=1″ 和 “a=1, b=2”, 然後想查詢a=1的資料可能會使用這樣的語句db.query(a=1),結果就是返回前面插入的兩條資料; 如果想查詢a=1, b=2的資料,就使用這樣的語句db.query(a=1, b=2),結果就返回前面的第二條資料。

那麼是否擁有實現上述需求的現成的第三方庫呢?幾經查詢,發現PyDbLite能夠滿足這樣的需求。其實,PyDbLite和Python自帶的SQLite均支援記憶體資料庫模式,只是前者是Pythonic的用法,而後者則是典型的SQL用法。
他們具體的用法是這樣的:

PyDbLite

SQLite

2 pydblite和sqlite的效能

毫無疑問,pydblite的使用方式非常地Pythonic,但是它的效率如何呢?由於我們主要關心的是資料插入和查詢速度,所以不妨僅對這兩項做一個對比。寫一個簡單的測試指令碼:

在建立索引的情況下,10w次的插入和查詢的時間如下:

在未建立索引的情況(把建立索引的測試語句註釋掉)下,1w次的插入和查詢時間如下:

我們不難得出如下結論:

sqlite的插入速度是pydblite的3-5倍;而在建立索引的情況下,sqlite的查詢速度和pydblite相當;在未建立索引的情況下,sqlite的查詢速度比pydblite慢1.5倍左右。

3 優化

我們的目標非常明確,使用Pythonic的記憶體資料庫,提高插入和查詢效率,而不考慮持久化。那麼能否既擁有pydblite的pythonic的使用方式,又同時具備pydblite和sqlite中插入和查詢速度快的那一方的速度?針對我們的目標,看看能否對pydblite做一些優化。

閱讀pydblite的原始碼,首先映入眼簾的是對python2和3做了一個簡單的區分。給外部呼叫的Base基於_BasePy2或者_BasePy3,它們僅僅是在__iter__上有細微差異,最終呼叫的是_Base這個類。

然後看下_Base的建構函式,做了簡單的初始化檔案的操作,由於我們就是使用記憶體資料庫,所以檔案相關的內容完全可以拋棄。

緊接著比較重要的是create(建立欄位)、create_index(建立索引)兩個函式:

可以看出,pydblite在記憶體中維護了一個名為records的字典變數,用來存放一條條的資料。它的key是內部維護的id,從0開始自增;而它的value則是使用者插入的資料,為了後續查詢和記錄的方便,這裡在每條資料中額外又加入了__id__和__version__。其次,內部維護的indices字典變數則是是個索引表,它的key是欄位名,而value則是這樣一個字典:其key是這個欄位所有已知的值,value是這個值所在的那條資料的id。

舉個例子,假設我們插入了“a=-1,b=0,c=1”和“a=0,b=1,c=2”兩條資料,那麼records和indices的內容會是這樣的:

比方說現在我們想查詢a=0的資料,那麼就會在indices中找key為’a’的value,即{-1: set([0]), 0: set([1])},然後在這裡面找key為0的value,即[1],由此我們直到了我們想要的這條資料它的id是1(也可能會有多個);假設我們對資料還有其他要求比如a=0,b=1,那麼它會繼續上述的查詢過程,找到a=0和b=1分別對應的ids,做交集,就得到了滿足這兩個條件的ids,然後再到records里根據ids找到所有對應的資料。

明白了原理,我們再看看有什麼可優化的地方:

資料結構,整體的records和indeices資料結構已經挺精簡了,暫時不需要優化。其中的__version__可以不要,因為我們並不關注這個資料被修改了幾次。其次是由於indices中最終的ids是個list,在查詢和插入的時候會比較慢,我們知道內部維護的id一定是唯一的,所以這裡改成set會好一些。

python語句,不難看出,整個_Base為了同時相容python2和python3,不得不使用了2和3都支援的語句,這就導致在部分語句上針對特定版本的python就會造成浪費或者說是效能開銷。比如說,d是個字典,那麼為了同事相容python2和3,作者使用了類似與for key in d.keys()這樣的語句,在python2中,d.keys()會首先產生一個list,用d.iterkeys是個更明智的方案。再如,作者會使用類似set(d.keys()) – set([1])這樣的語句,但是python2中,使用d.viewkeys() – set([1])效率將會更高,因為它不需要將list轉化成set。

對特定版本python的優化語句就不一一舉例,概括地說,從資料結構,python語句以及是否需要某些功能等方面可以對pydblite做進一步的優化。前面只是說了create和create_index兩個函式,包括insert和__call__的優化也十分類似。此外,用普通方法來代替魔法方法,也能稍微提升下效率,所以在後續的優化中將__call__改寫為了query。

優化後的程式碼,請見MemLite

4 memlite、pydblite和sqlite的效能

讓我們在上文的測試程式碼中加入對memlite的測試:

在建立索引的情況下,10w次的插入和查詢的時間如下:

在未建立索引的情況(把建立索引的測試語句註釋掉)下,1w次的插入和查詢時間如下:

可以看出,在建立索引的情況下,memlite的插入和查詢效能在sqlite和pydblite之上;而在未建立索引的情況下,memlite的插入效能和sqlite一樣,好於pydblite,memlite的查詢效能比pydblite稍差,但好於sqlite。綜合來看,memlite即擁有pydblite的pythonic的使用方式,又擁有pydblite和sqlite中效能較高者的效率,符合預期的優化目標。

相關文章