最近没事做了一个数据库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、创建表,生成元数据

 在插入记录之前需要建表,确定表的格式和完整性约束,如执行一下建表操作:

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

将生成一个meta.table的元数据文件,该元数据文件第一行保存的是数据库的所有表名,以下的每一行为一个表的详细描述,格式如下:

  1. [表名1]|...|[表名n]
  2. [表名1]|[表1主键]|[表1第一个属性,约束]|[表1第二个属性,约束]|...|[[表1N个属性,约束]]
  3. .
  4. .
  5. .

  测试数据共有8个表,REGION,NATION,LINEITEM,ORDERS,CUSTOMER,PARTSUPP,PART,SUPPLIER。示例如下(省略了一些表的描述):  

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

  建立元数据的作用,是在查询处理时可以知道某个表的某个属性在原记录文件中的列号,以及该属性属于什么类型,要知道对属性排序和比较时必须知道属性类型。因此原数据表meta.table的属性必须按原记录的顺序保存。该数据表常驻内存,以python的有序字典(map)在内存中存放:

  1. {'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表):

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

  属性之间用‘|’分割,在抽取属性列之前,记录文件不能压缩,我们将在生成列索引时压缩这个原始记录。

3、抽取属性列(同时建立记录行号与行地址的对应表)

  将表中的每个属性单独抽取出来,格式为:

  1. [属性值0]|[行号0]
  2. [属性值1]|[行号1]
  3. [属性值2]|[行号2]
  4. .
  5. .
  6. .

  抽取的列示例如下:

  1. #REGION_R_NAME 
  2. AFRICA|
  3. AMERICA|
  4. ASIA|
  5. EUROPE|
  6. MIDDLE EAST|

  上面的示例是REGION表中的R_NAME属性的列,后来发现行号可以不用保存,读取到数组中后,数组下标就是行号,这样可以节省一些空间,不过,这个属性列是按原始记录的顺序存放,不能实现按块读取,当记录数很多时,不能放入内存,因此这个属性列文件在下一步之后可以删掉。

  抽取列的同时还要完成两个工作,第一个是压缩原始记录表,压缩后原始记录不再需要,读记录只需要读压缩记录即可;第二个就是建立行号到压缩后的行首地址的对应表,这样以后的操作都是按行号进行。在扫描原记录文件的每行时,写压缩文件保存压缩记录表,并记录压缩后的每一行的首地址(获得压缩后的地址在这里)。行号与行首地址的对应表格式如下:

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

  行号与行首地址的对应表示例如下: 

  1. #REGION
  2. 0
  3. 127
  4. 171
  5. 212
  6. 269

  用行号代替行地址有,可以节省空间,单个表文件只要大于3*2^32B=12GB(乘以3是因为压缩比率约为3:1),字节地址就超过就超过long int能表示的范围,而行号可以表示更大的表;另一个好处,如果后续需要建立位图索引,用行号比行地址好,因为行号是连续的整数,而行地址是离散在整数空间中,如上面的示例行地址从0直接跳到了127,中间的一串整数都没有用到,那建立的位图索引将是相当稀疏的。

  行首地址在查询中不会用到,只有在最后读取原始记录时才需要转换为行号,因此可以将它进行压缩,我们用gzib压缩,gzib为我们提供一个透明的文件压缩,所谓透明,就是像读写普通文件一样,gzib自动在缓冲区进行压缩和解压。

  python写压缩文件主要代码如下:

  1. import gzip
  2. condenseFile= gzip.open(os.path.join(path,fileName+".gz"),'wb',compresslevel = 4)#以二进制写,压缩等级为4,值越大压缩率越高,但时间越长
  3. block = '...'
  4. condenseFile.write(block)
  5. condenseFile.flush()
  6. condenseFile.close()

  读压缩文件:

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

4、属性列压缩并建立分块索引

  上一步我们得到的属性列只是简单从表中抽取出来,这样的属性列有很多冗余,比如一个表有10000行,某个属性只有10个取值,那在属性列中就需要保存保存10000行,我们可以按属性值进行分组,记录出现该属性值得行号,格式如下:

  1. [属性值0]|[出现该值的行0]|[出现该值的行1]|...|[出现该值的行n]
  2. [属性值1]|[出现该值的行0]|[出现该值的行1]|...|[出现该值的行n]
  3. .
  4. .
  5. .

  接下来按属性值排序,这样得到的属性列就有序了,排序的过程中需要用到外部排序。排序后的结果,示例如下:

  1. #PARTSUPP_PS_SUPPLYCOST
  2. 1.0|81868|307973|409984|490169|620444|632371
  3. 1.01|25328|36386|172687|243808|287934|558840|774633|775920
  4. 1.02|108457|137974|175055|206681|246824|297497|374608
  5. 1.03|38563|117772|175895|289935|381497|486960|630290|644984|723651|726647
  6. 1.04|284511|314284|327411|392035|639283|721325|754065|783577
  7. .
  8. .
  9. .
  10. 6.5|193020|436686|746401
  11. 6.51|46883|59908|129012|189045|398695|437094|455012|458310|490801|598787
  12. 6.52|54123|129198|145810|223578|336148|377020|377755|379426|430717|442844|500296|549401
  13. 6.53|32341|54384|149844|208256|437181|528380
  14. 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. 1.0|0|32812
  2. 6.52|32812|32810
  3. 12.03|65622|32800
  4. 17.46|98422|32835
  5. 22.92|131257|32787
  6. 28.39|164044|32771
  7. 33.77|196815|32794
  8. 39.29|229609|32810
  9. 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. ORA-29857: domain indexes and/or secondary objects

    dmp导入的时候出了问题,想把表空间和用户删除重建,然后再重新导入,却在删除表空间时报错:   > ORA-29857: domain indexes and/or secondary obje ...

  2. 打造属于自己的vim利器

    毋庸置疑vim很强大,然而没有插件的话对于大多数人来说他的界面是很不友好的.下面简单写一下我对vim的配置 这是我的vim配置,装的插件不是很多,对我来说已经够用.左边的侧边栏是NERD插件提供的,还 ...

  3. Astyle编程语言格式化工具的中文说明

    Artistic Style 1.23Maintained by: Jim PatteeOriginal Author: Tal Davidson Usage  :  astyle [options] ...

  4. Oracle PLSQL

    Oracle :show explain plan select * from table(dbms_xplan.display); EXPLAIN PLAN FOR statements In fa ...

  5. floyd算法 青云的机房组网方案(简单)

    青云的机房组网方案(简单) 青云现在要将 nn 个机房连成一个互相连通的网络.工程师小王设计出一个方案:通过在 nn 个机房之间铺设 n-1n−1 条双向的光纤,将所有的机房连接.可以假设数据在两个机 ...

  6. 怎样获取优酷站内视频的MP4格式地址,嵌入到手机页面播放

    最近的有关项目需要使用video标签播放视频,并且视频的路径src是优酷里面的视频,所以需要得到优酷里面的mp4路径才能播放. 但是在网上查了下资料,看到优酷的播放格式是一个m3u8文件,如图所示: ...

  7. disabled和readonly的区别?

    在博客园中看到这样一篇文章,关于disabled和readonly的区别,以前还真的没有注意它们的区别,还是有必要知道它们的区别的,所以转载了. 这两个属性有类似之处,但是区别也是巨大的,之所以说类似 ...

  8. Hadoop 一二事(1) - 简单介绍与杂谈

    大数据大数据,身边很多朋友都在谈大数据,Big Data!!! 到底是什么,用来干嘛的,也很少有人说得出一二,那今天开始就简单说说这一二事吧 hadoop 的来源:是作者女儿的一个玩具 - 一只黄色的 ...

  9. Java语法基础(一)----关键字、标识符、常量、变量

    一.关键字: 关键字:被Java语言赋予特定含义的单词.组成关键字的字母全部小写.注:goto和const作为保留字存在,目前并不使用.main并不是关键字. 二.标识符: 标识符:就是给类,接口,方 ...

  10. Unity3D开发赛车Demo遇到的问题

    遇到问题 在3D Max中导出的跑车在Unity中轴向不对,不知有没有朋友遇到过呢? 切换坐标系统 在Unity3D中按X键,切换坐标系统 车轮方向变了 运行游戏之后,赛车的车轮方向变歪了 车依然能跑 ...