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 并不始终使用部分索引的主要内容,如果未能解决你的问题,请参考以下文章