oracle 12c 列式存储 ( In Memory 理论)
随着Oracle 12c推出了in memory组件,使得Oracle数据库具有了双模式数据存放方式,从而能够实现对混合类型应用的支持:传统的以行形式保存的数据满足OLTP应用;列形式保存的数据满足以查询为主的OLAP应用。in memory组件可以和其他数据库组件功能使用,并不需要用户单独开发或者修改应用程序,就可以非常方便的实现基于实时数据库分析的转变。本文会介绍in memory组件的一些相关知识,包含了以下的内容:
-列式存储的基本知识
-访问in memory area中的数据
-In memory和RAC的融合
1.列式存储的基本知识。
1.1内存结构
传统的数据库采用的是行式存储,当一个事务发生时,oracle会对一行(或多行)数据进行操作,也就是说数据的操作单位是一行数据,即使可能需要被访问的数据只是其中的几个列,这种数据保存方式对以DML为主的OLTP应用是非常适合,也是非常高效的。但是在OLAP系统当中,针对大量数据的查询操作是占绝对地位的,而这些查询往往只针对表中一些特定的列。另外,数据的改变都是以数据装载的方式发生的,也就是说数据被装载到数据库后是极少发生改变的,毫无疑问以列的方式组织数据无疑是更好的选择。正是因为这两种存放数据的方式各有利弊,无论以哪一种方式来保存数据都无法很好的满足混合式应用的数据库系统的要求,Oracle推出了所谓的双模式数据存放方式:在磁盘(也就是数据文件)和database buffer cache中以行的形式存放数据;单独开辟一块内存空间(in memory area),其中以列的方式保存数据,满足OLAP类型的查询需求。而Oracle之所以选择单独开辟一块内存来保存列模式数据的主要原因之一就是OLAP的应用是以查询为主的,而且数据改变的发生方式绝大部分都是以数据加载的方式发生的,这意味着oracle完全也通过批量数据加载的方式来完成in memory area空间中的数据加载从而保证数据的实时性。接下来,从in memory area内存结构,数据加载过程两个方面来介绍in memory组件的一些基本知识。
首先,in memory area是独立于传统的SGA和PGA的单独的内存空间,由1Mpool和64Kpool两部分构成。其中1M pool用于保存列格式的数据,IMCU(in memoryCompressionUnit)是基本的存储单位;64Kpool用于保存和IMCU相对的元数据信息,SMU(SnapshotMetadataUnit)是这部分内存的基本单位。读者可以通过下面的查询了解相关的信息。
IMCU是用于在内存中保存列格式数据的基本存储单位,oracle会尽量保证每个IMCU的大小为1M,每个IMCU由图1所示的两部分构成
SMU部分主要用于保存IMCU的原数据信息,例如:IMCU对应的指针,IMCU包含的extent范围,DBA范围,Journaltable的指针,Rowid位图等。
1.2数据加载(populate)
在了解了in memory如何在内存中保存数据之后,再来看一下数据是如何被加载到内存中的。根据之前内容的介绍,数据在数据文件中是以行格式来保存的,那么就需要一种机制来把数据加载到in memory area当中,并且在加载过程当中完成从行模式到列模式的转变。
首先,oracle支持对表,分区或表空间指定in memory属性,也就是说in memory属性是针对物理数据库对象的,而不是逻辑数据库对象的。例如:我们可以使用下面的语句来为数据库对象指定in memory属性:
SQL>alter table sales inmemory no memcompress priority critical;
SQL>ALTER TABLESPACE ts_data INMEMORY;
SQL>ALTER TABLE sales MODIFY PARTITION SALES_201501 INMEMORY;
需要说明的是,由于in memory组件主要是针对OLAP应用的,而这种应用绝大部分的操作都是查询,而且很多时候只关心表中特定的一个或多个列,所以in memory特性还可以指定只把表中的特定的一个或多个列加载到in memory area当中。
由于in memory area区域的大小是有限的,主机的内存资源也是有限的,而数据库的容量往往会超过已有的内存资源,所以Oracle建议将性能要求很高的表装载到in memory area当中,而将性能要求比较低的表保存到闪存或者磁盘上。当然,如果内存资源充足,而且数据库不大,大部分的应用是以查询为主,也可以考虑将所有的表都装载到in memory区域中。另外,也正是由于资源的限制,Oracle允许用户为不同的表设置in memory加载优先级,基本的原则是优先级高的对象被首先加载到in memory区域当中,优先级低的对象需要等到高优先级的对象加载完毕之后才能够被加载。Oracle提供了5种in memory加载优先级,表1包含了每种优先级的详细信息。
另外,由于in memory主要是面向查询为主的OLAP或者决策支持系统,也就是说绝大部分的数据再被装载(Load)到数据库之后就不会再改变了,那么在加载数据的同时对数据进行压缩无疑可以节省内存空间,而且还能够提高查询的效率(主要的原因是很多被查询的列会包含大量的重复值)。所以in memory组件提供了丰富的压缩选项,允许用户在为对象指定in memory选项的同时指定压缩方法。表2列出了支持的压缩级别:
上表中的压缩比率由上至下,越来越高。以下的sql语句说明在将表salse加载到in memory area时的优先级最高,而且需要使用“memcompress for query”方式进行压缩:SQL>alter table sales inmemory memcompress for query low priority critical;
如果需要在指定压缩选项之前了解每种压缩选项能够获得的压缩比,可以使用Oracle Compression Advisor(DBMS_COMPRESSION包)来进行估算。
最后,加载过程是通过后台进程IMCO和工作进程(W00)进程来协同实现的,在数据库启动后或者一些对象的in memory选项被启用后,IMCO进程会创建出一些加载任务,并根据需要分配给若干个工作进程,每个工作进程负责一部分数据的加载工作,当所有工作进程完成了对应部分数据的加载之后,通知IMCO进程加载完成。
2.In memory的数据一致性
如果我们的数据库是只读的,那么事情就变得简单多了,因为数据就不会存在一致性问题,但是事实并非如此,对于大部分的数据库,事务处理是一直都会发生的,那么数据的一致性就需要得到保证。对于in memory组件也不例外,如果DML语句修改的数据并没有被加载到in memory区域当中,那么DML语句的修改就仅限于SGA当中;反之如果修改的数据已经被加载到了in memory区域中,那么就需要一种机制来确保数据的一致性。例如:没有被提交的数据不能被看到,而执行改变的会话应该能看到最新的数据。
Oracle 是通过journal table 的方式来确保数据的一致性的。每个IMCU都会对应一个自己的journal table, 如果DML语句修改的数据包含在IMCU当中,就在journal table 当中把修改后的数据记录下来,我们称之为private journal;当事务提交之后,再把journal table当中对应的记录标识成为shared journal。这样就可以保证查询在访问IMCU时能够获得一致的数据,而如果查询需要的数据在journal table 中也无法找到时,oracle 会自动根据IMCU中记录的Rowid 位图中的信息映射到buffer cache 当中相应的位置来找到满足查询要求的数据。图2描述了journal table和IMCU的基本关系。
然而,如果DML 语句不断发生的话,就会使journal table 中的数据越来越多,甚至出现IMCU中大部分的数据都是旧数据,而新数据都保存在journal table中的情况,这对于in memory查询的性能伤害是很大的。所以,Oracle定义了一个阀值(threshold),当IMCU中旧数据的比例达到这个阀值时就会触发重新加载的过程,也就是说,IMCO后台进程会每隔一段时间(默认2分钟)检查一次是否有IMCU 满足重新加载的条件,如果发现了满足条件的IMCU,就会通知W00工作进程对相应的IMCU进行重新加载,但是由于重新加载的成本是比较高的,而且可能会影响一些正在运行的语句,所以Oracle 会采用渐进的方式来对IMCU进行重新加载的操作,也就是每次只选择一部分满足重新加载条件的IMCU进行处理,而具体的程度可以通过INMEMORY_TRICKLE_REPOPULATE_SERVERS_PERCENT参数来进行调整。
对于事务所产生的journal table对系统产生的额外负载到底有多大,这个是很难进行量化的,因为有太多的因素会对它产生影响,例如加载时采用的压缩方法,改变的方式,应用程序访问数据的行为。但是,仍然有一些基本的原则是可以尽量减少数据改变对in memory 产生的影响的。由于数据再被加载到in memory area时是以extent 为单位的,如果对数据的改变是随机分布到表的各个extent的话,重新加载的成本就会很高,因为这意味着大量的IMCU需要被重新构建;而如果数据的改变能够集中到特定范围的extent中,或者大部分的改变都是数据插入而且使用直接路径加载的话,那么重新加载的成本就会被大大降低。另外的建议就是对尽量使用分区表来保存数据,这样有利于将数据的改变限定到特定的分区当中,而且针对这些分区不使用或者尽量使用 DML,MEMORYCOMPRESS FOR DML这些轻量级的压缩方式。
3.访问in memory area中的数据
3.1单表访问
在数据被加载到in memory区域之后就可以通过sql语句对它们进行访问了。分析型查询的一个很大的特点就是它只关心表当中特定的一些列而不是全部的列,而且这些列的值很多时候会有大量的重复值,并且作为条件的列很多时候都是常见的数据类型(例如:数值,字符串,日期),基于这些特点,Oracle的in memory组件也做了相应的设计来提高这些分析型查询语句的性能。
首先,在IMCU当中每一个列都会包含对应的字典信息和存储索引信息。在加载过程当中,工作进程会将对应的IMCU中每个列所拥有的不同值编写成一个字典,之后为该列的每一行数据指定一个keyvalue,用这个keyvalue来代替具体的值,这样做既可以节省空间也为将来查询时能够使用CPU的SIMD技术做准备。而存储索引(StorageIndex)实际上是数据仓库中常见的一种技术,他通过记录某一个列的最大值和最小值的方式能够避免访问大量不满足条件的数据。在IMCU中每个列的头信息当中都会保存这个列在对应的IMCU当中的最大值和最小值,以及他们所对应的偏移量。通过这种方法就可以在查询数据时通过对比最大和最小值的方式快速过滤掉不满足条件的数据,而且一旦数据改变影响到了存储索引中的信息,可以快速定位到对应的位置。但是需要指出的是,存储索引并不见得适用于所有的where条件(谓词)。
另外,由于数据已经被加载到了内存当中,所以绝大部分的操作都是需要通过cpu来实现的,I/O相关的操作基本不会出现了(除非被查询的表有一部分数据还没有被加载到in memory区域中来)。如何能够更加高效的利用CPU资源就成为了决定性能的一个重要因素,所以Oracle采用了SIMD技术(Single Instruction processing Multiple Data values)使CPU能在一个指令当中访问多个数据,但是由于SIMD所支持的指令是有限的,所以这也解释了为什么Oracle在构建IMCU时会为每个列都创建字典信息。图3描述了SIMD访问数据的基本概念.
在上图中,sales表被加载到了in memory area当中,而且IMCU中PROMO_ID列的头信息当中也包含了该列的字典信息,该列当中的每一行的值都已经被转换成为了keyvalue,当查询条件为PROMO_ID=9999是,就可以利用SIMD技术使CPU每次比较多行数据,从而极大地提升了查询的性能。
最后,我们可以通过在执行计划中查找“TABLE ACCESS INMEMORY FULL TEST”信息来确认是否使用了in memory选项访问表。例如:
3.2多表连接
除了针对访问表的优化,in memory组件针对表连接也进行了改进,主要的特性有:布隆过滤器和in memory聚合。
对于布隆过滤器(Bloom Filters),相信大家并不陌生。它的主要作用就是判断某一个数据是否出现在另一个集合当中,或者用于比较大数据集合之间的共同元素。Oracle从10g开始就在处理一些SQL语句中的表连接时使用布隆过滤器。如果表连接中涉及到的表都已经指定了in memory属性,并且已经加载到了in memory area当中,那么优化器会首先选择连接中的一个表(通常是较小的表),对作为链接条件的列进行一系列的hash函数,并产生一个结果位图(bitmap),之后再对另一个表的数据分批进行同样的hash函数,并和之前的结果位图进行比较,在整个过程中并不会产生I/O而且SIMD技术在比较过程中也可以被使用,所以布隆过滤器的引入使in memory在处理表连接时变得更加高效。
CBO会在制定执行计划时自动判断是否使用布隆过滤器,用户不需要手动指定。如果在执行计划中看到了以下的信息,说明布隆过滤器被使用了。
在上面的执行计划说明:
1.首先在in memory area中访问了表“TEST_SMALL”,就是执行计划中的第5步,之后构建了链接使用的过滤器(BF0000),也就是执行计划中的第4步。
2.之后在in memory area中访问了表“TEST_BIG”,就是执行计划中的第7步,之后使用了之前构建的过滤器。
3.3多表连接
在以分析型的查询语句为主的数据仓库应用当中,除了简单的表连接,还经常出现多表的链接,而且经常会包含一些聚合和分组操作,例如数据仓库应用当中的星型查询。针对这种查询,oracle提出了向量分组(VectorGroupBY)特性来提高select语句的性能。向量分组是一个两阶段的过程:
阶段1:CBO会找到查询中数据量较小的维度表(Dimension table),将满足条件的作为和庞大的事实表(Fact table)进行连接的列找出来并生成向量组(Vector Group)。之后将向量组和需要进行分组或者聚合的事实表中的列组合,形成一个多维数组和若干个临时表。
阶段2:在事实表上应用上一阶段产生的向量分组,之后向临时表当中添加需要计算分组或聚合结果的列的值。最后将这些临时表的数据应用到多维数组中,计算出最后的分组或者聚合结果。
在整个过程中向量分组的构建和向量于事实表的比较都是在内存中完成的,而且SIMD也会被使用,所以可以极大的提升这种查询的性能。当然,由于这种操作都是在内存中完成的,所以对系统的内存资源要求也是比较大的,要求运行这种查询的进程拥有足够的PGA空间。下面的执行计划说明了分组向量在查询中的应用:
根据上面的执行计划:
-首先,表”TEST_SMALL_1”和”TEST_SMALL_2”被访问,当然它们都已经被加载到了in memory area
当中。之后分组向量被构建,他们是”KV0000”和”KV0001”,而且在和需要分组的表进行结合后,临时表也被创建了出来,它们是“SYS_TEMP_0FD9D6604_116B7C6”和“SYS_TEMP_0FD9D6604_116B7C6”。
-表“TEST_BIG”被访问,之后向量分组被应用到了这个表上。然后开始向临时表当中添加分组结果。
-多维数组中的结果被生成,它是“VW_VT_F486F43F”。最后通过“HASH GROUPBY”的方式完成最后的分组。
4.in memory和RAC的融合
延续Oracle新特性的一贯特点,in memory特性也可以和已经存在的其它数据库组件兼容,例如RAC,从而实现系统的高可用性和可扩展性。由于RAC属于典型的share everything结构,它可以同时在多个节点打开相同的数据库,所以对于同一个数据库对象,它可以被加载(populate)到多个节点上去。当然,前提条件是这些节点的数据库实例都设置了in memory area(参数in memory_size不等于0)。既然数据可以被加载到多个节点,那么就意味着我们需要思考两个问题:
-问题1:如何将数据分布到多个节点。
-问题2:数据是否有必要在in memory area当中保存冗余来确保高可用性。
对于数据的分布方式,oracle提供了根据数据的ROWID范围或根据表的分区(或子分区)两种方式来将数据分布到多个节点上。第一种方法是指将表的数据按照rowid的范围划分成若干份,之后将每份数据均匀的加载到不同的节点当中去,这种分布方式比较适用于数据分布不均匀的表,而且应用程序对表的访问在每个实例上都是比较均匀的场景。例如:
ALTER TABLE test INMEMORY DISTRIBUTE BY ROWID RANGE;
第二种方式适用于分区表,oracle会根据分区的定义将每个分区加载到不同节点的in memory area当中去,这种分布方式比较适合数据分布均匀的表。如果应用程序对表的访问在每个实例上都是比较均匀的,尤其适合hash分区表。例如:ALTER TABLE lineorder INMEMORY DISTRIBUTE BY PARTITION;
对于数据是否应该在in memory area中保存冗余,如果是普通的RAC数据库,那么数据并不会在in memory area中保存冗余;而对于Exadata一体机,in memory area中的数据是可以设置冗余的。之所以选择这样做的原因在于,非Exadata一体机的RAC系统的私网配置千差万别,如果选择保存冗余的话,一旦当某一个实例down掉之后,意味着会有大量的数据需要在节点的私网之间进行传输,以便确保数据的冗余,如果私网的性能不能得到保证的时候,这种数据的传输可能消耗大量的时间和网络资源,并导致严重的后果。而Exadata一体机的私网采用光纤网络,而且使用了先进的RDS协议,数据传输可以达到几十G每秒,所以在处理由于节点故障导致的大量私网数据传输时,仍然可以保证集群的私网正常工作。
另外,由于目前硬件层面的高可用技术已经非常成熟,一个数据库实例或者节点down掉的事故大部分都是一次性的,很快就能恢复。所以Oracle在发现某一个实例或者节点fail之后并不会马上触发数据的重新分布,而是会等待一段时间以便让问题节点或实例能够重新启动并加载自己的数据,只有当等待时间超时之后,其他节点才会触发数据重新分布的过程,将失败节点的in memory area中的数据重新分布到正常节点。基于这种设计模式,建议在使用RAC系统上的in memory选项时应该为每个节点的in memory area预留出一部分空间,以便确保数据重新分配时仍然有足够的空间。
下面的图4和图5描述了Exadata环境和非Exadata环境in memory area保存数据的区别
根据上面的图形不难发现,在RAC环境下的,每个节点都不会包含表当中的所有数据。所以在RAC环境下,需要启用oracle的自动并行查询(AutoDOP)才能够使用in memory的方式访问加载到in memory area中的表。另外还要说明的是在多实例的并发查询中实例之间传输的并不是IMCU,而是每个节点都会对本节点的数据运行相同的sql语句,之后把自己的结果集发送给发起sql语句的实例,组成最终的结果返回给用户。例如:一个4节点的RAC数据库,表sales已经被加载到了in memory area当中。运行下面的查询:
select sum(stock) from sales where store_id in (100,200,300) and order_date=to_date(‘2016-01-01’, ‘yyyy-mm-dd’);
CBO首先会计算使用in memory scan的成本,如果成本最低,CBO就会选择使用在in memory area中访问sales表。接下来,oracle会访问数据字典中的信息,找到这个表被加载到了哪些实例,并在对应的节点启动相应的并发进程(parallelslave),把这个查询语句发送给并发进程运行。每个实例的并发进程运行完对应的sql语句之后,把产生的汇总值发送给发起查询的实例,生成最终的汇总值并返回给客户。在整个过程中,并不是IMCU在实例之间传递,而是汇总值在传递,所以能够避免大量的私网数据通信。
以上就是作者对oracle 12c in memory组件一些粗浅的介绍,希望对各位使用Oracle数据库进行开发的人员有所帮助,能够在使用了in memoery组件的oracle数据库上开发应用程序时有些借鉴作用。
转:http://blog.sina.com.cn/s/blog_74a7d3390102wegl.html