我们遇到了一个奇怪的问题,Postgres,最初是在我们准备从10.21升级到11+时发现的。在较新版本的Postgres中,我们最初认为的是性能回归,实际上,一旦为我们的数据库生成了更准确的统计数据,性能回归就变成了性能回归。结果是,对于有问题的表,last_analyze
和last_autoanalyze
都是NULL
。升级纠正了这一点,但使查询的某个子集变得慢得多。
背景
我们目前正在生产中运行PG 10.21。我们在这里交互的表events_test
包括两个我们正在测试的列,account
(varchar(40)
)和timestamp
(timestamptz
)。存在关于account
、timestamp
的指数和关于account
和timestamp
的综合指数。
我们的测试查询是
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM events_test
WHERE account = '1234' AND TIMESTAMP >= (NOW() AT TIME ZONE 'EST' - interval '1 month');
我们正在对两个不同的account
值进行测试--有些我们预计会返回大量数据(帐户1234
返回数万行),而另一些返回数量较少(帐户5678
返回数十行)。
在我们当前的生产数据库上,这对于我们的目的来说运行得足够快,对于大响应(帐户1234
)大约运行80-100ms,对于小响应(帐户5678
)大约运行大约1ms。在这两种情况下,查询规划器都选择从对复合索引events_test_account_timestamp_idx
进行索引扫描开始。
过期统计
对于返回大量行的帐户,查询规划器大大低估了要返回的行数-它估计大约500行,但实际上大约是60,000行。这是因为我们的生产数据库上的表统计数据已过期。
手动运行ANALYZE events_test;
或升级到pg11、pg12或pg14(这似乎会导致在升级后不久运行此重新分析)更新表统计信息,并导致行估计值更接近真实值-对于返回更多数据的帐户,新估计值约为20,000行。
查询规划器
不幸的是,这种更准确的估计似乎会导致查询规划器运行效率低得多的查询。对于数据很少的帐户,行估计值不变,规划者继续使用综合指数。然而,对于返回数据量较大的帐户,规划器现在选择在events_test_timestamp_idx
上进行索引扫描,然后在account
字段上进行筛选,这会使查询的执行时间达到400-500ms。
预期返回少量行的查询将继续在所有版本的Postgres上使用复合索引。
我们尝试了什么
我们一直在寻找关于Postgres绕过综合指数的类似问题,但我们没有发现任何有用的东西。另外:
分析、重新索引、统计目标
我们已经尝试将default_statistics_target
从100%更新到1000%(并且在10%和10000处也进行了短暂的尝试),然后重新分析我们的表并重新建立索引,并且较慢的查询计划没有改变。
Postgres版本
这个问题最初是在我们针对较新版本的Postgres进行验证时发现的。我们在升级到PG11、PG12和PG14后进行了测试,在所有情况下都显示出性能较慢的查询规划。我们也可以通过重新分析来更新我们的统计数据,从而导致它在PG10上发生,所以这似乎不是Postgres更新中规划者行为的变化。
强制综合索引
我们已经通过以下测试命令验证了我们确实可以强制Postgres使用综合索引,方法是(临时)删除规划者现在首选的timestamp
索引:
BEGIN;
DROP INDEX events_test_timestamp_idx;
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM events_test
WHERE account = '1234' AND TIMESTAMP >= (NOW() AT TIME ZONE 'EST' - interval '1 month');
ROLLBACK;
这会导致postgres恢复到综合索引events_test_account_timestamp_idx
,但是它现在选择对该索引执行位图索引扫描。这将继续以低于100ms的速度运行,在性能上可与之前对综合指数的直接指数扫描相媲美。
查询EXPLAIN
s
以下是在生产环境中,在PG10上更高效地运行该查询时的样子:
Index Scan using events_test_account_timestamp_idx on events_test (cost=0.11..1109.11 rows=579 width=206) (actual time=0.062..116.439 rows=71700 loops=1)
Index Cond: (((account)::text = '1234'::text) AND ("timestamp" >= (timezone('EST'::text, now()) - '1 mon'::interval)))
Buffers: shared hit=68619 read=1249
I/O Timings: read=23.426
Planning time: 0.662 ms
Execution time: 119.339 ms
下面是在PG14上更新统计数据后的结果(但所有版本的输出都是相似的):
Index Scan using events_test_timestamp_idx on events_test (cost=0.11..31867.79 rows=18994 width=204) (actual time=0.164..311.960 rows=55583 loops=1)
Index Cond: ("timestamp" >= ((now() AT TIME ZONE 'EST'::text) - '1 mon'::interval))
Filter: ((account)::text = '1234'::text)
Rows Removed by Filter: 462327
Buffers: shared hit=492867
Planning:
Buffers: shared hit=144 read=30
I/O Timings: read=6.160
Planning Time: 7.021 ms
Execution Time: 314.676 ms
最后,如果我们通过临时删除时间戳索引来强制它使用综合索引,则在PG14上(但同样,在所有版本上都类似):
Bitmap Heap Scan on events_test (cost=187.05..35867.12 rows=18992 width=204) (actual time=11.373..38.937 rows=55582 loops=1)
Recheck Cond: (((account)::text = '1234'::text) AND ("timestamp" >= ((now() AT TIME ZONE 'EST'::text) - '1 mon'::interval)))
Heap Blocks: exact=13586
Buffers: shared hit=13803
-> Bitmap Index Scan on events_test_account_timestamp_idx (cost=0.00..186.10 rows=18992 width=0) (actual time=9.376..9.376 rows=55582 loops=1)
Index Cond: (((account)::text = '1234'::text) AND ("timestamp" >= ((now() AT TIME ZONE 'EST'::text) - '1 mon'::interval)))
Buffers: shared hit=217
Planning:
Buffers: shared hit=5
Planning Time: 0.316 ms
Execution Time: 41.446 ms
- 编辑*:这里的核心问题似乎是,在准确的估计下,postgres预计单个指数的索引和筛选**将比使用综合指数(35867.12)快,而实际上在上面的情况下,综合指数快近10倍。这很有趣,因为上面最后两个
EXPLAIN
使用的是同一组统计数据来得出这些预测。
3条答案
按热度按时间yrefmtwq1#
我很确定我知道这里发生了什么,但我不知道该怎么办。
TIMESTAMP列与表中行的顺序高度相关(即,对于这个表名和属性名,pg_stats.relationship几乎是1)。(我看到你在一条评论中证实了这种相关性)。因此,规划器假定
(timestamp)
上索引的该段的范围扫描最终将读取表的一小块,并或多或少地按顺序执行,因此在使用该索引的IO成本上有很大的优势。然而,对于(account, timestamp)
上的索引的使用,它没有得到这一突破。规划器只查看索引的第一列的相关统计信息,由于Account与行顺序不相关,因此它假定它将在整个表中执行随机读取。这是错误的,因为一旦您将特定帐户等同于时间戳列并设置了边界,索引的该块仍然与行顺序高度相关,因此您仍然读取表的(小)相关块并按顺序执行,但是规划者从来没有足够远地意识到这一点。因此,“错误的”索引因IO便利性而获得不公平的信用,这是正确的,但对于“正确的”索引也是正确的(但未知的)。但是,索引错误的计划需要进行大量筛选,以删除帐户值错误的行。这会消耗CPU时间,规划者知道这一点,但与它错误地认为将节省的IO量相比,消耗的CPU时间量被认为是很小的。
对此您可以做的一件事是为该表设置
autovacuum_enabled = off
。但要使其生效,您必须重新创建表,然后在进行更改后填充表。与此相当的是,我怀疑有人以某种方式在您的旧服务器上做了什么。您可以考虑的另一件事是分区。如果您只需要考虑最近的分区(或两个分区),那么关联效果就会小得多,因为规划者已经知道,无论使用哪个索引,它都将查看一个小得多的表。您可以做的另一件事是手动摆弄统计表,尽管这很糟糕。在我的手中,这就完成了工作:
但当然,在您的系统中,Relid会与我的有所不同,小调也可能不同,我不确定Stanumbers2是否适合在所有系统上设置。而这只会在下一次分析完成时再次被覆盖。当然,它可能会干扰规划者试图制定的其他计划,导致它在那里做出比你在这里可能获得的好处差得多的选择。
nbnkbykc2#
有时,当选择小数据时,或者当选择过程中带来的数据比剩余数据多得多时,PostgreSQL会使用过滤器而不是使用索引,相反,会删除剩余数据。如果您想干扰这一点并始终使用索引,那么尝试使用临时设置的
enable_seqscan = off
来检查执行计划和时间,如果可能,还要阅读以下内容,您可能需要它:Examining Index Usagexxe27gdn3#
我不能确定这个问题,但这里有几个考虑因素:
(account, "timestamp")
上创建了综合指数?顺序很重要,这个顺序会更好。account
上的条件不是很有选择性,但这是错误的。也许扩展的统计数据可以改善它的估计:random_page_cost
?