由 John Doe 四月 23, 2025
不知道你有没有注意到,在 PostgreSQL 中创建 GIN 索引时,需要很长时间才能创建完成。现在,PostgreSQL 支持并行创建 GIN 索引了。
特性提交日志
支持并行创建 GIN 索引的操作。
允许使用并行工作进程构建 GIN 索引,类似于 B 树(BTREE)和 BRIN 索引的构建方式。对于大型表而言,如果构建过程受 CPU 性能限制,这一改进可能会显著提升构建速度。
具体工作分配方式为,每个工作进程基于表的一个子集构建索引项,这个子集由用于读取数据的常规并行扫描来确定。每个工作进程使用本地元组排序(tuplesort)对相同键的索引项进行排序和合并。TID 列表(对于给定键)不会重叠,这意味着合并排序只需简单地连接两个列表。合并后的索引项会被写入到供主进程使用的共享元组排序中。
主进程在将排序后的索引项写入索引之前,需要再次进行合并。但通过这种方式,大部分工作由工作进程完成,主进程只需合并数量较少的大型索引项,从而提高了效率。
大部分并行构建的基础架构是对 B 树索引所用代码的简化复制,省略了与 GIN 索引无关的部分(例如唯一性检查)。
讨论:https://postgr.es/m/6ab4003f-a8b8-4d75-a67f-f25ad98582dc@enterprisedb.com
示例
GIN 索引功能强大,但创建速度相当缓慢。因此,在某些情况下,这一改进可能会带来实质性的变化。
让我们来尝试进行测试。我们需要一些可以使用 GIN 索引进行搜索的数据,所以编写一些数组数据:
#!/usr/bin/env ruby
# frozen_string_literal: true
arr = (1..100_000).to_a
1_000_000.times do
puts "{#{arr.sample(rand() * 100 + 50).join(',')}}"
end
这段代码会输出 100 万个随机数组,每个数组的元素数量随机(介于 50 到 149 之间),且每个元素都是 1 到 100000 之间的数字。将输出保存到/tmp/test.arrays.txt
文件中,文件大小约为 560MB。
现在,我们可以创建一个表并加载数据:
-- 创建表
postgres=# create table test_gin (
the_array int4[]
);
-- 从文件导入数据
postgres=# \copy test_gin from /tmp/test.arrays.txt
之后,通过\dt+ test_gin
命令查看,表大小为 444MB。
接下来,请确保 PostgreSQL 不会使用并行操作,再创建索引:
-- 设置最大并行维护工作线程数为0
set max_parallel_maintenance_workers = 0;
-- 创建GIN索引
create index qq on test_gin using gin (the_array);
耗时:77111.482 毫秒(01:17.111)。
当然,我们应该验证索引是否在查询中被使用:
-- 执行查询并分析查询计划
explain (analyze, buffers) select * from test_gin where the_array @> '{1,2}'::int4[];
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on test_gin (cost=51.31..150.05 rows=25 width=421) (actual time=0.093..0.094 rows=1.00 loops=1)
Recheck Cond: (the_array @> '{1,2}'::integer[])
Heap Blocks: exact=1
Buffers: shared hit=4 read=6
I/O Timings: shared read=0.017
-> Bitmap Index Scan on qq (cost=0.00..51.30 rows=25 width=0) (actual time=0.082..0.082 rows=1.00 loops=1)
Index Cond: (the_array @> '{1,2}'::integer[])
Buffers: shared hit=4 read=5
I/O Timings: shared read=0.013
Planning:
Buffers: shared hit=12 read=6 dirtied=4
I/O Timings: shared read=0.550
Planning Time: 0.764 ms
Execution Time: 0.112 ms
(14 rows)
再次检查下索引大小,通过\di+ qq
命令显示索引大小为 1099MB。
现在,让我们看看是否可以并行创建索引:
-- 删除索引
drop index qq;
-- 设置最大并行维护工作线程数为10
set max_parallel_maintenance_workers = 10;
-- 并行创建GIN索引
create index qq on test_gin using gin (the_array);
耗时:53602.822 毫秒(00:53.603)。
在索引创建过程中,ps
命令的输出显示:
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
...
postgres 361901 10.2 0.3 282400 209020 ? Rs 12:41 2:41 \_ postgres: postgres postgres [local] CREATE INDEX
postgres 391026 99.4 0.0 270036 60428 ? Rs 13:07 0:29 \_ postgres: parallel worker for PID 361901
...
这表明仅有 2 个并行工作进程(主进程和一个额外的工作进程)。创建时间减少了约 30%,效果不错。但奇怪的是:
postgres=# \di+ qq
List of indexes
Schema | Name | Type | Owner | Table | Persistence | Access method | Size | Description
--------+------+-------+----------+----------+-------------+---------------+--------+-------------
public | qq | index | postgres | test_gin | permanent | gin | 769 MB | [null]
(1 row)
索引大小怎么变小了?让我们来验证下索引是否工作正常:
-- 执行查询并分析查询计划
explain (analyze, buffers) select * from test_gin where the_array @> '{1,2}'::int4[];
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on test_gin (cost=34.31..133.05 rows=25 width=421) (actual time=0.152..0.153 rows=1.00 loops=1)
Recheck Cond: (the_array @> '{1,2}'::integer[])
Heap Blocks: exact=1
Buffers: shared hit=4 read=6
I/O Timings: shared read=0.026
-> Bitmap Index Scan on qq (cost=0.00..34.30 rows=25 width=0) (actual time=0.143..0.144 rows=1.00 loops=1)
Index Cond: (the_array @> '{1,2}'::integer[])
Buffers: shared hit=4 read=5
I/O Timings: shared read=0.023
Planning:
Buffers: shared hit=5 read=2 dirtied=1
I/O Timings: shared read=0.015
Planning Time: 0.200 ms
Execution Time: 0.176 ms
(14 rows)
并行创建 GIN 索引会使用排序的方式对数据进行预先整理,使其能以高效的方式插入到索引中(这与 B 树索引类似,但不完全相同),而以非并行方式创建索引时,则是将数据直接插入到索引中。从索引的角度来看,这与先创建表、再创建索引、然后向表中插入数据的操作没有太大区别。
因为 GIN 会按顺序存储数据,所以按照相同的顺序插入数据将提高性能。索引插入操作是顺序进行的,而非随机的,从而减少了随机的缓冲区 I/O 操作,并且能让 GIN 的创建过程以更紧凑的方式存储元组。插入操作通常只发生在最右侧的页面上,这样能实现更高效的页面拆分。
非常不错的改进。感谢社区的所有相关人员。
参考
提交日志:https://git.postgresql.org/pg/commitdiff/e839c8ecc9352b7754e74f19ace013c0c0d18613