最近没事做了一个数据库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. python 连接 db2

    export IBM_DB_DIR=/home/db2inst1/sqllib export IBM_DB_LIB=/home/db2inst1/sqllib/lib http://programmi ...

  2. 我眼中的自己ps:自我介绍

    我是一个理科生,学的是计算机科学与技术专业,目前在桂林理工大学信息科学与工程学院就读.我心里不是很喜欢这个专业,但是因为当初的某种原因就入了这个门.自从学习了计算机以后,我发现这个专业可谓“博大精深, ...

  3. IIS管理

    1.缓存的处理 http://www.cnblogs.com/dudu/p/iis_user-mode_caching_cache-control_public.html 2.负载均衡的使用 ARR ...

  4. 问题解决——multimap中统计key的种类

    ================声明================= 本文原创,转载请注明出处和作者,并保持文章的完整性. 本文链接:http://www.cnblogs.com/wlsandwho ...

  5. 问题解决——SolidWorks 已停止工作 (Windows7 + SolidWorks 2010 SP0.0)

    给同事的SolidWorks解决问题时偶然间发现的. -------------------------------------------------------------- 本文原创,转载请注明 ...

  6. wampserver安装之后连接phpMyAdmin 不成功的解决方法

    情况:我原先安装了本地的mysql数据库,默认密码不是为空,而是123456,但是wampserver安装默认mysql的密码是为空的.所以需要修改一下默认的配置.不然会出现连不上数据库. 解决方案: ...

  7. Redis系列(二)—— 数据类型及其使用

    Redis数据类型及其使用 参考:http://www.cnblogs.com/jackluo/p/3173436.html Redis支持五种数据类型:string(字符串),hash(哈希),li ...

  8. 10分钟制作UWP汉堡菜单

    什么是汉堡菜单? 汉堡菜单,指的是一个可以弹出和收回的侧边栏.在UWP和Android应用中,汉堡菜单都非常常见. 首先我们列出所有需要掌握的前置知识: 1,SplitView 2,StackPane ...

  9. C# url信息获取

    假设当前页完整地址是:http://www.360jht.com/game/bbb.aspx?id=5&name=kelli "http://"是协议名 "www ...

  10. 一个Azure VM RDP连接问题

    由于Azure上的VM都是通过同一个镜像文件创建的,有时会需要修改SID. 在给一台VM修改SID重启后,就无法通过RDP连接到虚机了,从Azure管理界面的启动诊断界面上可以看到虚拟停在一个要求用户 ...