上一篇中,介紹了我們的存儲和索引建立過程,這篇將介紹SQL查詢、單表查詢和TOPN實現。
一、SQL解析
正規的sql解析是用語法分析器,但是我找了好久,只知道可以用YACC、BISON等,sqlite使用的lemon,搗整了一天沒實現,就用了python的正則表達式。
1、刪除無用的空格、跳格符、換行符等;
我們以分號‘;’作為一個sql語句的結束符,在輸入分號之前,我們將輸入的sql語句串接成一個string,在將整個sql語句的一些無用的字符刪掉,
1 def rmNoUseChar(sql): 2 while sql.find("'") != -1:#將引號刪除,不論什么類型都當字符類型處理 3 sql = sql.replace("'","") 4 while sql.find('"') != -1: 5 sql = sql.replace('"','') 6 while sql.find('\t') != -1:#刪除制表符 7 sql = sql.replace("\t"," ") 8 while sql.find('\n') != -1:#刪除換行符 9 sql = sql.replace("\n"," ") 10 statements = sql.split(" ")#分割成列表,刪除多余空格后在拼接成字符串 11 while "" in statements: 12 statements.remove("") 13 sql="" 14 for stmt in statements: 15 sql += stmt+ " " 16 return sql[0:-1]#最后一個空格刪掉
2、關鍵詞大寫;
在sql語句中掃描關鍵字,將關鍵字大寫。這里我們使用了一個技巧,在每個select語句前面多加一個空格,每個關鍵字前后都加一個空格,這樣可以替換單詞的部分,如果不加空格,像charity 就會被替換為CHARrity,這不是我們想要的。
oneKeywords = [" SELECT "," FROM "," WHERE ", " DESC "," ASC ", " DATE "," DAY "," INT "," CHAR "," VARCHAR "," DECIMAL ", " SUM "," AVG ","MAX","MIN"," COUNT "," AS "," TOP "," AND "," OR "] twoKeywords = [" GROUP BY "," ORDER BY "]
3、解析和格式化SELECT子語句;
一個常見的select語句一般包含select、from、where、group by、order by五部分(不考慮嵌套查詢),where、group by、order by可以不出現,但如果出現的,在sql語句中必定滿足select、from、where、group by、order by的順序,因此我們定義:
stmtTag = ["SELECT","FROM","WHERE","GROUP BY","ORDER BY",";"]#select 子語句標志詞
找到各個子語句的標志詞,根據標志詞來解析子語句,這里我們定義了一個方法,用來找下一個標志詞:
def nextStmtTag(sql,currentTag):#根據當前標志詞找下一個標志詞 index = sql.find(currentTag,0) for tag in stmtTag: if sql.find(tag,index+len(currentTag)) != -1: return tag
比如我們測試發現sql語句中有WHERE標志詞,那么它一定有where子句,我們通過nextStmtTag()方法得到下一個關鍵詞,如果sql中有GROUP BY, 則下一個標志詞就是GROUP BY,如果沒有GROUP BY而有ORDER BY,那下一個標志詞就是ORDER BY,否則下一個標志詞就是分號";",因為一個sql中一定有結束符分號。
4、結合元數據表檢查語法錯誤;
解析完sql的子語句后,我們就可以進行簡單的語法檢查,結合元數據檢查WHERE子句的表是否在數據庫中存在,以及其他子語句中的屬性是否在WHERE子句的表中,檢查的過程中,順便將屬性大寫,並將的表名添上,屬性的格式統一為:[表名].[屬性名],同時對多表查詢的where條件做優化,即將單表查詢條件放在列表的前面,多表連接放在后面。具體請看下面的例子:
我們輸入如下sql語句:
select l_orderkey,o_orderdate,o_shippriority, min(l_orderkey) as min_odkey, max(o_shippriority) as max_priority from customer,orders,lineitem where c_mktsegment = "MACHINERY" and c_custkey = o_custkey and l_orderkey = o_orderkey and o_orderdate < "1995-05-20" and l_shipdate > "1995-05-18" group by l_orderkey,o_orderdate,o_shippriority order by o_orderdate desc,o_orderdate;
解析的結果(已通過語法檢查):
{'FROM': ['CUSTOMER', 'ORDERS', 'LINEITEM'], 'GROUP': ['LINEITEM.L_ORDERKEY', 'ORDERS.O_ORDERDATE', 'ORDERS.O_SHIPPRIORITY'], 'ORDER': [['ORDERS.O_ORDERDATE', 'DESC'], ['ORDERS.O_ORDERDATE', 'ASC']], 'SELECT': [['LINEITEM.L_ORDERKEY', None, None], ['ORDERS.O_ORDERDATE', None, None], ['ORDERS.O_SHIPPRIORITY', None, None], ['LINEITEM.L_ORDERKEY', 'MIN', 'min_odkey'], ['ORDERS.O_SHIPPRIORITY', 'MAX', 'max_priority']], 'WHERE': [['CUSTOMER.C_MKTSEGMENT', '=', 'MACHINERY'], ['ORDERS.O_ORDERDATE', '<', '1995-05-20'], ['LINEITEM.L_SHIPDATE', '>', '1995-05-18'], ['CUSTOMER.C_CUSTKEY', '=', 'ORDERS.O_CUSTKEY'], ['LINEITEM.L_ORDERKEY', '=', 'ORDERS.O_ORDERKEY']]}
可以看到我們將整個sql解析成一個字典,字典的鍵是子語句標志詞,值是格式化的子語句,這個解析結果跟JSON格式差不多。對於group by和from子語句只是簡單的表名和屬性名,因此就使用一個list表示,而其他子語句比較復雜,我們對其子語句的每個部分用list表示,如order子語句,不光有屬性還有升序或降序描述;而select還有聚集函數和重命名;where子句我們只考慮大於、等於和小於的條件,即每個where條件可以用過一個三元組表示。不出現的部分我們用None補齊。
這就是我們的解析select sql的大體過程,細節不再介紹,因為這個解析方法實在不高明,上不了台面,正規軍都是用的句法和語法解析器,我們打游擊戰的。
二、單表查詢
上一篇,我們將存儲結構和索引建立好了,現在sql解析部分也已經完成了,接下來我們來實現一個簡單的單表查詢。
1、一個簡單的示例
先來看一個簡單示例,我們輸入的查詢如下:
select O_CUSTKEY from ORDERS where o_orderdate= '1995-02-08';
在orders表中查找o_orderdate= '1995-02-08'的記錄的o_custkey屬性值。先來看查詢結果:
Input SQL: select O_CUSTKEY from ORDERS where o_orderdate= '1995-02-08'; {'FROM': ['ORDERS'], 'GROUP': None, 'ORDER': None, 'SELECT': [['ORDERS.O_CUSTKEY', None, None]], 'WHERE': [['ORDERS.O_ORDERDATE', '=', '1995-02-08']]} Quering: ORDERS.O_ORDERDATE = 1995-02-08 The result hava 669 rows, here is the fisrt 10 rows: ------------------- rows ORDERS.O_CUSTKEY ------------------- 1 72703 2 65566 3 81263 4 65561 5 127322 6 16642 7 38953 8 82663 9 14543 10 21053 ------------------- Take 0.388 seconds.
從查詢結果中我們可以看到,ORDERS.O_ORDERDATE = 1995-02-08的記錄有669條,為了方便,我們將查詢結果寫了文件保存,這里只顯示10條結果,orders表共有150萬條記錄,這個等值查詢只有用了0.388秒,速度算比較快了。下面詳細介紹這個查詢的實現過程。
2、單表查詢詳解
單表查詢的流程圖如下:
當我們輸入完sql語句后,先解析並進行語法檢查,解析的結果我們打印出來了,在上面的示例中可以看到。解析並通過語法檢查后,開始執行sql。很明顯,執行的第一步是從where子句中開始,where子句只有一個條件:o_orderdate= '1995-02-08',於是我們去查找ORDERS的O_ORDERDATE的二級索引,二級索引是建立在一級索引塊上的索引,二級索引文件通常很小,只有幾十KB,我們可以把它放入內存,進行折半查找,折半查找速度相當快,對100萬長度的表查找最多只需要20次,關於折半查找可以參看非等值折半查找。
在二級索引中查找到滿足條件的塊后,將一級索引塊讀入內存,一塊的大小通常不大(后面會討論),可以在內存中進行折半查找,找到滿足屬性條件的記錄行。然后將行號轉換為行地址,就可以直接讀取原始記錄,選擇需要的屬性輸出,選擇輸出屬性需要結合元數據表確定屬性所在的列。看下圖,一目了然:
上面的示例是等值查詢o_orderdate= '1995-02-08',對於小於或大於查詢,處理方法稍微麻煩一些,比如我們查詢o_orderdate< '1995-02-08',也是先找到o_orderdate= '1995-02-08'所在的塊,在塊內找到滿足條件的行(上圖中的02-01--02-07),同時將前面的塊全部讀取,提取出行號;大於查詢也是類似的處理。
得到行地址后讀取原始記錄是很簡單的:
for recordLoc in satisLoc: tableFile.seek(int(recordLoc))#定位到行首 record = tableFile.readline().split("|")
讀取原記錄時,先定位再讀取一整行。tableFile.seek(loc)移動文件指針,而我們的loc沒有排序,所以會造成文件指針抖動,比如我們先tableFile.seek(0),定位到文件開始,下一次tableFile. seek(20000),接着tableFile.seek(10),這樣磁盤定位會比較花時間。讀記錄前將行滿足條件首地址集合排序,可以實現順序讀取,讀取記錄速度可以增加,但排序也是會花很長時間的,這兩者需要折中(我們的實現中沒有排序,因為我們發現排序話費的時間比亂序讀取話費的時間更多)。
另外注意在二級索引和一級索引中折半查找是不同的,因為二級索引是稀疏索引,並不包含所有的屬性值,即使找不到等值的條件也需要返回一個可能包含該記錄的塊,除非是要比較的值比第一個塊的第一個塊首屬性值都小,此時可以斷定原記錄集中沒有滿足條件的記錄。
細心的讀者可能會發現,我們定義的塊>=32KB,因此會出現有一個塊很大,大到內存放不下。測試數據的orders表共有150萬條記錄,而其o_shippriority屬性只有一個值:”0“,因此一級索引文件ORDERS_O_SHIPPRIORITY只有一行:”0|0|1|2|3|4|5|6|7|8|9|10|11|...1500000“,這一行單獨成一塊,這一塊的大小為11MB,當然還可以放入內存,但假設orders有1500萬條記錄,那么單個塊就是110MB,直接放入內存就不適合了,但仔細想一想,這個塊中所有的行號都是屬性值等於’0‘的記錄,因此不需要再這個塊上作折半查找,我們直接得到滿足記錄的行號便可以分多次讀取這110MB的大塊。
三、TOP N
上面我們已經實現了一個單表單條件的查詢。接下來實現一個top N查詢,在top N語句中必定含有order by子句,即取最大或最小的N條記錄;否則top N的結果將是不確定的。最直接的方法實現top N就是先根據where記錄塞選記錄,然后在根據order by屬性排序,排序完畢后,取前N條記錄輸出即可。但這樣效率比較低,如果N很小,而where子句的條件又不嚴格,滿足條件的記錄很多,將會花大量的時間去讀取原始記錄和排序,當然也可以進行N趟冒泡排序,可以提高效率。
但我們已經建立好了順序索引,相當於我們已經事先排序好了,TOP N必定是有更快的方法的。現在來看這樣一個sql語句:
select top 10 O_ORDERKEY,O_ORDERPRIORITY,O_TOTALPRICE,o_orderdate from ORDERS where o_orderdate<'1995-02-08' order by o_totalprice;
從orders表中找1995-02-08以前總價格最低的10條記錄,這里默認是升序。首先根據where條件找出1995-02-08以前的記錄的行號,下一步我們不是直接讀取記錄並按o_totalprice排序,而是先對滿足條件的行號排序,然后去掃描o_totalprice的索引,因為o_totalprice的索引已經有序了,我們從前往后依次掃描o_totalprice屬性值得行號,查找每一個行號是否在滿足條件的行號集合中,如果在,就添加到一個新的行號集合中,只要添加了10條記錄,我們就停止掃描,這樣就已經找到前10條滿足條件的記錄。還是看圖比較容易理解:
如果是降序,則需要找出最大的前10個,那我們只需要從后往前掃描o_totalprice的索引,找到10個行號即可。
對於記錄記錄數為m的表,最多可能需要進行mlog(m1)次比較操作,其中m1是where子語句選出的記錄個數,m1<=m,但實際情況中,需要比較的次數遠遠小於mlog(m1)。這樣的方法可以減少I/O次數,因為滿足 o_orderdate<'1995-02-08'有上10萬條記錄,如果直接讀這10萬條記錄進內存並排序,顯然浪費了時間,因為最終又只需要10條記錄。
來看一下執行的結果:
Input SQL: select top 10 O_ORDERKEY,O_ORDERPRIORITY,O_TOTALPRICE,o_orderdate from ORDERS where o_orderdate<'1995-02-08' order by o_totalprice; Quering: ORDERS.O_ORDERDATE < 1995-02-08 ------------------------------------------------------------ rows O_ORDERKEY O_ORDERPRIORITY O_TOTALPRICE O_ORDERDATE ------------------------------------------------------------ 1 1600323 1-URGENT 866.90 1992-04-18 2 823814 1-URGENT 870.88 1992-01-31 3 5267200 3-MEDIUM 875.52 1993-11-21 4 5363650 5-LOW 877.30 1994-01-14 5 4318946 4-NOT SPECIFIED 884.82 1993-08-29 6 5195557 1-URGENT 891.74 1993-05-02 7 3309383 1-URGENT 908.18 1992-07-01 8 674436 1-URGENT 908.20 1993-08-06 9 2934784 3-MEDIUM 912.10 1992-02-23 10 5174117 2-HIGH 913.92 1993-07-30 ------------------------------------------------------------ Take 8.479 seconds.
再看一下降序的結果:
Input SQL: select top 10 O_ORDERKEY,O_ORDERPRIORITY,O_TOTALPRICE,o_orderdate from ORDERS where o_orderdate<'1995-02-08' order by o_totalprice desc; Quering: ORDERS.O_ORDERDATE < 1995-02-08 ------------------------------------------------------------ rows O_ORDERKEY O_ORDERPRIORITY O_TOTALPRICE O_ORDERDATE ------------------------------------------------------------ 1 1750466 4-NOT SPECIFIED 555285.16 1992-11-30 2 4722021 1-URGENT 544089.09 1994-04-07 3 3586919 1-URGENT 522644.48 1992-11-07 4 2185667 1-URGENT 511359.88 1992-10-08 5 4515876 4-NOT SPECIFIED 510061.60 1993-11-02 6 972901 3-MEDIUM 508668.52 1992-07-18 7 1177378 4-NOT SPECIFIED 508010.56 1992-09-19 8 631651 5-LOW 504509.06 1992-06-30 9 3883783 1-URGENT 500241.33 1993-07-28 10 3342468 3-MEDIUM 499794.58 1994-06-12 ------------------------------------------------------------ Take 11.04 seconds.
看到結果了吧,由於記錄數比較多,查詢的時間還是比較長,但相比而言還是比較快了。
這一篇講述了sql的語法解析和單表查詢與TOP N 查詢,下一篇將講述多表查詢和group by實現,敬請關注。