2016-02-02 1 views
2

У меня есть головоломка производительности Postgresql (версия 9.4). У меня есть функция (prevd), объявленная как STABLE (см. Ниже). Когда я запускаю эту функцию в константе в пункте where, она вызывается несколько раз - вместо одного. Если я правильно понимаю документацию postgres, запрос должен быть оптимизирован для вызова prevd только один раз.Postgres: Функция 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'); 

Плохой план запроса t он выше запрос

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) 

Performance

Запрос выше, на моей машине работает около 3.5с. После изменения prevd на IMMUTABLE он меняется на 0.035.

+1

Возможный дубликат [Почему PostgreSQL вызова мои STABLE/непреложных функции несколько раз?] (HTTP: // stackoverflow.com/questions/8529690/why-is-postgresql-calling-my-stable-immutable-function-multiple-times) - тестовый пример в вопросе отличается, но ответ также охватывает ваш случай. – IMSoP

+0

Мой запрос вызывает функцию STABLE на константе. Запрос по указанному вопросу вызывает функцию IMMUTABLE для не-costant value (по крайней мере, не напрямую) –

+0

Взгляните на * ответ * на связанный вопрос, а не на сам вопрос. Он охватывает почти любую комбинацию стабильного/неизменяемого и постоянного/непостоянного аргумента, в том числе точно так же, как и у вас: «Когда' где test_multi_calls1 (30)! = Num' переписывание запроса произойдет для неизменяемых, но не для просто стабильных функций ». – IMSoP

ответ

2

Я начал писать это как комментарий, но он немного затянулся, поэтому я расширяю его в ответ.

Как обсуждалось в this previous answer, Postgres не обещает всегда оптимизируют на основе STABLE или IMMUTABLE аннотаций, только то, что он может иногда сделать это. Это делается путем планирования запроса по-разному, используя определенные предположения. Эта часть предыдущего ответа прямо аналогична вашему делу:

Этот особый вид переписывания зависит от неизменности или стабильности. С 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 

Поместив его в подзапрос, вы перемещаете вызов функции из предложения 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 $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)) выполняется один раз на отдельном узле из основного Seq Scan (который проверяет предложение where). В этом положении можно оптимизировать и функцию VOLATILE.

Короче говоря, в то время как вы знаете, что запрос вы произведенный может быть оптимизирована за счет функции только один раз, это не соответствует ни одному из шаблонов, планировщик запросов Postgres в Умеет переписывать, поэтому он использует наивный план, который выполняет функцию несколько раз.

[Примечание: все тесты, проведенные на Postgres 9.1, потому что это то, что мне пришлось иметь под рукой.]

+0

Спасибо! Это очень удивительно для меня. Я ожидал, что Postgres в общем расширяет функциональные вызовы в тело функции (и в этом случае эффективно запускает запрос с подзапросом). По крайней мере, функции IMMUTABLE и STABLE, так как они не могут изменять базу данных. –

+0

Вы бы сказали, что использование вызовов функций в соединениях (выберите * из соединения b (...) on .. join c (...) on ...) приведет к более плохую схему запроса, чем запрос, написанный с замещенным b (..) и c (..) с их телами? –

+0

@MarcinKrupowicz Помните, что функции в Postgres не обязательно записываются в SQL; в основном, они будут в формате pl/pgsql, но вы можете подключить другие языки, такие как Perl и т. д. Таким образом, без специальной оптимизации функций, где язык установлен на «sql», поведение по умолчанию должно «выполнять функцию всякий раз, когда вы нужна его ценность ». Там * может * быть более специальных случаев, встроенных, но чем больше сложностей вы добавляете, тем больше риск ошибок или неблагоприятных побочных эффектов. – IMSoP