【问题标题】:Bad estimates in Postgres when joining tables连接表时 Postgres 中的错误估计
【发布时间】:2021-03-31 14:34:19
【问题描述】:

目前我面临一个问题,即 Postgres 的查询规划器根据(我认为的)错误估计做出错误的决定,即在更大的查询中,查询规划器选择从这篇文章中进行(散列)连接为这是第一个/最里面的部分,因为这个连接的估计只有 274 行,但实际上连接这两个表时有 31770 行。这会导致在更大的查询中对这 31770 行进行嵌套循环,尽管当查询计划器考虑/知道正确的行数时肯定会有更便宜的路径,即估计会更好。

这可以用来重现问题:

CREATE TABLE b (id int primary key, name text, is_visible boolean);
INSERT INTO b (id, name, is_visible) SELECT x, 'B #' || x, CASE WHEN x % 116 = 0 THEN true ELSE false END FROM generate_series(1, 29499) AS x;
CREATE INDEX ON b (is_visible);

CREATE TABLE a (id bigserial primary key, b_id int references b(id), name text);

WITH dist AS (
    SELECT '{7236,4431,3012,2339,2246,1907,1661,1356,1173,1029,533,505,415,354,336,275,188,168,168,153,152,133,126,125,113,90,73,72,65,64,64,48,46,35,34,31,26,26,26,25,25,25,24,22,22,21,20,20,19,19,15,15,15,15,13,13,12,12,12,12,12,11,11,11,11,8,8,8,8,8,8,8,8,7,7,7,6,6,6,6,6,6,6,6,6,6,5,5,5,5,5,5,5,5,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::INT[] cnt
)
INSERT INTO a (b_id, name)
SELECT x.id, x.index || '/' || y
FROM (SELECT id, row_number() over (ORDER BY id) AS index FROM b WHERE is_visible = true) AS x,
generate_series(1, (SELECT cnt[x.index] FROM dist)) AS y
ORDER BY x.id;

我试图实现相同的数据分布,因此出现了带有计数的奇怪数组。这实际上似乎奏效了,因为当我对测试数据执行以下查询时,估计值和实际行完全相同:

postgres=> explain analyze select * from a join b on b.id = a.b_id where b.is_visible=true;
                                                              QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------
 Hash Join  (cost=35.88..841.99 rows=274 width=31) (actual time=5.251..637.037 rows=31770 loops=1)
   Hash Cond: (a.b_id = b.id)
   ->  Seq Scan on a  (cost=0.00..722.70 rows=31770 width=18) (actual time=0.020..205.874 rows=31770 loops=1)
   ->  Hash  (cost=32.70..32.70 rows=254 width=13) (actual time=5.195..5.212 rows=254 loops=1)
         Buckets: 1024  Batches: 1  Memory Usage: 20kB
         ->  Index Scan using b_is_visible_idx on b  (cost=0.29..32.70 rows=254 width=13) (actual time=0.044..2.728 rows=254 loops=1)
               Index Cond: (is_visible = true)
 Planning Time: 0.374 ms
 Execution Time: 836.821 ms
(9 rows)

我可以做些什么来优化估算?

[编辑]
我可能问错了问题,但我实际上对优化这个特定查询并不感兴趣,而是了解 274 的估计值来自何处以及如何更接近实际行数 31770 的估计值

[编辑 2]
使用 Postgres 12.4

【问题讨论】:

  • 如果我没记错的话,过去统计/MCV 存在问题,该问题已得到纠正。请将您的 Postgres 版本添加到问题中。
  • 众所周知,这种错误估计很难纠正。您可能必须为该查询禁用嵌套循环。
  • 感谢您的提示,我已将版本添加到原始帖子中。关于禁用嵌套循环不是一个选项,因为较大的查询会执行 4 个额外的连接,并且完全禁用嵌套循环会很痛。但真正有帮助的是SET join_collapse_limit = 1; 用于更大的查询,据我所知,这会禁用 Postgres 的优化并按照我编写的方式执行查询。但我不知道,我宁愿纠正错误估计,也可能从 Postgres 获得优化,这可能实际上有助于提高性能,而不是禁用所有优化。

标签: postgresql query-optimization


【解决方案1】:
  • 布尔列上的索引没有意义
  • (条件索引在您的情况下更有意义)
  • 对于 FK,您需要支持索引
  • [vacuum] ANALYZE 填充表格后

-- \i tmp.sql

CREATE TABLE b (id int primary key, name text, is_visible boolean NOT NULL); -- <<HERE
INSERT INTO b (id, name, is_visible) SELECT x, 'B #' || x, CASE WHEN x % 116 = 0 THEN true ELSE false END FROM generate_series(1, 29499) AS x;
-- CREATE INDEX ON b (is_visible);
CREATE UNIQUE INDEX ON b (id) WHERE is_visible = True; -- <<HERE

CREATE TABLE a (id bigserial primary key, b_id int references b(id), name text);
CREATE INDEX zzzz ON a (b_id ); -- <<HERE

WITH dist AS (
    SELECT '{7236,4431,3012,2339,2246,1907,1661,1356,1173,1029,533,505,415,354,336,275,188,168,168,153,152,133,126,125,113,90,73,72,65,64,64,48,46,35,34,31,26,26,26,25,25,25,24,22,22,21,20,20,19,19,15,15,15,15,13,13,12,12,12,12,12,11,11,11,11,8,8,8,8,8,8,8,8,7,7,7,6,6,6,6,6,6,6,6,6,6,5,5,5,5,5,5,5,5,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::INT[] cnt
)
INSERT INTO a (b_id, name)
SELECT x.id, x.index || '/' || y
FROM (SELECT id, row_number() over (ORDER BY id) AS index FROM b WHERE is_visible = true) AS x,
generate_series(1, (SELECT cnt[x.index] FROM dist)) AS y
ORDER BY x.id;

-- I tried to achieve the same distribution of the data, hence the strange array with counts. And that actually seems to have worked, because when I execute the following query on the test data the estimates and actual rows are exactly the same:

VACUUM ANALYZE a; -- <<HERE
VACUUM ANALYZE b; -- <<HERE

explain analyze
select *
from a join b on b.id = a.b_id
where b.is_visible=true;

结果计划:


DROP SCHEMA
CREATE SCHEMA
SET
CREATE TABLE
INSERT 0 29499
CREATE INDEX
CREATE TABLE
CREATE INDEX
INSERT 0 31770
VACUUM
VACUUM
                                                         QUERY PLAN                                                          
-----------------------------------------------------------------------------------------------------------------------------
 Hash Join  (cost=11.73..615.84 rows=274 width=31) (actual time=0.285..11.952 rows=31770 loops=1)
   Hash Cond: (a.b_id = b.id)
   ->  Seq Scan on a  (cost=0.00..520.70 rows=31770 width=18) (actual time=0.006..2.577 rows=31770 loops=1)
   ->  Hash  (cost=8.55..8.55 rows=254 width=13) (actual time=0.263..0.264 rows=254 loops=1)
         Buckets: 1024  Batches: 1  Memory Usage: 20kB
         ->  Index Scan using b_id_idx on b  (cost=0.14..8.55 rows=254 width=13) (actual time=0.009..0.223 rows=254 loops=1)
 Planning Time: 0.435 ms
 Execution Time: 13.011 ms
(8 rows)

【讨论】:

  • 谢谢。当然所有有效点。创建测试模式时我错过了支持索引。而且我忘了提到我在执行查询之前手动完成了ANALYZE。但正如我所说,这只是较大查询的一小部分,即使在您进行更改后,生成的计划仍然认为只有 274 行,而他实际上得到了 31770 行。我很确定这是主要问题,因为据我了解,我的较大查询中的查询计划器根据估计成本 * 274(应该是估计成本 * 31770)选择这条路径而不是其他路径
  • 我还尝试过使用 default_statistics_target 和扩展统计信息。强制SET enable_seqscan = 0; 确实会导致索引+索引连接(运行时间大致相同)并且:抱歉,我无法评论您不可见的较大查询。
  • 是的,可能不好。我的问题不够好/不够精确。至于较大的查询,我将不得不花费大量时间来提出匿名模式和准确的测试数据,因此我将其分解为这个较小的部分,因为我认为(希望)当我以某种方式让 Postgres 有更好的估计这里,应该可以解决大查询的问题。
  • 是的,我明白这一点。但是您的问题很好地说明了错误估计。 (顺便说一句:31770 正是表 a 中的行数)
  • 是的,表 a 只能包含表 b 中的项目,其中 is_visible = true。当我们开始表 a 是空的,但随着时间的推移,表 b 中的所有这些项目在表 a 中至少有一个条目(有些有更多,有些更少),这就是为什么实际行数目前是所有行表a,但不幸的是查询计划似乎没有得到这个事实。
【解决方案2】:

您说您的目标不是优化该查询的执行,而是改进其基数估计,以便自动优化相关查询。

根本问题是没有合适的条件统计。我认为解决这个问题的唯一方法是对表进行分区。

create table c (like b) partition by list (is_visible);
create table c_true partition of c for values in (true);
create table c_false partition of c for values in (false);
insert into c select * from b;
vacuum ANALYZE c;
explain analyze select * from a join c as b on b.id = a.b_id where b.is_visible=true;

现在它足够聪明,可以使用来自正确分区的统计信息进行估算。 所以估计现在是正确的。通常我会说对这种大小的表进行分区是荒谬的,所以一定要记录下这样做的原因。

如果 PostgreSQL 能够收集和使用部分索引上的统计信息,那就太好了,就像它在表达式索引上所做的那样。然后只制作部分索引将是分区的合适替代方案。您还可以设想扩展“CREATE STATISTICS”以采用 WHERE 子句。但目前这些都没有实现。

【讨论】:

  • 抱歉,我对整个统计数据都很满意,所以请告诉我,但是表c 或分区c_true 上的哪些统计数据确实使连接估计突然失效?因为即使我添加了更多具有is_visible=true 且在表a 中没有等价物的项目,估计仍然是确定的。至于分区表b,不幸的是这是不可能的,因为a)我在那个(真实)表中有(其他)列需要在所有分区中都是唯一的,b)可以更改标志 is_visible 所以我会手动覆盖这个移动到另一个分区
猜你喜欢
  • 2023-03-03
  • 2022-07-08
  • 2014-04-25
  • 2018-07-13
  • 2016-04-15
  • 2021-07-24
  • 1970-01-01
  • 2021-08-30
  • 2023-04-09
相关资源
最近更新 更多