Как агрегировать данные за несколько лет в MM-DD, игнорируя год

Postgres версии 9.4.18, PostGIS версии 2.2.

Вот таблицы, с которыми я работаю (и вряд ли они могут внести существенные изменения в структуру таблицы):

Таблица ltg_data (с 1988 по 2018 год):

 Column   |           Type           | Modifiers 
----------+--------------------------+-----------
intensity | integer                  | not null
time      | timestamp with time zone | not null
lon       | numeric(9,6)             | not null
lat       | numeric(8,6)             | not null
ltg_geom  | geometry(Point,4269)     | 
Indexes:
"ltg_data2_ltg_geom_idx" gist (ltg_geom)
"ltg_data2_time_idx" btree ("time")

Размер ltg_data (~ 800M строк):

ltg=# select pg_relation_size('ltg_data');
pg_relation_size 
------------------
 149729288192

Таблица counties:

  Column   |            Type             |                       Modifiers                      
-----------+-----------------------------+--------------------------------- -----------------------
gid        | integer                     | not null default nextval('counties_gid_seq'::regclass)
objectid_1 | integer                     | 
objectid   | integer                     | 
state      | character varying(2)        | 
cwa        | character varying(9)        | 
countyname | character varying(24)       | 
fips       | character varying(5)        | 
time_zone  | character varying(2)        | 
fe_area    | character varying(2)        | 
lon        | double precision            | 
lat        | double precision            | 
the_geom   | geometry(MultiPolygon,4269) | 
Indexes:
"counties_pkey" PRIMARY KEY, btree (gid)
"counties_gix" gist (the_geom)
"county_cwa_idx" btree (cwa)
"countyname_cwa_idx" btree (countyname)

Желаемый результат: мне нужен временной ряд с одной строкой для каждого дня года в формате MM-DD без учета года: 01-01, 01-02, 01-03, ..., 12–31. И количество строк в таблице ltg_data для каждого дня в году. Я также в конечном итоге хочу одно и то же для каждого часа каждого дня в году («ММ-ДД-ЧЧ»).

Это должно выполняться с помощью оператора group by, но мне трудно объединить "большую" таблицу с днями, сгенерированными с помощью generate_series().

MM-DD  | total_count   
-------+------------
12-22  |       9
12-23  |       0
12-24  |       0
12-25  |       0
12-26  |      23
12-27  |       0
12-28  |       5
12-29  |       0
12-30  |       0
12-31  |       0

Некоторые из моих многочисленных попыток запросов:

SELECT date_trunc('day', d),
   count(a.lat) AS strikes
FROM generate_series('2017-01-01', '2018-12-31', interval '1 day') AS d
LEFT JOIN
(SELECT date_trunc('day', TIME) AS day_of_year,
      ltg_data.lat
 FROM ltg_data
 JOIN counties ON ST_contains(counties.the_geom, ltg_data.ltg_geom)
 WHERE cwa = 'MFR' ) AS a ON d = day_of_year
GROUP BY d
ORDER BY d ASC;

Но это не игнорирует год. Я не должен удивляться, потому что "день" в date_trunc все еще учитывает год, я думаю.

2017-12-27 00:00:00-08 |       0
2017-12-28 00:00:00-08 |       0
2017-12-29 00:00:00-08 |       0
2017-12-30 00:00:00-08 |       0
2017-12-31 00:00:00-08 |       0
2018-01-01 00:00:00-08 |       0
2018-01-02 00:00:00-08 |       12
2018-01-03 00:00:00-08 |       0

И этот запрос, в котором я пытаюсь преобразовать данные из generate_series() в text в формате «DD-MM», чтобы присоединиться к таблице ltg_data в формате text. Говорит, что типы данных не совпадают. Я тоже пробовал extract, так как это может дать "doy" и "hour", которые будут работать, но я тоже не могу сопоставить типы данных в этом запросе. Трудно сделать эту "generate_series" двойной точностью.

SELECT to_char(d, 'MM-DD') AS DAY,
   count(a.lat) AS strikes
FROM
(SELECT generate_series('2017-01-01', '2018-12-31', interval '1 day') AS d) 
AS f
LEFT JOIN
(SELECT to_char(TIME, 'MM-DD') AS day_of_year,
      ltg_data.lat
FROM ltg_data
JOIN counties ON ST_contains(counties.the_geom, ltg_data.ltg_geom)
WHERE cwa = 'MFR' ) AS a ON f = day_of_year
GROUP BY d
ORDER BY d ASC;

Результат:

ERROR:  operator does not exist: record = text
LINE 4: ON f = day_of_year group by d order by d asc;
         ^
HINT:  No operator matches the given name and argument type(s). You might 
need to add explicit type casts.

Заключение. Я стремлюсь получать ежедневные и ежечасные общие подсчеты, охватывающие много лет, но сгруппированные по «ММ-ДД» и «ММ-ДД-ЧЧ» (без учета года), с отображением результатов запроса все дни / часы, даже если они равны нулю.

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


person user1610717    schedule 16.05.2018    source источник
comment
дикая догадка: _1 _? ..   -  person Vao Tsun    schedule 16.05.2018
comment
My desired result is to left join .... Нет. Сначала объясните желаемый результат простым английским языком. Не начинайте с техник, которые у вас есть в голове, чтобы этого добиться. Определите ваши временные рамки (какие строки из таблицы будут учитываться) и какие дни должны быть в результате.   -  person Erwin Brandstetter    schedule 16.05.2018
comment
Извини за это. Я добавил версию сейчас в начало вопроса. Я перепробовал много запросов на generate_series. Все, что я хочу, чтобы generate_series делала, это генерировать серию месяцев и дней (а также месяцев, дней и часов для следующего запроса, который я пытаюсь) ... чтобы оставить соединение с данными, которые у меня есть, которые охватывают 30 лет. Я не должен был помещать туда данные за 2 года generate_series, потому что это немного вводит в заблуждение. Кажется, мне сложно создать серию, которая не соответствует определенному времени. Я хочу присоединиться к месяцу и дню (день года) и, в конце концов, часу ... игнорируя год.   -  person user1610717    schedule 16.05.2018
comment
Редактировать вопрос о желаемом результате ... на простой английский.   -  person user1610717    schedule 16.05.2018


Ответы (1)


По сути, чтобы сократить год, to_char(time, 'MMDD'), как вы уже пробовали, выполняет свою работу. Вы просто забыли также применить его к отметкам времени, созданным с помощью generate_series() перед присоединением. И еще несколько мелких деталей.

Для упрощения и для повышения производительности и удобства я предлагаю эту простую функцию для вычисления integer из шаблона «MMDD» заданного timestamp.

CREATE FUNCTION f_mmdd(date) RETURNS int LANGUAGE sql IMMUTABLE AS
'SELECT (EXTRACT(month FROM $1) * 100 + EXTRACT(day FROM $1))::int';

Сначала я использовал to_char(time, 'MMDD'), но перешел на приведенное выше выражение, которое оказалось самым быстрым в различных тестах.

db ‹> скрипт здесь

Его можно использовать в индексах выражений, поскольку он определен IMMUTABLE. И он по-прежнему позволяет встраивать функцию, потому что он использует только EXTRACT (xyz FROM date), который реализован с помощью функции IMMUTABLE date_part(text, date) внутренне. (Обратите внимание, что datepart(text, timestamptz) - это только STABLE).

Тогда этот тип запроса выполняет свою работу:

SELECT d.mmdd, COALESCE(ct.ct, 0) AS total_count
FROM  (
   SELECT f_mmdd(d::date) AS mmdd  -- ignoring the year
   FROM   generate_series(timestamp '2018-01-01'  -- any dummy year
                        , timestamp '2018-12-31'
                        , interval '1 day') d
   ) d
LEFT  JOIN (
   SELECT f_mmdd(time::date) AS mmdd, count(*) AS ct
   FROM   counties c
   JOIN   ltg_data d ON ST_contains(c.the_geom, d.ltg_geom)
   WHERE  cwa = 'MFR'
   GROUP  BY 1
   ) ct USING (mmdd)
ORDER  BY 1;

Поскольку time (я бы использовал другое имя столбца) является типом данных timestamptz, приведение time::date зависит от настройки часового пояса вашего текущего сеанса. («Дни» определяются часовым поясом, в котором вы находитесь.) Чтобы получить неизменные (но более медленные) результаты, используйте конструкцию AT TIME ZONE с часовым поясом name, например:

SELECT f_mmdd((time AT TIME ZONE 'Europe/Vienna')::date) ...

Подробности:

Отформатируйте mmdd как хотите для отображения.

Приведение к integer необязательно для данного конкретного запроса. Но поскольку вы планируете выполнять все виды запросов, вам понадобится индекс для выражения:

CREATE INDEX ltg_data_mmdd_idx ON event(f_mmdd(time));

(Не требуется для этого запроса.)
integer для этой цели немного быстрее. И для этого вам понадобится (в противном случае необязательная) оболочка функции, поскольку to_char() определена только STABLE, но нам нужно IMMUTABLE для индекса. Обновленное выражение (EXTRACT(month FROM $1) * 100 + EXTRACT(day FROM $1))::int - IMMUTABLE, но оболочка функции по-прежнему удобна.

Связанный:

person Erwin Brandstetter    schedule 16.05.2018
comment
Большое спасибо, Эрвин. В основном это имеет смысл ... прочитал кое-что. Я только что воспользовался своей первой функцией! Я знаю, ужасно так говорить. Не слишком ли много спрашивать, как я должен создать функцию, которая будет делать то же самое, но часами? Я создал функцию, используя CREATE FUNCTION f_mmddhh (date) RETURNS int LANGUAGE sql IMMUTABLE as $$ SELECT to_char ($ 1, 'MMDDHH') :: int $$, но она все еще возвращает итоги за день и всего 12 секунд в час. Я прочитал весь ваш пост, но часами не мог понять, как мне его использовать. Спасибо за любую помощь. Я могу опубликовать новый вопрос, если он не имеет отношения к делу. - person user1610717; 16.05.2018
comment
Я изменил :: date на :: timestamp в функции и запросе, и теперь я получаю разумные результаты, но он показывает только 12 часов и дублирует часы. Работаю над этим. - person user1610717; 16.05.2018
comment
Хорошо, думаю, у меня это есть ... на случай, если это кому-то поможет ... Мне нужно было использовать HH24 вместо HH, чтобы получить полный 24-часовой период. - person user1610717; 16.05.2018
comment
@ user1610717: Совсем не страшно задать вопрос. Это сайт вопросов и ответов! Но сделайте это в другом вопросе, комментарии не к месту. Вы всегда можете ссылаться на этот для контекста и оставить здесь комментарий, чтобы дать обратную ссылку (и привлечь мое внимание). - person Erwin Brandstetter; 17.05.2018
comment
Разобрался для HH24. Мне не только нужно было использовать HH24, но мне нужно было использовать f_mmddhh (временная метка), потому что временная метка работает с часами, а дата - нет. Наверное, очевидно, но на случай, если это кому-то понадобится. Еще раз спасибо! Если у вас возникнут дополнительные вопросы, я опубликую как полностью новый вопрос. - person user1610717; 19.05.2018
comment
@ user1610717: Я обнаружил, что функция, основанная на (EXTRACT(month FROM $1) * 100 + EXTRACT(day FROM $1))::int, работает лучше, поскольку позволяет встраивать функции. Считайте обновление. - person Erwin Brandstetter; 20.05.2018