Postgres:多次调用 STABLE 函数

Postgres: STABLE function called multiple times on constant

我遇到了 Postgresql(9.4 版)性能难题。我有一个函数 (prevd) 声明为 STABLE(见下文)。当我 运行 这个函数在 where 子句中的常量上时,它被多次调用 - 而不是一次。 如果我正确理解 postgres 文档,查询应该优化为只调用一次 prevd

A STABLE function cannot modify the database and is guaranteed to return the same results given the same arguments for all rows within a single statement

为什么在这种情况下不优化对 prevd 的调用? 我不希望对同一参数使用 prevd 的所有后续查询调用一次 prevd(就像它是 IMMUTABLE 一样)。我希望 postgres 只需调用一次 prevd('2015-12-12')

即可为我的查询创建一个计划

请找到下面的代码:

架构

create table somedata(d date, number double precision);
create table dates(d date);

insert into dates
select generate_series::date
from   generate_series('2015-01-01'::date, '2015-12-31'::date, '1 day');

insert into somedata
select '2015-01-01'::date + (random() * 365 + 1)::integer, random()
from   generate_series(1, 100000);

create or replace function prevd(date_ date)
returns date
language sql
stable
as $$
  select max(d) from dates where d < date_;
$$

慢查询

select avg(number) from somedata where d=prevd('2015-12-12');

上面查询的查询计划很差

 Aggregate  (cost=28092.74..28092.75 rows=1 width=8) (actual time=3532.638..3532.638 rows=1 loops=1)
   Output: avg(number)
   ->  Seq Scan on public.somedata  (cost=0.00..28091.43 rows=525 width=8) (actual time=10.210..3532.576 rows=282 loops=1)
         Output: d, number
         Filter: (somedata.d = prevd('2015-12-12'::date))
         Rows Removed by Filter: 99718
 Planning time: 1.144 ms
 Execution time: 3532.688 ms
(8 rows)

性能

上面的查询,在我的机器上 运行s 大约 3.5s。将 prevd 更改为 IMMUTABLE 后,它正在更改为 0.035s。

我开始将其作为评论来写,但它有点长,所以我将其扩展为一个答案。

this previous answer 中所述,Postgres 不承诺 总是 基于 STABLEIMMUTABLE 注释进行优化,只是它可以有时这样做。它通过利用某些假设以不同方式规划查询来实现这一点。之前答案的这一部分直接类似于你的情况:

This particular sort of rewriting depends upon immutability or stability. With where test_multi_calls1(30) != num query re-writing will happen for immutable but not for merely stable functions.

如果你把函数改成IMMUTABLE,再看看查询计划,你会发现它做的重写真的很激进:

Seq Scan on public.somedata  (cost=0.00..1791.00 rows=272 width=12) (actual time=0.036..14.549 rows=270 loops=1)
  Output: d, number
  Filter: (somedata.d = '2015-12-11'::date)
  Buffers: shared read=541 written=14
Total runtime: 14.589 ms

它实际上运行在计划查询时使用函数,并在查询执行之前替换值 .使用 STABLE 函数,这种优化显然不合适 - 数据可能会在计划和执行查询之间发生变化。

评论中提到此查询会产生优化计划:

select avg(number) from somedata where d=(select prevd(date '2015-12-12'));

这很快,但请注意,该计划看起来与 IMMUTABLE 版本所做的完全不同:

Aggregate  (cost=1791.69..1791.70 rows=1 width=8) (actual time=14.670..14.670 rows=1 loops=1)
  Output: avg(number)
  Buffers: shared read=541 written=21
  InitPlan 1 (returns [=12=])
    ->  Result  (cost=0.00..0.01 rows=1 width=0) (actual time=0.001..0.001 rows=1 loops=1)
          Output: '2015-12-11'::date
  ->  Seq Scan on public.somedata  (cost=0.00..1791.00 rows=273 width=8) (actual time=0.026..14.589 rows=270 loops=1)
        Output: d, number
        Filter: (somedata.d = [=12=])
        Buffers: shared read=541 written=21
Total runtime: 14.707 ms

通过将其放入子查询,您将函数调用从 WHERE 子句移动到 SELECT 子句。更重要的是,子查询可以总是执行一次并被其余查询使用;所以这个函数在计划的一个单独节点中是 运行 一次。

为了证实这一点,我们可以将 SQL 完全从函数中取出:

select avg(number) from somedata where d=(select max(d) from dates where d <  '2015-12-12');

这给出了一个相当长的计划,但性能非常相似:

Aggregate  (cost=1799.12..1799.13 rows=1 width=8) (actual time=14.174..14.174 rows=1 loops=1)
  Output: avg(somedata.number)
  Buffers: shared read=543 written=19
  InitPlan 1 (returns [=14=])
    ->  Aggregate  (cost=7.43..7.44 rows=1 width=4) (actual time=0.150..0.150 rows=1 loops=1)
          Output: max(dates.d)
          Buffers: shared read=2
          ->  Seq Scan on public.dates  (cost=0.00..6.56 rows=347 width=4) (actual time=0.015..0.103 rows=345 loops=1)
                Output: dates.d
                Filter: (dates.d < '2015-12-12'::date)
                Buffers: shared read=2
  ->  Seq Scan on public.somedata  (cost=0.00..1791.00 rows=273 width=8) (actual time=0.190..14.098 rows=270 loops=1)
        Output: somedata.d, somedata.number
        Filter: (somedata.d = [=14=])
        Buffers: shared read=543 written=19
Total runtime: 14.232 ms

需要注意的重要一点是内部聚合(max(d))在与主 Seq Scan(检查 where 子句)不同的节点上执行一次。在这个位置,即使是VOLATILE函数也可以用同样的方式优化。

简而言之,虽然 知道你生成的查询可以通过只执行一次函数来优化,但它不匹配 Postgres 查询的任何模式planner 知道如何重写,所以它使用了一个朴素的计划,运行s 函数多次。

[注:所有测试都是在Postgres 9.1上进行的,因为正好我手上有这个。]