我在一个正在开发的应用程序中查询遗留表时遇到了困难。有一个查询命中了一个大约有1500万行的表,该表一直需要大约15秒才能完成,但我觉得它应该比这快得多。该表看起来像这样:
CREATE TABLE foo_files (
id uuid PRIMARY KEY,
deleted boolean NOT NULL,
person_id varchar,
parent_id varchar
);
字符串
在deleted
、person_id
和parent_id
列上也定义了索引:
CREATE INDEX index_foo_files_on_parent_id ON foo_files(parent_id);
CREATE INDEX index_foo_files_on_person_id ON foo_files(person_id);
CREATE INDEX index_foo_files_on_deleted ON foo_files(deleted);
型
我正在运行的查询是查找deleted
是false
、person_id
是null
、parent_id
是特定值(比如abc123
)的所有文件
如果我在启用EXPLAIN ANALYZE
的情况下运行此查询,则会看到以下内容:
=> EXPLAIN ANALYZE SELECT * FROM foo_files
WHERE
deleted IS FALSE AND
person_id IS NULL AND
parent_id = 'abc123';
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on foo_files (cost=10.45..964.90 rows=162 width=62) (actual time=0.014..0.014 rows=0 loops=1)
Recheck Cond: ((parent_id)::text = 'abc123'::text)
Filter: ((deleted IS FALSE) AND (person_id IS NULL))
-> Bitmap Index Scan on index_foo_files_on_parent_id (cost=0.00..10.41 rows=246 width=0) (actual time=0.012..0.013 rows=0 loops=1)
Index Cond: ((parent_id)::text = 'abc123'::text)
Planning Time: 0.216 ms
Execution Time: 0.032 ms
(7 rows)
型
因此,deleted
和person_id
列上的条件比第三列多花了近两个数量级的时间。我不明白为什么这里没有使用deleted
列和person_id
列上的索引(即位图堆扫描),而parent_id
* 上的索引正在被使用(即索引扫描)。
作为一个实验,我尝试在deleted
和person_id
列上创建一个部分索引,如下所示:
CREATE INDEX index_foo_files_composite ON foo_files(parent_id) WHERE deleted IS FALSE AND person_id IS NULL;
型
这加快了查询速度 * 一些 *
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on foo_files (cost=5.81..634.55 rows=161 width=62) (actual time=0.031..0.032 rows=0 loops=1)
Recheck Cond: (((parent_id)::text = 'abc123'::text) AND (deleted IS FALSE) AND (person_id IS NULL))
-> Bitmap Index Scan on index_foo_files_composite (cost=0.00..5.77 rows=161 width=0) (actual time=0.030..0.030 rows=0 loops=1)
Index Cond: ((parent_id)::text = 'abc123'::text)
Planning Time: 0.178 ms
Execution Time: 0.045 ms
(6 rows)
型
我错过什么了吗?这 * 感觉 * 就像它应该执行得更快。我如何修改索引以提高性能,或者修改查询以使用可用的索引?
1条答案
按热度按时间xxslljrj1#
首先,你的性能很好。我认为你误解了解释结果。你的第一个查询执行时间为0.032 ms,这不算什么。你的第二个复合索引查询执行时间为0.045 ms。请参阅PostgreSQL文档中的使用EXPLAIN - EXPLAIN ANALYZE。
在这种情况下,查询规划器已经决定位图索引扫描
parent_id = 'abc123'
将返回很少的行(246),然后它可以简单地使用位图堆扫描deleted IS FALSE AND person_id IS NULL
来扫描这些行。它已经确定这将比从已删除的索引中检索所有
deleted IS FALSE
行更快(表中每一个未删除的行,可能有很多行),并从person_id索引中检索person_id is NULL
的所有行。(取决于您的数据,可能很多,可能很少),并且还从parent_id索引(少数行)中检索其中parent_id = 'abc123'
的所有行,然后取所有三个的交集。而且,从解释中可以看出,您的查询在0.032ms内执行。这非常快。
您可以阅读更多关于位图堆扫描here和
由于大多数查询可能也会查询
deleted is FALSE
,因此您可以尝试包含deleted
的复合索引,例如(parent_id, deleted)
或(person_id, deleted)
。这种索引的实用性取决于删除行的百分比。三个值有7个查询组合。
复合B树索引可以覆盖对第一个键的搜索和排序,也可以覆盖对第一个键和第二个键的搜索和排序。虽然也可以只覆盖对第二个键的搜索,但效率不高。
你可以用这三个指数来覆盖所有的可能性。
字符串
然而,这并不一定是最有效的索引方式。复合索引可能比非复合索引慢。现在你的性能很好。
另一种方法是在
deleted
上对表进行分区。这将大大简化索引:只需(parent_id, person_id)
和(person_id)
就可以覆盖所有可能的查询。它还可以避免在删除删除行时锁定表。作为最后的优化,您可以将
deleted
标志替换为deleted_at
时间戳。然后您可以按时间段(可能按月或年,取决于删除的行增长的速度)在deleted_at
上进行分区。这可以让您通过简单地删除旧分区来非常有效地删除旧行。这可以避免表无限增长的问题。另一种替代方法是使用“审计”表来存储更改和删除的行,而不是将删除的行存储在主表中。有关如何工作的示例,请参阅paper_trail。当您很少想要查询删除的行时,这通常是最好的。