У меня есть таблица (в PostgreSQL 9.4), которая выглядит следующим образом:
CREATE TABLE dates_ranges (kind int, start_date date, end_date date);
INSERT INTO dates_ranges VALUES
(1, '2018-01-01', '2018-01-31'),
(1, '2018-01-01', '2018-01-05'),
(1, '2018-01-03', '2018-01-06'),
(2, '2018-01-01', '2018-01-01'),
(2, '2018-01-01', '2018-01-02'),
(3, '2018-01-02', '2018-01-08'),
(3, '2018-01-05', '2018-01-10');
Теперь я хочу подсчитать для заданных дат и для каждого вида, во сколько строк dates_ranges
попадает каждая дата. Нули могут быть опущены.
Желаемый результат:
+-------+------------+----+
| kind | as_of_date | n |
+-------+------------+----+
| 1 | 2018-01-01 | 2 |
| 1 | 2018-01-02 | 2 |
| 1 | 2018-01-03 | 3 |
| 2 | 2018-01-01 | 2 |
| 2 | 2018-01-02 | 1 |
| 3 | 2018-01-02 | 1 |
| 3 | 2018-01-03 | 1 |
+-------+------------+----+
Я придумал два решения, одно с LEFT JOIN
иGROUP BY
SELECT
kind, as_of_date, COUNT(*) n
FROM
(SELECT d::date AS as_of_date FROM generate_series('2018-01-01'::timestamp, '2018-01-03'::timestamp, '1 day') d) dates
LEFT JOIN
dates_ranges ON dates.as_of_date BETWEEN start_date AND end_date
GROUP BY 1,2 ORDER BY 1,2
и один с LATERAL
, который немного быстрее:
SELECT
kind, as_of_date, n
FROM
(SELECT d::date AS as_of_date FROM generate_series('2018-01-01'::timestamp, '2018-01-03'::timestamp, '1 day') d) dates,
LATERAL
(SELECT kind, COUNT(*) AS n FROM dates_ranges WHERE dates.as_of_date BETWEEN start_date AND end_date GROUP BY kind) ss
ORDER BY kind, as_of_date
Мне интересно, есть ли лучший способ написать этот запрос? А как включить пары date-kind с 0 count?
В действительности существует несколько различных видов, период до пяти лет (1800 дат) и ~ 30 тыс. Строк в dates_ranges
таблице (но это может значительно возрасти).
Там нет индексов. Если быть точным, в моем случае это результат подзапроса, но я хотел ограничить вопрос одной проблемой, поэтому она носит более общий характер.
postgresql
join
postgresql-9.4
functions
BartekCh
источник
источник
(1,2018-01-01,2018-01-15)
и(1,2018-01-20,2018-01-25)
вы хотите принять это во внимание при определении количества перекрывающихся дат?2018-01-31
или2018-01-30
или2018-01-29
в ней , когда первый диапазон имеет все из них?generate_series
являются внешними параметрами - они не обязательно охватывают все диапазоны вdates_ranges
таблице. Что касается первого вопроса, я полагаю, что я его не понимаю - строки в немdates_ranges
независимы, я не хочу определять перекрытие.Ответы:
Следующий запрос также работает, если «отсутствующие нули» в порядке:
но это не быстрее, чем
lateral
версия с небольшим набором данных. Хотя он может масштабироваться лучше, так как соединение не требуется, но вышеприведенная версия агрегирует по всем строкам, поэтому может снова потерять.Следующий запрос пытается избежать ненужной работы, удаляя все серии, которые не перекрываются в любом случае:
- и я должен использовать
overlaps
оператора! Обратите внимание, что вы должны добавитьinterval '1 day'
справа, так как оператор перекрытий считает периоды времени открытыми справа (что довольно логично, потому что дата часто считается меткой времени с компонентом времени полуночи).источник
generate_series
можно ли так использовать. После нескольких тестов у меня есть следующие наблюдения. Ваш запрос действительно хорошо масштабируется с выбранной длиной диапазона - практически нет разницы между 3 годами и 10 годами. Однако в течение более коротких периодов (1 год) мои решения работают быстрее - я предполагаю, что причина в том, что существуют очень длинные диапазоныdates_ranges
(например, 2010-2100), которые замедляют ваш запрос. Ограничениеstart_date
иend_date
внутренний запрос должны помочь. Мне нужно сделать еще несколько тестов.Создайте сетку из всех комбинаций, затем
LATERAL
присоединитесь к вашему столу, например так:Также должно быть максимально быстро.
LEFT JOIN LATERAL ... on true
Сначала у меня было , но в подзапросе есть агрегатc
, поэтому мы всегда получаем строку и можем использоватьCROSS JOIN
также. Нет разницы в производительности.Если у вас есть таблица, содержащая все соответствующие виды , используйте ее вместо генерации списка с подзапросом
k
.Приведение к не
integer
является обязательным. Иначе вы получитеbigint
.Помогут индексы, особенно многоколоночный индекс на
(kind, start_date, end_date)
. Поскольку вы строите подзапрос, это может быть или не быть возможным достичь.Использование функций, возвращающих множество, как
generate_series()
вSELECT
списке, обычно не рекомендуется в версиях Postgres до 10 (если вы точно не знаете, что делаете). Увидеть:Если у вас есть много комбинаций с несколькими строками или без них, эта эквивалентная форма может быть быстрее:
источник
SELECT
списке - я читал, что это не рекомендуется, однако, похоже, что это работает просто отлично, если есть только одна такая функция. Если я уверен, что будет только один, может что-то пойти не так?SELECT
списке работает как положено. Возможно добавьте комментарий, чтобы предупредить против добавления другого. Или переместите его вFROM
список для начала в более старых версиях Postgres. Почему риск осложнений? (Это также стандартный SQL, который не смущает людей, приходящих из других СУБД.)Используя
daterange
типPostgreSQL имеет
daterange
. Используя это довольно просто. Начиная с ваших образцов данных, мы переходим к использованию типа в таблице.Теперь, чтобы запросить его, мы перевернем процедуру и сгенерируем ряд дат, но здесь есть уловка, в которой сам запрос может использовать
@>
оператор contentsment ( ), чтобы проверить, что даты находятся в диапазоне, используя индекс.Обратите внимание, что мы используем
timestamp without time zone
(чтобы остановить DST опасности)Который является подробным перекрытием дня в индексе.
В качестве дополнительного бонуса, с типом daterange вы можете остановить вставку диапазонов, которые перекрываются с другими, используя
EXCLUDE CONSTRAINT
источник
JOIN
я думаю , это слишком много.count(DISTINCT kind)
1
даты2018-01-01
находится в первых двух строках отdates_ranges
, но Ваш запрос дает8
.count(DISTINCT kind)
вы добавилиDISTINCT
ключевое слово там?DISTINCT
ключевым словом он все еще не работает, как ожидалось. Он рассчитывает различные виды для каждой даты, но я хочу подсчитать все строки каждого вида для каждой даты.