Implementation of Global Temp Table

一、工作介绍

开发特性过程以及实现特性技术细节:

第一个和Postgre SQL 相关的云产品是RDS PostgreSQL,是把主备流复制架构的PostgreSQL 放到云上,做自动售卖。

Implementation of Global Temp Table

内核层面,定期升级PostgreSQL 的大版本和小版本,根据用户需求支持各类的插件以及开发云上独有的插件。比如打通PostgreSQL 和其他云产品数据通路的插件(Oss_fdw)。


这个版本对PostgreSQL 的内核改动并不大,但和PostgreSQL 的社区交流较多。当用户提出的新的需求时,首先会从社区中寻找灵感,如果社区在某个版本中支持了该应用,则会建议用户直接使用这个版本。如果没有,将会做定制化的开发。


2018 年,开始自研了云原生数据库PolarDB,而PolarDB 也有支持PostgreSQL引擎的版本。


Implementation of Global Temp Table

该版本有两个主要的特点:

首先,它基于计算存储分离的架构,可以同时支持很多的企业级存储。在公共云上,支持PolarStore 分布式的存储,在线下也支持传统的块存储。这使得存储上的扩展性有了一个大幅的提高;另一方面,计算节点也支持多读一个架构,它可以实现扩展读的能力,使计算能力可扩展,相较于RDS PostgreSQL 有了比较大的提高。

其次,自研了很多高级的SQL 特性,使PolarDB for PostgreSQL 在企业级的应用场景里,或者传统的数据库使用场景有一个高度的Oracle 兼容性,更加易于在复杂场景中替换Oracle 应用。


SQL 功能

SQL 大致分为以下几个类型:

第一部分的工作相对简单,比如支持某类驱动,支持功能性的函数和视图,比如数学函数、字符串处理函数等。

第二部分是SQL 层的一些特性例如聚集函数,或PL/SQL 里的特性。

相对复杂的第三部分是表级的特性以及SQL 优化相关的特性。

Implementation of Global Temp Table

二、Global Temp table 的实现分享

三个部分

1. 站在Postgre SQL 的肩膀上

2. Global Temp Table 的设计

3. Global Temp Table 在社区Review


什么是全局临时表?

Implementation of Global Temp Table

用普通的表做类比,对数据库中的普通表a 而言,不同的application 连接到数据库中查看表a 的时候,能够看到表a 中的所有的数据。


而对于全局临时表而言,不同的连接到数据库中,都能看到临时表b。但是,对于临时表b 中的数据,每个连接都只能看到自己的这一份数据,看不到其他连接中的数据,那么这就是临时表的定义。


对于会话中的数据生命周期而言,可以选择绑定事物或者会话,换一种说法也就是我们可以选择这张表中的会话数据,使得在事务提交的时候被自动清理,或者是在会话退出的时候被自动清理。这就是全局临时表的定义。


全局临时表的使用场景?

一般情况下我们使用多表join 再叠加子查询的方式来实现对复杂数据场景的数据处理。这种做法业务的开发难度会随着SQL 的复杂程度的提高而急剧的提高,最终业务SQL 将转变得不可运维。


自然的,使用存储过程加临时表处理数据的方案,就成为了一个更加合适的选择。

Implementation of Global Temp Table

具体来说是把复杂SQL 中的逻辑进行拆分,拆分成存入过程中的多个步骤,进而分步骤完成,每个步骤之间,使用临时表来做数据的交换。


这样可以使它发挥出临时表的很多的优势。


比如,临时表可以使用辅助索引来做数据处理的加速,由于临时表在会话之间不共享数据,也很容易开发出并行数据处理的逻辑来进一步的加速业务,整体的开发难度不会随着业务的复杂而急剧的提高。


最后,由于临时表中的数据支持在特定的时机被自动化清理,那么开发者不用花费额外的精力去维护临时表的定义信息,维护临时表达索引信息,以及主动的清理临时表中的临时数据。


大量的用户使用Global Temp Table 处理自己的业务数据。我们调查发现,高达80%的Oracle 数据库用户使用了全局临时表特性,有超过50%的客户,使用了存储过程加临时表结合的方式来完成自己的业务逻辑。在调研Global Temp Table 的现状时发现,该特性还属于SQL 标准。几乎所有的商业数据库都支持这个特性。


在PostgreSQL 社区里面,从2007 年到2015 年都陆续有多个内核开发者,尝试实现该特性,但终究没有成功。这个特性当前放在PostgreSQL 的TODO LIST 里。


Implementation of Global Temp Table


如何支持Global Temp Table 特性?

达成一致的结论:

1. 相对于社区已经支持的临时表,我们可以叫它Local Temp Table,Global Temp Table 通过持久化表定义的方式,减少系统表的膨胀。

2. Global Temp Table 的一些状态的信息,不用存放在系统表中,可以存放在每个会话的内存机构中。


Implementation of Global Temp Table

未能达成一致的问题:

1. 对于这个特性在Global Temp Table 的每个会话中的数据,是否应该有一份为它支持的独立的统计信息?

2. 对于数据和数据的事务信息应该怎么样存储和处理?是否应该为临时表这么一种特相对特殊的表开发一种全新的更加简洁的存储方案来存放它?


这些问题都暂未达成一致意见。


Implementation of Global Temp Table

这里总结了相对于社区已经实现的临时表Local Temp Table 和我们开发的Global Temp Table 在需求层面的一些差异点。


可以看到Local Temp Table 的表定义和数据,它的生存的周期基本上是一致的,但是Global Temp Table 的表定义和本地数据则不一致,GTT 的表定义是持久化的,但是每个会话中的数据各有自己生命周期,这一点带来了一系列的问题,即是Global Temp Table 最大的实现难点。


要实现Global Time Table 需要解决4 个维度的难题:

1. 存储层面

2. 元数据层面

3. 统计信息层面

4. 事务层面


Implementation of Global Temp Table


解决难度从低到高,这4 个维度分别又可以展开细化成一些子问题,例如

1. DDL 语句的实现、锁问题、Cache 问题。

2. 统计信息和优化器中的统计信息处理问题。

3. 事务信息的处理问题和数据的可见性问题、vacuum 问题等等。


这4 个方面的问题,前两点存储层面和原数据层面社区已经有了一个比较明确的方向,实现思路都已经非常明确,但是表中数据的统计信息的处理以及事务信息的处理还没有一个明确的结论。


以四个方面具体的阐述一下。( 以下,使用GTT 来简称global temp table,用LTT来简称社区与已经支持的特性local temp table)


Implementation of Global Temp Table


设计的第一部分:存储


这部分涉及GTT 的数据存储的文件格式和用于暂存数据的Buffer d 的设计。


在这两部分的设计上大体都复用LTT,但是不同之处是在于同一张GTT,在不同的会话中都可能有一份独立的存储和Buffer,而LTT 只会有一个会话使用。


即在那些已经使用过的会话中有一份存储和Buffer,没有使用的会话中则没有,那么这就需要使用一套DDL 语句来管理每个会话中的数据。为了达到使用DDL 语句来管理GTT 数据的目的,我设计了一个全局的哈希表,放在了共享内存上,用于追踪和管理GTT 在每个会话中的存储信息。当用户使用DDL 管理GTT 或在每个会话中发生第GTT的第一次DML 时,都会维护这个全局的哈希表。有了哈希表,普通表所支持的DML 和DDL 这些语句都可以在GTT 上全部实现。


极端的情况下,在一个DB 中,我们同时管理1 万个GTT 和同时建立1 万个连接,这会产生1 万个会话,这样的场景最多也只会使用到12 兆的物理内存,是一个可以接受的开销。


这样的设计保证了用最低的资源实现了GTT 的整套DDL。


而为了维护GTT 这一份元数据和多个会话中的数据的一致性,增加了一个限制,也就是说当多个会话都在使用这张GTT 的时候,那么它无法对GTT 做Drop Table 和Alter Table。


设计的第二部分:元数据的管理


如何给一个GTT 创建索引?


假设会话1 在GTT A 上正在创建一个索引,而其他的会话也可能同时使用表A,如果会话1 完成了本地索引的创建并把索引的元数据更新到系统表中,那么其他的会话也能马上看到A 上的索引。


在会话3,这时候会话3 的GTT 有的一份独立的数据,但这个数据并没有被新建的索引所维护,那么我们应该在触发索引扫描的语句中去新Build 索引A 吗?这样DML 中Build 索引会存在问题吗?


Implementation of Global Temp Table

事实上对于这一个问题,我觉得是有问题的。于是在第一版的设计中就禁止了在多个会话同时使用到表A 的时候,为表A 创建索引。


最新的设计:在会话1 中,当表A 的索引完成创建后,索引信息更新到catalog 中,其他的会话会看到索引信息,这时候其他会话会根据自己当前的情况,选择是否立即使用A 上的索引。


举例说明

1. 如果会话2 中表A 没有存放数据,这时激活这个表上的索引是安全的,在这之后表A又来了新的数据,索引是被维护起来的,那么索引是有效的和可用的。

2. 如果会话3 中表A 已经有数据了,不可能在一个查询语句或DML 语句触发Build 一个索引这样的动作,这是就要失效会话3 中的索引,当会话3 中的GTT 的数据被清理过后,即就是触发了on commit 子句,在事物提交的时,索引又重新被启用了,再来新的数据,也能及时被索引记录,索也能用于GTT 的查询加速。


这个设计相对就是简单粗暴的禁止掉索引创建,是一个比较完善的设计。


设计的第三个部分:统计信息


由于每个会话中数据的彼此独立,数据的存取又使用了会话内的local buffer,这样的设计使得没有一个会话有能力看到同样一张GTT 表的全局数据。同时我们知道PostgreSQL 的查询使用代价模型,好的查询性能基于准确的数据统计信息,这就使得GTT 需要为每个会话内的数据提供一份的独立的数据统计信息。


同时这些本地数据的统计信息不需要放到全局的系统表中。


Implementation of Global Temp Table

基于这个设计思路,我们设计了一个会话级的本地哈希表,用于保存这些GTT 表的统计信息,提供给本地会话内的优化器使用,帮助优化器做GTT 的查询优化,这就使得GTT 相关的SQL 有了一个优秀的查询执行路径,保证了GTT 相关SQL 的高性能。使GTT 的查询性能和普通的表抹平了差异。


设计的第四个部分:GTT 事物信息的处理

按照我的计划,GTT 和普通表都将使用相同的存储设计,不需要实现一个新的存储的方式。我们知道PostgreSQL 的传统行存数据的设计,每一行数据的头部都保存了数据的事务信息。


每个事务号,即XID 占用32Bit 存储空间,XID 是一类系统资源,一个实例中只能同时存在2 的32 次方减一个XID。


Implementation of Global Temp Table


事务资源的回收通过Vacuum 机制完成,这是PostgreSQL 独有的机制。


基于前面讲到的存储部分的设计,GTT 在每一个会话中的数据是独立的,它们自己独立的事务信息,自然需要被维护起来。


数据的可见性,依赖数据内的事务信息,对于具体某个事务的状态(也就是这个事务是否提交或回滚),存储在CLOG(commit log)中。


如果事务状态信息,也就是CLOG,一旦被Vacuum,GTT 在会话中的数据,由于丢失了事务状态信息,这份数据则不用了,也等于是造成了数据的丢失。


那么,我们将会话中数据的事务信息(这份数据最老的XID relfrozenxid)也同样保存在本地的哈希表中,同时维护会话级的GTT 的Oldest relfrozenxid,并且把它放到全局的共享区中。


这样Vacuum 在清理CLog 时,就有机会考虑到GTT 的事务信息了。我们用这个设计保证GTT 数据的完整性。


另一个方面,从全局的角度也能实时看到某个会话中GTT 中的数据是否包含了老旧的事务信息,需要被清理,也利于做事务信息的全局管理和清理。

上一篇:开放下载 |《深入MySQL实战》快速理解MySQL核心技术


下一篇:35岁后程序员该何去何从?