Запрос PostgreSQL для подсчета/группировки по дням и отображения дней без данных
Мне нужно создать запрос PostgreSQL, который возвращает
- в день
- количество найденных объектов за этот день
Важно, чтобы каждый день отображался в результатах, даже если в этот день не было найдено объектов. (Это обсуждалось ранее, но я не смог заставить вещи работать в моем конкретном случае.)
Во-первых, я нашел sql-запрос для создания диапазона дней, с которым я могу присоединиться:
SELECT to_char(date_trunc('day', (current_date - offs)), 'YYYY-MM-DD')
AS date
FROM generate_series(0, 365, 1)
AS offs
Результаты в:
date
------------
2013-03-28
2013-03-27
2013-03-26
2013-03-25
...
2012-03-28
(366 rows)
Теперь я пытаюсь присоединиться к таблице с именем "sharer_emailshare", которая имеет столбец "created":
Table 'public.sharer_emailshare'
column | type
-------------------
id | integer
created | timestamp with time zone
message | text
to | character varying(75)
Вот лучший GROUP BY
запрос, который у меня есть до сих пор:
SELECT d.date, count(se.id) FROM (
select to_char(date_trunc('day', (current_date - offs)), 'YYYY-MM-DD')
AS date
FROM generate_series(0, 365, 1)
AS offs
) d
JOIN sharer_emailshare se
ON (d.date=to_char(date_trunc('day', se.created), 'YYYY-MM-DD'))
GROUP BY d.date;
Результаты:
date | count
------------+-------
2013-03-27 | 11
2013-03-24 | 2
2013-02-14 | 2
(3 rows)
Желаемые результаты:
date | count
------------+-------
2013-03-28 | 0
2013-03-27 | 11
2013-03-26 | 0
2013-03-25 | 0
2013-03-24 | 2
2013-03-23 | 0
...
2012-03-28 | 0
(366 rows)
Если я правильно понимаю это, потому что я использую простой (подразумеваемый INNER
) JOIN
, и это ожидаемое поведение, так как обсуждается в документах postgres.
Я просмотрел десятки решений StackOverflow, и все те, у кого есть рабочие запросы, кажутся специфичными для MySQL/Oracle/MSSQL, и мне сложно перевести их на PostgreSQL.
Парень, задающий этот вопрос, нашел свой ответ с помощью Postgres, но положил его на ссылку pastebin, которая истекла некоторое время назад.
Я пытался переключиться на LEFT OUTER JOIN
, RIGHT JOIN
, RIGHT OUTER JOIN
, CROSS JOIN
, использовать оператор CASE
для sub в другом значении, если null, COALESCE
для предоставления значения по умолчанию и т.д., но я не смог использовать их таким образом, чтобы получить то, что мне нужно.
Любая помощь приветствуется! И я обещаю, что скоро приеду к чтению этой гигантской книги PostgreSQL;)
Ответы
Ответ 1
Вам просто нужно left outer join
вместо внутреннего соединения:
SELECT d.date, count(se.id)
FROM (SELECT to_char(date_trunc('day', (current_date - offs)), 'YYYY-MM-DD') AS date
FROM generate_series(0, 365, 1) AS offs
) d LEFT OUTER JOIN
sharer_emailshare se
ON d.date = to_char(date_trunc('day', se.created), 'YYYY-MM-DD'))
GROUP BY d.date;
Ответ 2
Расширяя полезный ответ Гордона Линоффа, я бы предложил пару улучшений, таких как:
- Используйте
::date
вместо date_trunc('day', ...)
- Присоединяется к типу даты, а не к типу символа (он чище).
- Используйте определенные диапазоны дат, чтобы их было легче изменить позже. В этом случае я выбираю за год до самой последней записи в таблице - то, что не могло быть легко выполнено с другим запросом.
- Вычислить итоговые значения для произвольного подзапроса (используя CTE). Вам просто нужно указать интересующий столбец типа даты и назвать его date_column.
- Включить столбец для общей суммы. (Почему бы и нет?)
Здесь мой запрос:
WITH dates_table AS (
SELECT created::date AS date_column FROM sharer_emailshare WHERE showroom_id=5
)
SELECT series_table.date, COUNT(dates_table.date_column), SUM(COUNT(dates_table.date_column)) OVER (ORDER BY series_table.date) FROM (
SELECT (last_date - b.offs) AS date
FROM (
SELECT GENERATE_SERIES(0, last_date - first_date, 1) AS offs, last_date from (
SELECT MAX(date_column) AS last_date, (MAX(date_column) - '1 year'::interval)::date AS first_date FROM dates_table
) AS a
) AS b
) AS series_table
LEFT OUTER JOIN dates_table
ON (series_table.date = dates_table.date_column)
GROUP BY series_table.date
ORDER BY series_table.date
Я протестировал запрос, и он дает те же результаты, плюс столбец для совокупного итога.
Ответ 3
На основании ответа Гордона Линофф я понял, что другая проблема заключалась в том, что у меня было предложение WHERE
, о котором я не упоминал в исходном вопросе.
Вместо голой WHERE
я сделал подзапрос:
SELECT d.date, count(se.id) FROM (
select to_char(date_trunc('day', (current_date - offs)), 'YYYY-MM-DD')
AS date
FROM generate_series(0, 365, 1)
AS offs
) d
LEFT OUTER JOIN (
SELECT * FROM sharer_emailshare
WHERE showroom_id=5
) se
ON (d.date=to_char(date_trunc('day', se.created), 'YYYY-MM-DD'))
GROUP BY d.date;
Ответ 4
Я постараюсь дать ответ, который включает некоторые объяснения. Я начну с самого маленького строительного блока и поработаю.
Если вы выполните запрос, подобный следующему:
SELECT series.number FROM generate_series(0, 9) AS series(number)
Вы получите такой вывод:
number
--------
0
1
2
3
4
5
6
7
8
9
(10 rows)
Это можно превратить в такие даты:
SELECT CURRENT_DATE + sequential_dates.date AS date
FROM generate_series(0, 9) AS sequential_dates(date)
Который выдаст такой результат:
date
------------
2019-09-29
2019-09-30
2019-10-01
2019-10-02
2019-10-03
2019-10-04
2019-10-05
2019-10-06
2019-10-07
2019-10-08
(10 rows)
Затем вы можете выполнить такой запрос (например), присоединив исходный запрос как подзапрос к любой таблице, в которой вы в конечном итоге заинтересованы:
SELECT sequential_dates.date,
COUNT(calendar_items.*) AS calendar_item_count
FROM (SELECT CURRENT_DATE + sequential_dates.date AS date
FROM generate_series(0, 9) AS sequential_dates(date)) sequential_dates
LEFT JOIN calendar_items ON calendar_items.starts_at::date = sequential_dates.date
GROUP BY sequential_dates.date
Который выдаст такой результат:
date | calendar_item_count
------------+---------------------
2019-09-29 | 1
2019-09-30 | 8
2019-10-01 | 15
2019-10-02 | 11
2019-10-03 | 1
2019-10-04 | 12
2019-10-05 | 0
2019-10-06 | 0
2019-10-07 | 27
2019-10-08 | 24