引言
數據庫插入操作的語句如下:
insert into table values (a1, b1)
涉及到SQL層和存儲層,其中SQL層需要解析SQL語句,生成抽象語法樹(AST),計算表達式等,存儲層需要判斷主鍵沖突,包括增量數據和基線數據上的主鍵沖突,如果是非重復主鍵,則將數據插入到增量數據中。
上條插入語句只插入一行數據,稱之為單條插入,相應地,還可以在一條語句中插入多行數據,稱之為批量插入。
insert into table values (a1, b1), (a2, b2), (a3, b3)
批量插入的多行數據作為一個事務,所有數據插入成功,或者所有數據插入失敗,不會出現部分數據插入成功的情況。批量插入相對于單條插入在性能上有很大優勢,SQL解析只需要做一次,事務只需要做一次,因此理應在相同的時間內插入更多行數據。
1. 單行插入引擎
此前,OceanBase的單條插入與批量插入使用的是同一套接口,從SQL層讀取一行,檢查沖突,插入數據,然后反復重復這個過程,直到沒有數據為止。這樣的代碼看起來非常優雅,卻沒有利用到批量插入的特點而做針對性的優化。
2. 批量插入引擎
批量插入引擎每次可以讀取一批數據,比如500行,然后做批量檢查沖突,再批量插入到增量數據中(內存B+樹),目前做的只有批量讀和檢查沖突,批量插入留到以后再做。看似很簡單的優化,性能卻提升了很多,在遞增插入場景,Sysbench bulk insert的單線程測試中,無基線數據時,性能提升30%,有基線數據時,性能提升了100%。性能提升的原因有如下幾點:
2.1 系統層面
- 正在處理的一批數據可以始終在CPU Cache中,L1 Cache的大小是32KB,一行的大小為32 bytes(元數據,指針等),可以存儲1024行,而讀L1 Cache的性能是讀內存性能的100倍。
- CPU不僅可以Cache數據,還可以Cache指令,在單條插入的時候,在一定時間內總是執行不同的指令,因此很難Cache,每次都需要從內存中取指令,將指令解碼后,才能再去取數據,而在批量插入中,在一個緊湊的循環中,每次都是執行相同的指令,因此這些指令基本上可以在Cache中。
- CPU訪問內存的過程為,進程的虛擬內存地址通過查找TLB(硬件高速緩存,空間較小),Page Table(內存中)轉化為內存的物理地址,若TLB中找不到對應的虛擬地址,需要訪問內存中的Page Table。若同時處理一個500行的數組,TLB的命中率會大很多,而訪問TLB的速度是內存的100倍。
- CPU有預取內存功能,當從SQL中讀到的行需要轉換為存儲層中的行時,以前是讀內存,轉換,讀內存,轉換,而現在是完全并行起來的,轉換完一行之后,后面的行已經從內存中被預取到CPU Cache中了,而且CPU讀內存的單位是Cache Line是64 bytes,每次可以讀兩行,而以前單行處理的時候,是把這個能力浪費了的。
- 存儲層從SQL拿數據的時候,會調用一個虛函數get_next_row,C++里虛函數是通過虛函數表實現的,對象里有一個指向虛函數表的指針,每次調用函數的時候,需要通過指針找到這個表,然后在表里再通過一個指針,找到相應的函數實現,也就是每次調用get_next_row都有兩次隨機內存訪問,而改成批量之后,就少了大量的這種操作,比如有4萬行數據,以前需要4萬次虛函數調用,而現在只需要80次。
2.2 算法層面
- 檢查主鍵沖突的時候,由于基線數據是靜態的,最大值不變,而后面插入的數據往往是越來越大的,因此只需要比較一下這一批數據的最小值和靜態數據的最大值即可,減少了大量的沖突檢測。
- 單行插入內存B+樹時,每一行都需要從根節點搜索,直到相應的葉子節點,需要多次加讀鎖寫鎖,批量插入后,對一批數據做一個排序,然后將相應的數據直接插入到相應的葉子節點而不再從根節點搜索,減少了大量的比較和加鎖操作,而且同一批數據基本在少量的葉子節點中,因此葉子節點基本都可以在CPU Cache中。