视图对聚合函数的性能影响与结果集限制

Performance impact of view on aggregate function vs result set limiting

问题

使用 PostgreSQL 13,我 运行 遇到性能问题 select 从连接两个表的视图中获取最高 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 开始,使用索引)直到可以立即连接行 returns.

SELECT id FROM testview ORDER BY ID DESC LIMIT 1;

然而,这个平均需要超过 1 秒(取决于行数),因为在 Postgres 使用索引到 select 最高 id 之前,这两个表是“完全连接”的。

SELECT MAX(id) FROM testview;

请参考 dbfiddle 上的这个示例来检查解释计划:
https://www.db-fiddle.com/f/bkMNeY6zXqBAYUsprJ5eWZ/1

我的真实环境

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

实际问题

为什么 Postgres 无法识别它可以在第二个 select 的行基础上使用 索引向后扫描 ? tables/indexes 有什么我可以改进的吗?

这是一个很好的问题,也是很好的测试用例。 我在 postgres 9.3 中测试过它,也许 13 可以更快。

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

  • 查看(没有查看速度慢)
  • JOIN 可以过滤一些行(不幸的是在你的测试中没有,但更多长度 md5 5-6 是)
  • 其他基本等效的select语句不能解决你的问题(内部查询或存在)
  • 我实现了只使用索引,但是因为 tables 不比索引大所以不是解决方案。

我觉得

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

没用,因为PK!

如果你改变这个

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

至此

CREATE INDEX ON TEST (joincol, id);

第二个查询只使用索引。

在你之后[​​=57=]这个

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

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

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

第一个目标是优化某些行。所以 运行 嵌套循环。你可以用限制 x 强制它。 第二个目标优化整体 table。 运行 这个查询对整个 table 来说很快。

在这种情况下,postgres 优化器没有注意到简单的 MAX 可以 运行 嵌套循环。或者 postgres 可能无法在聚合子句中使用限制(可以 运行 整个部分 select,用查询过滤的内容)。

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

也许 Window 功能也能帮到你。

查询不严格等价

why does Postgres not recognize that it could use a Index Scan Backward on row basis for the second select?

为了使上下文清晰:

  • 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 索引以 table test1 为目标时,他们会得到一个快速的计划。但不是基于视图(或直接底层连接查询 - 视图不是阻止程序)。在正确位置对 NULL 值进行排序的索引几乎没有任何效果。

我们知道这个查询中的id永远不可能是NULL。该列已定义 NOT NULL。视图中的联接实际上是一个 INNER JOIN,它不能为 id.
引入 NULL我们也知道test.id上的索引不能包含NULL值。
但是 Postgres 查询规划器不是 AI。 (它也不会尝试,这可能很快就会失控。)我看到 两个缺点:

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

不确定,是否可以(轻松)改进。

db<>fiddle here - 展示以上所有内容

索引

Is there anything I could improve on the tables/indexes?

这个索引完全没用:

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

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

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

扭曲的测试用例

测试用例与实际用例相距太远,没有意义。

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

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

但是目前问题中的所有内容都被扭曲了。等待清理干净。