加快Postgres部分索引的创建


8

我正在尝试在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;

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

看来我们受CPU限制(top正在显示该进程的100%)。
我有什么办法可以加快创建部分索引的速度?

系统规格:

  • 18核至强
  • 192GB内存
  • RAID中的12个SSD
  • 自动真空关闭
  • maintenance_work_mem: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_id?平均分配?磁盘上所得索引的大小?数据是静态的,可以。但是,您是唯一的用户吗?
Erwin Brandstetter,2015年

更新了更多信息。
Burnsy 2015年

1
关闭自动真空 ”-为什么?那真是个坏主意。这会阻止收集统计信息,从而产生错误的查询计划
a_horse_with_no_name 2015年

@a_horse_with_no_name插入所有数据后,我们将手动进行分析
burnsy

我仍然不清楚您的情况。您的查询是什么样的?如果您的桌子是completely static,那是什么意思We have a separate team onsite that consumes this data?您只是索引范围run_id >= 266 AND run_id <= 270还是整个表?每个索引的预期寿命是多少/将使用多少个查询?有多少个不同的值run_id?听起来像〜15Mio。每行run_id,这将使其大​​约有800个不同的值run_id?为什么obj_type_setby_s_idseq没有定义NOT NULL?每个值的NULL值的大致百分比是多少?
Erwin Brandstetter,2015年

Answers:


8

BRIN指数

自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;

您甚至可以只索引整个表

表格布局

无论您做什么,都可以通过按如下顺序对列进行排序,以节省由于填充要求而导致的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
);

如果所有列都没有NULL值,则使表缩小79 GB。细节:

另外,您只有三列可以为NULL。NULL位图占用9-72列的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_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

我只会将其设置为所需的高-这取决于未知的(对我们而言)索引大小。并且仅在本地执行会话。正如引言所解释的那样,太高的常规设置可能会使服务器饿死,因为自动清理也可能占用更多的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;

关于SET LOCAL

SET LOCAL无论是否提交,最后的影响仅持续到当前事务结束。

要测量对象大小:

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


我敢打赌他的工作是IO约束的,因为表比RAM大得多。更加频繁地读取表会使问题变得更糟,无论是否有足够的内存来对内存中创建的每个索引进行排序。
尔根·斯特罗贝尔

我和尤尔根在一起。我相信,由于表的大小,从本质上讲,您必须对创建的每个索引对表执行完整的顺序扫描。另外,我不确定创建单独的部分索引会带来很多性能提升(我90%肯定不会看到任何提升,但是在此方面我可能会关闭。)我相信会有更好的表现索引创建的解决方案将涉及在您希望查询的整个范围内创建一个索引作为“单个部分索引”,以减少总体构建时间。
克里斯

@Chris:我同意,创建5个索引所花的时间将比仅创建一个索引要长(即使它们在一起的总和较小,创建每个索引也会更便宜并且查询可能会更快)。再想一想,这应该是Postgres 9.5中BRIN索引的理想用例。
Erwin Brandstetter 2015年

3

也许这只是过度设计。您是否实际尝试过使用单个完整索引?覆盖整个表的局部索引不能为索引查找带来很大的好处(如果有的话),并且从您的文本中推断出您对所有run_id都有索引?使用部分索引进行索引扫描可能会有一些优势,但我仍然将首先对简单的单索引解决方案进行基准测试。

对于每个索引创建,您都需要对表进行完整的IO绑定扫描。因此,创建多个部分索引需要比读取单个索引更多的IO读取表,尽管对于单个大索引而言,排序将溢出到磁盘上。如果您坚持使用部分索引,则可以尝试同时并行构建所有(或几个)索引(在内存允许的情况下)。

要对在内存中对所有run_id(它们是8字节的bigint)进行排序所需的maintenance_work_mem进行粗略估算,您需要10.5 * 8 GB +一些开销。


0

您还可以在默认值以外的其他表空间上创建索引。这些表空间可能指向非冗余磁盘(如果它们失败则仅重新创建索引),或者指向速度更快的阵列。

您也可以考虑使用与部分索引相同的条件对表进行分区。在查询时,这将允许与索引相同的速度,而实际上根本不创建任何索引。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.