Postgres查询规划器不会使用成本较低的复合索引

x33g5p2x  于 2022-10-15  发布在  PostgreSQL
关注(0)|答案(3)|浏览(156)

我们遇到了一个奇怪的问题,Postgres,最初是在我们准备从10.21升级到11+时发现的。在较新版本的Postgres中,我们最初认为的是性能回归,实际上,一旦为我们的数据库生成了更准确的统计数据,性能回归就变成了性能回归。结果是,对于有问题的表,last_analyzelast_autoanalyze都是NULL。升级纠正了这一点,但使查询的某个子集变得慢得多。

背景

我们目前正在生产中运行PG 10.21。我们在这里交互的表events_test包括两个我们正在测试的列,account(varchar(40))和timestamp(timestamptz)。存在关于accounttimestamp的指数和关于accounttimestamp的综合指数。
我们的测试查询是

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使用的是同一组统计数据来得出这些预测。
yrefmtwq

yrefmtwq1#

我很确定我知道这里发生了什么,但我不知道该怎么办。
TIMESTAMP列与表中行的顺序高度相关(即,对于这个表名和属性名,pg_stats.relationship几乎是1)。(我看到你在一条评论中证实了这种相关性)。因此,规划器假定(timestamp)上索引的该段的范围扫描最终将读取表的一小块,并或多或少地按顺序执行,因此在使用该索引的IO成本上有很大的优势。然而,对于(account, timestamp)上的索引的使用,它没有得到这一突破。规划器只查看索引的第一列的相关统计信息,由于Account与行顺序不相关,因此它假定它将在整个表中执行随机读取。这是错误的,因为一旦您将特定帐户等同于时间戳列并设置了边界,索引的该块仍然与行顺序高度相关,因此您仍然读取表的(小)相关块并按顺序执行,但是规划者从来没有足够远地意识到这一点。
因此,“错误的”索引因IO便利性而获得不公平的信用,这是正确的,但对于“正确的”索引也是正确的(但未知的)。但是,索引错误的计划需要进行大量筛选,以删除帐户值错误的行。这会消耗CPU时间,规划者知道这一点,但与它错误地认为将节省的IO量相比,消耗的CPU时间量被认为是很小的。
对此您可以做的一件事是为该表设置autovacuum_enabled = off。但要使其生效,您必须重新创建表,然后在进行更改后填充表。与此相当的是,我怀疑有人以某种方式在您的旧服务器上做了什么。您可以考虑的另一件事是分区。如果您只需要考虑最近的分区(或两个分区),那么关联效果就会小得多,因为规划者已经知道,无论使用哪个索引,它都将查看一个小得多的表。
您可以做的另一件事是手动摆弄统计表,尽管这很糟糕。在我的手中,这就完成了工作:

update pg_statistic set stanumbers2='{0}' where starelid=25929 and staattnum=2;

但当然,在您的系统中,Relid会与我的有所不同,小调也可能不同,我不确定Stanumbers2是否适合在所有系统上设置。而这只会在下一次分析完成时再次被覆盖。当然,它可能会干扰规划者试图制定的其他计划,导致它在那里做出比你在这里可能获得的好处差得多的选择。

nbnkbykc

nbnkbykc2#

有时,当选择小数据时,或者当选择过程中带来的数据比剩余数据多得多时,PostgreSQL会使用过滤器而不是使用索引,相反,会删除剩余数据。如果您想干扰这一点并始终使用索引,那么尝试使用临时设置的enable_seqscan = off来检查执行计划和时间,如果可能,还要阅读以下内容,您可能需要它:Examining Index Usage

xxe27gdn

xxe27gdn3#

我不能确定这个问题,但这里有几个考虑因素:

  • 您是否在(account, "timestamp")上创建了综合指数?顺序很重要,这个顺序会更好。
  • 似乎PostgreSQL预计account上的条件不是很有选择性,但这是错误的。也许扩展的统计数据可以改善它的估计:
CREATE STATISTICS events_test_account_timestamp (dependencies, mvc)
   ON account, "timestamp" FROM events_test;
ANALYZE events_test;
  • 您是否在两个系统上设置了相同的random_page_cost

相关问题