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