美文网首页DDIA
存储与检索 -- 列式存储(Column-Oriented St

存储与检索 -- 列式存储(Column-Oriented St

作者: 珊瑚海的世界 | 来源:发表于2018-05-20 20:14 被阅读0次

            如果您的事实表(fact tables)中有万亿行和数PB的数据,则有效存储和查询它们成为一个具有挑战性的问题。 维度表(dimension table)通常要小得多(数百万行),因此在本节中我们将主要关注事实数据的存储。

    ​        尽管事实数据表通常超过100列,但典型的数据仓库查询一次只能访问4个或5个数据仓库查询(分析很少需要“SELECT *”查询)。 以示例3-1中的查询为例:它访问大量行(在2013日历年期间每次都有人购买水果或糖果),但它只需访问fact_sales表的三列:date_key,product_sk和 quantity。 查询忽略所有其他列。

    ​        例3-1。 分析人们是否更倾向于购买新鲜水果或糖果,具体取决于一周中的哪一天

    SELECT

        dim_date.weekday, dim_product.category,

        SUM(fact_sales.quantity) AS quantity_sold

    FROM fact_sales

        JOIN dim_date ON fact_sales.date_key = dim_date.date_key

        JOIN dim_product ON fact_sales.product_sk = dim_product.product_sk

    WHERE

        dim_date.year = 2013 AND

        dim_product.category IN ('Fresh fruit', 'Candy')

    GROUP BY

        dim_date.weekday, dim_product.category;

    ​        我们如何有效地执行此查询?

    ​        在大多数OLTP数据库中,存储都是以面向行的方式进行布局的:表格的一行中的所有值都相邻存储。 文档数据库是相似的:整个文档通常存储为一个连续的字节序列。 您可以在图3-1的CSV示例中看到这一点。

    ​        为了处理像例3-1这样的查询,您可能在fact_sales.date_key或fact_sales.product_sk上有索引,它们告诉存储引擎在哪里查找特定日期或特定产品的所有销售情况。 但是,面向行的存储引擎仍然需要将所有这些行(每个包含超过100个属性)从磁盘加载到内存中,解析它们,并过滤掉那些不符合所需条件的行。 这可能需要很长时间。

    ​        面向列的存储背后的想法很简单:不要将所有来自一行的值存储在一起,而是将每个列的所有值存储在一起。 如果每列都存储在一个单独的文件中,则查询只需要读取和分析查询中使用的那些列,这可以节省大量工作。 这个原理如图3-10所示。

    列存储在关系数据模型中最容易理解,但它同样适用于非关系数据。 例如,Parquet是一种列式存储格式,支持基于Google Dremel的文档数据模型。

    图3-10. 按列存储关系数据,而不是按行存储 

    ​        面向列的存储布局依赖于包含相同顺序的行的每个列文件。 因此,如果您需要重新组装整行,您可以从每个单独的列文件中获取第23项,并将它们放在一起形成表格的第23行。

    列压缩

    ​        除了仅从磁盘加载查询所需的那些列以外,我们还可以通过压缩数据来进一步降低对磁盘吞吐量的需求。 幸运的是,面向列的存储通常非常适合压缩。

    ​        查看图3-10中每列的值序列:它们通常看起来很重复,这是压缩的好兆头。 根据列中的数据,可以使用不同的压缩技术。 一种在数据仓库中特别有效的技术是位图编码,如图3-11所示。

    图3-11. 压缩的,单列的位图索引存储  

    ​      通常情况下,一列中不同值的数量与行数相比较小(例如,零售商可能拥有数十亿的销售交易,但只有100,000个不同的产品)。 现在我们可以获取一个具有n个不同值的列,并将其转换为n个独立的位图:每个不同值的一个位图,每行一位。 如果该行具有该值,则该位为1,否则为0。

    ​        如果n非常小(例如,国家/地区列可能具有大约200个不同的值),那么这些位图可以每行存储一位。 但是,如果n更大,大部分位图中都会有很多零(我们说它们很稀疏)。 在这种情况下,位图可以进行运行长度编码,如图3-11底部所示。 这可以使列的编码非常紧凑。

            这些位图索引非常适合数据仓库中常见的各种查询。 例如:

    WHERE product_sk IN (30, 68, 69): 

            加载product_sk = 30,product_sk = 68和product_sk = 69的三个位图,并计算三个位图的按位或,这可以非常有效地完成。WHERE product_sk = 31 AND store_sk = 3:   

            加载product_sk = 31和store_sk = 3的位图,并计算按位AND。 这是因为列按照相同的顺序包含行,因此一列的位图中的第k位对应于与另一列的位图中的第k位相同的行。

            对于不同类型的数据也有各种其他压缩方案,在这里我们暂时不讨论。

                                    面向列的存储和列系列

            Cassandra和HBase有一个列家族的概念,他们继承了Bigtable。 然而,将它们称为面向列是非常具有误导性的:在每个列系列中,它们一起存储行中的所有列以及行密钥,并且它们不使用列压缩。 因此,Bigtable模型仍然主要是面向行的。

    ​        对于需要扫描数百万行的数据仓库查询,一个很大的瓶颈是从磁盘获取数据到内存的带宽。 但这不是唯一的瓶颈。 分析型数据库的开发人员也担心如何有效地使用从主存储器到CPU高速缓存的带宽,避免CPU指令处理流水线中的分支预测误差和气泡,以及在现代CPU中使用单指令多数据指令。

    ​         除了减少需要从磁盘加载的数据量外,面向列的存储布局对于有效利用CPU周期也很有帮助。 例如,查询引擎可以将大量压缩的列数据放在CPU的L1缓存中,并在紧密的循环中遍历它(即没有函数调用)。 一个CPU可以执行这样一个循环比代码要快得多,这个代码需要处理每个记录的大量函数调用和条件。 列压缩允许列中的更多行适合相同数量的L1缓存。 运算符(如前面所述的按位AND和OR)可以设计为直接在这种压缩列数据块上运行。 这种技术被称为矢量化处理。

    列存储中的排序顺序

    ​        在列存储中,存储行的顺序不一定很重要。 按插入顺序存储它们是最容易的,因为插入一个新行就意味着附加到每个列文件。 但是,我们可以选择强制执行命令,就像我们之前对SSTables所做的那样,并将其用作索引机制。

            请注意,对每个列单独排序没有意义,因为那样我们就不会再知道列中的哪些项目属于同一行。 我们只能重建一行,因为我们知道一列中的第k项与另一列中的第k项属于同一行。

            相反,即使数据按列存储,数据也需要一次排序整行。 数据库的管理员可以使用他们对常见查询的知识来选择表格应该被排序的列。 例如,如果查询通常以日期范围为目标,比如上个月,则可以将date_key作为第一个排序键。 然后,查询优化器只能扫描上个月的行,这比扫描所有行快得多。

            第二列可以确定第一列中具有相同值的任何行的排序顺序。 例如,如果date_key是图3-10中的第一个排序关键字,那么product_sk可能是第二个排序关键字,因此同一天的同一产品的所有销售都将在存储中组合在一起。 这将有助于在某个日期范围内按产品对销售进行分组或过滤的查询。

            排序顺序的另一个优点是它可以帮助压缩列。 如果主排序列没有多个不同的值,那么在排序后,它将具有很长的序列,其中相同的值连续重复多次。 一个简单的运行长度编码,就像我们用于图3-11中的位图一样,可以将该列压缩到几千字节,即使该表有数十亿行。

            第一个排序键上的压缩效果最强。 第二个和第三个排序键会更混乱,因此不会有这么长时间的重复值。 排序优先级下面的列以基本上随机的顺序出现,所以它们可能不会压缩。 但排序的前几列仍然是一个整体。

    几个不同的排序顺序

            这个想法的巧妙扩展在C-Store中引入,并在商业数据仓库Vertica中采用。 不同的查询受益于不同的排序顺序,为什么不以不同的方式存储相同的数据? 无论如何,数据需要复制到多台机器,以便在一台机器发生故障时不会丢失数据。 您可能会存储以不同方式排序的冗余数据,以便在处理查询时,可以使用最适合查询模式的版本。

            在面向列的存储中有多个排序顺序有点类似于在面向行的存储中具有多个二级索引。 但是最大的区别在于,面向行的存储将每行保存在一个地方(在堆文件或聚簇索引中),并且二级索引只包含指向匹配行的指针。 在列存储中,通常没有任何指向其他数据的指针,只有包含值的列。

    写入面向列的存储

            这些优化在数据仓库中很有意义,因为大多数负载由分析人员运行的大型只读查询组成。 面向列的存储,压缩和排序都有助于更快地读取这些查询。 然而,他们有写作更加困难的缺点。

            压缩列无法实现更新就地的方法,如B树方法。 如果你想在排序表中间插入一行,你很可能不得不重写所有的列文件。 由于行由列中的位置标识,因此插入必须始终更新所有列。

            幸运的是,本章早些时候我们已经看到了一个很好的解决方案:LSM树。 所有写入操作都先到内存中存储,然后将它们添加到已排序的结构中并准备写入磁盘。 无论内存存储是面向行还是面向列都无关紧要。 当已经积累了足够的写入数据时,它们将与磁盘上的列文件合并并批量写入新文件。 这基本上是Vertica所做的。

            查询需要检查磁盘上的列数据和最近在内存中写入的数据,并将两者结合起来。 但是,查询优化器隐藏了用户的这种区别。 从分析人员的角度来看,通过插入,更新或删除进行修改的数据会立即反映在后续查询中。

    聚合:数据立方体和物化视图

            并非每个数据仓库都必定是一个列存储:传统的面向行的数据库和其他一些架构也被使用。 但是,对于临时分析查询,列式存储可能会快得多,因此它正在迅速普及。

            数据仓库的另一个值得一提的是物化汇总。 如前所述,数据仓库查询通常涉及一个聚合函数,如SQL中的COUNT,SUM,AVG,MIN或MAX。 如果许多不同的查询使用相同的聚合,那么每次都需要通过原始数据进行紧缩是非常浪费的。 为什么不缓存查询最常用的一些计数或总和?

            创建这种缓存的一种方式是物化视图。 在关系数据模型中,它通常被定义为一个标准(虚拟)视图:一个类似于表的对象,其内容是某些查询的结果。 不同之处在于物化视图是查询结果的实际副本,写入磁盘,而虚拟视图只是写入查询的快捷方式。 从虚拟视图读取时,SQL引擎会将其展开为视图的基础查询,然后处理扩展的查询。

            当基础数据发生变化时,物化视图需要更新,因为它是数据的非规范化副本。 数据库可以自动执行此操作,但这种更新会使写入操作更加昂贵,这就是物化视图在OLTP数据库中不常使用的原因。 在读取量大的数据仓库中,它们可以更有意义(无论它们是否实际提高读取性能取决于个别情况)。

            物化视图的常见特例称为数据立方体或OLAP立方体。 它是按不同维度分组的聚合网格。 图3-12显示了一个例子。

    图3-12.  数据立方体的两个维度,通过求和来汇总数据  

            想象一下,现在每个事实都只有两个外部关键字 - 在图3-12中,这些是日期和产品。 您现在可以绘制一个二维表格,其中一个轴线上的日期和另一个轴上的产品。 每个单元包含具有该日期 - 产品组合的所有事实的属性(例如,net_price)的聚集(例如,SUM)。 然后,您可以沿每行或每列应用相同的汇总,并获得一个已减少一个维度的汇总(按产品的销售额(无论日期),还是按日期的销售额(不论产品))。

            一般来说,事实往往有两个以上的维度。 在图3-9中有五个维度:日期,产品,商店,促销和客户。 很难想象五维超立方体是什么样子,但其原理仍然相同:每个单元格包含特定日期 - 产品 - 商店 - 促销 - 客户组合的销售。 这些值可以在每个维度上重复汇总。

            物化数据立方体的优点是某些查询变得非常快,因为它们已经被有效地预先计算。 例如,如果您想知道每个商店的总销售额,则只需查看适当维度的总计 - 无需扫描数百万行。

            缺点是数据立方体不具有查询原始数据的灵活性。 例如,没有办法计算哪些销售比例来自成本超过100美元的商品,因为价格不是其中的一个维度。 因此,大多数数据仓库尽量保留尽可能多的原始数据,并且仅将聚合数据(如数据立方体)用作某些查询的性能提升。

    相关文章

      网友评论

        本文标题:存储与检索 -- 列式存储(Column-Oriented St

        本文链接:https://www.haomeiwen.com/subject/azgtjftx.html