PostgreSQL 并不始终使用部分索引

Posted

技术标签:

【中文标题】PostgreSQL 并不始终使用部分索引【英文标题】:PostgreSQL doesn't consistently use partial index 【发布时间】:2021-12-01 21:48:36 【问题描述】:

我有一个 postgres 13.3 表,如下所示:

CREATE TABLE public.enrollments (
    id bigint NOT NULL,
    portfolio_id bigint NOT NULL,
    consumer_id character varying(255) NOT NULL,
    identity_id character varying(255) NOT NULL,
    deleted_at timestamp(0) without time zone,
    batch_replace boolean DEFAULT false NOT NULL
);
CREATE UNIQUE INDEX enrollments_portfolio_id_consumer_id_index ON public.enrollments 
  USING btree (portfolio_id, consumer_id) WHERE (deleted_at IS NULL);

每个作品集通常包含数百万个注册。我的客户通常会定期向我发送一个包含他们所有注册的批处理文件,因此我必须使数据库与此文件匹配。我尝试一次读取大约 1000 个数据块,然后使用如下查询来检查注册是否预先存在:

SELECT * FROM enrollments WHERE deleted_at IS NULL AND portfolio_id = 1 
  AND consumer_id = ANY(ARRAY["C1", "C2", ..., "C1000"])

似乎对于新的投资组合,它不使用唯一的部分索引,因此此查询可能需要长达 30 秒。当投资组合中已经有数百万注册时,该索引似乎起作用并且需要大约 20 毫秒。我不得不将 sql 更改为一次只查询一个注册,这大约需要 1 秒/1000 秒。这并不理想,因为完成一个文件可能需要一天的时间,但至少它完成了。

有人知道在选择中使用许多 consumer_id 时我可以做些什么来获得一致使用的唯一部分索引吗?

下面是一些解释输出。冗长的查询花费了 4 秒多一点,随着越来越多的注册被插入到投资组合中,这个时间增加到至少 30 秒,直到它到达某个点并下降到大约 20 毫秒

Existing enrollments in this portfolio: 78140485

Index Scan using enrollments_portfolio_id_consumer_id_index on enrollments e0  (cost=0.70..8637.14 rows=1344 width=75) (actual time=3.529..37.827 rows=1000 loops=1)
  Index Cond: ((portfolio_id = '59031'::bigint) AND ((consumer_id)::text = ANY ('C1,C2,...,C1000'::text[])))
  I/O Timings: read=27.280
Planning Time: 0.477 ms
Execution Time: 37.914 ms

Benchmark time: 20 ms


Existing enrollments in this portfolio: 136000

Index Scan using enrollments_portfolio_id_consumer_id_index on enrollments e0  (cost=0.70..8.87 rows=1 width=75) (actual time=76.615..4354.081 rows=1000 loops=1)
  Index Cond: (portfolio_id = '59028'::bigint)
  Filter: ((consumer_id)::text = ANY ('C1,C2,...,C1000'::text[]))
  Rows Removed by Filter: 135000
Planning Time: 1.188 ms
Execution Time: 4354.341 ms

Benchmark time: 4398 ms

【问题讨论】:

edit您的问题并添加使用explain (analyze, buffers, format text)生成的execution plans(快速和慢速)不只是一个“简单”的解释)作为formatted text,并确保您保留计划的缩进。粘贴文本,然后将``` 放在计划前一行和计划后一行。 哪一列包含最独特的值,portfolio_id 或 consumer_id?您的索引针对portfolio_id 拥有最多唯一值的情况进行了优化。您的查询可能会受益于您首先使用 consumer_id 和第二个投资组合 ID 的索引。但是你必须检查,如果没有查询计划,这只是我的猜测。 有趣的问题+1。请包括执行计划。优化器可能试图“太聪明”。 @FrankHeikens 也有同样的想法。 @FrankHeikens 我的顺序可能有误,这绝对不是我的专业领域。我想我应该首先使用portfolio_id,因为我也有不包括consumer_id的查询,用于获取计数/投资组合并在开头设置标记标志,以便我可以在最后删除不存在的注册。 两个查询都使用相同的索引,enrollments_portfolio_id_consumer_id_index。那是您为此目的创建的索引吗?因为在你的问题中你提到了一个不同的名字。这是您的问题:过滤器删除的行数:135000 【参考方案1】:

这里实际上很慢的是 =ANY 是通过循环遍历数组的 1000 个成员并测试每个成员来实现的,并对需要检查的 136000 行中的每一行执行此操作。这是很多循环(但不是我手中的 4 秒,对我来说“只有”1.5 秒)。更糟糕的是,计划者没有预料到=ANY 的实现如此糟糕,因此没有理由选择其他计划来避免它。

v14 将通过使用哈希表来实现=ANY 来解决这个问题,因此它不再那么慢。

如果您不能/不想升级到 v14,您可以通过加入 VALUES 列表来重写查询,而不是使用 =ANY

SELECT * FROM enrollments join (VALUES ('C1'),...,('C1000')) f(c) on c=consumer_id
  WHERE deleted_at IS NULL AND portfolio_id = 1 

【讨论】:

谢谢!这加快了很多事情。在 1M 行时,查询现在需要大约 1.3 秒,而旧查询大约需要 33 秒。在另一个计划开始之前,这至少是可用的。我仍在试图弄清楚什么时候会发生。 在我手中,截止值在 1200 左右。但问题是,它不会知道你已经超过了这个值,直到发生分析以获得新的统计数据。

以上是关于PostgreSQL 并不始终使用部分索引的主要内容,如果未能解决你的问题,请参考以下文章

postgresql----唯一索引,表达式索引,部分索引

PostgreSQL索引分类及使用

postgrey9.5哪年

PostgreSQL分区介绍

为 Django 全文搜索创建索引

Postgres 索引未使用正确的计划