精华内容
下载资源
问答
  • 列存储在大数据分析中的应用与优化.pdf
  • 列存储索引有推荐的最佳导入行数,过小会导致数据先进入Delta Store(行组,row group),这个是行存储的B 树结构,然后通过Tuple Mover对数据进行编码和压缩最终成为片段(segment),这时候才能说进入了真正的...

    接上文:SQL Server 列存储索引性能总结(6)——列存储等待信息,这一篇还是本人工作中的难点,列存储导入速度的问题,我们都知道有索引的表导入肯定比堆表慢,可是究竟为什么呢?对于列存储来说,很可能就是Delta Store的原因。

      列存储索引有推荐的最佳导入行数,过小会导致数据先进入Delta Store(行组,row group),这个是行存储的B 树结构,然后通过Tuple Mover对数据进行编码和压缩最终成为片段(segment),这时候才能说进入了真正的列存储索引中,才能真正发回去列存储索引的优势。
      在我目前的项目中,导入数据是个头痛的事情,数据量不是非常大,但是表的列很多,4~500列。导致即使是纯粹导进堆表,最高可能也就50000行每秒,更别说聚集列存储索引,而且加上公司使用固定工具带来的不合理的导入方式。
      从外部导入数据到数据库时,不管是导入列存储还是堆表,最好的方式还是使用各种大容量导入工具比如BCP/Bulk insert API等,可以使得数据能够直接进入片段而不用先到Delta store然后再进行转换。
      当然,在现实世界中很多时候没法真的尽如人意。言归正传,在没有办法使用Bulk insert的时候,到底发生了什么?下面创建一个单列的表,然后插入6700万数据用于创建足够的行组。这个数不是随便选的,后面会看到理由。

    环境准备

    本系列的脚本由于有时效性,所以建议先看完再执行,需要连续执行。
      首先初始化一批数据,这个可以在TempDB里面做:

    --创建表
    create table dbo.Test(id int not null );
    --创建聚集列存储索引
    create clustered columnstore index CCI on dbo.Test;
    --导入刚好够一个片段的数据,也就是1048576行
    declare @i as int;
    set @i = 1;
    
    begin tran
    while @i <= 1048576
    begin
    	insert into dbo.Test
    		values (@i);
    
    	set @i = @i + 1;
    end;
    commit;
    

      在我的虚拟机上面运行了 46秒,对于这个数量级而言也还算可以。这个时候片段已经满了,接下来循环插入6次,让数据量达到6700万。这里并不是循环这么多次,而是对原有的数据复制一份然后追加,另外在跑下面脚本的时候,打开另外一个窗口同时执行接下来的命令查看元组的信息,可以看到Closed部分是越来越多:

    declare @i as int;
    declare @max as int;
    set @i = 1;
    
    while @i <= 6
    begin
    	select @max = isnull(max(id),0) from dbo.Test;
    
    	insert into dbo.Test
    		select id + @max from dbo.Test;
    		
    	set @i = @i + 1;
    end;
    
    --另外一个窗口执行
    select state, state_description, count(*) as 'RowGroup Count'
    from sys.column_store_row_groups
    where object_id = object_id('test')
    group by state, state_description
    order by state;
    

      另外你还可能看到总共下面4种状态:OPEN/CLOSED/COMPRESSED/TOMBSTONE:
    在这里插入图片描述
      第一个导入命令执行了46秒,第二个导入6600万数据的命令在本机上用了13分钟 ,在执行完毕之后,再次使用上面命令检查行组信息,可以看到1一个OPEN状态,63个是CLOSED状态,也就是说还是Delta Store。也就是说几乎所有的数据还是再B-Tree结构即行存储结构上。这将很难达到列存储所能达到的I/O性能。

    Tuple Mover

    简介

      前面提到过,Tuple Mover是一个后台进程,默认每5分钟自动运行一次,它会检查已关闭的Delta Store然后转换成片段。Delta Store这个咋一看上去并没有什么问题,但是请想象一下,如果本身就是行存储(没有压缩的堆表,它自己并不是索引,同时最大只有1048576行),那跟传统索引有什么不同?而且还要额外加一步转换成片段,开销自然就会上升,前面说过转换过程不仅资源利用率会上升,而且会锁住对应的行组直到转换完成,这个过程不阻塞读但是阻塞增删改。这对常规的ETL工作而言是比较大的挑战。
      很多环境下,确实没有办法总能产生10~100万每批的理想导入量。那么这个时候最好的方式就是先进入行存储模式(堆或者b-tree),然后创建聚集列存储索引。
      不用担心创建聚集列存储索引会产生非常大的日志增长,因为CREATE INDEX使用最小日志记录的方式,而且即使是完整日志模式,聚集列存储索引不会产生大量的一行一行的日志。

    运行模式

      Tuple Mover在Delta Store存够1048576行数据之后,会自动或者被动发起转换,在索引重建过程是不启动的。同时要注意,也是我所在项目遇到的问题——宽行表(也就是表有很多列)会导致转换非常耗时,因为不能从并行运行中获益。细节一点来说,在两个行组被压缩之间,会有15秒的空隙,这个时间并不用来等待某些资源,那究竟发生了什么事呢?

    1. 同一时间只压缩每个表的一个行组(假设同时又多个表需要压缩)
    2. 完成后进入休眠。
    3. 开始第一步中的下一个行组
    4. 再进入休眠
    5. 重复上面步骤直到全部完成。

      Tuple Mover不会再大量的Bulk insert时持续跟踪,所以合理的情况是直接插入到片段也就是已压缩的格式,或者通过索引重建或重组来触发。Tuple Mover的设计初衷更多的是针对普通的insert命令(INSERT INTO … SELECT除外)。

    建议

      如果插入后又大量的delta store,TUple Mover只会大概每分钟处理5~6个片段,有时候甚至更少。如果索引有成百上千的CLOSED delta store,就需要使用REBUILD操作。再这种情况下处理时间还是非常大的,有时候可以达到小时级别。
      但是如果后续需要大量查询,这种时间还是必要的,毕竟数百个delta store并不能真正使用到列存储的优点(压缩,仅针对列,片段消除),同时数据也会以“假”的列模式运行,因为在执行过程中delta store是不能被识别的。
      那么如果真的出现大量的Delta Store的时候该怎么做呢?最好的方式是rebuild,因为这样会创建全局的Dictionary (global dictionary)。次优的方法是REORGANIZE 索引,这个操作会使用所有可用的资源来加快速度。
      这一步可以在导入过程中运行:

    select state, state_description, count(*) as 'RowGroup Count'
    from sys.column_store_row_groups
    where object_id = object_id('Test')
    group by state, state_description
    order by state;
    
    waitfor delay '00:01:00';
    
    select state, state_description, count(*) as 'RowGroup Count'
    from sys.column_store_row_groups
    where object_id = object_id('Test')
    group by state, state_description
    order by state;
    

      结果可以看到第一次只有2个Row Groups是Compressed的,但是1分钟之后,已经有8个了。由于表非常小,转换可以瞬间完成,同时由于在启动执行到执行结束实际上并非真的是60秒整,所以这个过程实际上是6个而不是60秒/15秒每个=4个这种简单的计算,如果你不相信,可以在导入数据过程中多执行几次。
    在这里插入图片描述

    应对方案

      前面说到,如果用单纯的Insert来导入数据,时间开销最多的部分往往在Tuple Mover对Delta Store的压缩转换。对此,可以对聚集列存储索引进行重建和重组。它们可以运行在分区级别,同时可以两个Tuple Mover一起进行(自动和手动)。
      说到这里,大概应该也知道为什么我的项目导入慢,其实问题很简单:

    1. 列太多:普遍4~500列。
    2. 导入方式:外部导入使用某商业软件,拼接每行数据成为一条insert into xx values(),然后以1000行一个批提交,delta store非常多。

      通过使用BCP命令,可以从大概50000行/秒提升到13万行/秒。可见对于数据行多的表,以传统的insert into并不高效。

      前面多次提到Dictionary, 那么下一章我们来看看这个是什么东西。
      下一篇:SQL Server 列存储索引性能总结(8)——列存储中的Dictionary

    展开全文
  • 点击上方“蓝字”可以关注我们哦卞昊穹中国人民大学博士嘉宾简介:中国人民大学博士研究生,研究方向为数据库系统和实时数据分析,从事SQL-on-Hadoop系统测试和开发。在HDFS列存储优...

    点击上方“蓝字”可以关注我们哦

    卞昊穹

    中国人民大学博士

    嘉宾简介:

    中国人民大学博士研究生,研究方向为数据库系统和实时数据分析,从事SQL-on-Hadoop系统测试和开发。在HDFS列存储优化、高性能数据索引和装载、分布式等值连接算法、海量知识库扩充等方面的研究成果发表在SIGMOD、CIKM等国际顶级学术会议上。

    宽表列存储在大数据分析中的应用与优化

    目前在很多企业的数据分析任务中,常见几百上千列的宽表数据。这些宽表通常数据量巨大、存储在HDFS等分布式文件系统中。HDFS上宽表存储优化重要且具有挑战。宽表在数据分析中的可以避免一些频繁的连接操作,在用户画像、数据挖掘负载中,宽表也是一种高效的数据存取方式。对于宽表上的分析型负载,列存储可以有效减少不必要的I/O、提高数据压缩效果。HDFS上也已经有RC File、ORC、Parquet等成熟的列存储格式,但这些存储格式的设计只考虑了传统的数据分析负载,并没有针对宽表做深入的研究和优化。本工作介绍了HDFS上宽表列存储的特征和优化方法。该工作已在微软Bing日志分析产品中应用,并发表在SIGMOD 17上。

    ■议题详解

    品读之后,

    愿享同感。

    by.数据库技术大会

    展开全文
  • 什么是存储数据库?

    万次阅读 多人点赞 2018-03-14 10:52:46
    列存储不同于传统的关系型数据库,其数据在表中是按行存储的,列方式所带来的重要好处之一就是,由于查询中的选择规则是通过列来定义的,因此整个数据库是自动索引化的。按列存储每个字段的数据聚集存储,在查询只...

    列存储不同于传统的关系型数据库,其数据在表中是按行存储的,列方式所带来的重要好处之一就是,由于查询中的选择规则是通过列来定义的,因此整个数据库是自动索引化的。按列存储每个字段的数据聚集存储,在查询只需要少数几个字段的时候,能大大减少读取的数据量,一个字段的数据聚集存储,那就更容易为这种聚集存储设计更好的压缩/解压算法

    传统的行存储和列存储的区别

    这里写图片描a 
    1、数据是按行存储的 
    2、没有索引的查询使用大量I/O 
    3、建立索引和物化视图需要花费大量时间和资源 
    4、面对查询的需求,数据库必须被大量膨胀才能满足性能需求

    这里写图片描述 
    1、数据按列存储–每一列单独存放 
    2、数据即是索引 
    3、只访问查询涉及的列–大量降低系统IO 
    4、每一列由一个线索来处理–查询的并发处理 
    5、数据类型一致,数据特征相似–高效压缩


    列式数据库

    什么是列式数据库?可能大家也才到了,既然有列式数据库,那么肯定就有行式的喽!确实是这样的。也许大多数人并不了解数据库储存模型(storage model)和数据库的数据模型(data model),不过对上层是使用者也没多大关系。不过我们现在讲的列式和行式就是指数据库的storage model,而他们支持同样的data schema,即对data model感知不到storage model的实现区别。 
    一个数据库的data model约定可以进行上层数据操作,而storage model决定这些操作的性能。比如,No Sql数据库使用的是data model是key-value,而储存模型有map结构实现,也可以由tree结构实现。而对于sql数据库,其数据模型是一张二维表,而至于怎么存储这张二维表,很容易就可以想到可以按行存储和按列储存。按行存储就是我们现在常见操作型数据库,而是最大众的数据库,比如MySql、Oracle、……等等你所知道大部分数据库。而按列储存的数据库现在也是很有名,比如Hive、Vertica、Druid、Infobright等。

    为什么要行式数据库又要列式数据库?

    先让让我们想象关于二维表我们有哪些操作?—— select、update、delete和insert。这些操作都会需要找到相应的位置,所以这些操作的基础都是search。 
    而基本的算法都是即从时间考虑也是从空间考虑的。我们开始具体举个例子。

    在数据库储存作为实际的一堆储存在磁盘上的文件,在设计不得不考虑磁盘的特性。一般的磁盘特性,其实所有的储存都有一个特性就是对于locality良好的存取性能是随机存取的好几倍。我们现在把一块想像成一组固定大小的块,如图: disk logic model 而文件的内容实际会被分开按照磁盘逻辑块来储存,数据库主要任务就是怎么组织这些逻辑块来取得更好的读取性能和便捷性。 

    在不考虑索引的情况下,所有的磁盘读取都是顺序读取,这意味了要查找一个东西,都需要扫描全表或者部分表。很直观的道理,读取的性能就是取决于扫描的范围。范围越大,速度当然越慢。 
    我们先假设我们有一堆如下的数据:

    RowIdEmpIdLastnameFirstnameSalary
    00110SmithJoe40000
    00212JonesMary50000
    00311JohnsonCathy44000
    00422JonesBob55000

    行式储存模型

    好现在我们开始让磁盘里塞,假设我们的磁盘块只能容下5个字段(抽象的,假设我们的这些字段的大小都一样),因为我们是按找行优先的,所以结果就如下: 
    这里写图片描述

    于是当我们要找Jones的所有信息的工资时候,我们会依次从第一块磁盘块直到扫描到最后(为什么要扫到最后,因为是在找全部叫Jones的信息,所以不扫都最后都不能确定是否会遗漏)。一共需要扫4块,然后取出其第二块和第四块信息,找出其工资的信息。

    其实基于行式储存,对于where语句处理都需要处理全表。对于磁盘的不停seek,速度就可想而知。当然一般数据库为了应对这种全数据扫描,找到了建立索引的方法。而索引就是对某个或者某些字段的组合的信息,即取出数据的部分信息,以减少每次扫描从全表到部分信息的扫描的过渡。

    这种查询方式很适合于一次取出一个行数据,而对于日常应用系统来说这种方式是非常合适的,因为我们设计应用的时候都是针对一个事务,而我们会把一个事务所有属性存储成一行,使用的时候也是有很大的概率涉及到整行的信息,很利于做缓存。还比如我们经常使用的那些经典sql 语句:

    select * from user where id = 1001;
    select id, user_name, email, address, gender, ... from user where id = 1001;
    • 1
    • 2

    !!还敢不敢列出些更多的字段!!

    列式储存模型

    而列储存就是下图这种按列优先储存。为了方便我们每块只储存了一个一列,没有存满。 
    这里写图片描述
    这下我们再考虑上面的查找所有Jones的工资,这下我们只扫描第三个磁盘块,找出Jones都再那些行,然后根据查出来的行号,直接去第五块磁盘(这块对应的式salary列)找出第二、四行的数据,然后输出。一共2次seek。大大小于row-oriented的4次。

    这种查询方式的前提就是你就需要这列数据就行了,其前提假设就是查询基本不会使用这个行的其他列数据。显然这种假设对于日常操作系统的围绕着一个主题进行的活动是不合适旳。但是却在分析型数据大显身手。

    列式的另一大优势是压缩。因为列的天然凝聚性(比如上面的两个Jones就可以压缩成一个)大大强与行,所以列式储存可以有很高的压缩比,这个进一步使使用的磁盘的数量减少,因为使用的磁盘块少,进一步减少了需要扫描的次数。这方面很利于加快查找速度,但是因为解压缩也是耗时耗内存的过程,所以压缩的控制也是需要一个定平衡点。

    优劣总结

    从上面的例子可以明显看出列式数据库在分析需求(获取特点——每次查询几个维度,通常是)时候,不仅搜索时间效率占优势,其空间效率也是很明显的。特别是针对动辄按T计算的数据量来说,在分布式环境中能进行压缩处理能节省宝贵的内部带宽,从而提高整个计算任务性能。


    展开全文
  • 上一篇内容我们介绍了openGauss存储技术(一)——行存储引擎,本文重点介绍openGauss列存储引擎和内存引擎。 openGauss列存储引擎 传统行存储数据压缩率低,必须按行读取,即使读取一列也必须读取整行。在分析性的...

    上一篇内容我们介绍了openGauss存储技术(一)——行存储引擎,本文重点介绍openGauss列存储引擎和内存引擎。

    openGauss列存储引擎

    传统行存储数据压缩率低,必须按行读取,即使读取一列也必须读取整行。在分析性的作业以及业务负载的情况下,数据库往往会遇到针对大量表的复杂查询,而这种复杂查询中往往仅涉及一个较宽(表列数较多)的表中个别列。此类场景下,行存储以行作为操作单位,会引入与业务目标数据无关的数据列的读取与缓存,造成了大量IO 的浪费,性能较差。因此openGauss提供了列存储引擎的相关功能。创建表的时候,可以指定行存储还是列存储。

    总体来说,列存储有以下优势:

    • 列的数据特征比较相似,适合压缩,压缩比很高,在数据量较大(如数据仓库) 场景下会节省大量磁盘空间,同时也会提高单位作业下的IO 效率。
    • 当表中列数比较多,但是访问的列数比较少时,列存储可以按需读取列数据,大大减少不必要的读IO,提高查询性能。
    • 基于列批量数据向量运算,结合向量化执行引擎,CPU 的缓存命中率比较高,性能比较好,更适合 OLAP大数据统计分析的场景。
    • 列存储表同样支持 DML操作和 MVCC,功能完备,且在使用角度上做了良好的兼容,基本是对用户透明的,方便使用。

    (一)列存储引擎的总体架构

    列存储引擎的存储基本单位是 CU(Compression Unit,压缩单元),即表中一列的一部分数据组成的压缩数据块。行存储引擎中是以行作为单位来管理,而当使用列存储时,整个表整体按照不同列划分为若干个 CU,划分方式如图1所示。

    在这里插入图片描述

    图1 CU 划分方式

    如图1所示,假设以6万行作为一个单位,则一个12万行、4列宽的表被划分为8个 CU,每个 CU 对应一个列上的6万个列数据。图中有列0、列1、列2、列3四列,数据按照行切分了两个行组(Row Group),每个行组有固定的行数。针对每个行组按照列做数据压缩,形成 CU。每个行组内部各个列的 CU 的行边界是完全对齐的。当然,大部分时候,CU 在经过压缩后,因为数据特征与压缩率的不同,文件大小会完全不同,如图2所示。
    在这里插入图片描述

    图2 示意图

    为了管理表对应的CU,与执行器层进行对接来提供各种功能,列存储引擎使用了CUDesc(压缩单元描述符)表来记录一个列存储表中CU 对应的元信息,如图3所示。
    在这里插入图片描述

    图3 列存储引擎整体架构图

    注:Cmn表示第 m 列的、CUid是n(第n个)的压缩单元。每个 CU 对应一个 CUDesc的记录,在 CUDesc里记录了整个 CU 的事务时间戳信息、CU 的大小、存储位置、magic校验码、min/max等信息。

    与此同时,每张列存储表还配有一张 Delta表,Delta表自身为行存储表。当有少量的数据插入到一张列存储表时,数据会被暂时放入 Delta表,等到到达阈值或满足一定条件或操作时再行整合为 CU 文件。Delta表可以帮助避免单点数据操作带来的加重的 CU 操作与开销。

    设计采用级别的多版本并发控制,删除通过引入虚拟列映射 (Virtual Column Bitmap)来标记删除。映射(Bitmap)是多版本的。

    (二)列存储的页面组织结构

    上文讲到了CUDesc表及其用来记录元信息的目的。CUDesc的典型结构如图4所示。

    在这里插入图片描述

    图4 CUDesc的典型结构

    其中:

    • _rowTupleHeader为传统行存储记录的行头,其中包含了前面提到过的事务及位置信息等,用来进行可见性判断等。
    • cu_mode实际为此 CUDesc对应 CU 的infomask,记录了一些 CU 的特征信息(比如是否为 Full,是否有 NULL等)。
    • magic是 CUDesc与 CU 文件之间校验的关键信息。
    • min/max(最小值/最大值)为稀疏索引,后续会进一步展开介绍。
      CU 文件结构如图5所示。

    在这里插入图片描述

    图5 文件结构

    列存储在 CUDesc表的存储信息基础上设计了一套与上层交互的操作 API。除了上面列存储的页面组织结构以及文件管理中天然可以展示出的结构机制之外,列存储还有如下一些关键的技术特征:

    • 列存储的 CU 中数据的删除,实际上是标记的删除。删除操作,相当于更新了CUDesc表中CU 对应CUDesc记录的删除位图(delete bitmap)结构,标记列中某行对应数据已被删除,而CU 文件数据不会被更改。这样可以避免删除操作带来大量的IO开销及压缩、解压的高额 CPU 开销。这样的设计,也可以使得对于同一个 CU 的查询(select)和删除(delete)互不阻塞,提升并发能力。
    • 列存储CU 中数据更新,则是遵循仅允许追加(append-only)原则的,即CU 文件仅会向后进行延展扩充,抑或是启用新的 CU 文件,而不是就对应行在 CU 中的位置就地更新。
    • 由于 CU 以及 CUDesc的元数据管理模式,原有系统中的 Vacuum 机制实际上并不会非常有效地清除 CU 中已经失效的存储空间,因为 LazyVacuum(清理数据时,只是标识无用行的状态,使得空间可以复用,不会影响对表数据的操作)仅能在CUDesc级别进行操作,在多数场景下无法对 CU文件本身进行清理。列存储内部如果要对列存储数据表进行清理,需要执行 VacuumFull(除了清理无用行,还会合并数据块,整个过程会锁定表)操作。

    (三)列存储的 MVCC设计

    理解了 CU、CUDesc的基本结构,以及 CUDesc的管理,或者说是其“代理”角色,列存储的 MVCC设计以及管理,实际上就非常好理解了。

    由于列存储的操作基本单位 CU 是由 CUDesc表中的行进行管理的,因此列存储表的CU 可见性判断也是由CUDesc的行头信息,按照传统的行存储可见性进行判断的。

    同样的,列存储可见性的单位也是CU 级别(CUDesc),不同于行存储的 Tuple级别。

    列存储表的并发控制是 CU 文件级别的,实际上也等同于其 CUDesc代理表的CUDesc行之间的并发控制。多个事务之间在一个 CU 上的并发管控,实际上取决于其在对应的 CUDesc记录上是否冲突。例如:

    • 两个事务并发去读一个CU 是可行的,两个事务都可以拿到此CU 对应 CUDesc 行级别的共享锁(sharelock)。
    • 两个事务并发去更新一个 CU,会因为在 CUDesc上的锁冲突而触发一个事务回滚[当然,如果是读已提交(read committed)隔离级别并打开允许并发更新的开关,这里会做的事情是拿到此 CUDesc最新版 本 的 ctid,然后重运行一部分查询树 (queryTree)来进行更新操作。此部分内容,后面文章将会介绍]。
    • 两个事务并行执行,一个事务对一个 CU 执行了删除操作并先行提交,则另一个事务在可重读(repeatableread)的隔离级别下,其获取的快照只能看到这个CUDesc在操作发生前的版本,这个版本的 CUDesc中的删除位图(delete_bitmap)对应数据没有被标记删除,也由于 CU 的行删除是标记删除的机制,因此数据在原有 CU 的数据文件中依旧可用,此事务依旧可以在其对应的快照下读到对应行。

    删除 CU 中部分数据所进行的实际操作如图6所示。

    图6 删除 CU 中部分数据所进行的实际操作

    从上面的几个例子可以看出,列存储对于更新的仅允许追加策略以及对于删除操作的标记删除方式,对于列存储事务 ACID的支持,是至关重要的。

    (四)列存储的索引设计

    列存储支持的索引设计有:

    • B树索引;
    • 稀疏索引;
    • 聚簇索引。

    1.列存储的B树索引

    列存储引擎在 B树索引的支持角度,与传统的行存储引擎无本质差别。对于一般用于应对大数据批量分析性负载的列存储引擎来说,B树索引有助于帮助列存储大大提升自身的点查效率,更好地适应混合负载。

    行存储相关 B树索引的索引页面上,存储的是key→ctid(键→行号)的映射,在列存储的场景下,这个映射依旧为key→ctid,但列存储的结构并不能像行存储一样,通过ctid中的块号(block number)和偏移量(offset)直接找到此行数据在数据文件页面中的位置。列存储ctid中记录的是(cu_id,offset),要通过 CUDesc结构来进行查找。

    在基于 B树索引的扫描中,从索引中拿到ctid后,需要在对应的 CUDesc表中,根据 CUDesc在cu_id列的索引找到对应的 CUDesc记录,并由此打开对应的 CU 文件,根据偏移量找到数据。

    如果此操作设计大量的存储层性能开销,因此列存储的 B树索引,与列存储的其他操作一样,统一都为批量操作,会根据 B树索引找到ctid的集合,然后对此集合进行排序,再批量地对排序后的ctid进行 CU 文件级别的查找与操作。这样可以做到顺序单调地进行索引遍历,大大减少了反复操作文件带来的 CPU 以及IO 开销。

    2.列存储的稀疏索引

    列存储引擎每个列自带 min/max稀疏索引,每个CUDesc存储该CU 的最小值和最大值。

    那么在查询的时候,可以根据查询条件做简单的 min/max判断,如果查询条件不在(min,max)范围内,肯定不需要读取这个 CU,可以大大地减少IO 读取的开销,稀疏索引如图7所示。

    在这里插入图片描述

    图7 稀疏索引

    注:txn_info表示事务信息;CUPtr表示压缩单元的指针;CU-None表示肯定不命中;CU-Some表示可能有数据匹配;CU_Full表示压缩单元数据全命中。

    3.列存储的聚簇索引

    列存储表在建立时可以选择在列上建立聚簇索引(partial sort index)。

    如果业务的初始数据模型较为离散,那么稀疏索引在不同 CU 之间的 min、max会有大量交集,这种情况下在给定谓词对列存储表进行检索的过程中,会出现大量的CU 误读取,甚至可能导致其查询效率与全表扫描近似。如图8所示,查询2基本命中了所有 CU,min/max索引没有能够有效筛选。

    在这里插入图片描述

    图8数据模型较为离散时的查询效果图

    聚簇索引可以对部分区间内的数据做相应的排序(一般区间会包含多个CU所覆盖的行数),可以保证 CU 之前交集尽量少,可以极大地提升在数据离散场景下稀疏索引的效率。

    其示意图如图9和图10所示。
    在这里插入图片描述

    图9 聚簇索引生效前

    在这里插入图片描述

    图10 聚簇索引生效后

    同时,聚簇索引会使得 CU 内部的数据临近有序,提升 CU 文件本身的压缩比以及压缩效率。

    (五)列存储自适应压缩

    每个列自适应选择压缩,支持差分编码(delta value encoding)、游 程 编 码 (Run length encoding)、字典编码(dictionary encoding)、LZ4、zlib等混合压缩。根据数据特性的不同,压缩比一般可以有3X~20X。

    列存储引擎支持低、中、高三种压缩级别,用户在创建表的时候可以指定压缩级别。

    导入1TB原始数据量,分别测试低、中、高三种压缩级别,入库后数据大小分别是100GB、73GB、61GB,如图11所示。

    在这里插入图片描述

    图11 压缩比示意图

    每次数据导入,首先对每列的数据按照向量组装,对前几批数据做采样压缩,根据数值类型和字符串类型,选择尝试不同的压缩算法。一旦采样压缩完成后,接下来的数据就选择优选的压缩算法了。如图12所示,面向列的自适应压缩主要分为数值压缩和字符压缩。其中对 Numeric小数类型,会转换为整数后,再进行数值压缩。对数值型字符串,也会尝试转换为整数再进行数值压缩。

    在这里插入图片描述

    图12 面向列的自适应压缩

    (六)列存储的持久化设计

    在列存储的组织结构与 MVCC机制的介绍中提到,列存储的存储单位由 CUDesc和CU文件共同组成,其中 CUDesc记录了CU相关的元信息,控制其可见性,实际上充当了一个 “代 理”的角色。但是CUDesc和CU,实质上还是分离的文件状态。CUDesc表本质上还是行存储表,其持久化流程遵从行存储的共享缓冲区脏页与 Redo日志的持久化流程,在事务提交前,CUDesc的改动会被记录在 Redo日志中进行持久化。单个 CU 文件本身,由于含有大量的数据,使用正常的事务日志进行持久化需要消耗大量的事务日志,引入非常大的性能开销,并且恢复也十分缓慢。因此根据其应用场景,仅允许追加(append-only)的属性及与 CUDesc的对应关系,列存储的 CU 文件,为了确保 CUDesc和 CU 持久化状态的一致,在事务提交、CUDesc对应事务日志持久化前,会先行强制刷盘(Fsync),来确保事务改动的持久化。

    由于数据库主备实例的同步也依赖事务日志,而 CU 文件并不包含在事务日志内,因此在与列存储同步时,主备实例之间除去正常的日志通道外,还有连接的数据通道,用于传输列存储文件。CUDesc的改动会通过日志进行同步,而 CU 文件则会被直接通过数据通道传输到备机实例,并通过 BCM(bitchangemap)文件来记录主备实例之间文件的同步状态。

    openGauss内存引擎

    内存引擎作为在openGauss中与传统基于磁盘的行存储、列存储并存的一种高性能存储引擎,基于全内存态数据存储,为openGauss提供了高吞吐的实时数据处理分析能力及极低的事务处理时延,在不同业务负载场景下可以达到其他引擎事务处理能力的3~10倍。

    内存引擎之所以有较强的事务处理能力,并不单是因为其基于内存而非磁盘所带来的性能提升,而更多是因为其全面地利用了内存中可以实现的无锁化的数据及索引结构、高效的数据管控、基于 NUMA 架构的内存管控、优化的数据处理算法及事务管理机制。

    值得一提的是,虽然是全内存态存储,但是并不代表着内存引擎中的处理数据会因为系统故障而丢失。相反,内存引擎有着与openGauss的原有机制相兼容的并行持久化、检查点能力,使得内存引擎有着与其他存储引擎相同的容灾能力以及主备副本带来的高可靠能力。

    内存引擎总体架构如图13所示。

    在这里插入图片描述

    图13 内存引擎总体架构图

    可以看到,内存引擎通过原有的 FDW(Foreign Data Wrapper,外部数据封装器) 扩展能力与 openGauss 的优化执行流程相交互,通过事务机制的回调以及与 openGauss相兼容的 WAL机制,保证了与其他存储引擎在这一体系架构内的共存,保 证了整体对外的一致表现;同时通过维护内部的内存管理结构、无锁化索引、乐观事务 机制来为系统提供极致的事务吞吐能力。

    以下将逐步展开讲解相关关键技术点与设计。

    (一)内存引擎的兼容性设计

    由于数据形态的不同以及底层事务机制的差别,此处如何与一个以段页式为基础的系统对接是内存引擎存在于openGauss中的重点问题之一。

    此处openGauss原有的 FDW 机制为内存引擎提供了一个很好的对接接口,优化器可以通过 FDW 来获取内存引擎内部的元信息,内存引擎的内存计算处理机制可以直接通过 FDW 的执行器接口算子实现直接调起,并通过相同的结构将结果以符合执行器预期的方式[比如扫描(Scan)操作的流水线(pipelining)]将结果反馈回执行器进行进一步处理[如排序、分组(Groupby)]后返回给客户端应用。

    与此同时内存引擎自身的错误处理机制(ErrorHandling),也可以通过与FDW的交互,提交给上次的系统,以此同步触发上层逻辑的相应错误处理(如回滚事务、线程退出等)。

    内存引擎借助 FDW 的方式接近无缝地工作在整个系统架构下,与以磁盘为基础的行、列存储引擎实现共存。

    在内存引擎中创建表(CreateTable)的实际操作流程如图14所示。

    在这里插入图片描述

    图14 内存引擎创建表的操作流程图

    从图中可以看到,FDW 充当了一个整体交互 API的作用。实现中同时扩展了FDW 的机制,使其具有更完备的交互功能,具体包括:

    • 支持 DDL接口;
    • 完整的事务生命周期对接;
    • 支持检查点操作;
    • 支持持久化 WAL;
    • 支持故障恢复(Redo);
    • 支持 Vacuum 操作。

    借由 FDW 机制,内存引擎可以作为一个与原有openGauss代码框架异构的存储引擎存在于整个体系中。

    (二)内存引擎索引

    内存引擎的索引结构以及整体的数据组织都是基于 Masstree实现的。其主体结构如图15所示。

    在这里插入图片描述

    图15 内存引擎索引主体结构

    图15很好地呈现了内存引擎索引的组织架构。主键索引(primary index)在内存引擎的一个表中是必须存在的要素,因此要求表在组织时尽量存在主键索引;如果不存在,内存引擎也会额外生成代理键(surrogatekey)用于生成主键索引。主键索引指向各个代表各个行记录的行指针(sentinel),由行指针来对行记录数据进行内存地址的记录以及引用。二级索引(secondaryindex)索引后指向一对键值,键的值(value)部分为到对应数据行指针的指针。

    Masstree作为并行 B+树(Concurrent B+tree),集成了大量 B+树的优化策略,并在此基础上做了进一步的改良和优化,其大致实现方式如图16所示。
    在这里插入图片描述

    图16 Masstree实现方式

    相比于传统的 B树,Masstree实际上是一个类似于诸多 B+树以前缀树(trie)的组织形式堆叠的基数树(radix tree)模式,以键(key)的前缀作为索引,每k 个字节形成一层 B+ 树结构,在每层中处理键中这k 个 字 节 对 应 所 需 的INSERT/LOOKUP/ UPDATE/DELETE流程。图17为k=8时情况。

    在这里插入图片描述

    图17 k等于8时的Masstree

    Masstree中的读操作使用了类 OCC(OptimisticConcurrency Control,乐观并发控制)的实现,而所有的更新(update)锁仅为本地锁。在树的结构上,每层的内部节点(interior node)和叶子节点(leaf node)都会带有版本,因此可以借助版本检查(version validation)来避免细粒度锁(fine-grained lock)的使用。

    Masstree除了无锁化(lockless)之外,最大的亮点是缓存块(cache line)的高效利用。无锁化本身在一定程度避免了 LOOKUP/INSERT/UPDATE 操作互相失效共享缓存块(invalidat ecacheline)的情况。而基于前缀(prefix)的分层,辅以合适的每层中 B+树扇出(fanout)的设置,可以最大限度地利用 CPU 预取(prefetch)的结果(尤其是在树的深度遍历过程中),减少了与 DRAM 交互所带来的额外时延。

    预取在 Masstree的 设 计 中 显 得 尤 为 关 键,尤 其 是 在 Masstree 从 根 节 点 (tree root)向叶子节点遍历,也就是树的下降过程中。此过程中的执行时延大部分由于内存

    交互的时延组成,因此预取可以有效地提高遍历(masstreetraverse)操作的执行效率以及缓存块的使用效率(命中)。

    (三)内存引擎的并发控制

    内存引擎的并发控制机制采用 OCC,在操作数据冲突少的场景下,并发性能很好。

    内存引擎的事务周期及并发管控组件结构,如图18所示。

    在这里插入图片描述

    图18 内存引擎的事务周期及并发管控组件结构

    这里需要解释一下,内存引擎的数据组织为什么整体是一个接近无锁化的设计。

    除去以上提到的 Masstree本身的无锁化机制外,内存引擎的流程机制也进一步最小化了并发冲突的存在。

    每个工作线程会将事务处理过程中所有需要读取的记录,复制一份至本地内存,保存在读数据集(read set)中,并在事务的全程基于这些本地数据进行相应计算。相应的运算结果保存在工作线程本地的写数据集(writeset)中。直至事务运行完毕,工作线程会进入尝试提交流程,对读数据集和写数据集进行检查验证(validate)操作并在允许的情况下对写数据集中数据对应的全局版本进行更新。

    这样的流程,是把事务流程中对于全局版本的影响缩小到检查验证的过程,而在事务进行其他任何操作的过程中都不会影响到其他的并发事务,并且在仅有的检查验证过程中,所需要的也并不是传统意义上的锁,而仅是记录头部信息中的代表锁的数位(lock bit)。相应的这些考虑,都是为了最小化并发中可能出现的资源争抢以及冲突,并更有效地使用 CPU 缓存。

    同时读数据集和写数据集的存在可以良好地支持各个隔离级别,不同隔离级别可以通过在检查验证阶段对读数据集和写数据集进行不同的审查机制来获得。通过检查两个数据集(set)中行记录在全局版本中对应的锁定位(lock bit)以及行头中的TID结构,可以判断自己的读、写与其他事务的冲突情况,进而判断自己在不同隔离级别下是否可以提交(commit)或是终止(abort)。同时由于 Masstree的 Trie节点(node)中存在版本记录,Masstree的结构性改动(insert/delete,插入/删 除)操作会更改相关Trie节点上面的版本号。因此维护一个范围查询(Range query)涉及的节点集(node set),并在检查验证(validation)阶段对其进行对比校验,可以比较容易地在事务提交阶段检查此范围查询所涉及的子集是否有过变化,从而能够检测到幻读(Phantom)的存在,这是一个时间复杂度很低的操作。

    (四) 内存引擎的内存管控

    由于内存引擎的数据是全内存态的,因此可以按照记录来组织数据,不需要遵从页面的数据组织形式,从而从数据操作的冲突粒度这一点上有着很大优势。摆脱了段页式的限制,不再需要共享缓存区进行缓存以及与磁盘间的交互淘汰,设计上不需要考虑IO 以及磁盘性能的优化[比如索引 B+树的高度以及 HDD(HardDiskDrive,磁盘)对应的随机读写问题],数据读取和运算就可以进行大量的优化和并发改良。

    由于是全内存的数据形态,内存资源的管控就显得尤为重要,内存分配机制及实现会在很大程度上影响内存引擎的计算吞吐能力。内存引擎的内存管理主要分为3 层,如图19所示。

    图19 内存引擎的内存管理示意图

    下面分别对3层设计进行介绍:

    • 第一层为应用消费者层,为内存引擎自身,包含了临时的内存使用以及长期的内存使用(数据存储)。
    • 第二层为应用对象资源池层,主要负责为第一层对象,如表、索引、行记录、键值以及行指针提供内存。该层从底层索取大块内存,再进行细粒度的分配。
    • 第三层为内存管理层,主要负责与操作系统之间的交互及实际的内存申请。为降低内存申请的调用开销,交互单位一般在2MB 左右。此层同时也有内存预取和预占用的功能。

    第三层实际上是非常重要的,主要因为:

    • 内存预取可以非常有效地降低内存分配开销,提高吞吐量。
    • 与 NUMA 库进行交互的性能成本非常高,如果直接放在交互层会对性能产生很大影响。

    内存引擎对短期与长期的内存使用针对 NUMA 结构适配的角度也是不同的。短期使用,一般为事务或会话(session)本身,那么此时一般需要在处理该会话的 CPU 核对应的 NUMA 节点上获取本地内存,使得交易(transaction)本身的内存使用有着较小的开销;而长期的内存使用,如表、索引、记录的存储,则需要用到 NUMA 概念中类似全局分布(interleaved)内存,并且要尽量将其平均分配在各个 NUMA 节点上,以防止单个 NUMA 节点内存消耗过多所带来的性能下降。

    短期的内存使用,也就是 NUMA 角度的本地内存,也有一个很重要的特性,就是这部分内存仅供本事务自身使用(比如复制的读取数据及做出的更新数据),因此也就避免了这部分内存上的并发管控。

    (五)内存引擎的持久化

    内存引擎基于同步的 WAL机制以及检查点来保证数据的持久化,并且此处通过兼容openGauss的 WAL机制(即 Transaction log,事务日志),在数据持久化的同时,也可以保证数据能够在主备节点之间进行同步,从而提供 RPO=0的高可靠以及较小RTO 的高可用能力。

    内存引擎的持久化机制如图20所示。

    在这里插入图片描述

    图20 内存引擎的持久化机制

    可以看到,openGauss的 Xlog模块被内存引擎对应的管理器(manager)所调用,持久化日志通过 WAL的写线程(刷新磁盘线程)写至磁盘,同时被 wal_sender(事务日志发送线程)调起发往备机,并在备机 wal_receiver(事务日志接收线程)处接收、落盘与恢复。

    内存引擎的检查点也是根据 openGauss自身的检查点机制被调起。openGauss中的检查点机制是通过在做检查点时进行shared_buffer(共享缓冲区)中脏页的刷盘,以及一条特殊检查点日志来实现的。内存引擎由于是全内存存储,没有脏页的概念,因此实现了基于 CALC的检查点机制。

    这里主要涉及一个部分多版本(partial multi-versioning)的概念:当一个检查点指令被下发时,使用两个版本来追踪一个记录:活跃(live)版本,也就是该记录的最新版本;稳定(stable)版本,也就是在检查点被下发且形成虚拟一致性点时此记录对应的版本。在一致性点之前提交的事务需要更新活跃和稳定两个版本,而在一致性点之后的事务仅更新活跃版本,保持稳定版本不变。在无检查点状态的时候,实际上稳定版本是空的,代表稳定与活跃版本在此时实际上其值是相同的;仅有在检查点过程中,在一致性点后有事务对记录进行更新时,才需要根据双版本来保证检查点与其他正常事务流程的并行运作。

    CALC(CheckpointingAsynchronously using Logical Consistency,逻辑一致性异步检查点)的实现有下面5个阶段:

    • 休息(rest)阶段:这个阶段内,没有检查点的流程,每个记录仅存储活跃版本。
    • 准备(prepare)阶段:整个系统触发检查点后,会马上进入这个阶段。在这个阶段中事务对读写的更改,也会更新活跃版本;但是在更新前,如果稳定版本不存在,那么在更新活跃版本前,活跃版本的数据会被存入稳定版本。在此事务的更新结束,在放锁前,会进行检查:
      如果此时系统仍然处于准备阶段,那么刚刚生成的稳定版本可以被移除;反之,如果整个系统已经脱离准备阶段进入下一阶段,那么稳定版本就会被保留下来。
    • 解析(resolve)阶段:在进入准备阶段前发生的所有事务都已提交或回滚后,系统就会进入解析阶段,进入这个阶段也就代表着一个虚拟一致性点已经产生,在此阶段前提交的事务相关的改动都会被反映到此次检查点中。
    • 捕获(capture)阶段:在准备阶段所有事务都结束后,系统就会进入捕获阶段。此时后台线程会开始将检查点对应的版本(如果没有稳定版本的记录即则为活跃版本)写入磁盘,并删除稳定版本。
    • 完成(complete)阶段:在检查点写入过程结束后,并且捕获阶段中进行的所有事务都结束后,系统进入完成阶段,系统事务的写操作的表现会恢复和休息阶段相同的默认状态。

    CALC有着以下优点:

    • 低内存消耗:每个记录至多在检查点时形成两份数据。在检查点进行中如果该记录稳定版本和活跃版本相同,或在没有检查点的情况下,内存中只会有数据自身的物理存储。
    • 较低的实现代价:相对其他内存库检查点机制,对整个系统的影响较小。
    • 使用虚拟一致性点:不需要阻断整个数据库的业务以及处理流程来达到物理一致性点,而是通过部分多版本来达到一个虚拟一致性点。

    小结

    openGauss的整个系统设计是可插拔、自组装的,openGauss通过支持多个存储引擎来满足不同场景的业务诉求,目前支持行存储引擎、列存储引擎和内存引擎。其中面向 OLTP不同的时延要求,需要的存储引擎技术是不同的。例如在银行的风控场景里,对时延的要求是非常苛刻的,传统的行存储引擎的时 延很难满足业务要求。openGauss除了支持传统行存储引擎外,还支持内存引擎。在 OLAP(联机分析处理) 上openGauss提供了列存储引擎,有极高的压缩比和计算效率。另外一个事务里可以同时包含三种引擎的 DML操作,且可以保证 ACID特性。
    在这里插入图片描述

    Gauss松鼠会是汇集数据库爱好者和关注者的大本营,
    大家共同学习、探索、分享数据库前沿知识和技术,
    互助解决问题,共建数据库技术交流圈。
    展开全文
  • 1 为什么要按列存储列式存储(Columnar or column-based)是相对于传统关系型数据库的行式存储(Row-basedstorage)来说的。简单来说两者的区别就是如何组织表(翻译不好,直接抄原文了):Ø Row-based storage stores a...
  • 存储和行式存储的真正区别

    千次阅读 2019-05-04 11:07:42
    1 存储和行式存储的真正区别 参考文献: 存储和行式存储的真正区别_过往记忆大数据_公众号文章 一般原因:查询需要的字段时,Column-Store 只需读取需要的,Row-Store读一条记录会把 所有字段都读出来。...
  • 数据库市场需要细分,行式数据库不再满足所有的需求,而有很多需求需要通过内存数据库和式数据库解决,式数据库在数据分析、海量存储、BI这三个领域有自己独到。 1. 关系型数据库(行式数据库) 关系模型使用...
  • 导读:本文带你了解面向与面向行的数据库。作者:Alex Petrov来源:大数据DT(ID:hzdashuju)大多数数据库系统存储一组数据记录,这些记录由表中的和行组成。字段是和...
  • 几张图看懂存储

    万次阅读 多人点赞 2014-11-15 09:50:18
    最近看到一篇很好资料,里面三言两语配...1 为什么要按列存储列式存储(Columnar or column-based)是相对于传统关系型数据库的行式存储(Row-basedstorage)来说的。简单来说两者的区别就是如何组织表(翻译不好,直接抄原
  • 深入分析Parquet存储格式

    千次阅读 2018-09-20 13:07:41
    Parquet是面向分析型业务的存储格式,由Twitter和Cloudera合作开发,2015年5月从Apache的孵化器里毕业成为Apache顶级项目,最新的版本是1.8.0。 存储 存储和行式存储相比有哪些优势呢? 可以跳过不...
  • 行式存储可以看成是一个行的集合,其中每一行都要求对齐,哪怕某个字段为空(下图中的左半部分),而存储则可以看成一个的集合(下图中的右半部分)。存储的优点很明显,主要有以下 4 点: 查询时可以只...
  • 导读:本文带你了解面向与面向行的数据库。作者:Alex Petrov来源:大数据DT(ID:hzdashuju)大多数数据库系统存储一组数据记录,这些记录由表中的和行组成。字段是和...
  • 在介绍HBase是不是存储数据库之前,我们先来了解一下什么是行式数据库和式数据库。 行式数据库和式数据库 在维基百科里面,对行式数据库和式数据库的定义为:式数据库是以相关存储架构进行数据存储...
  • Parquet与ORC:高性能存储格式

    万次阅读 多人点赞 2016-07-09 20:37:22
    背景随着大数据时代的到来,越来越多的数据流向了Hadoop生态圈,同时...例如Hive、Spark SQL、Impala、Presto等,同时也产生了多个高性能的存储格式,例如RCFile、ORC、Parquet等,本文主要从实现的角度上对比分析
  • ClickHouse是一个用于联机分析(OLAP)的式数据库管理系统(DBMS)。 在传统的行式数据库系统中,数据按如下顺序存储: Row WatchID JavaEnable Title GoodEvent EventTime #0 89354350662 1 Investor ...
  • HDFS存储Parquet与行式存储(Avro)Benchmark(hadoop, Spark)
  • 深入分析Parquet存储格式 作者 梁堰波 发布于 2015年8月7日 | 讨论 分享到:微博微信FacebookTwitter有道云笔记邮件分享 稍后阅读 我的阅读清单 Parquet是面向分析型业务的式...
  • Hadoop学习笔记 --- 深入理解 parquet 存储格式
  • 转行(表转窄表) from pyspark.sql import functions as F def unpivot(df, keys): #参数说明 dfdataframekeys 待转换表中需要保留的主键key,以list[]类型传入 #转换是为了避免字段类不匹配,...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 51,482
精华内容 20,592
关键字:

宽列存储