python實現簡易數據庫之一——存儲和索引建立


  最近沒事做了一個數據庫project,要求實現一個簡單的數據庫,能滿足幾個特定的查詢,這里主要介紹一下我們的實現過程,代碼放在過ithub,可參看這里。都說python的運行速度很慢,但因為時間比較急,工作量大,我們還是選擇了高效實現的python。

一、基本要求

1、設計存儲方式

測試的數據量大小為1.5GB,最大的表有6,001,215條記錄。最大限度減少I/O次數,減少磁盤占有空間。

2、實現和優化group by,order by

對大表進行group by 聚集、排序,提高查詢效率

3、實現和優化TOP k 

高效率實現TOP k

4、對插入不作要求

二、總體設計

由於要求主要是對查詢做優化,對插入刪除不作考慮,完全可以采用列式數據庫,發揮列式數據庫的優勢。

以下是主要框架(忽略了建表和插入數據的過程),圖中藍色箭頭表示生成流,橘黃色箭頭表示執行流。

整個過程其實是至上而下建立索引,從下往上執行查詢,具體細節在詳細設計中講述。

三、詳細設計

  為了實現高效查詢,我將表的各個列從表中抽取出來,單獨存放,在查詢時,只讀取所需要的列,不需要讀取原始記錄,可以大大的減少I/O次數,但對於select * 這樣的查詢語句需要讀取其他屬性,因此我采用行式和列式結合的方式。

1、創建表,生成元數據

 在插入記錄之前需要建表,確定表的格式和完整性約束,如執行一下建表操作:

create table NATION (N_NATIONKEY int primary key,N_NAME varchar(25),
          N_REGIONKEY int,N_COMMENT varchar(152),
          foreign key (N_REGIONKEY) references REGION(R_REGIONKEY));

將生成一個meta.table的元數據文件,該元數據文件第一行保存的是數據庫的所有表名,以下的每一行為一個表的詳細描述,格式如下:

[表名1]|...|[表名n]
[表名1]|[表1主鍵]|[表1第一個屬性,約束]|[表1第二個屬性,約束]|...|[[表1第N個屬性,約束]]
.
.
.

  測試數據共有8個表,REGION,NATION,LINEITEM,ORDERS,CUSTOMER,PARTSUPP,PART,SUPPLIER。示例如下(省略了一些表的描述):  

#meta.table:
REGION|NATION|LINEITEM|ORDERS|CUSTOMER|PARTSUPP|PART|SUPPLIER| NATION|N_NATIONKEY |N_NATIONKEY INT|N_NAME VARCHAR(25)| N_REGIONKEY INT|N_COMMENT VARCHAR(152) REGION|R_REGIONKEY |R_REGIONKEY INT|R_NAME VARCHAR(25)|R_COMMENT VARCHAR(152)
.
.
.

  建立元數據的作用,是在查詢處理時可以知道某個表的某個屬性在原記錄文件中的列號,以及該屬性屬於什么類型,要知道對屬性排序和比較時必須知道屬性類型。因此原數據表meta.table的屬性必須按原記錄的順序保存。該數據表常駐內存,以python的有序字典(map)在內存中存放:

{'NATION': OrderedDict([('N_NATIONKEY', 'INT'), ('N_NAME', 'VARCHAR(25)'), ('N_REGIONKEY', 'INT'), ('N_COMMENT', 'VARCHAR(152)'), ('primary', ['N_NATIONKEY'])]),
'REGION': OrderedDict([('R_REGIONKEY', 'INT'), ('R_NAME', 'VARCHAR(25)'), ('R_COMMENT', 'VARCHAR(152)'), ('primary', ['R_REGIONKEY'])]),...
}

2、插入記錄

  元數據建立好后,可以進行插入數據,由於時間有限,插入數據時我沒有進行完整性檢查,假設插入的記錄都是合法的,整個插入過程完成后,數據記錄如下(REGION表):

#REGION
0
|AFRICA|lar deposits. blithely final packages cajole. regular waters are final requests. regular accounts are according to | 1|AMERICA|hs use ironic, even requests. s| 2|ASIA|ges. thinly even pinto beans ca| 3|EUROPE|ly final courts cajole furiously final excuse| 4|MIDDLE EAST|uickly special accounts cajole carefully blithely close requests. carefully final asymptotes haggle furiousl|

  屬性之間用‘|’分割,在抽取屬性列之前,記錄文件不能壓縮,我們將在生成列索引時壓縮這個原始記錄。

3、抽取屬性列(同時建立記錄行號與行地址的對應表)

   將表中的每個屬性單獨抽取出來,格式為:

[屬性值0]|[行號0]
[屬性值1]|[行號1]
[屬性值2]|[行號2]
.
.
.

  抽取的列示例如下:

#REGION_R_NAME 
AFRICA|0
AMERICA|1
ASIA|2
EUROPE|3
MIDDLE EAST|4

  上面的示例是REGION表中的R_NAME屬性的列,后來發現行號可以不用保存,讀取到數組中后,數組下標就是行號,這樣可以節省一些空間,不過,這個屬性列是按原始記錄的順序存放,不能實現按塊讀取,當記錄數很多時,不能放入內存,因此這個屬性列文件在下一步之后可以刪掉。

  抽取列的同時還要完成兩個工作,第一個是壓縮原始記錄表,壓縮后原始記錄不再需要,讀記錄只需要讀壓縮記錄即可;第二個就是建立行號到壓縮后的行首地址的對應表,這樣以后的操作都是按行號進行。在掃描原記錄文件的每行時,寫壓縮文件保存壓縮記錄表,並記錄壓縮后的每一行的首地址(獲得壓縮后的地址在這里)。行號與行首地址的對應表格式如下:

[第0行首地址]
[第1行首地址]
[第2行首地址]

  行號與行首地址的對應表示例如下: 

#REGION
0
127
171
212
269

   用行號代替行地址有,可以節省空間,單個表文件只要大於3*2^32B=12GB(乘以3是因為壓縮比率約為3:1),字節地址就超過就超過long int能表示的范圍,而行號可以表示更大的表;另一個好處,如果后續需要建立位圖索引,用行號比行地址好,因為行號是連續的整數,而行地址是離散在整數空間中,如上面的示例行地址從0直接跳到了127,中間的一串整數都沒有用到,那建立的位圖索引將是相當稀疏的。

  行首地址在查詢中不會用到,只有在最后讀取原始記錄時才需要轉換為行號,因此可以將它進行壓縮,我們用gzib壓縮,gzib為我們提供一個透明的文件壓縮,所謂透明,就是像讀寫普通文件一樣,gzib自動在緩沖區進行壓縮和解壓。

  python寫壓縮文件主要代碼如下:

import gzip
condenseFile= gzip.open(os.path.join(path,fileName+".gz"),'wb',compresslevel = 4)#以二進制寫,壓縮等級為4,值越大壓縮率越高,但時間越長
block = '...'
condenseFile.write(block)
condenseFile.flush()
condenseFile.close()

  讀壓縮文件:

path = os.path.join(DATABASE,"line2loc")    
with gzip.open(os.path.join(path,fileName),'rb') as transFile:
     locations = transFile.read().split("\n")#也可以只讀以行 transFile.readline()
     transFile.close()

4、屬性列壓縮並建立分塊索引

  上一步我們得到的屬性列只是簡單從表中抽取出來,這樣的屬性列有很多冗余,比如一個表有10000行,某個屬性只有10個取值,那在屬性列中就需要保存保存10000行,我們可以按屬性值進行分組,記錄出現該屬性值得行號,格式如下: 

[屬性值0]|[出現該值的行0]|[出現該值的行1]|...|[出現該值的行n]
[屬性值1]|[出現該值的行0]|[出現該值的行1]|...|[出現該值的行n]
.
.
.

   接下來按屬性值排序,這樣得到的屬性列就有序了,排序的過程中需要用到外部排序。排序后的結果,示例如下:

#PARTSUPP_PS_SUPPLYCOST
1.0|81868|307973|409984|490169|620444|632371
1.01|25328|36386|172687|243808|287934|558840|774633|775920
1.02|108457|137974|175055|206681|246824|297497|374608
1.03|38563|117772|175895|289935|381497|486960|630290|644984|723651|726647
1.04|284511|314284|327411|392035|639283|721325|754065|783577
.
.
.
6.5|193020|436686|746401
6.51|46883|59908|129012|189045|398695|437094|455012|458310|490801|598787
6.52|54123|129198|145810|223578|336148|377020|377755|379426|430717|442844|500296|549401
6.53|32341|54384|149844|208256|437181|528380
6.54|7164|41427|377948|417213|432345|625698|652283|757838

  上面的示例截取自PARTSUPP表的PS_SUPPLYCOST。排序之后,我們就可以對它進行分塊讀取,為了節省空間和減少I/O次數,我們對這個屬性表進行分塊壓縮,並在塊上建立索引,我們把屬性表稱為一級索引,這個在塊上的索引稱為二級索引。實現時,我們以32KB為一塊,不過實際操作時我們的塊大於等於32KB,我們依次將各行添加到一個字符串string中,每添加一行我們都會檢查string的是否大於等於32*1024B,如果小於32KB,就繼續添加一行;直到大於等於32KB,將string寫壓縮文件,同時記錄壓縮后的大小,保存該塊的首地址和塊大小,然后清空string,開始記錄下一個塊。實現的主要代碼如下:

 1 scwf = open(os.path.join(scddir,fileName),"w")   
 2 wf = gzip.open(os.path.join(sortdir,fileName+".gz"),'wb',compresslevel = 4)#壓縮屬性表
 3 block = ""
 4 newblock = True#新塊標志
 5 for k in li:#li存放的是有序的屬性值和行號表
 6     if newblock == True:
 7         blockattr = str(k[0])#塊首屬性值
 8         newblock = False
 9     line = str(k[0])+"|"
10     for loc in k[1]:
11         line += loc+"|"
12     block += line+"\n"
13     if len(block) > BLOCKSIZE:
14         startloc = endloc
15         wf.write(block)
16         endloc = wf.tell()
17         size = endloc - startloc
18         scwf.write(blockattr+SPLITTAG+str(startloc)+SPLITTAG+str(size)+'\n')#保存塊頭的屬性值,塊首地址和塊大小
19         block = ""#塊清空
20         newblock = True#新塊,下次循環記住新塊首部屬性值

  壓縮后生成二級索引,二級索引示例如下:

1.0|0|32812
6.52|32812|32810
12.03|65622|32800
17.46|98422|32835
22.92|131257|32787
28.39|164044|32771
33.77|196815|32794
39.29|229609|32810
44.7|262419|32843

  這兩級索引的示意圖如下:

    

  上面的示例中,第一行表示該屬性值1.0-6.52為一塊,塊內的屬性值在[1.0,6.52)之間,塊的起始地址為0,塊大小為32812B,二級索引也是有序的,因此建立二級索引后,我們可以在二級和一級索引上都進行折半查找,查詢速度很快。

  整個測試數據壓縮后的一級索引列表:

  

  對原始記錄表進行壓縮后,不能指定抽取屬性列建立索引,只能同時對一個表的所有屬性建立索引,這在實際應用中有很大缺陷,因為有一些屬性根本就不會再查詢條件中使用,建立的索引浪費了磁盤空間,也延長了建立索引的時間。雖然設計了壓縮原始記錄表,但最后我們實現沒有壓縮原始記錄表,行到地址的對應表存的是原始記錄的行首部地址。原始記錄文件不會刪除,這樣可以指定表和屬性建立索引。

  到此,自頂向下的存儲和索引一級建立好了,下一篇將介紹SQL語句解析和查詢處理。

 


免責聲明!

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



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