加速创建 Postgres 部分索引

bur*_*nsy 8 postgresql performance index ddl performance-tuning postgresql-performance

我正在尝试为 Postgres 9.4 中的大型(1.2TB)静态表创建部分索引。

我的数据是完全静态的,所以我可以插入所有数据,然后创建所有索引。

在这个 1.2TB 的表中,我有一个名为的列run_id,可以清晰地划分数据。通过创建涵盖一系列run_ids 的索引,我们获得了出色的性能。下面是一个例子:

CREATE INDEX perception_run_frame_idx_run_266_thru_270
ON run.perception
(run_id, frame)
WHERE run_id >= 266 AND run_id <= 270;
Run Code Online (Sandbox Code Playgroud)

这些部分索引为我们提供了所需的查询速度。不幸的是,每个部分索引的创建大约需要 70 分钟。

看起来我们的 CPU 有限(top进程显示为 100%)。
我可以做些什么来加快部分索引的创建?

系统规格:

  • 18核至强
  • 192GB 内存
  • RAID 中的 12 个 SSD
  • 自动吸尘器关闭
  • 维护工作内存:64GB(太高?)

表规格:

  • 大小:1.26 TB
  • 行数:105.37亿
  • 典型的索引大小:3.2GB(有 ~.5GB 的差异)

表定义:

CREATE TABLE run.perception(
id bigint NOT NULL,
run_id bigint NOT NULL,
frame bigint NOT NULL,
by character varying(45) NOT NULL,
by_anyone bigint NOT NULL,
by_me bigint NOT NULL,
by_s_id integer,
owning_p_id bigint NOT NULL,
obj_type_set bigint,
seq integer,
subj_id bigint NOT NULL,
subj_state_frame bigint NOT NULL,
CONSTRAINT perception_pkey PRIMARY KEY (id))
Run Code Online (Sandbox Code Playgroud)

(不要过多地阅读列名——我已经对它们进行了一些混淆。)

背景资料:

  • 我们在现场有一个单独的团队来使用这些数据,但实际上只有一两个用户。(这些数据都是通过模拟生成的。)用户只有在插入完成并完全建立索引后才开始分析数据。我们主要关心的是减少生成可用数据所需的时间,现在的瓶颈是索引创建时间。
  • 使用partials时查询速度已经完全够用了。事实上,我认为我们可以增加每个索引覆盖的运行次数,并且仍然保持足够好的查询性能。
  • 我的猜测是我们将不得不对表进行分区。在采取该路线之前,我们正在尝试用尽所有其他选择。

Erw*_*ter 9

布林指数

自 Postgres 9.5起可用,可能正是您正在寻找的。更快的索引创建,小的索引。但是查询通常没有那么快。手册:

BRIN 代表块范围索引。BRIN 设计用于处理非常大的表,其中某些列与它们在表中的物理位置有一些自然的相关性。阿范围是一组属于在表中物理相邻的页; 对于每个块范围,索引存储了一些摘要信息。

继续阅读,还有更多。
Depesz 进行了初步测试。

对于你的情况,最佳:如果你能写行群集run_id,索引变得非常小,创作便宜得多。

CREATE INDEX foo ON run.perception USING brin (run_id, frame)
WHERE run_id >= 266 AND run_id <= 270;
Run Code Online (Sandbox Code Playgroud)

您甚至可能只是索引整个表

表格布局

无论您做什么,您都可以通过对列进行排序来节省由于每行对齐要求而丢失的 8 个字节:

CREATE TABLE run.perception(
  id               bigint NOT NULL PRIMARY KEY
, run_id           bigint NOT NULL
, frame            bigint NOT NULL
, by_anyone        bigint NOT NULL
, by_me            bigint NOT NULL
, owning_p_id      bigint NOT NULL
, subj_id          bigint NOT NULL
, subj_state_frame bigint NOT NULL
, obj_type_set     bigint
, by_s_id          integer
, seq              integer
, by               varchar(45) NOT NULL -- or just use type text
);
Run Code Online (Sandbox Code Playgroud)

如果没有任何列具有 NULL 值,则使您的表小 79 GB。细节:

此外,您只有三列可以为 NULL。对于 9 - 72 列,NULL 位图占用 8 个字节。如果只有一个 整数列是 NULL,则存在存储悖论的一个极端情况:使用虚拟值会更便宜:浪费了 4 个字节,但由于不需要该行的 NULL 位图而节省了 8 个字节。更多细节在这里:

部分索引

根据您的实际查询,使用这五个部分索引而不是上面的索引可能更有效:

CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 266;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 267;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 268;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 269;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 270;
Run Code Online (Sandbox Code Playgroud)

为每个运行一个事务。

run_id以这种方式删除作为索引列可以为每个索引条目节省 8 个字节 - 32 个而不是每行 40 个字节。每个索引的创建成本也更低,但是对于一个太大而无法保留在缓存中的表,创建五个而不是一个所花费的时间要长得多(如@Jürgen 和@Chris 评论)。所以这可能对你有用,也可能没用。

分区

基于继承- 直到 Postgres 9.5 的唯一选择。
(Postgres 11 或最好是 12 中的新声明式分区更智能。)

手册:

在约束排除期间检查对父表的所有子表的所有约束,因此大量分区可能会显着增加查询计划时间。因此,基于遗留继承的分区可以很好地处理多达一百个分区;不要尝试使用成千上万个分区。

大胆强调我的。因此,为 估计 1000 个不同的值run_id,您将创建跨越大约 10 个值的分区。


maintenance_work_mem

我错过了你maintenance_work_mem在我的第一次阅读中已经在适应的情况。我会在我的回答中留下报价和建议以供参考。根据文档:

maintenance_work_mem (整数)

指定要通过维护操作中使用的存储器的最大量,例如VACUUMCREATE INDEX,和ALTER TABLE ADD FOREIGN KEY。它默认为 64 兆字节 ( 64MB)。由于数据库会话一次只能执行这些操作中的一个,并且安装通常不会同时运行许多操作,因此将此值设置为明显大于work_mem. 较大的设置可能会提高清理和恢复数据库转储的性能。

请注意,在autovacuum运行时,最多autovacuum_max_workers可能会分配此内存,因此请注意不要将默认值设置得太高。单独进行控制可能会很有用 setting autovacuum_work_mem

我只会根据需要将它设置为高 - 这取决于(对我们而言)未知的索引大小。并且仅在本地用于执行会话。正如引用所解释的那样,否则过高的常规设置可能会使服务器饿死,因为 autovacuum 也可能会占用更多 RAM。此外,不要将其设置得比需要的高太多,即使在执行会话中,空闲 RAM 也可能会很好地用于缓存数据。

它可能看起来像这样:

BEGIN;

SET LOCAL maintenance_work_mem = 10GB;  -- depends on resulting index size

CREATE INDEX perception_run_frame_idx_run_266_thru_270 ON run.perception(run_id, frame)
WHERE run_id >= 266 AND run_id <= 270;

COMMIT;
Run Code Online (Sandbox Code Playgroud)

关于SET LOCAL

SET LOCALlast的效果只持续到当前事务结束,无论是否提交。

测量物体尺寸:

显然,服务器通常应该合理配置。