合 GreenPlum 数据存储介绍(分区表、堆表、AO追加优化表、分布键、分布策略)
Tags: GreenPlumAOCO表AORO表AO表分布键堆表复制表数据存储分布策略存储结构
- 数据存储
- 分区表
- 存储格式(堆表和AO表)
- 存储格式介绍
- Heap表
- AO表
- 堆表和AO表对比
- AORO表
- 建表语句
- 压缩选项
- 最佳实践
- AOCO表
- 建表语句
- 压缩选项
- BLOCKSIZE
- 物理文件
- 最佳实践
- 查询AO表
- AO表示例
- 修改表结构
- 混合存储
- 外部表
- 对比测试
- 各类型表占用空间比较
- 各级别压缩率比较
- 分布策略
- 哈希( Hash )分布
- 随机( Random )分布
- 复制表(Replicated Table)
- 表分布键选择原则
- 表分布键的约束
- AO表的膨胀说明
- Greenplum产生垃圾空间说明
- 查看表的储存类型
- 执行查看命令
- 名词解释
- AO表分析
- 3.1 查看当前数据库中有哪些AO表
- 3.1.1 查看当前数据库的所有AO表
- 3.1.2 查看制定schema下的AO表
- 3.2 查看AO表的膨胀率
- 3.2.1 执行查看命令
- 3.2.3 名词解释
- 3.3 检查系统中膨胀率超过N的AO表
- 3.3.1 执行命令
- 3.3.2 名词解释
- 3.4 查看膨胀数据的占用大小
- 3.5 查看表的行数
- 3.6 释放膨胀的空间
- 3.7 查看释放后的占用空间
- 3.7.1 释放膨胀空间
- 3.7.2 再次查看AO的膨胀率
- 3.8 再次查看表的行数
- 3.9 使用更改随机的方式释放空间
- 3.9.1 查看膨胀占用空间
- 3.9.2 随机改变表的分布键
- 3.9.3 查看释放后的空间
- 3.10 使用多分布键的形式释放空间
- 3.10.1 执行重新分布命令
- 3.10.2 查看数据的膨胀率
- AO表总结
- 4.1 查看表的行的个数
- 4.2 更新数据的行数与占用大小
- 4.2.1 更新数据
- 4.2.2 查看表的膨胀率
- AO表释放空间SHELL脚本
- 建表相关SQL
- 总结
- 参考
数据存储
分布式数据存储基本原理相对简单,实现比较容易,很多数据库中间件也可以做到基本的分布式数据存储。Greenplum 在这方面不单单做到了基本的分布式数据存储,还提供了很多更高级灵活的特性,譬如多级分区、多态存储。Greenplum 6 进一步增强了这一领域,实现了一致性哈希和复制表,并允许用户根据应用干预数据分布方法。
如下图所示,用户看到的是一个逻辑数据库,每个数据库有系统表(例如 pg_catalog 下面的 pg_class, pg_proc 等)和用户表(下例中为 sales 表和 customers 表)。在物理层面,它有很多个独立的数据库组成。每个数据库都有它自己的一份系统表和用户表。
master 数据库仅仅包含元数据而不保存用户数据。master 上仍然有用户数据表,这些用户数据表都是空表,没有数据。优化器需要使用这些空表进行查询优化和计划生成。segment 数据库上绝大多数系统表(除了少数表,例如统计信息相关表)和 master 上的系统表内容一样,每个 segment 都保存用户数据表的一部分。
在 Greenplum 中,用户数据按照某种策略分散到不同节点的不同 segment 实例中。每个实例都有自己独立的数据目录,以磁盘文件的方式保存用户数据。使用标准的 INSERT SQL 语句可以将数据自动按照用户定义的策略分布到合适的节点,然而 INSERT 性能较低,仅适合插入少量数据。Greenplum 提供了专门的并行化数据加载工具以实现高效数据导入,详情可以参考 gpfdist 和 gpload 的官方文档。此外 Greenplum 还支持并行 COPY,如果数据已经保存在每个 segment 上,这是最快的数据加载方法。下图形象的展示了用户的 sales 表数据被分布到不同的 segment 实例上。
分区表
参考:https://www.dbaup.com/greenplumzhongdefenqubiao.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | CREATE TABLE partitioned_table ( id INT, name TEXT, created_date DATE ) PARTITION BY RANGE (created_date) ( PARTITION p_2023_01 START ('2023-01-01') END ('2023-02-01'), PARTITION p_2023_02 START ('2023-02-01') END ('2023-03-01'), PARTITION p_2023_03 START ('2023-03-01') END ('2023-04-01'), -- 继续添加分区 DEFAULT PARTITION p_default ); INSERT INTO partitioned_table (id, name, created_date) VALUES (1, 'Alice', '2023-01-15'), (2, 'Bob', '2023-02-20'), (3, 'Charlie', '2023-03-10'), (4, 'David', '2023-04-05'); -- 这条数据将进入默认分区 |
存储格式(堆表和AO表)
存储格式介绍
Greenplum(以下简称GP)有2种存储格式,Heap表和AO表(AORO表,AOCO表) 。
- Heap表:这种存储格式是从PostgreSQL继承而来的,目前是GP默认的表存储格式,只支持行存储。
- AO表: AO表最初设计是只支持append的 (就是只能insert), 因此全称是Append-Only,在4.3之后进行了优化,目前已经可以update和delete了,全称也改为Append-Optimized。AO支持行存储(AORO)和列存储(AOCO)。
在GreenPlum中,如何将堆表修改为AO表?
表存储、压缩和存储方向只能在创建时声明。要改变存储类型,我们必须创建一个新的表,再把原始表的数据载入到新表中,接着删除原始表并且把新表重命名为原始表的名称。用户还必须重新授权原始表上有的权限。
Heap表
Heap表是从PostgreSQL继承而来,使用MVCC来实现一致性。如果你在创建表的时候没有指定任何存储格式,那么GP就会使用Heap表。
Heap表支持分区表,只支持行存,不支持列存和压缩。需要注意的是在处理update和delete的时候,Heap表并没有真正删除数据,而只是依靠version信息屏蔽老的数据,因此如果你的表有大量的update或者delete,表占用的物理空间会不断增大,这个时候需要依靠vacuum来清理老数据。
堆存储是默认存储模型,并且是PostgreSQL为所有数据库表使用的模型。为频繁进行UPDATE、 DELETE以及单个INSERT操作的表和分区使用堆存储。这种存储格式是从PostgreSQL继承而来的,目前是GP默认的表存储格式,
Heap表不支持逻辑增量备份,因此如果要对Heap表做快照,每次都需要导出全量数据。
1 2 3 4 | CREATE TABLE heap( a int, b varchar(32) ) DISTRIBUTED BY (a); |
最佳实践:
- 如果该表是一张小表,比如数仓中的维度表,或者数据量在百万以下,推荐使用Heap表。
- 如果该表的使用场景是OLTP的,比如有较多的update和delete,查询多是带索引的点查询等,推荐使用Heap表。
AO表
AO表是GP特有的,设计的目的就是为了数仓中大型的事实表。AO表支持行存和列存,并且也支持对数据进行压缩。
AO表无论是在表的逻辑结构还是物理结构上,都与Heap表有很大的不同。比如上文所述Heap表使用MVCC控制update和delete之后数据的可见性,而AO表则使用一个附加的bitmap表来实现,这个表的的内容就是表示AO表中哪些数据是可见的。
对于有大量update和delete的AO表,同样需要vacuum进行维护,不过在AO表中,vacuum需要对bitmap进行重置并压缩物理文件,因此通常比Heap的vacuum要慢。
Greenplum数据库可以使用追加优化(append-optimized,AO)的存储格式来批量装载和读取数据,并且能提供HEAP表上的性能优势。 追加优化的存储为数据保护、压缩和行/列方向提供了校验和。行式或者列式追加优化的表都可以被压缩。
GreenPlum的AO表是一种附加优化(append-optimized)的表存储模型,它可以提高数据压缩和查询性能。AO表适用于在表的末尾追加数据,而不是频繁更新或删除现有数据。这样可以减少碎片和重写,并提高I/O效率。AO表还支持分区、索引和约束等功能。AO表最初设计是只支持append的(就是只能insert),因此全称是Append-Only,在4.3之后进行了优化,目前已经可以update和delete了,全称也改为Append-Optimized。AO支持行存储(AORO)和列存储(AOCO)。
可以使用CREATE TABLE命令来创建一个AO表,只需在表定义中指定APPENDONLY = TRUE
选项即可。例如:
1 2 3 4 5 | CREATE TABLE ao_table ( id int, name text ) WITH (APPENDONLY = TRUE); db1=# create unique index idx1 on ao_table(id); ERROR: append-only tables do not support unique indexes db1=# |
还可以指定其他选项,如压缩类型、块大小等,以优化AO表的性能和存储空间。
AO表可以做更新和删除操作,但是这些操作会导致AO表的可见性映射(visibility map)发生变化,需要额外的处理。这可能会增加一些读取和查询的开销。因此,如果您的表需要频繁更新或删除数据,那么AO表可能不是最佳选择。
您应该在以下情况下使用AO表:
- 您的表包含大量的数据,可以按照逻辑分区进行组织。
- 您的表主要用于插入和查询操作,而不是更新和删除操作。
- 您的表可以从压缩技术中受益,以节省存储空间和提高查询性能。
- AO表不支持唯一索引,即不支持主键。
如果您的表符合这些条件,那么AO表可能是一个合适的选择。
GreenPlum中的AO表是一种支持列存储的表,主要面向OLAP场景。AO表有两种类型:AORO表和AOCO表,分别表示行组织和列组织。
堆表和AO表对比
AORO表
AORO就是行存的AO表,同时行存也是AO表的默认存储方式。
AORO支持表级别的压缩,不支持列级别的压缩。
建表语句
1 2 3 4 5 6 7 8 | CREATE TABLE aoro( a int, b int, c varchar(32), d varchar(32) ) WITH (appendonly=true, orientation=row, compresstype=zlib, compresslevel=4) DISTRIBUTED BY (a) |
重点是with后的appendonly=true,由于AO表默认是行存,因此orientation=row也可以不要,后面的compresstype=zlib, compresslevel=4都是压缩相关选项。
压缩选项
compresstype :压缩格式,开源版本的AORO表只支持zlib。
compresslevel :压缩级别,从1-9,简单说来,级别越低(1最低),压缩比越低,但是压缩与解压消耗的cpu资源就越少。默认压缩级别是1。
最佳实践
AO表主要是针对大表,比如数仓中的事实表。
AO表支持逻辑增量备份,对于比较大的表,如果需要定期做快照,建议使用AO表,否则每次都要导出全量数据。
如果该表是大表,使用场景偏OLTP并且update和delete频率不高,可以考虑使用AORO表。
如果该表是大表,并且查询通常都需要扫描大多数列比如查询明细(最典型的就是SELECT * FROM),可以考虑使用AORO表。
在设置压缩级别的时候,通常对于Snova用户,设置到4或者5是比较折中的一个选择。
AOCO表
AOCO表就是列存的AO表。
AOCO不仅支持表级别的压缩,同时也支持列级别的压缩。
建表语句
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | CREATE TABLE aoco( a int ENCODING (compresstype=zlib, compresslevel=5), b int ENCODING (compresstype=none), c varchar(32) ENCODING (compresstype=RLE_TYPE, blocksize=32768), d varchar(32), fdate date ) WITH (appendonly=true, orientation=column, compresstype=zlib, compresslevel=6, blocksize=65536) DISTRIBUTED BY (a) PARTITION BY RANGE(fdate) ( PARTITION pn START ('2018-11-01'::date) END ('2018-11-10'::date) EVERY ('1 day'::interval), DEFAULT PARTITION pdefault ); |
压缩选项
compresstype:支持2种压缩格式,zlib和RLE_TYPE,其中RLE_TYPE(Run-length Encoding)对于有较多重复值的列压缩比很高,因为它会将多个重复值存储为一个值,从而大大降低存储量,比如日期,性别,年龄等字段。
compresslevel:compresstype如果是zlib,compresslevel在1-9,compresstype如果是RLE_TYPE,compresslevel在1-4。
列压缩与表压缩:AOCO表除了支持表级别的压缩外,还支持列级别的压缩,列级别的压缩配置会覆盖表级别的压缩配置,比如上述语法中4个字段,每个字段都采用了不用的压缩方式,d列没有定义,则会默认使用表级别的压缩方式。
分区压缩:在使用分区表的时候,每个分区表也可以设置不同的压缩配置,这个常用于对数据进行冷热分离,比如对于非常老的数据,由于访问频率较低,可以考虑采用较大的压缩比,减少存储量。
BLOCKSIZE
表的存储块大小,通常表数据对应的物理文件就是按blocksize的粒度增加,也就是初始就是blocksize大,并且保持blocksize的倍数。在AOCO表中,每一列也可以设置自己的blocksize,列的配置会覆盖表的配置。
blocksize大小在8192和2097152之间,必须是8192的倍数,默认是32768。
物理文件
AOCO表之所以能够按照列来设置压缩等参数,本质原因在于AOCO表中每一列的数据都会单独存储在一个文件中。因此不同文件之间可以按不同的参数进行存储,互不影响。
对于AOCO表,如果使用了分区,那么对于每一个分区的每一列都会有一个文件,如果一个表的分区很多,又是一张大宽表,那么产生的文件就会很多,也会对性能有一些影响。
最佳实践
AOCO表通常用于数仓中的核心事实表,这种表字段多,数据量大,主要是用于OLAP场景,也就是查询的过程不会SELECT * FROM,而是对其中部分字段进行读取和聚合。
由于AOCO表一般用于大表,因此经常搭配压缩和分区,以减少表的实际存储量来提升性能。
一般情况下,压缩格式选择zlib,压缩级别可以采用折中的4或者5,但是对于有大量重复值的字段,记得要采用RLE_TYPE压缩格式。
blocksize不要设置过大,特别是对于分区表,GP对于每个分区的每个字段都会维护一个buffer,blocksize过大,会导致消耗的内存过大,通常就采用默认值32768即可。
查询AO表
如果您想知道某个表是否是AO表,您可以使用以下SQL语句查询:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | -- select pg_size_pretty(pg_table_size('t1')); -- 1300 MB CREATE TABLE t1 AS SELECT id, md5(id::text) FROM generate_series(1, 20000000) AS id; drop table t1_bk; drop table t1_bk2; drop table t1_bk3; CREATE TABLE t1_bk (LIKE t1) WITH (APPENDONLY=true) ; CREATE TABLE t1_bk2 (LIKE t1) WITH (APPENDONLY=true, COMPRESSLEVEL=5); CREATE TABLE t1_bk3 WITH (APPENDONLY=true,orientation=column,compresstype=zlib, COMPRESSLEVEL=5) as select * from t1; insert into t1_bk select * from t1; insert into t1_bk2 select * from t1; SELECT relname, relstorage,reloptions FROM pg_class WHERE relname like 't1%'; select pg_size_pretty(pg_table_size('t1')); select pg_size_pretty(pg_table_size('t1_bk')); select pg_size_pretty(pg_table_size('t1_bk2')); select pg_size_pretty(pg_table_size('t1_bk3')); |
其中,reloptionsrelstorage字段表示存储格式,如果是AO表,它的值会是a或c。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | lhrdb=# SELECT relname, relstorage,reloptions FROM pg_class WHERE relname like 't1%'; relname | relstorage | reloptions ---------+------------+------------------------------------------------------------------------ t1 | h | t1_bk | a | {appendonly=true,compresslevel=5} t1_bk2 | a | {appendonly=true} t1_bk3 | c | {appendonly=true,orientation=column,compresstype=zlib,compresslevel=5} (4 行记录) lhrdb=# select pg_size_pretty(pg_table_size('t1')); pg_size_pretty ---------------- 53 GB (1 行记录) lhrdb=# select pg_size_pretty(pg_table_size('t1_bk')); pg_size_pretty ---------------- 14 GB (1 行记录) lhrdb=# select pg_size_pretty(pg_table_size('t1_bk2')); pg_size_pretty ---------------- 53 GB (1 行记录) lhrdb=# select pg_size_pretty(pg_table_size('t1_bk3')); pg_size_pretty ---------------- 9849 MB (1 行记录) 时间:270.657 ms |
AO表示例
您可以使用以下SQL语句创建一个AO表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | CREATE TABLE aoc_table ( id int, name text, age int ) WITH (appendonly=true, orientation=column); CREATE TABLE aoa_table ( id int, name text, age int ) WITH (appendonly=true); db1=# SELECT relname, relstorage FROM pg_class WHERE relname = 'aoc_table'; relname | relstorage -----------+------------ aoc_table | c (1 row) db1=# SELECT relname, relstorage FROM pg_class WHERE relname = 'aoa_table'; relname | relstorage -----------+------------ aoa_table | a (1 row) db1=# db1=# \d+ aoa_table Append-Only Table "public.aoa_table" Column | Type | Modifiers | Storage | Stats target | Description --------+---------+-----------+----------+--------------+------------- id | integer | | plain | | name | text | | extended | | age | integer | | plain | | Compression Type: None Compression Level: 0 Block Size: 32768 Checksum: t Distributed by: (id) Options: appendonly=true db1=# \d+ aoc_table Append-Only Columnar Table "public.aoc_table" Column | Type | Modifiers | Storage | Stats target | Compression Type | Compression Level | Block Size | Description --------+---------+-----------+----------+--------------+------------------+-------------------+------------+------------- id | integer | | plain | | none | 0 | 32768 | name | text | | extended | | none | 0 | 32768 | age | integer | | plain | | none | 0 | 32768 | Checksum: t Distributed by: (id) Options: appendonly=true, orientation=column |
这个语句会创建一个名为ao_table的AO表,它有三个列:id,name和age。其中,WITH子句指定了表的存储选项,appendonly=true表示启用AO存储,orientation=column表示使用列组织方式。
修改表结构
Heap、AORO和AOCO这3种表在修改表结构时表现是不一样的,对于不同的表类型,同样的修改语法耗时可能会差异很多,主要原因在于对于有些修改操作会导致表重写,而表重写的时间就取决于表本身的数据量。
以下列出了不同的表结构,在不同的ALTER语法下的行为,其中YES代表需要重写表,NO代表不需要重写表。
操作 | Heap | AORO | AOCO |
---|---|---|---|
ADD COLUMN | NO | YES | NO |
DROP COLUMN | NO | NO | NO |
ALTER COLUMN TYPE | YES | YES | YES |
ADD COLUMN DEFAULT NULL | YES | YES | NO |
ADD COLUMN DEFAULT VALUE | YES | YES | NO |
可以看出AOCO表由于每个列都是单独一个文件,因此在修改列结构时影响最小,这也是AOCO表的一个优势。
混合存储
一张表是否可以同时使用多种存储方式呢?对于分区表,是可以的。
混合存储一般用于这样的场景,对于一张按时间分区的表,通常对于不同时间点的数据行为是不一样的,比如对于最近的数据,会有较多的明细查询,而对于比较老的数据,则是以分析为主。同时由于业务可能要保存较长时间的数据,为了节约成本,较老的数据会考虑使用压缩比较大的存储方式。
混合存储的关键就是使用到了GP的交换分区语法,也就是将一张独立的表与自己的一个分区表进行交换,当然这里前提是新表的结构是一样,并且交换的过程没有新数据进入。
流程如下:
1.创建一张分区表(1到5月份,每月一张表),采用Heap存储
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | CREATE TABLE hyper_storage ( a int, b varchar(32), fdate date ) DISTRIBUTED BY (a) PARTITION BY RANGE(fdate) ( PARTITION pn START ('2018-01-01'::date) END ('2018-06-01'::date) EVERY ('1 month'::interval), DEFAULT PARTITION pdefault ); storage=# \d List of relations Schema | Name | Type | Owner | Storage --------+------------------------------+-------+--------------+--------- public | hyper_storage | table | test | heap public | hyper_storage_1_prt_pdefault | table | test | heap public | hyper_storage_1_prt_pn_1 | table | test | heap public | hyper_storage_1_prt_pn_2 | table | test | heap public | hyper_storage_1_prt_pn_3 | table | test | heap public | hyper_storage_1_prt_pn_4 | table | test | heap public | hyper_storage_1_prt_pn_5 | table | test | heap |
2.现在要对1月份的表修改存储格式,因此创建一张新的AOCO表
1 2 3 4 5 6 | CREATE TABLE exchange_table( a int, b varchar(32), fdate date ) WITH (appendonly=true, ORIENTATION=column, compresstype=zlib, compresslevel=6) DISTRIBUTED BY (a) |
3.将1月份的数据导入新表
1 | INSERT INTO exchange_table SELECT * FROM hyper_storage_1_prt_pn_1; |
4.交换分区
1 | ALTER TABLE hyper_storage EXCHANGE PARTITION pn_1 WITH TABLE exchange_table; |