Postgresql генерирует серию дат (производительность) - PullRequest
0 голосов
/ 10 ноября 2018

Используя версию postgresql> 10, я столкнулся с проблемой при создании ряда дат с использованием встроенной функции generate_series. По сути, это не соответствует day of the month правильно.

У меня много разных частот (предоставленных пользователем), которые необходимо рассчитать между заданной датой начала и окончания. Датой начала может быть любая дата и, следовательно, любой день месяца. Это создает проблемы при использовании частот, таких как monthly в сочетании с датой начала 2018-01-31 или 2018-01-30, как показано в выходных данных ниже.

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

Однако после некоторых тестов я увидел, что мое решение отличается по сравнению со встроенным generate_series при использовании (абсурдно) больших диапазонов дат. У кого-нибудь есть понимание того, как это можно улучшить?

TL; DR : если возможно, избегайте циклов, поскольку они снижают производительность, прокрутите вниз, чтобы улучшить реализацию.

Встроенный выход

select generate_series(date '2018-01-31', 
                       date '2018-05-31', 
                       interval '1 month')::date
as frequency;

генерирует: * * тысяча двадцать-два

 frequency
------------
 2018-01-31
 2018-02-28
 2018-03-28
 2018-04-28
 2018-05-28

Как видно из выходных данных, день месяца не учитывается и усекается до минимального дня, встречающегося в пути, в данном случае: 28 due to the month of februari.

Ожидаемый результат

В результате этой проблемы я создал пользовательскую функцию:

create or replace function generate_date_series(
  startsOn date, 
  endsOn date, 
  frequency interval)
returns setof date as $$
declare
  intervalOn date := startsOn;
  count int := 1;
begin
  while intervalOn <= endsOn loop
    return next intervalOn;
    intervalOn := startsOn + (count * frequency);
    count := count + 1;
  end loop;
  return;
end;
$$ language plpgsql immutable;

select generate_date_series(date '2018-01-31', 
                            date '2018-05-31', 
                            interval '1 month')
as frequency;

генерирует:

 frequency
------------
 2018-01-31
 2018-02-28
 2018-03-31
 2018-04-30
 2018-05-31

Сравнение производительности

Независимо от того, какой диапазон дат указан, встроенный generate_series имеет производительность 2 мс в среднем для:

select generate_series(date '1900-01-01', 
                       date '10000-5-31', 
                       interval '1 month')::date 
as frequency;

в то время как пользовательская функция generate_date_series имеет производительность 120 мс в среднем для:

select generate_date_series(date '1900-01-01', 
                            date '10000-5-31', 
                            interval '1 month')::date 
as frequency;

Вопрос

На самом деле, такие диапазоны никогда не появятся, и поэтому это не проблема. Для большинства запросов пользовательский generate_date_series будет иметь ту же производительность. Хотя мне интересно, что вызывает разницу.

Существует ли причина, по которой встроенная функция может достигать постоянной производительности в среднем 2 мс независимо от того, какой диапазон предоставляется?

Есть ли лучший способ реализовать generate_date_series, который работает так же хорошо, как встроенный generate_series?

Улучшена реализация без циклов

(получено из ответа @eurotrash)

create or replace function generate_date_series(startsOn date, endsOn date, frequency interval)
returns setof date as $$
select (startsOn + (frequency * count))::date
from (
  select (row_number() over ()) - 1 as count
  from generate_series(startsOn, endsOn, frequency)
) series
$$ language sql immutable;

с улучшенной реализацией, функция generate_date_series имеет производительность 45 мс в среднем для:

select generate_date_series(date '1900-01-01', 
                            date '10000-5-31', 
                            interval '1 month')::date 
as frequency;

Реализация, предоставляемая @eurotrash, дает мне 80мс в среднем , что, как я полагаю, связано с двойным вызовом функции generate_series.

Ответы [ 3 ]

0 голосов
/ 10 ноября 2018

Вы можете использовать date_trunc и добавить месяц к выводу generate_series, производительность должна быть почти одинаковой.

SELECT 
  (date_trunc('month', dt) + INTERVAL '1 MONTH - 1 day') ::DATE AS frequency 
FROM 
  generate_series(
    DATE '2018-01-31', DATE '2018-05-31', 
    interval '1 MONTH'
  ) AS dt 

Демо

Тест

knayak=# select generate_series(date '2018-01-31',
knayak(#                        date '2018-05-31',
knayak(#                        interval '1 month')::date
knayak-# as frequency;
 frequency
------------
 2018-01-31
 2018-02-28
 2018-03-28
 2018-04-28
 2018-05-28
(5 rows)

Time: 0.303 ms
knayak=#
knayak=#
knayak=# SELECT
knayak-#   (date_trunc('month', dt) + INTERVAL '1 MONTH - 1 day' ):: DATE AS frequency
knayak-# FROM
knayak-#   generate_series(
knayak(#     DATE '2018-01-31', DATE '2018-05-31',
knayak(#     interval '1 MONTH'
knayak(#   ) AS dt
knayak-# ;
 frequency
------------
 2018-01-31
 2018-02-28
 2018-03-31
 2018-04-30
 2018-05-31
(5 rows)

Time: 0.425 ms
0 голосов
/ 10 ноября 2018

Почему ваша функция медленная: вы используете переменные и (что более важно) цикл. Петли медленные. Переменные также означают чтение и запись в эти переменные.

CREATE OR REPLACE FUNCTION generate_date_series_2(starts_on DATE, ends_on DATE, frequency INTERVAL)
        RETURNS SETOF DATE AS
$BODY$
        SELECT (starts_on + (frequency * g))::DATE
        FROM generate_series(0, (SELECT COUNT(*)::INTEGER - 1 FROM generate_series(starts_on, ends_on, frequency))) g;
$BODY$
        LANGUAGE SQL IMMUTABLE;

Концепция в основном такая же, как у вашей функции plpgsql, но с помощью одного запроса вместо цикла. Единственная проблема состоит в том, чтобы решить, сколько итераций необходимо (то есть второй параметр для generate_series). К сожалению, я не мог придумать лучшего способа получить требуемое количество интервалов, кроме как вызвать generate_series для дат и использовать их счет. Конечно, если вы знаете, что ваши интервалы будут когда-либо только определенными значениями, тогда можно будет оптимизировать; однако эта версия обрабатывает любые значения интервала.

В моей системе это примерно на 50% медленнее, чем чистый generate_series, и примерно на 400% быстрее, чем ваша версия plpgsql.

0 голосов
/ 10 ноября 2018

ПЕРЕСМОТРЕННОЕ РЕШЕНИЕ

Это дает мне 97 212 строк менее чем за 7 секунд (приблизительно 0,7 мс на строку), а также поддерживает leap-years, где в феврале 29 дней:

SELECT      t.day_of_month
FROM        (
                SELECT  ds.day_of_month
                        , date_part('day', ds.day_of_month) AS day
                        , date_part('day', ((day_of_month - date_part('day', ds.day_of_month)::INT + 1) + INTERVAL '1' MONTH) - INTERVAL '1' DAY) AS eom
                FROM    (
                            SELECT generate_series( date '1900-01-01', 
                                                    date '10000-12-31', 
                                                    INTERVAL '1 day')::DATE as day_of_month
                        ) AS ds
            ) AS t
            --> REMEMBER to change the day at both places below (eg. 31)
WHERE       t.day = 31 OR (t.day = t.eom AND t.day < 31)

Результирующий вывод: Пожалуйста, убедитесь, что вы изменили день на ОБА КРАСНЫХ числах. Performance Output

Выходные данные:

Data Output

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...