基於Renascence架構的SQL查詢引擎設計

基於Renascence架構的sqlite查詢優化

sqlite查詢優化方案是一開始是在Hw時設計的,但當時只實現一些簡單case,並未完成sql的普遍支持。後面考慮到這可以做爲 Renascence 架構的一個實驗場景,因此將其方案做了一番修改,代碼也重寫了一遍,現在做成一個能支持普通sql查詢的demo。

sqlite架構

參考:http://wiki.dzsc.com/info/7440.html
sqlite是移動設備廣泛使用的輕量級數據庫引擎。它主要由前端——虛擬機——後端三部分組成,這裏不做詳細介紹。
Sqlite架構圖

VM機制

參考:
http://www.cnblogs.com/hustcat/archive/2010/06/23/1762987.html
對於任何一條sql語句,sqlite都是先編譯爲一串虛擬機碼(執行計劃),然後基於虛擬機碼執行程序:
sqlite語句執行過程

如:
select * from TEST where age >20 and salary<99999666;
被翻譯爲如下的虛擬機碼:

addr:0  opcode:Init p1:0    p2:16   p3:0    p4: p5:00   comment:
addr:1  opcode:OpenRead p1:0    p2:2    p3:0    p4:5    p5:00   comment:
addr:2  opcode:Rewind   p1:0    p2:14   p3:0    p4: p5:00   comment:
addr:3  opcode:Column   p1:0    p2:2    p3:1    p4: p5:00   comment:
addr:4  opcode:Le   p1:2    p2:13   p3:1    p4:(BINARY) p5:53   comment:
addr:5  opcode:Column   p1:0    p2:3    p3:3    p4: p5:00   comment:
addr:6  opcode:Ge   p1:4    p2:13   p3:3    p4:(BINARY) p5:53   comment:
addr:7  opcode:Column   p1:0    p2:0    p3:5    p4: p5:00   comment:
addr:8  opcode:Column   p1:0    p2:1    p3:6    p4: p5:00   comment:
addr:9  opcode:Copy p1:1    p2:7    p3:0    p4: p5:00   comment:
addr:10 opcode:Copy p1:3    p2:8    p3:0    p4: p5:00   comment:
addr:11 opcode:Column   p1:0    p2:4    p3:9    p4: p5:00   comment:
addr:12 opcode:ResultRow    p1:5    p2:5    p3:0    p4: p5:00   comment:
addr:13 opcode:Next p1:0    p2:3    p3:0    p4: p5:01   comment:
addr:14 opcode:Close    p1:0    p2:0    p3:0    p4: p5:00   comment:
addr:15 opcode:Halt p1:0    p2:0    p3:0    p4: p5:00   comment:
addr:16 opcode:Transaction  p1:0    p2:0    p3:3    p4:0    p5:01   comment:
addr:17 opcode:TableLock    p1:0    p2:2    p3:0    p4:TEST p5:00   comment:
addr:18 opcode:Integer  p1:20   p2:2    p3:0    p4: p5:00   comment:
addr:19 opcode:Integer  p1:99999666 p2:4    p3:0    p4: p5:00   comment:
addr:20 opcode:Goto p1:0    p2:1    p3:0    p4: p5:00   comment:

架構缺陷與優化思路

缺陷:
1、採用虛擬機模式(VM),且粒度爲1行,switch case 太頻繁,使CPU的cache命中率不高。
2、操作粒度太小,一列一列地取值操作,不利於進一步優化(GPU加速,多線程等)。
3、進行優化時,需要同時修改前端與後端,雙重工作。

優化思路:
1、保留前端與後端,繞開VM機制,採用有向無環圖(DAG)——算子的架構,每個算子次處理一批數據。
2、對於算子本身可作GPU/多線程優化加速

Renascence(Genetic-Program-Frame)介紹

Renascence是自動編程架構的框架庫,一般而言它可視爲一個特殊的編譯器,算法庫不直接提供對外API,而是將往Renascence中註冊函數。上層應用或上層庫調用算法庫時,輸入一條公式,然後 Renascence將公式編譯爲有向無環圖,間接調用算法庫。
Renascence的作用,優化上層應用調用下層引擎的邏輯,並起自適配作用。

1、優化調用邏輯,防止重複計算

新查詢引擎的設計,主要便利用了Renascence這個功能。
比如如下的調用邏輯:f(g(x0), g(x0)),便可優化爲
gp_opt

T節點表示轉發,R節點表示傳遞,如是便省去g(x0)的第二次計算。

2、允許自動化參數調節

寫程序,尤其是算法類程序時,調參數是不可避免的事情,如多線程優化中開啓的線程數,開啓多線程優化的閾值,使用OpenCL時device端緩存的大小,一次運算的globalsize, localsize,等等。
Renascence可提供一個框架,以公式鏈接起來的所有函數參數,會通過一個通一的方案調優。這個優化的目標,對查詢引擎的優化而言,可以設爲時間的快慢或內存佔用的多少。
對sqlite查詢優化這個場景而言,主要就是內存池一個BLOCK的大小,每次取多少行的數據,哈希表初始大小等等。

3、允許部分程序邏輯根據實際應用場景動態生成與調節

比如一個where條件 where A > 30 and B < 50 and C >100,且 A、B、C 三列均無索引
公式解釋爲:FILTER(x0, AND(AND(x1, x2), x3)),其中x0爲源數據,x1、x2、x3是算符,分別表示 A>30、B<50和C>100
但由於註冊等價匹配: FILTER(x0, AND(x1, x2)) = FILTER(FILTER(x0, x1), x2) = FILTER(FILTER(x0, x2), x1)
將會被轉換爲如下一系列等價形式:
FILTER(x0, AND(AND(x1, x2), x3)) = FILTER(FILTER(x0, AND(x1, x2)), x3) | FILTER(FILTER(x0, x3), AND(x1,x2)) =
FILTER(FILTER(FILTER(x0, x1), x2), x3) | FILTER(FILTER(FILTER(x0, x2), x1), x3)
FILTER(FILTER(FILTER(x0, x3), x1), x2) | FILTER(FILTER(FILTER(x0, x3), x2), x1)
這四個等價形式中選取哪一個,可以通過數據庫採樣實驗(目標函數)動態決定。

2、3事實上是Renascence架構的核心,但對於數據庫的查詢引擎而言,程序邏輯相對固定,沒有太多讓Renascence發揮的空間,儘管如此,在Renascence架構之上實現CBO類似的優化還是會容易一些。
注:CBO 即 Cost Based Optimizer,爲ORACLE的優化器,
參考:http://blog.csdn.net/suncrafted/article/details/4239237

查詢引擎設計

Opt_frame
圖中 libGP.so 即 Renascence 庫。
新引擎分爲如下部分:

對接層

1、轉換sqlite解析的語法樹爲 dag庫所支持的格式
2、調用sqlite後端B-Tree的代碼,獲取數據源
3、將dag庫返回的結果轉換爲sqlite的內存格式(Mem)

公式層

數據類型

在描述公式生成過程之前,先說明dag庫的主要數據類型定義:
RawData:從對接層調用sqlite後端,讀取出來的原始數據格式,sqlite是按行存儲的,但會作一個序列化操作
FormalData:RawData中的數據,經過投影器解析(實際是反序列化),將所需要的列解出而得的數據
VirtualColumn:一列數據,由 FormalData 中複製或經表達式運算而得
VirtualColumnSet:列集,VirtualColumn 的集合

每個查詢子句最終輸出的是一個 VirtualColumnSet,而輸入是一系列 RawData

語法分析與公式生成

我們先來看一下sqlite解析出來的語法樹結構
Select From sqlite
如圖所示,Select 結構包含的主要是表達式ExprList(由一系列Expr組成)和源SrcList(由一系列SrcList_item組成)
解析的核心思路就是依次解析從句,然後依次解析表達式提取數值。

SQL語句:select age, age*2, SNO from TEST where salary=99999666 order by SNO, SNAME; (salary 無索引)

Sort(MergeSet(MergeSet(SetAlloc(Column(Project(Filter(x0, Op2(Column(Project(x0, x1), x3), Const(x4), x2)), x5), x6)), Op2(Column(Project(Filter(x0, Op2(Column(Project(x0, x1), x3), Const(x4), x2)), x5), x6), Const(x8), x7)), Column(Project(Filter(x0, Op2(Column(Project(x0, x1), x3), Const(x4), x2)), x5), x9)),MergeSet(SetAlloc(Column(Project(Filter(x0, Op2(Column(Project(x0, x1), x3), Const(x4), x2)), x5), x9)), Column(Project(Filter(x0, Op2(Column(Project(x0, x1), x3), Const(x4), x2)), x5), x10)), x11)

這個公式是如何生成的呢:
第一步,決定數據源
與這一步有關的子項是SrcList和where
查看 Select 結構體的內容可知,只有一個Source,且不是子查詢,存在where條件條件,需要作過濾
select_info
select_src

得到的公式爲:
Project(Filter(x0, Op2(Column(Project(x0, x1), x3), Const(x4), x2)), x5)
爲了避免解析不通過where條件的行,在第一步解析where子句所需的列,在過濾之後再解析其他所需要的列。

(當需要連接時,處理邏輯會複雜一些,如兩個表內連接的對應公式爲Join(Project(x0, x2),SortAndMask(Project(x1, x3),x4), x5),x0 是大表的 RawData,x1是小表的 RawData,本篇暫不詳述)

第二步,解析Select的表達式組
解析表達式,直接按表達式樹一層組織公式就好,如圖所示的表達式樹:
被解析爲公式:
Op2(Column(x0, x1), Op2(Column(x0, x1), Const(x3), x2), x2)
Op2表示通用二目算子,x2爲算符*
x0爲 FormalData數據源
x3爲 表達式 2,經過 Const 解析爲常量 2
Column爲取列算子,x1記錄取的是age這一列
(其實這樣解析並不太好,最好方式是每個表達式對應一個算子,但這樣工作量很大,因此暫時這麼做)

在解析時,Column對應的源,會被替換爲第一步所得到的數據源,即Project(Filter(x0, Op2(Column(Project(x0, x1), x3), Const(x4), x2)), x5)
因此公式中會重複出現這一段,顯得所長。但後面 Renascence 作解析時,會去除冗餘,因此不會影響性能。
每個表達式解析完後,輸出是一個 列(VirtualColumn),需要用 SetAlloc 和 MergeSet 算子將其一個個合併爲列集(VirtualColumnSet)

第三步,加前綴(GroupBy和AggFunc)
由於此語句無 GroupBy,不詳細介紹

第四步,加後綴(Distinct、Sort、Limit)
這裏只有一個Sort子句。
Sort算子輸入是 Select 和 Order By 兩個表達式組產生的列集,輸出與 Select 表達式組的列集相同的一個列集。
因此解析 Order by 表達式集,將其合併,然後將其與 Select 列集一併輸入 Sort 算子,即得出最終的列集。

經過 Renascence 優化之後,這個公式將被拆成如下片段計算:
Root: Sort(MergeSet(MergeSet(SetAlloc(x12), Op2(x12, Const(x8), x7)), x13), MergeSet(SetAlloc(x13), Column(x15, x10)), x11)
x12: Column(x15, x6)
x13: Column(x15, x9)
x14: Filter(x0, Op2(Column(Project(x0, x1), x3), Const(x4), x2))
x15: Project(x14, x5)

對應的有向無環圖太複雜就不畫了。

運行DAG並讀取其輸出結果

首先根據公式創建一個 ADF:

AGPProducer* producer = ProducerInstance::getInstance();
mADF = GP_Function_Create_ByFormula(producer, formula, NULL);

這個ADF的輸入由兩部分組成:參數與數據源
參數parameters在創建公式的過程中生成,數據源則在運行ADF時臨時讀取。

配置輸入的代碼:

int i=0; i<maxnumber; ++i)
{
    mInputs.push(NULL, NULL);
}
for (auto pair:parameters)
{
    HWASSERT(pair.first < mInputs.size());
    mInputs.contents[pair.first] = pair.second;
}
for (auto pair:sources)
{
    HWASSERT(pair.first < mInputs.size());
    mInputs.contents[pair.first].type = gHwOriginDataBatch;
}

運行的代碼:

for (auto pair : mSources)
{
    auto batch = pair.second.produce(pair.second);
    mInputs.contents[pair.first].content = batch;
}
mOutput = GP_Function_Run(mADF, &mInputs);

算子層

1、基於RawData的運算:
投影(Project):作數據解析,將 RawData 需要提取的列取出來,生成 FormalData
過濾(Filter):作數據過濾,將 RawData 中不滿足條件的行去掉。
2、基於列的運算:
Column:從FormalData複製一列數據出來
Const:將常數映射爲一個數據列
Op(表達式):對數據列作運算,生成新數據列
3、基於列集的運算:
去重(Distinct):對列集進行去重,目前使用 stl的set實現,效率比較低。
排序(Sort):對列集進行排序
分組(GroupBy):實際上是在排序的基礎額外做一個標誌,把相同的行合併。
這部分算子是最耗時間的,最需要重點優化
4、列轉換爲列集的算子:SetAlloc、MergeSet
SetAlloc:生成一個列集
MergeSet:往列集中加一個列

執行的過程如圖所示:
Frame

性能測試結果

小米pad上數據,TEST表數據量爲91W
TEST2錶行數 20W
時間爲運行5次的總時間:

語句 優化前時間/ms 優化後時間/ms 性能提升率%
select * from TEST where age >20 and salary<99999666 14252.2 12389.5 15.0
select * from TEST where age >20 13416.2 11889.1 12.8
select age, SNO, SNAME from TEST order by SNO, SNAME 30262.1 22727.2 33.2
select age*2, SNO, SNAME from TEST order by SNO, SNAME 31254.6 24319.0 28.5
select * from TEST where SNO in (select SNO from TEST2) 23383.9 19860.9 17.7
select * from TEST where SNO in (‘aa’, ‘bb’, ‘cc’) 2697.3 1914.26 40.9
select * from TEST2 3316.53 3061.97 8.3
select TEST.age, TEST.SNO, TEST.salary, TEST2.SNAME from TEST JOIN TEST2 on (TEST.salary=TEST2.SALARYDD) 50112.1 42631.2 17.5
select sno, SNAME, sum(age+salary), count(age) from TEST group by SNO, SNAME 31813.3 25966.2 22.5
select count(sno), sum(age) from TEST 3167.34 2830.28 11.9

Renascence並行機制實現

(2016.7.15修改)

Renascence並行機制

並行與代碼優化的解耦

當前的並行計算框架,如多線程的openMP、大數據計算的hadoop-map-reduce、spark,都需要做算法代碼本身做改造。算法並行實現後的性能、效果跟改造的情況強相關,且進一步優化受限於平臺引擎(比如基於spark開發的代碼,再用simd、GPGPU加速就很困難)。
origin

而Renascence的並行計算框架,意圖將代碼優化與並行計算的實現解耦,以並行腳本代替算法代碼的並行化改造,用並行計算引擎去解析腳本,實現多線程/分佈式的並行計算。這樣,代碼的優化就不依賴於並行計算的平臺,可以同步進行。
re

這個並行計算框架實現的是比較粗粒度的並行,而GPGPU、SIMD加速中的並行是數據並行,可認爲是對代碼本身的優化。

並行語言規範

參見
https://github.com/jxt1234/Renascence/blob/master/doc/Parallel.txt

文件組成

如下是一套完整的並行框架文件組成示例
so文件:
libGP.so:Renascence框架庫
libmthpGP.so:多線程並行引擎庫
libpics.so:圖像算法庫

xml配置文件:
func.xml:圖像算法列表
Map-Reduce.xml:並行化語言描述
mgpfunc.xml:並行計算引擎的函數列表

並行化實現結果

在sql查詢引擎中,排序的並行描述爲

REDUCE((MAP((x0,x1,x2), Sort(x0,x1,x2), [a0,b0,c0]->[a0], a0==b0),x1,x2), MergeSort(x0, y0, x1, x2), [a0,b0,c0]->[1], a0==b0)

Sort 對一個數組進行排序,使之有序。
MergeSort對兩個有序數組進行歸併,合併成一個有序數組。
這個描述表示:對所有數組執行一次Sort,使各自成爲有序數據,然後進行兩兩合併,執行MergeSort,最終合成一個大的有序數據。

如下爲引入 GP並行機制後,最新的數據(其他算子未完全支持):

語句 優化前時間/ms 優化後時間/ms 性能提升率%
select age, SNO, SNAME from TEST order by SNO, SNAME 29418.7 16990.7 73.1
select age*2, SNO, SNAME from TEST order by SNO, SNAME 30823.2 20118.2 53.2

基於Renascence架構的查詢引擎設計思想

依賴Renascence組織調用邏輯

查詢引擎分爲公式層與算子層,公式層生成一個模糊的公式,Renascence生成適合的程序邏輯(這裏是有向無環圖DAG),調用算子層進行計算。

性能優化集中於算子/執行層面

查詢引擎的性能優化只需要優化算子,邏輯性的優化由Renascence完成。

代碼不便公開,一些細節也未完全描述,關於並行化計算可參考Renascence中的文檔。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章