自律给我自由

积累 自驱 自制 坚持

0%

【译】列式数据库

翻译自Column-oriented DBMS

Column-oriented DBMS

面向列的DBMS(或列式数据库管理系统)是按列而不是按行存储数据表的数据库管理系统。在关系型数据库管理系统领域,使用列式存储和行式存储并没有什么区别。列和行数据库都可以使用传统的数据库查询语言(如sql)来加载数据并执行查询。列和行数据库都可以成为系统的支柱,为通用extract-transform-load(ETL)和数据可视化系统提供数据。然而,将数据存储在列中而不是行中,数据库可以更精确的访问到某个查询请求需要的数据,而不是扫描并丢弃行中不需要的数据。查询性能由此而提高,尤其是在非常大的数据集中。

描述

背景

关系型数据库管理系统的数据描述了一个包含列和行的二维表。比如说一个数据库里有这样一张表:

RowId EmpId Lastname Firstname Salary
001 10 Smith Joe 40000
002 12 Jones Mary 50000
003 11 Johnson Cathy 44000
004 22 Jones Bob 55000

这个简单的表包含了一个员工的标识(EmpId)、名称字段(Lastname和Firstname)和工资。这个二维表的格式是一种抽象,实际的实现中硬件存储会要求数据被序列化成某一种格式。

涉及到硬盘的最昂贵的操作是寻址,为了改善整体的性能,应该用可以最小化寻址次数的方式存储关联数据。这就是人们熟知的Locality of reference,基本概念出现在不同的上下文环境中。硬盘以一些列固定大小的块组织起来,通常可以存放表里若干行数据。通过组织表的数据,使行适应这些块,并将相邻的行分组到连续的块中。在很多情况下,需要读取或寻找的块数和寻址的次数一起被最小化了(译者注:这句没懂…)。

面向行的系统

存储一张表的通用做法是序列化每一行数据,就像这样:

1
2
3
4
001:10,Smith,Joe,40000;
002:12,Jones,Mary,50000;
003:11,Johnson,Cathy,44000;
004:22,Jones,Bob,55000;

当数据插入表中时,它被分配一个内部的id,这个rowid字段在系统内部被用来关联数据。在这种情况下,记录具有独立于用户指定的empid的顺序rowid。这个例子当中,DBMS用短整型(short integer)存放rowid,实际使用会用更大的数字,比如64位、128位。

基于行的系统被设计的用尽可能少的操作,就可以有效的返回整行或记录数据。这适合一些常见的用例,比如系统尝试获取某一特定对象的信息,像是名片系统里用户的联系信息,或是线上购物系统里的商品信息。通过将这些数据和其它相邻数据存放到一个单独的块中,系统可以很快的用最少的操作获取到记录。

与一小部分特定的数据相反,行式系统在整张表上做集合范围(set-wide)的操作往往很低效。例如在这个例子里,要想找到工资在40000到50000之间的全部记录,DBMS必须完全遍历整个表来寻找匹配的记录。在上述的表格里数据可能在一个单独的块中,有上百行记录的表就不会这样,这时候就需要用多次磁盘操作来获取数据,并检查它。

为了提高这些操作的性能(这是很常见的,并且也是DBMS的关键点),大多数DBMS支持使用索引,将一列里所有的数据和可以指引到原始表的rowid存放到一起。salary列的索引看起来是这个样子的:

1
2
3
4
001:40000;
003:44000;
002:50000;
004:55000;

由于它们只存储了一小片数据,而不是整行,索引总的来说要比主表要小。扫描这个小数据集减少了磁盘的操作。如果索引被重度使用,它可以显著的减少常见操作的时间。然而维护索引增加了系统的开销,尤其是新数据写入到数据库的时候,记录不仅要存放到主表,任何附加的索引都要同步更新。

在某一列或某些列的数据库索引通常都是按列存储,因此范围查找的操作(比如上面说的“获取工资在40000到50000的全部记录”)会非常快。

一些行数据库被设计的完全填充到内存中,成为一个内存数据库,这些系统不依赖磁盘操作,而且对于整个数据集有相等的访问时间。这样就不需要索引了,因为在典型的聚合场景中,扫描原始数据和整个索引所需要的操作数量是相同的。这样的系统可能更简单更小,但是只能管理能放进内存的数据库。

面向列的系统

面向列的数据库将一列的全部数据序列化到一起,然后是另外一列,以此类推。例如我们的样例表,数据存储的风格是这样的:

1
2
3
4
10:001,12:002,11:003,22:004;
Smith:001,Jones:002,Johnson:003,Jones:004;
Joe:001,Mary:002,Cathy:003,Bob:004;
40000:001,50000:002,44000:003,55000:004;

这个布局,列更像是行式系统里的索引结构,这会让人们误以为:列式存储“不就是”每一列都带有索引的行式存储吗。其实这和数据映射还是有明显不同的。在面向行的索引系统里,主键是rowid,是从索引数据到主键的映射。而在面向列的系统里,主键是数据,是从数据到rowid的映射(译者注:文中的顺序与此相反)。这之间的区别很微妙,不过稍微修改一下存储方式就能看出来:

1
…;Smith:001;Jones:002,004;Johnson:003;…

列式系统是否更高效很大程度上取决于工作负载的自动化(译者注:大概说的是一个操作从请求到响应之间的一系列工作workload)。获取一个给定对象的全部数据(一整行)就很慢。基于行的系统只要一次磁盘操作就可以获取到一行数据,而列式数据库里收集多列的数据就需要多次磁盘操作。不过,整行的操作比较罕见,大多数情况只需要数据的有限的子集。比如在名片系统,收集firstname和lastname来构建一个联系人列表就远比读取一个address的全部数据要常见。写入数据到数据库更是如此,尤其是数据很稀疏,很多列式可选的(optional)。因此,列存储尽管有很多理论上的缺陷,却依然展示出了优异的性能。

分区、索引、缓存、视图、OLAP cube和事务性系统(比如write-ahead logging多版本并发控制)都会显著的影响任何一个系统的物理架构。也就是说,基于联机事务处理(OLTP)的RDBMS更多的面向行,而基于联机分析处理(OLAP)的系统则在面向行与列中权衡。

优势

比较面向行与面向列数据库主要关注在给定工作负载下硬盘访问的效率。因为相比较计算机的其它瓶颈,寻址时间是相当长的。例如说,典型的SATA硬盘驱动器的平均寻址时间在16到22毫秒之间,而动态随机存取存储器(DRAM,也就是内存)在Intel Core i7处理器上的平均时间是60纳秒,几乎快400000倍之多。显然,处理大数据的主要瓶颈是磁盘访问。列式数据库通过减少磁盘访问的数量,和对相似列数据的有效压缩(译者注:同一列的数据是压缩存储)提升了系统的性能。

通过实践得知,列式系统非常适合类似联机分析处理(OLAP)的场景(比如数据仓库),这些系统通常会在全部数据上(PB级)执行高度复杂的查询。但是,总会有一些数据要写进列式数据库的,事务(INSERTs)必须分割成列并在存储时压缩,这使得它不适合OLTP场景。面向行的数据库非常适合于重度依赖交互事务的OLTP-like场景,比如说,当一行的全部数据位于一个单独的位置时,检索这些行数据会非常高效(因为最小化了磁盘寻址),正如面向行的体系架构。不过,面向列的系统已经发展成为可以处理OLTP和OLAP操作的混合系统,面向列的系统面临的一些OLTP的限制通过内存数据存储来解决。适合于OLAP和OLTP角色的基于列的系统,通过消除对单独系统的依赖(译者注:数据存储系统?),有效地减少了总数据占用。

压缩

一列数据的格式时统一的,这意味着可以在存储大小上做一些列式数据能用而行式数据不能用的优化,例如很多流行的压缩方案,比如LWZ游程编码,利用临近数据的相似性进行压缩。实际数据中常见的缺失值和重复值可以用两位标记来表示。相同的技术可以运用到行式数据上,但是效果差强人意。

为了提升压缩效率,对行进行排序是一个行之有效的方案。例如,使用位图索引排序可以将压缩性能提升一个量级。为了使字典顺序在运行长度编码方面的压缩效益最大化,最好使用低基数列作为第一个排序键(译者注:一脸懵逼)。例如,一个表含有性别、年龄和名称列,最好先对性别(基数为2)进行排序,然后对年龄进行排序(基数<150),然后是名称(译者注:哦~)。

列压缩以牺牲检索效率为代价减少了磁盘空间,相邻压缩越大,随机访问就越困难,因为数据可能需要解压缩才能读取。因此,面向列的体系结构有时需要通过额外的机制来强化,旨在最大限度地减少访问压缩数据的需求(译者注:比如ORCFile格式的轻量级索引)。