Postgres:STABLE函数在常量上多次调用

时间:2016-02-02 18:18:21

标签: sql postgresql database-performance

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

  

STABLE函数不能修改数据库,并保证在单个语句中为所有行赋予相同参数的情况下返回相同的结果

为什么在这种情况下它不会优化对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)

性能

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

1 个答案:

答案 0 :(得分:2)

我开始把它写成评论,但它有点长,所以我把它扩展成答案。

正如this previous answer中所讨论的,Postgres不承诺总是根据STABLEIMMUTABLE注释进行优化,只是它有时可以 / em>这样做。它通过利用某些假设来不同地规划查询来实现这一点。上一个答案的这一部分与您的案例直接相似:

  

这种特殊的改写取决于不变性或稳定性。对where test_multi_calls1(30) != num immutable进行stable查询重写,但仅针对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 $0) -> 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 = $0) Buffers: shared read=541 written=21 Total runtime: 14.707 ms 版本的内容不同:

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

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

为了确认这一点,我们可以将SQL完全取出一个函数:

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 $0)
    ->  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 = $0)
        Buffers: shared read=543 written=19
Total runtime: 14.232 ms

这提供了一个相当长的计划,具有非常相似的性能:

max(d)

需要注意的重要一点是,内部聚合(where)在主Seq Scan(检查VOLATILE子句)的单独节点上执行一次。在这个位置,甚至可以以相同的方式优化{{1}}函数。

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

[注意:所有测试都是在Postgres 9.1上进行的,因为这是我碰巧不得不提供的。]