Postgresql计数极慢(带索引,简单查询)

col*_*rco 8 postgresql performance count postgresql-9.5 query-performance

我需要在包含数百万行的表上运行这些简单的查询:

SELECT COUNT(*) FROM "subscriptions" WHERE "subscriptions"."project_id" = 123;
Run Code Online (Sandbox Code Playgroud)
SELECT COUNT(*) FROM "subscriptions" WHERE "subscriptions"."project_id" = 123 AND "subscriptions"."trashed_at" IS NULL;
Run Code Online (Sandbox Code Playgroud)

对于项目 123,两个查询的计数结果约为 5M。

我在 上有一个索引project_id,在 上还有另一个索引(project_id, trashed_at)

"index_subscriptions_on_project_id_and_created_at" btree (project_id, created_at DESC)
"index_subscriptions_on_project_id_and_trashed_at" btree (project_id, trashed_at DESC)
Run Code Online (Sandbox Code Playgroud)

问题是这两个查询都非常慢,每个查询大约需要 17 秒。

这些是以下结果EXPLAIN ANALIZE

      QUERY PLAN                                                                                      
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=2068127.29..2068127.30 rows=1 width=0) (actual time=17342.420..17342.420 rows=1 loops=1)
   ->  Bitmap Heap Scan on subscriptions  (cost=199573.94..2055635.23 rows=4996823 width=0) (actual time=1666.409..16855.610 rows=4994254 loops=1)
         Recheck Cond: (project_id = 123)
         Rows Removed by Index Recheck: 23746378
         Heap Blocks: exact=131205 lossy=1480411
         ->  Bitmap Index Scan on index_subscriptions_on_project_id_and_trashed_at  (cost=0.00..198324.74 rows=4996823 width=0) (actual time=1582.717..1582.717 rows=4994877 loops=1)
               Index Cond: (project_id = 123)
 Planning time: 0.090 ms
 Execution time: 17344.182 ms
(9 rows)
Run Code Online (Sandbox Code Playgroud)
      QUERY PLAN                                                                                      
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=2047881.69..2047881.70 rows=1 width=0) (actual time=17557.218..17557.218 rows=1 loops=1)
   ->  Bitmap Heap Scan on subscriptions  (cost=187953.70..2036810.19 rows=4428599 width=0) (actual time=1644.966..17078.378 rows=4994130 loops=1)
         Recheck Cond: ((project_id = 123) AND (trashed_at IS NULL))
         Rows Removed by Index Recheck: 23746273
         Heap Blocks: exact=131144 lossy=1480409
         ->  Bitmap Index Scan on index_subscriptions_on_project_id_and_trashed_at  (cost=0.00..186846.55 rows=4428599 width=0) (actual time=1566.163..1566.163 rows=4994749 loops=1)
               Index Cond: ((project_id = 123) AND (trashed_at IS NULL))
 Planning time: 0.084 ms
 Execution time: 17558.522 ms
(9 rows)
Run Code Online (Sandbox Code Playgroud)

问题是什么?

我可以做些什么来提高性能(即在几秒钟内计数)?

Lau*_*lbe 11

获取和计数 500 万行是一项缓慢的工作。

有两个问题:

  • 位图堆扫描花费的时间比必要的要长,因为work_mem它太小以至于无法包含每个表行一位的位图。

    然后,它会降级为每 8KB 块存储一位,这会导致在位图堆扫描阶段进行更多的堆获取,以排除误报。

    因此,提高work_mem会加快执行速度,但根据您的评论,不会很多。

  • PostgreSQL 还可以只读取其中一个索引,而不是查询表本身(这会很慢),因为它们包含所有所需的数据。

    那会快得多,那么为什么 PostgreSQL 不这样做呢?

    解释是 PostgreSQL 表和索引包含可见的行版本(元组)和不可见的元组(因为它们已被删除或更新且尚未清理)。这些“死元组”是 PostgreSQL 多版本控制实现所必需的。

    现在,哪些元组可见、哪些不可见的信息仅存储在表(堆)中,而不存储在索引中。因此 PostgreSQL 必须查阅堆来确定哪些元组要计数,哪些元组不计数。

    如果您VACUUM在表上运行,PostgreSQL 将更新一个称为“可见性映射”的好东西,其中每个 8KB 块包含一位,指示该块中的所有元组是否对每个人都可见。

    如果可见性映射中有一个块是所有可见的,则索引扫描不必查阅表来确定是否可以看到该块中的元组。如果这适用于大多数元组,您将获得仅索引扫描,这显然要快得多。

所以你应该看到桌子经常被吸尘。

为此,让 autovacuum 在此表上运行得更快、更频繁:

ALTER TABLE subscriptions SET (
   autovacuum_vacuum_scale_factor = 0.05,
   autovacuum_vacuum_cost_delay = 2
);
Run Code Online (Sandbox Code Playgroud)

如果您仅在 上有索引,您会更快project_id


col*_*rco 7

TL;DR 我通过运行以下命令解决了这个问题:

vacuum analyze subscriptions;
Run Code Online (Sandbox Code Playgroud)

在该命令之后,查询仅需要约 1 秒而不是约 17 秒。详细解释请参阅@Laurenz回答

现在,我使用以下设置更频繁地运行 autovacuum postgresql.conf

autovacuum_vacuum_scale_factor = 0.01
autovacuum_analyze_scale_factor = 0.01
Run Code Online (Sandbox Code Playgroud)

更新:随着表大小的增长,上述设置还不够(因为随着表的增长,真空变得不那么频繁)。对于非常大的表(即 10M+ 记录),我使用了这个:

ALTER TABLE subscriptions SET (autovacuum_vacuum_scale_factor = 0, autovacuum_analyze_scale_factor = 0,  autovacuum_vacuum_threshold = 10000, autovacuum_analyze_threshold = 10000);
Run Code Online (Sandbox Code Playgroud)