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

背景

我们目前正在生产中运行PG 10.21.我们在这里与之交互的表events_test包括我们正在测试的两列account(varchar(40))和timestamp(timestamptz).在account,timestamp有指数,在account,and,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-1234ms,对于小响应(帐户5678)大约运行1ms.在这两种情况下,查询规划器 Select 从对复合索引events_test_account_timestamp_idx的索引扫描开始.

过期的统计信息

对于返回大量行的帐户,查询规划器大大低估了要返回的行数-它估计大约500行,但实际上大约是60,000行.这是因为我们的生产数据库上的表统计数据已过期.

手动运行ANALYZE events_test;或升级到PG11、PG12或PG14(这似乎会导致在升级后不久运行此重新分析)更新表统计信息,并使行估计值更接近真实值-对于返回更多数据的帐户,新的估计值约为20,000行.

查询规划器

不幸的是,这种更准确的估计似乎会导致查询规划器运行效率低得多的查询.对于数据很少的帐户,行估计值不变,规划者继续使用综合指数.然而,对于返回数据量较大的帐户,规划者现在 Select 对events_test_timestamp_idx进行索引扫描,然后对account字段进行筛选,这会使查询的执行时间达到400-500ms.

预期返回少量行的查询将继续在所有版本的Postgres上使用复合索引.

我们已经try 了什么

我们一直在寻找关于Postgres绕过综合指数的类似问题,但我们没有发现任何有用的东西.另外:

分析、重新编制索引、统计目标

我们已经try 将default_statistics_targetdefault_statistics_target更新为default_statistics_target0(也短暂地try 了10和default_statistics_target00),然后重新分析我们的表并重新建立索引,并且较慢的查询计划没有改变.

Postgres版本

这个问题最初是在我们针对较新版本的Postgres进行验证时发现的.我们在升级到PG11、PG12和PG14后进行了测试,在所有情况下都显示出性能较慢的查询规划.我们也可以通过重新分析来更新我们的统计数据,从而导致它在PG10上发生,所以这似乎不是Postgres更新中规划者行为的变化.

力综合指数

我们已经验证了,我们确实可以通过以下测试命令(暂时)删除规划者现在首选的timestamp索引来让forcePostgres使用综合指数:

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,但是它现在 Select 在该索引上执行bitmap索引扫描.这将继续以低于events_test_account_timestamp_idxms的速度运行,在性能上可与之前对综合指数的直接指数扫描相媲美.

Query EXPLAINs

以下是在生产环境中,在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

这里的核心问题似乎是,在准确估计的情况下,Postgresexpects对单个指数进行筛选(31867.79)要比使用综合指数(35867.12)快,而实际上在上面的情况下,综合指数要快近10倍.这很有趣,因为上面的最后两个EXPLAIN人使用了相同的一组统计数据来做出这些预测.

推荐答案

我很确定我知道这里发生了什么,但我不知道该怎么办.

TIMESTAMP列与表中行的顺序高度相关(即,对于这个表名和属性名,pg_stats.relationship几乎是1).(我看到你在一条 comments 中证实了这种相关性).因此,规划器假设对(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是否适合在所有系统上设置.而这只会在下一次分析完成时再次被覆盖.当然,它可能会干扰规划者试图制定的其他计划,导致它在那里做出比你在这里可能获得的好处差得多的 Select .

Postgresql相关问答推荐

Trunc函数不删除小数

使用多个分隔符拆分SQL

为什么32632投影中的几何图形并不完美?

PG 16 的 AGE 安装抛出错误:无法创建 src/backend/parser/ag_scanner.c

至少 pgRouting 的某些部分可以与并行查询并行运行吗?

从dump文件中恢复三张表遇到的问题

查找列中的数据是否满足sql中的数据类型条件

从网址获取第一个字符串

PostgreSQL - 如何获得前一个月和一周的价值?

PostgreSQL 返回准确或最接近查询日期的日期

PostgreSQL 查询到 Excel 表

如何将 DELETE 的返回值插入到 postgresql 中的 INSERT 中?

我应该同时指定 INDEX 和 UNIQUE INDEX 吗?

postgresql:致命:password authentication failed for user "douglas"

使用 current_setting() 判断值

解释结果中的Recheck Cond是什么意思?

Hibernate 启动很慢

Postgres 中有多少表分区太多了?

使用 SQLAlchemy 进行 PostgreSQL ILIKE 查询

在 Select (PostgreSQL/pgAdmin) 中将布尔值返回为 TRUE 或 FALSE