【问题标题】:Performance impact of view on aggregate function vs result set limiting视图对聚合函数与结果集限制的性能影响
【发布时间】:2021-10-10 09:34:59
【问题描述】:

问题

使用 PostgreSQL 13,我遇到了一个性能问题,从连接两个表的视图中选择最高 id,具体取决于我执行的 select 语句。

这是一个示例设置:

CREATE TABLE test1 (
  id BIGSERIAL PRIMARY KEY,
  joincol VARCHAR
);

CREATE TABLE test2 (
  joincol VARCHAR
);

CREATE INDEX ON test1 (id);
CREATE INDEX ON test1 (joincol);
CREATE INDEX ON test2 (joincol);

CREATE VIEW testview AS (
SELECT test1.id,
       test1.joincol AS t1charcol,
       test2.joincol AS t2charcol
FROM   test1, test2
WHERE  test1.joincol = test2.joincol
);

我发现了什么

我正在执行两个语句,这会导致完全不同的执行计划和运行时。以下语句在不到 100 毫秒内执行。据我了解执行计划,运行时与行数无关,因为 Postgres 会逐行迭代(从最高 id 开始,使用索引),直到可以连接一行并立即返回。

SELECT id FROM testview ORDER BY ID DESC LIMIT 1;

但是,在 Postgres 使用索引选择最高 id 之前,这两个表平均需要 1 秒以上(取决于行数),因为这两个表是“完全连接的”。

SELECT MAX(id) FROM testview;

请参阅 dbfiddle 上的此示例以查看解释计划:
https://www.db-fiddle.com/f/bkMNeY6zXqBAYUsprJ5eWZ/1

我的真实环境

在我的真实环境中,test1 仅包含一整行 (joincol 中具有唯一值。 test2 最多包含 ~10M 行,其中 joincol 始终匹配 test1joincol 的值。 test2joincol 不可为空。

实际问题

为什么 Postgres 无法识别它可以在第二个选择的行基础上使用 Index Scan Backward?表/索引有什么我可以改进的吗?

【问题讨论】:

  • 附带说明:CREATE VIEW 语句中 SELECT 周围的括号完全没用
  • @a_horse_with_no_name 感谢您的提示。我喜欢使用这种风格,因为我的 IDE (IntelliJ IDEA) 应用了一些更好的颜色模式,使其更易于阅读。
  • 然后 IntelliJ 对 SQL 的外观有一个非常奇怪的假设。它是否也对括号中的“独立”查询应用不同的颜色?例如:(select 42);select 42;
  • @a_horse_with_no_name 不。着色基本上只是“分离”。当我的光标在括号内时,查询的“其他所有内容”都会稍微模糊
  • 您的问题“为什么 postgres 会这样”的答案是:因为这就是它的优化器的编码方式。优化器并不完美,无法识别和/或执行一些它可以进行的转换。

标签: sql postgresql performance sql-execution-plan postgresql-performance


【解决方案1】:

查询不严格等价

为什么 Postgres 无法识别它可以使用基于行的索引扫描向后进行第二次选择?

为了明确上下文:

  • max(id) 不包括 NULL 值。但ORDER BY ... LIMIT 1 没有。
  • NULL 值按升序排列最后,降序排列在第一个。所以Index Scan Backward 可能不会首先找到最大值(根据max()),而是找到任意数量的NULL 值。

正式等价于:

SELECT max(id) FROM testview;

不是:

SELECT id FROM testview ORDER BY id DESC LIMIT 1;

但是:

SELECT id FROM testview ORDER BY id DESC NULLS LAST LIMIT 1;

后一个查询没有得到快速查询计划。但它会使用具有匹配排序顺序的索引:(id DESC NULLS LAST)

这对于聚合函数 min()max() 是不同的。当直接使用(id) 上的普通 PK 索引定位表 test1 时,这些人会获得快速计划。但不是基于视图(或直接基于底层连接查询 - 视图不是阻止程序)。在正确位置排序 NULL 值的索引几乎没有任何效果。

我们知道此查询中的id 永远不可能是NULL。该列定义为NOT NULL。视图中的联接实际上是一个 INNER JOIN,它不能为 id 引入 NULL 值。
我们还知道 @987654344 上的索引@ 不能包含 NULL 值。
但是 Postgres 查询规划器不是人工智能。 (它也没有尝试这样做,这可能会很快失控。)我看到两个缺点

  • min()max() 只针对表获取快速计划,不考虑索引排序顺序,增加一个索引条件:Index Cond: (id IS NOT NULL)
  • ORDER BY ... LIMIT 1 仅使用完全匹配的索引排序顺序获取快速计划。

不确定,是否可以(容易地)改进。

dbfiddle here - 演示以上所有内容

索引

表/索引有什么我可以改进的吗?

这个索引完全没用:

CREATE INDEX ON "test" ("id");

test.id 上的 PK 是通过列上的唯一索引实现的,它已经涵盖了附加索引可能为您做的所有事情。

可能还有更多,等待问题解决。

扭曲的测试用例

测试用例离实际用例太远,没有意义。

在测试设置中,每个表有 100k 行,不能保证 joincol 中的每个值在另一侧都有匹配,并且两列都可以为 NULL

您的真实案例在table1 中有10M 行,在table2 中有table1.joincol 中的每个值在table2.joincol 中都有一个匹配项,两者都定义为NOT NULL,并且table2.joincol 是唯一的。经典的一对多关系。 table2.joincol 上应该有一个 UNIQUE 约束和一个 FK 约束 t1.joincol --> t2.joincol

但目前这一切都在问题中被扭曲了。等到清理干净为止。

【讨论】:

  • 非常感谢您的解释,尤其是对NULLS LAST 的详细说明,以匹配max 功能。我对我的“真实”数据进行了更多查询 - 但是优化器似乎不够“聪明”,无法确定更好的查询计划。
  • 谢谢欧文!有趣的是,该索引不存储 NULL(如果我知道的话)。因此,如果优化器仅使用索引,则优化器必须知道,该列中没有 NULL。我可以像这样创建索引: CREATE INDEX ON test (joincol, id DESC NULLS LAST);而且没有影响?计划顶部有一个聚合行。我们无法消除的,可以吗?大约是 8% 的成本。
  • @LászlóTóth:Interesting, that index doesn't store NULL (If I know well). 不确定我理解。索引确实存储 NULL 值。但是有问题的索引在PRIMARY KEY 列上,根据定义,它不能为NULL。
  • 我认为那很糟糕。对不起。是的 postgres 索引存储 null-s,我可以指定第一个或最后一个! :) CREATE INDEX ON test (joincol, id DESC NULLS LAST);为什么不影响计划?
  • @LászlóTóth:以id DESC NULLS LAST 作为第一个或唯一表达式的索引确实 影响ORDER BY ... LIMIT 1 的查询计划。查看更新的解释和与之相关的小提琴:dbfiddle.uk/…
【解决方案2】:

这是一个很好的问题,也是很好的测试用例。 我在 postgres 9.3 中对其进行了测试,也许 13 可以更快。

我使用了奥卡姆剃刀,我排除了一些可能性

  • 查看(没有查看就慢了)
  • JOIN 可以过滤某些行(不幸的是在您的测试中没有,但更长的 md5 5-6 是)
  • 其他基本等效选择语句不能解决您的问题(内部查询或存在)
  • 我实现了只使用索引,但由于表不大于索引,因此不是解决方案。

我认为

CREATE INDEX on "test" ("id");

没用,因为PK!

如果你改变这个

CREATE INDEX on "test" ("joincol");

到这里

CREATE INDEX ON TEST (joincol, id);

第二个查询只使用索引。

运行后

REINDEX table test;
REINDEX table test2;
VACUUM ANALYZE test;
VACUUM ANALYZE test2;

您可以进行一些性能调整。因为您在插入之前创建了索引。

我认为原因是 DB 的两个目的。

第一个目标是优化某行。所以运行嵌套循环。您可以使用限制 x 强制它。 第二个目标优化整个表。对整个表快速运行此查询。

在这种情况下,postgres 优化器没有注意到简单的 MAX 可以使用 NESTED LOOP 运行。或者 postgres 不能在聚合子句中使用限制(可以在整个部分选择上运行,查询过滤的内容)。

而且这是非常昂贵的。但是您可以在那里编写其他聚合,例如 SUM、MIN、AVG stb。

也许也可以帮助您处理 Window 功能。

【讨论】:

    猜你喜欢
    • 2021-11-15
    • 1970-01-01
    • 1970-01-01
    • 2010-09-13
    • 1970-01-01
    • 1970-01-01
    • 2015-08-17
    • 1970-01-01
    • 2014-12-05
    相关资源
    最近更新 更多