PostgreSQL SQL查询不使用在WHERE子句中的列上定义的索引

dldeef67  于 2023-11-18  发布在  PostgreSQL
关注(0)|答案(1)|浏览(127)

我在一个正在开发的应用程序中查询遗留表时遇到了困难。有一个查询命中了一个大约有1500万行的表,该表一直需要大约15秒才能完成,但我觉得它应该比这快得多。该表看起来像这样:

CREATE TABLE foo_files (
  id uuid PRIMARY KEY,
  deleted boolean NOT NULL,
  person_id varchar,
  parent_id varchar
);

字符串
deletedperson_idparent_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);


我正在运行的查询是查找deletedfalseperson_idnullparent_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)


因此,deletedperson_id列上的条件比第三列多花了近两个数量级的时间。我不明白为什么这里没有使用deleted列和person_id列上的索引(即位图堆扫描),而parent_id * 上的索引正在被使用(即索引扫描)。
作为一个实验,我尝试在deletedperson_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)


我错过什么了吗?这 * 感觉 * 就像它应该执行得更快。我如何修改索引以提高性能,或者修改查询以使用可用的索引?

xxslljrj

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个查询组合。

  • 父ID
  • 个人ID
  • 删除
  • parent_id + person_id
  • parent_id +删除
  • person_id +已删除
  • parent_id + person_id +已删除

复合B树索引可以覆盖对第一个键的搜索和排序,也可以覆盖对第一个键和第二个键的搜索和排序。虽然也可以只覆盖对第二个键的搜索,但效率不高。
你可以用这三个指数来覆盖所有的可能性。

-- For searches on parent_id, parent_id + person_id, parent_id + person_id + deleted
(parent_id, person_id, deleted)
-- For searches on person_id, person_id + deleted
(person_id, deleted)
-- For searches on deleted, parent_id + deleted
(deleted, parent_id)

字符串
然而,这并不一定是最有效的索引方式。复合索引可能比非复合索引慢。现在你的性能很好。
另一种方法是在deleted上对表进行分区。这将大大简化索引:只需(parent_id, person_id)(person_id)就可以覆盖所有可能的查询。它还可以避免在删除删除行时锁定表。
作为最后的优化,您可以将deleted标志替换为deleted_at时间戳。然后您可以按时间段(可能按月或年,取决于删除的行增长的速度)在deleted_at上进行分区。这可以让您通过简单地删除旧分区来非常有效地删除旧行。这可以避免表无限增长的问题。
另一种替代方法是使用“审计”表来存储更改和删除的行,而不是将删除的行存储在主表中。有关如何工作的示例,请参阅paper_trail。当您很少想要查询删除的行时,这通常是最好的。

相关问题