python實現簡易數據庫之二——單表查詢和top N實現


  上一篇中,介紹了我們的存儲和索引建立過程,這篇將介紹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實現,敬請關注。

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM