最近没事做了一个数据库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 |
|AMERICA|hs use ironic, even requests. s|
|ASIA|ges. thinly even pinto beans ca|
|EUROPE|ly final courts cajole furiously final excuse|
|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|
AMERICA|
ASIA|
EUROPE|
MIDDLE EAST|

  上面的示例是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,开始记录下一个块。实现的主要代码如下:

 scwf = open(os.path.join(scddir,fileName),"w")
wf = gzip.open(os.path.join(sortdir,fileName+".gz"),'wb',compresslevel = 4)#压缩属性表
block = ""
newblock = True#新块标志
for k in li:#li存放的是有序的属性值和行号表
if newblock == True:
blockattr = str(k[0])#块首属性值
newblock = False
line = str(k[0])+"|"
for loc in k[1]:
line += loc+"|"
block += line+"\n"
if len(block) > BLOCKSIZE:
startloc = endloc
wf.write(block)
endloc = wf.tell()
size = endloc - startloc
scwf.write(blockattr+SPLITTAG+str(startloc)+SPLITTAG+str(size)+'\n')#保存块头的属性值,块首地址和块大小
block = ""#块清空
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语句解析和查询处理。

python实现简易数据库之一——存储和索引建立的更多相关文章

  1. python实现简易数据库之二——单表查询和top N实现

    上一篇中,介绍了我们的存储和索引建立过程,这篇将介绍SQL查询.单表查询和TOPN实现. 一.SQL解析 正规的sql解析是用语法分析器,但是我找了好久,只知道可以用YACC.BISON等,sqlit ...

  2. python实现简易数据库之三——join多表连接和group by分组

    上一篇里面我们实现了单表查询和top N查询,这一篇我们来讲述如何实现多表连接和group by分组. 一.多表连接 多表连接的时间是数据库一个非常耗时的操作,因为连接的时间复杂度是M*N(M,N是要 ...

  3. Python操作Access数据库

    我们在这篇文章中公分了五个步骤详细分析了Python操作Access数据库的相关方法,希望可以给又需要的朋友们带来一些帮助. AD: Python编 程语言的出现,带给开发人员非常大的好处.我们可以利 ...

  4. Python全栈-数据库存储引擎

    一.存储引擎概述 在个人PC机中,不同的文件类型有不同的处理机制进从存取,例如文本用txt打开.保存:表格用excel读.写等.在数据库中,同时也存在多种类型的表,因此数据库操作系统中也应拥有对各种表 ...

  5. Python操作MySQL数据库完成简易的增删改查功能

    说明:该篇博客是博主一字一码编写的,实属不易,请尊重原创,谢谢大家! 目录 一丶项目介绍 二丶效果展示 三丶数据准备 四丶代码实现 五丶完整代码 一丶项目介绍 1.叙述 博主闲暇之余花了10个小时写的 ...

  6. mysql数据库之 存储引擎、事务、视图、触发器、存储过程、函数、流程控制、数据库备份

    目录 一.存储引擎 1.什么是存储引擎? 2.mysql支持的存储引擎 3. 使用存储引擎 二.事务 三.视图 1.什么是视图 2.为什么要用视图 3.如何用视图 四.触发器 为何要用触发器 创建触发 ...

  7. Python与Mysql 数据库的连接,以及查询。

    python与mysql数据库的连接: pymysql是python中对数据库的连接模块:因此应当首先安装pymysql数据库模块. 执行pip install pymysql 命令. 然后在pyth ...

  8. 《Python操作SQLite3数据库》快速上手教程

    为什么使用SQLite数据库? 对于非常简单的应用而言,使用文件作为持久化存储通常就足够了,但是大多数复杂的数据驱动的应用需要全功能的关系型数据库.SQLite的目标则是介于两者之间的中小系统.它有以 ...

  9. 【循序渐进学Python】14.数据库的支持

    纯文本只能够实现一些简单有限的功能.如果想要实现自动序列化,也可以使用 shelve 模块和 pickle 模块来实现.但是,如果想要自动的实现数据并发访问,以及更标准,更通用的数据库(databas ...

随机推荐

  1. 【UXPA工作坊小记】郎学明:做更“有用”的用户研究

    xueminglang@google.com 本来做了一些笔记,但郎老师后来发了相关教材.内容比现场PPT详细的多.由于,本人在网上也没有搜索到相关文章,还是决定做一回码字工,稍作精简后分享给大家. ...

  2. keepalived初探

    keepalived起初是为LVS设计的,专门用来监控LVS集群系统中各个real server的健康状况的,后来又在其中实现了VRRP协议,VRRP即virtual router redundanc ...

  3. dom4j操作xml

    dom4j是一个Java的XML API,类似于jdom,用来读写XML文件.是一个非常优秀的Java XML API,具有性能优异.功能强大和极端易用使用的特点,同时它也是一个开放源工具.可以在这个 ...

  4. nginx添加模块 (非覆盖安装)

    nginx添加模块(非覆盖安装) 原已经安装好的nginx,现在需要添加一个未被编译安装的模块: 查看原来编译时都带了哪些参数# /usr/local/nginx/sbin/nginx -V ngin ...

  5. R语言画图布局摆放(layout)

    require(ggplot2) require(Cairo) require(grid) p = ggplot(iris,aes(x = Species,y = Sepal.Length,colou ...

  6. pycharm 4.5在debian下安装

    1.去官网下载linux下的Tar包,下载后解压. 2.直接进入解压后的folder里面找bin下面的pycharm.sh,执行后发现没有任何反应. 3.查询资料发现是因为pycharm需要sun j ...

  7. java charset detector

    https://code.google.com/p/juniversalchardet/downloads/list java移植mozilla的编码自动检测库(源码为c++),准确率高. 通过svn ...

  8. #include <NOIP2008 Junior> 双栈排序 ——using namespace wxl;

    题目描述 Tom最近在研究一个有趣的排序问题.如图所示,通过2个栈S1和S2,Tom希望借助以下4种操作实现将输入序列升序排序. 操作a 如果输入序列不为空,将第一个元素压入栈S1 操作b 如果栈S1 ...

  9. 【读书笔记《Android游戏编程之从零开始》】15.游戏开发基础(剪切区域)

    剪切区域也称为可视区域,是由画布进行设置的:它指的是在画布上设置一块区域,当画布一旦设置了可视区域,那么除此区域外,绘制的任何内容都将看不到:可视区域可以是圆形.矩形等等. 画布提供了三种设置可视区域 ...

  10. Android开发环境搭建(转)

    转载:http://www.cnblogs.com/zoupeiyang/p/4034517.html#1 引言   在windows安装Android的开发环境不简单也说不上算复杂,本文写给第一次想 ...