Создание временных рядов между двумя датами в PostgreSQL

92

У меня есть такой запрос, который красиво генерирует серию дат между двумя заданными датами:

select date '2004-03-07' + j - i as AllDate 
from generate_series(0, extract(doy from date '2004-03-07')::int - 1) as i,
     generate_series(0, extract(doy from date '2004-08-16')::int - 1) as j

Он генерирует 162 даты между 2004-03-07и 2004-08-16и тем, что я хочу. Проблема с этим кодом в том, что он не даст правильного ответа, если две даты относятся к разным годам, например, когда я пытаюсь 2007-02-01и 2008-04-01.

Есть ли лучшее решение?

f.ashouri
источник

Ответы:

175

Может быть выполнено без преобразования в / из int (но вместо этого в / из метки времени)

SELECT date_trunc('day', dd):: date
FROM generate_series
        ( '2007-02-01'::timestamp 
        , '2008-04-01'::timestamp
        , '1 day'::interval) dd
        ;
Wildplasser
источник
3
зачем date_truncнужен?
Idefixx
2
Это просто презентация. Это исключает печать временной части метки времени, которая в данном случае всегда нулевая.
beemtee
73

Чтобы создать серию дат это оптимальный способ:

SELECT t.day::date 
FROM   generate_series(timestamp '2004-03-07'
                     , timestamp '2004-08-16'
                     , interval  '1 day') AS t(day);
  • Дополнительного date_trunc()не требуется. Приведение к date( day::date) делает это неявно.

  • Но также нет смысла преобразовывать литералы даты в dateкачестве входного параметра. Наоборот, timestampэто лучший выбор . Преимущество в производительности небольшое, но нет причин не брать его. И вам не нужно без нужды задействовать правила перехода на летнее время (DST) вместе с преобразованием из dateв timestamp with time zoneи обратно. Увидеть ниже.

Эквивалентный, менее явный короткий синтаксис:

SELECT day::date 
FROM   generate_series(timestamp '2004-03-07', '2004-08-16', '1 day') day;

Или с функцией возврата набора в SELECTсписке:

SELECT generate_series(timestamp '2004-03-07', '2004-08-16', '1 day')::date AS day;

ASКлючевое слово требуется в последнем варианте, Postgres бы извратить псевдоним столбца в dayпротивном случае. И я бы не советовал этот вариант до Postgres 10 - по крайней мере, с более чем одной функцией возврата набора в одном SELECTсписке:

(Кроме того, последний вариант обычно самый быстрый с небольшим отрывом.)

Почему timestamp [without time zone]?

Существует ряд перегруженных вариантов generate_series(). В настоящее время (Postgres 11):

SELECT oid::regprocedure   AS function_signature
     , prorettype::regtype AS return_type
FROM   pg_proc
where  proname = 'generate_series';
function_signature | return_type                
: ------------------------------------------------- ------------------------------- | : --------------------------
generate_series (целое, целое, целое) | целое число                    
generate_series (целое, целое) | целое число                    
generate_series (bigint, bigint, bigint) | Bigint                     
generate_series (bigint, bigint) | Bigint                     
generate_series (числовой, числовой, числовой) | числовой                    
generate_series (числовой, числовой) | числовой                    
generate_series (отметка времени без часового пояса, отметка времени без часового пояса, интервал) | отметка времени без часового пояса
generate_series (временная метка с часовым поясом, временная метка с часовым поясом, интервал) | метка времени с часовым поясом

( numericварианты были добавлены в Postgres 9.5.) Соответствующие два последних выделены жирным шрифтом take и return timestamp/ timestamptz.

Нет варианта взять или вернутьdate . Для возврата необходимо явное приведение date. Вызов с timestampаргументами преобразуется в лучший вариант напрямую, без перехода в правила разрешения типов функций и без дополнительного приведения для ввода.

timestamp '2004-03-07'совершенно верно, кстати. Пропущенная часть времени по умолчанию соответствует 00:00формату ISO.

Благодаря разрешению типа функции мы все еще можем пройти date. Но это требует от Postgres дополнительной работы. Существует неявное приведение от dateдо, timestampа также от одного dateдо timestamptz. Было бы неоднозначным, но timestamptzявляется «предпочтительным» среди «типов даты / времени». Таким образом, совпадение определяется на шаге 4d. :

Просмотрите всех кандидатов и оставьте те, которые принимают предпочтительные типы (из категории типа входных данных) в большинстве позиций, где потребуется преобразование типов. Оставьте всех кандидатов, если ни один из них не принимает предпочтительные типы. Если остается только один кандидат, используйте его; иначе переходите к следующему шагу.

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

Я добавил в скрипт демонстрационные ролики, показывающие более дорогой план запроса:

db <> скрипка здесь

Связанный:

Эрвин Брандштеттер
источник
7
Еще более короткая версия:SELECT generate_series(timestamp '2004-03-07', '2004-08-16', '1 day') :: DATE AS day;
Вацлав Кужель 07
Что означает синтаксис t (день)?
ренданг
@rendang: AS t(day)в SELECT * FROM func() AS t(day)это таблица и столбец псевдоним. В ASэтом контексте ключевое слово является необязательным шумом. См .: stackoverflow.com/a/20230716/939860
Эрвин Брандштеттер
35

Вы можете создавать ряды непосредственно с датами. Не нужно использовать целые числа или временные метки:

select date::date 
from generate_series(
  '2004-03-07'::date,
  '2004-08-16'::date,
  '1 day'::interval
) date;
фбонетти
источник
В зависимости от вашего часового пояса это может дать неожиданный результат. У меня была такая проблема. Вместо этого используйте метку времени. SET session TIME zone 'America / Sao_Paulo' SELECT d :: date FROM generate_series ('2019-11-01' :: date, '2019-11-03' :: date, '1 day') d ВЫБРАТЬ d :: date FROM generate_series ('2019-11-01' :: date, '2019-11-04' :: date, '1 day') d
palhares
1

Вы также можете использовать это.

select generate_series  ( '2012-12-31'::timestamp , '2018-10-31'::timestamp , '1 day'::interval) :: date 
Meyyappan
источник