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
。
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)