Greenplum 列存原理

Greenplum 在表的存储结构上分为 HEAP 表、AO 表。

其中 HEAP 表是 PG 数据库原生存储格式,也是 Greenplum 的默认存储格式,但是只支持行存储,并且不能压缩。AO 表为 append optimized 表,支持行存储、列存储,支持多种压缩算法与压缩级别。

AO 表原理

Greenplum 4.3 之前 AO 表为 appendonly 表,只能追加,即只能进行插入,不能更新和删除。Greenplum 4.3 之后,AO 表优化为 append optimized 表,支持插入、更新、删除。插入即在最后追加记录;删除通过维护一张 bitmap 表,标记被删除的行;更新则为删除和插入 2 个操作的组合。所以 append optimized 表在大量更新或删除之后,标记为无效的数据不会自动清除,需要使用 vaccum 命令回收这部分空间。

bitmap 表如下,通过 bit 位以及偏移量来判定 AO 表上的某一行是否被删除(其中 82342 为 AO 表的 relid,bitmap 表就为 pg_aoseg.pg_aovisimap_{relid}):

lzk=# select * from pg_aoseg.pg_aovisimap_82342;
segno | first_row_no |               visimap                

-------+--------------+----------------------------------------
    2 |     1081344 | \x0100000081002cbc002000008c00
    2 |     2064384 | \x0100000084002ff2ff00c80000000bfcbd80
    2 |     1114112 | \x0100000084002ff2ff266c00020000bfca48
    2 |     3440640 | \x0100000081002f5c000020008180
    2 |     4259840 | \x0100000084002ff2ff28bc00000100bfc9b4
    2 |     5079040 | \x01000000804022dc000800008380
    2 |     3211264 | \x0100000082002ff26dc00400000a34
    2 |     1638400 | \x0100000082002ff25dc00080000a74
    2 |       458752 | \x0100000084002ff2ff2ff223c00000020b54
(9 rows)

对于 heap 表,由 tupleid 决定每条记录的位置(tupleid 包含记录在文件中偏移位置),而对于 AO 表,数据是压缩的,没法确定 value 在文件中偏移位置,因此使用了 rownum,每条记录都有自己的 rownum,rownum 一直增长,每个 block 中记录了起始 rownum 以及 block 在文件中偏移位置,所以只要给定一个 rownum,就能定位到所在的 block,然后从 block 中就可以遍历到这个 rownum 对应的记录。

列存原理

列存只能是 AO 表,所以上一节中所有的 AO 表原理都适用。列存表,按列,每列对应一个或一批文件,每个列占用一个至多个文件,最多 128 个(预留 128 个 id),不同列的值不会同时出现一个文件。

列存表的文件存储,即(25854.1,25854.2 为第一列,25854.129、25854.130 为第二列,以此类推):

[gpadmin@host10372181 /data1/primary/gpseg0/base/82341]$ll|grep 25854
-rw------- 1 gpadmin gpadmin         0 Jul 14 16:48 25854
-rw------- 1 gpadmin gpadmin       5280 Jul 16 14:14 25854.1
-rw------- 1 gpadmin gpadmin       6168 Jul 16 14:14 25854.1025
-rw------- 1 gpadmin gpadmin   3108976 Jul 15 15:47 25854.1026
-rw------- 1 gpadmin gpadmin       5280 Jul 16 14:14 25854.1153
-rw------- 1 gpadmin gpadmin   1476776 Jul 15 15:47 25854.1154
-rw------- 1 gpadmin gpadmin       5976 Jul 16 14:14 25854.1281
-rw------- 1 gpadmin gpadmin   2746744 Jul 15 15:47 25854.1282
-rw------- 1 gpadmin gpadmin       6160 Jul 16 14:14 25854.129
-rw------- 1 gpadmin gpadmin   2626616 Jul 15 15:47 25854.130
-rw------- 1 gpadmin gpadmin       5280 Jul 16 14:14 25854.1409
-rw------- 1 gpadmin gpadmin   1360584 Jul 15 15:47 25854.1410
-rw------- 1 gpadmin gpadmin       5848 Jul 16 14:14 25854.1537
-rw------- 1 gpadmin gpadmin   2559240 Jul 15 15:47 25854.1538
-rw------- 1 gpadmin gpadmin       5280 Jul 16 14:14 25854.1665
-rw------- 1 gpadmin gpadmin   1257912 Jul 15 15:47 25854.1666
-rw------- 1 gpadmin gpadmin       6368 Jul 16 14:14 25854.1793
-rw------- 1 gpadmin gpadmin   2106232 Jul 15 15:47 25854.1794
-rw------- 1 gpadmin gpadmin       5280 Jul 16 14:14 25854.1921
-rw------- 1 gpadmin gpadmin   17856504 Jul 15 15:47 25854.1922
-rw------- 1 gpadmin gpadmin     279528 Jul 15 15:47 25854.2
-rw------- 1 gpadmin gpadmin       7016 Jul 16 14:14 25854.2049
-rw------- 1 gpadmin gpadmin   66013896 Jul 15 15:47 25854.2050
-rw------- 1 gpadmin gpadmin       5280 Jul 16 14:14 25854.2177
-rw------- 1 gpadmin gpadmin   16055520 Jul 15 15:47 25854.2178
-rw------- 1 gpadmin gpadmin       6208 Jul 16 14:14 25854.2305
-rw------- 1 gpadmin gpadmin   22774616 Jul 15 15:47 25854.2306
-rw------- 1 gpadmin gpadmin       5280 Jul 16 14:14 25854.2433
-rw------- 1 gpadmin gpadmin   7181688 Jul 15 15:47 25854.2434
-rw------- 1 gpadmin gpadmin       7032 Jul 16 14:14 25854.2561
-rw------- 1 gpadmin gpadmin   14922064 Jul 15 15:47 25854.2562
-rw------- 1 gpadmin gpadmin     11176 Jul 16 14:14 25854.257
-rw------- 1 gpadmin gpadmin   11304392 Jul 15 15:47 25854.258
-rw------- 1 gpadmin gpadmin       5280 Jul 16 14:14 25854.2689
-rw------- 1 gpadmin gpadmin   5351744 Jul 15 15:47 25854.2690
-rw------- 1 gpadmin gpadmin       5280 Jul 16 14:14 25854.2817
-rw------- 1 gpadmin gpadmin     201488 Jul 15 15:47 25854.2818
-rw------- 1 gpadmin gpadmin       6160 Jul 16 14:14 25854.2945
-rw------- 1 gpadmin gpadmin   3593248 Jul 15 15:47 25854.2946
-rw------- 1 gpadmin gpadmin       9824 Jul 16 14:14 25854.3073
-rw------- 1 gpadmin gpadmin   12571176 Jul 15 15:47 25854.3074
-rw------- 1 gpadmin gpadmin       5280 Jul 16 14:14 25854.3201
-rw------- 1 gpadmin gpadmin   3308464 Jul 15 15:47 25854.3202
-rw------- 1 gpadmin gpadmin       5280 Jul 16 14:14 25854.3329
-rw------- 1 gpadmin gpadmin   3258376 Jul 15 15:47 25854.3330
-rw------- 1 gpadmin gpadmin       5280 Jul 16 14:14 25854.3457
-rw------- 1 gpadmin gpadmin   1604200 Jul 15 15:47 25854.3458
-rw------- 1 gpadmin gpadmin       5280 Jul 16 14:14 25854.3585
-rw------- 1 gpadmin gpadmin   3483096 Jul 15 15:47 25854.3586
-rw------- 1 gpadmin gpadmin       5280 Jul 16 14:14 25854.3713
-rw------- 1 gpadmin gpadmin     354840 Jul 15 15:47 25854.3714
-rw------- 1 gpadmin gpadmin       5280 Jul 16 14:14 25854.3841
-rw------- 1 gpadmin gpadmin     329024 Jul 15 15:47 25854.3842
-rw------- 1 gpadmin gpadmin       5280 Jul 16 14:14 25854.385
-rw------- 1 gpadmin gpadmin   1204480 Jul 15 15:47 25854.386
-rw------- 1 gpadmin gpadmin       5280 Jul 16 14:14 25854.3969
-rw------- 1 gpadmin gpadmin     363400 Jul 15 15:47 25854.3970
-rw------- 1 gpadmin gpadmin       5280 Jul 16 14:14 25854.4097
-rw------- 1 gpadmin gpadmin     368080 Jul 15 15:47 25854.4098
-rw------- 1 gpadmin gpadmin       5280 Jul 16 14:14 25854.4225
-rw------- 1 gpadmin gpadmin     213120 Jul 15 15:47 25854.4226
-rw------- 1 gpadmin gpadmin       5280 Jul 16 14:14 25854.4353
-rw------- 1 gpadmin gpadmin     187264 Jul 15 15:47 25854.4354
-rw------- 1 gpadmin gpadmin       5280 Jul 16 14:14 25854.4481
-rw------- 1 gpadmin gpadmin     218800 Jul 15 15:47 25854.4482
-rw------- 1 gpadmin gpadmin       5280 Jul 16 14:14 25854.4609
-rw------- 1 gpadmin gpadmin   1687376 Jul 15 15:47 25854.4610
-rw------- 1 gpadmin gpadmin       5280 Jul 16 14:14 25854.4737
-rw------- 1 gpadmin gpadmin     761744 Jul 15 15:47 25854.4738
-rw------- 1 gpadmin gpadmin       5280 Jul 16 14:14 25854.4865
-rw------- 1 gpadmin gpadmin   10054680 Jul 15 15:47 25854.4866
-rw------- 1 gpadmin gpadmin       6624 Jul 16 14:14 25854.513
-rw------- 1 gpadmin gpadmin   2743616 Jul 15 15:47 25854.514
-rw------- 1 gpadmin gpadmin       5280 Jul 16 14:14 25854.641
-rw------- 1 gpadmin gpadmin   1149328 Jul 15 15:47 25854.642
-rw------- 1 gpadmin gpadmin       5632 Jul 16 14:14 25854.769
-rw------- 1 gpadmin gpadmin   1861056 Jul 15 15:47 25854.770
-rw------- 1 gpadmin gpadmin       6160 Jul 16 14:14 25854.897
-rw------- 1 gpadmin gpadmin   2020608 Jul 15 15:47 25854.898

若只更新某1列或某几列,对于列存 AO 表,每个列文件之间并没有指针或其他关联,依靠相同的 rownum 来定位一行数据,所有列文件中该 rownum 数据全部作废,然后使用更新后的数据插入到新的一行中,此行中的所有 rownum 保留。因此,此时的数据更新,并不是只更新指定的列。所以单条 update 带来的 IO 开销很大,需要对每一个列文件都写入。

小结

对于 Greenplum 中的 AO 表,其实无论是行存或列存,更新所带来的性能降低是可以预估的。一条 SQL 语句的执行时间基本上可以简单的分为 IO 消耗的时间和计算时间。一张表在大量更新之后只会线性的增加 IO 时间,计算时间并不会受到影响。即假设表 A 通过 update 更新了 30% 的数据,同样的一条 SQL 执行时间的延长理论上不高于 30%。并且可以通过 vacuum 回收被删除数据的空间来恢复原有的查询性能。

Greenplum 备份方式

Greenplum 官方提供备份恢复工具:gpbackup、gprestore,可以对 Greenplum 集群进行逻辑备份恢复。同时可以使用pg物理备份工具pg_basebackup对master 以及所有 primary segment 分别单独备份恢复。

1. 测试环境

集群架构:

结构机器IP部署使用硬盘
1 Master + 2 Segment Host10.37.2.180(Master) 10.37.2.181(Seg) 10.37.2.182(Seg)12 segmentSSD

机器配置:

机器IPCPU(逻辑核数)内存(RAM)硬盘操作系统
10.37.2.18032C128GHDD+SSDCentOS7.3
10.37.2.18132C128GHDD+SSDCentOS7.3
10.37.2.18232C128GHDD+SSDCentOS7.3

测试库详情:

库大小表数量索引数量表数据大小
全量:672G41个90个317G
全量+增量:888G(增量:216G)41个90个369G(增量:52G)

2. 逻辑备份

2.1 全量备份

备份命令(备份详情请参考附录1):

gpbackup --leaf-partition-data --dbname 库名 --backup-dir 备份目录 --jobs 并发数

恢复命令:

gprestore -backup-dir 备份目录 --create-db --timestamp 备份时间戳

耗时信息:

源库大小备份文件大小备份(1并发)备份(2并发)恢复(1并发)恢复(2并发)
672G119G54m4s30m19s4h29m3s2h31m18s

注:2并发时已达测试环境千兆网络瓶颈,故不再增加。

2.2 增量备份

逻辑备份支持增量备份,只需在备份命令中指定 –incremental 参数即可,增量备份仅支持 append only 表,heap 表即使指定增量参数,也进行全量备份。

备份命令:

gpbackup --leaf-partition-data --dbname 库名 --backup-dir 备份目录 --jobs 并发数 --incremental

恢复命令同全量备份,时间戳指定增量备份的时间戳即可

备份文件大小备份时间恢复时间表数据恢复时间Index
119G+54G25m55s5h29m35s2h57m29s2h32m2s

2.3 全量增量性能对比

备份库大小备份时间备份速度
全量备份672G54m4s12G/m
增量备份(仅增量)216G25m55s8G/m
恢复库大小恢复时间恢复速度
全量672G4h29m3s149G/h
全量+增量888G(增量:216G)5h29m35s161G/h

全量备份与增量备份的备份恢复速度差距不大。

3. 物理备份

3.1 备份

备份命令:

pg_basebackup -p segment1端口号 -X f -c fast -D 备份目录/gpseg0 --target-gp-dbid segment1_DBID -h 127.0.0.1
......
pg_basebackup -p segment12端口号 -X f -c fast -D 备份目录/gpseg5 --target-gp-dbid segment12_DBID -h 127.0.0.1

备份耗时:

源库大小物理备份(串行)物理备份(并行)
672G25m47s11m33s

注:串行与并行是指单台 segment host 内的各个 segments 之间,segment host 之间均为并行。

3.2 恢复

物理备份并非 Greenplum 官方推荐的方式,恢复需要对每个 segment 进行单独恢复。

恢复步骤:

1、将每个 segment 的备份文件分别拷贝至 master 与 standby master、primary 与 mirror 对应的数据目录中

2、在 mirror 数据目录中生成 recovery.conf 文件,内容如下,其中 host 与 port 需要根据不同 segment 设置为不同值:

standby_mode = 'on'
primary_conninfo = 'user=gpadmin host=sdw1 port=6000 sslmode=prefer sslcompression=1 krbsrvname=postgres application_name=gp_walreceiver'
primary_slot_name = 'internal_wal_replication_slot'

3、在 master 上执行 gpstart -a 启动数据库(注:此时 master 与 standby master、primary 与 mirror 的复制关系尚不存在)

4、登陆到 master 与 每个 primary segment 创建复制槽,命令如下(segment 无法正常连接,需要使用 utility 模式):

PGOPTIONS='-c gp_session_role=utility' psql -p 6000

执行创建复制槽:

select pg_create_physical_replication_slot('internal_wal_replication_slot');

5、在 master 上执行 gpstate -s 查看主备之间复制状态是否正常

3.3 增量

在上述物理备份的基础上,拷贝增量 WAL 日志(或解压归档日志)到 segment 的 pg_xlog 目录中之后启动数据库。查询 WAL 日志增量数据是否可以正常应用:HEAP 表可以正常应用 WAL 日志中的增量数据,AO 表无法应用,并且查询报错

ERROR:  read beyond eof in table "t_col" file "base/16436/16396.1", read position 0 (small offset 10648), actual read length 0 (large read length 21296) (cdbbufferedread.c:211)  (seg0 slice1 10.243.145.205:6000 pid=1235) (cdbbufferedread.c:211)

4. 总结

根据如上的调查结果,对比两种备份方案,得出:

备份方案优点缺点
逻辑备份1、Greenplum 官方推荐
2、保证全局数据一致性
3、备份、恢复方案简单
4、可支持增量备份
1、恢复速度非常慢(10TB 恢复超过 3 天)
2、备份速度较慢
3、只能恢复到备份时间点
物理备份1、备份速度较快
2、恢复速度快
1、非 Greenplum 官方推荐备份方式
2、由于是逐节点备份,无法确保全局数据一致性
3、备份、恢复方案较复杂
4、只能恢复到备份时间点

由于 Greenplum 本身 master 与 segment 都自带高可用,segment 宕机 mirror 可以自动切换,master 宕机可以手动切换至 standby master,所以只有在主备都无法启动时需要使用备份进行恢复操作。无论物理备份还是逻辑备份,均只能恢复到备份时间点,若使用备份文件进行恢复,需要通过其他方式将备份点之后的数据补上。

附录1 逻辑备份详细

逻辑备份时,由 master 向每个 segment 发送类似如下命令 COPY 表数据:

COPY public.t_ao_co_zstd10 TO PROGRAM 'gzip -c -1 > /pgdata_test/gpback/gpadmin/gpseg<SEGID>/backups/20200727/2020072709201
2/gpbackup_<SEGID>_20200727092012_33282.gz' WITH CSV DELIMITER ',' ON SEGMENT IGNORE EXTERNAL PARTITIONS;

Greenplum数据库设计与优化

GP表设计

1.1 分布键

GP作为一个MPP数据库,分布键的选择对于后期使用的性能影响非常大。即使GP运行在单机环境,GP数据也是存放在多个segment上,所以必须要选择合适的分布键,特别是从单机数据库迁移到GP上的业务,因为单机数据库 不涉及分布键,所以迁移之前一定要设计好所有表的分布键。每张表必须显式指定分布键,避免使用默认分布。

  1. 尽可能的避免各个节点之间的数据交互以提高性能。两表关联查询时如果关联字段包含分布键,则数据可以在各自节点内完成运算;如果关联字段不包含分布键,则需要进行数据重分布,产生大量网络、内存开销,性能很低。
  2. 尽可能选择能使数据均匀分布在各个节点的分布键。如果数据分布不均匀,性能瓶颈就会出现在数据最多的节点上,导致其他节点很空闲,而都在等待该节点执行完成,造成性能低及资源浪费。

例如人员主题库类的业务,表之间查询、关联都是以人员为单位,而区分人员则使用的是身份ID。并且身份ID 散列度非常高,可以保证数据均匀分布。所以这类业务里的绝大多数表都非常适合使用身份ID字段作为分布键。

可以通过select gp_segment_id,count(*) from table_name group by gp_segment_id检查表数据是否均匀分发到每个segment上。

1.2 表分区

GP只支持range、list两种分区,不支持hash分区。GP使用表继承方式实现分区表,实际上每个分区都是一张单独的表,并且对于列存类型的表,每一列都需要一个单独的文件,每台服务器上的文件数=segment数*表数量*表分区数量*列数,所以为了避免文件系统上文件过多,仅对大表进行分区。

按照某个字段进行分区不会影响数据在节点上的分布,但是合理的选择分区字段可以让查询只扫描需要访问的分区,加快在单个数据节点的查询速度。

例1:

某张明细数据类的表,每天都有明细数据,经常按天查询明细数据,且数据保留N个月。则适合使用业务日期作为表分区键。按天查询数据可以只访问需要查询的分区,不会全表查询。删除过期数据时可以直接删除过期分区。

例2:

某张人员基本信息类的表,业务经常需要按省份进行统计各项指标。则适合使用省级代码作为表分区键。需要更新某省份数据时可以清空该省份分区后导入。并且统计时绝大多数操作都可以在分区内完成。

1.3 表存储方式

行存储是一行的所有列在磁盘上连续存储,所以一次IO可以从磁盘上读取整个行的数据。

列存储是在磁盘上将同一列的只保存在一起。

GP同时支持行存及列存。行列存储的特点和对比网上有很多文章可以参考,这里只列举一下各自适用的场景:

适合使用行存的场景:

  1. 数据经常需要修改
  2. 每次查询的字段占表全部列的大部分

适合使用列存的场景:

  1. 数据不需要修改或极少修改
  2. 表字段数多且经常查询其中少量字段
  3. 表数据量很大,需要压缩来提高IO性能

1.4 索引


GP则是通过索引设计提高数据检索性能 。

  1. 对于经常需要根据条件检索少量数据的高基数列建立索引
  2. 对于经常用于表关联的字段建立索引
  3. 频繁更新的列不要建立索引
  4. GP索引对数据加载性能影响很大,所以在加载大量数据前可以先删除索引,加载结束后重建索引

最佳实践

2.1 ANALYZE

ANALYZE用于收集表的统计信息,以优化执行计划。

如果使用INSERT、UPDATE、DELETE操作或修改大量数据之后,或导入数据后,建议执行ANALYZE。

创建新的索引之后也建议执行ANALYZE。

2.2 VACUUM

VACUUM用于回收表空间并可以再次使用,不会申请排他锁,其他会话仍可以正常读写该表。如果加上FULL参数,则会请排他锁,阻塞其他操作。

所以建议对于经常修改或删除的表,定期在闲时执行VACUUM操作,释放空间。不建议使用VACUUM FULL,可以使用create table … as操作,然后删除原表重命名新表。

2.3 加载数据

GP支持COPY方式入库和外部表方式入库。COPY方式所有数据会经过MASTER节点,由MASTER进行分发,性能较低,但是使用方便,对于少量数据可以使用COPY方式入库。外部表方式所有的segment节点都会并行加载数据,需要开启gpfdist服务并创建外部表,但是性能较高,对于大量数据建议使用外部表方式入库。

2.4 执行计划

GP采用基于成本的优化器来评估执行查询的不同策略,并选择成本最低的方法。和其他关系数据库系统的优化器相似,在计算不同执行计划的成本时,GP的优化器会考虑诸如关联表的行数、是否有索引、字段数据的基数等因素。还会考虑到数据的位置,尽可能在segment上完成任务,降低在不同segments间传输的数据量。

在一个查询运行速度比预期慢时,可以查看优选器生成的执行计划以及执行每一步的代价。这有助于确定哪一步最耗资源,进而可以修改查询或者模式,以便生成更好的计划。查看查询计划使用 EXPLAIN 语句。

执行详细描述了GP执行查询的步骤。查询计划是颗节点树,从下向上阅读,每个节点将它的执行结果数据传递给其上的节点。每个节点表示查询计划的一个步骤,每个节点都有一行信息描述了该步骤执行的操作,例如扫描、关联、聚合或者排序操作等,此外还有显示执行该操作的具体方法。例如,扫描操作可能是顺序扫描或者索引扫描,关联操作可能是哈希关联或者嵌套循环关联。

例:

postgres=# explain select datasetid,count(*) from t1 group by datasetid;

                                                      QUERY PLAN                                                       

———————————————————————————————————————–

 Gather Motion 8:1  (slice2; segments: 8)  (cost=8272479.90..8272484.15 rows=340 width=90)

   ->  HashAggregate  (cost=8272479.90..8272484.15 rows=43 width=90)

         Group By: t1.datasetid

         ->  Redistribute Motion 8:8  (slice1; segments: 8)  (cost=8272468.00..8272474.80 rows=43 width=90)

               Hash Key: t1.datasetid

               ->  HashAggregate  (cost=8272468.00..8272468.00 rows=43 width=90)

                     Group By: t1.datasetid

                     ->  Append-only Columnar Scan on t1  (cost=0.00..6662468.00 rows=40250000 width=33)

 Optimizer status: legacy query optimizer

(9 rows)

可以从上面执行计划看到会依次执行:

  1. Append-only Columnar Scan
  2. HashAggregate
  3. Redistribute Motion
  4. HashAggregate
  5. Gather Motion。

即:

  1. 在8个segments上对Append-only表进行列扫描
  2. 因为有汇聚函数,进行hash汇聚
  3. 未使用表分布键进行汇聚,所以需要进行数据重分布
  4. 重分布后进行hash汇聚
  5. master收集各节点结果并返回结果集

分析执行计划时首先要找估算代价较高的步骤。对比估算的行数及代价与操作实际需要处理的行数,以判断是否合理。

如果有分区表,判断分区裁剪是否有效。分区裁剪需要查询条件(WHERE 子句)必须和分区条件一样,不能包含子查询。

如果执行计划顺序不是最优的,检查数据库统计信息是否最新的。这时就要运行ANALYZE,生成最优计划。

注意计算倾斜。当执行诸如哈希聚合和哈希关联等操作符时,若不同Segment执行代价分布不均,就会发生计算倾斜。由于某些Segment比其他Segment使用更多的CPU和内存,性能下降明显。原因可能是关联、排序、聚合字段基数低或者数据分布不均匀。通过EXPLAIN ANALYZE 可以检测计算倾斜。每个节点都含有Segment处理的最多行数和所有Segment的平均行数。如果最大行数比均值大很多,那么至少有一个Segment需要处理更多的工作,因而有计算倾斜的可能性。

例:

postgres=# explain analyze select c1,count(*) from t1 group by c1;

                                                                      QUERY PLAN                                                                      

——————————————————————————————————————————————————

 Gather Motion 8:1  (slice1; segments: 8)  (cost=10821491.42..15184888.40 rows=145149885 width=90)

   Rows out:  311959999 rows at destination with 44285 ms to first row, 125533 ms to end, start offset by 42 ms.

   ->  HashAggregate  (cost=10821491.42..15184888.40 rows=18143736 width=90)

         Group By: c1

         Rows out:  Avg 38994999.9 rows x 8 workers.  Max 39002895 rows (seg2) with 46671 ms to first row, 75735 ms to end, start offset by 44 ms.

         Executor memory:  131464K bytes avg, 131464K bytes max (seg0).

         Work_mem used:  127901K bytes avg, 127901K bytes max (seg0). Workfile: (8 spilling, 0 reused)

         Work_mem wanted: 3020300K bytes avg, 3020885K bytes max (seg2) to lessen workfile I/O affecting 8 workers.

         (seg2)   39002895 groups total in 32 batches; 1 overflows; 40032573 spill groups.

         (seg2)   Hash chain length 4.7 avg, 19 max, using 8568049 of 8650752 buckets.

         ->  Append-only Columnar Scan on t1  (cost=0.00..6662468.00 rows=40250000 width=26)

               Rows out:  Avg 40250000.0 rows x 8 workers.  Max 40257783 rows (seg2) with 80 ms to first row, 10405 ms to end, start offset by 46 ms.

 Slice statistics:

   (slice0)    Executor memory: 413K bytes.

   (slice1)  * Executor memory: 131822K bytes avg x 8 workers, 131822K bytes max (seg0).  Work_mem: 127901K bytes max, 3020885K bytes wanted.

 Statement statistics:

   Memory used: 128000K bytes

   Memory wanted: 3021084K bytes

 Optimizer status: legacy query optimizer

 Total runtime: 144500.021 ms

(20 rows)

可以看出上面的执行计划在各个阶段内存和IO对比中max与avg差别都很小,说明数据分布很好,没有倾斜。