Найшвидший спосіб підрахувати, скільки діапазонів дат охоплює кожну дату з серії


12

У мене є таблиця (в 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

Мені цікаво, чи є кращий спосіб написати цей запит? І як включити пари-дату з числом 0?

Насправді існує кілька різних видів, період до п'яти років (1800 дат) і ~ 30 к рядків у dates_ranges таблиці (але він може значно зрости).

Індекси немає. Якщо бути точним, в моєму випадку це результат підзапиту, але я хотів обмежити питання однією проблемою, тому це більш загальне.


Що робити, якщо діапазони таблиці не перетинаються або торкаються. Наприклад, якщо у вас є діапазон, де (вид, початок, кінець) = (1,2018-01-01,2018-01-15)і (1,2018-01-20,2018-01-25)ви хочете це врахувати, визначаючи, скільки дат перекриття у вас є?
Еван Керролл

Я також розгублений, чому ваш стіл маленький? Чому не 2018-01-31чи 2018-01-30або 2018-01-29в ній , коли перший діапазон має всі з них?
Еван Керролл

Дати @EvanCarroll - generate_seriesце зовнішні параметри - вони не обов'язково охоплюють усі діапазони dates_rangesтаблиці. Щодо першого питання, я вважаю, що я його не розумію - рядки в dates_rangesнезалежних, я не хочу визначати збіги.
BartekCh

Відповіді:


4

Наступний запит також працює, якщо "відсутні нулі" в порядку:

select *
from (
  select
    kind,
    generate_series(start_date, end_date, interval '1 day')::date as d,
    count(*)
  from dates_ranges
  group by 1, 2
) x
where d between date '2018-01-01' and date '2018-01-03'
order by 1, 2;

але це не швидше ніж lateral версія з невеликим набором даних. Хоча це може бути кращим масштабом, оскільки не потрібно з'єднувати, але вищевказана версія збирається у всіх рядках, тому вона може знову втратити.

Наступний запит намагається уникнути зайвої роботи, видаляючи будь-які серії, які все одно не перекриваються:

select
  kind,
  generate_series(greatest(start_date, date '2018-01-01'), least(end_date, date '2018-01-03'), interval '1 day')::date as d,
  count(*)
from dates_ranges
where (start_date, end_date + interval '1 day') overlaps (date '2018-01-01', date '2018-01-03' + interval '1 day')
group by 1, 2
order by 1, 2;

- і я повинен користуватися overlapsоператором! Зауважте, що ви повинні додати interval '1 day'праворуч, оскільки оператор перекриття вважає часові періоди відкритими праворуч (що досить логічно, оскільки дата часто вважається часовою позначкою із часовою складовою опівночі).


Добре, я не знав, що generate_seriesможна так використовувати. Після кількох тестів у мене є наступні спостереження. Ваш запит дійсно добре масштабується із вибраною довжиною діапазону - різниці між періодом від 3 до 10 років практично немає. Однак на менші терміни (1 рік) мої рішення швидші - я здогадуюсь, що причина полягає в тому, що існують деякі дійсно великі діапазони dates_ranges(наприклад, 2010-2100), які сповільнюють ваш запит. Обмеження start_dateта end_dateвнутрішній запит мають допомогти. Мені потрібно зробити ще кілька тестів.
BartekCh

6

І як включити пари-дату з числом 0?

Створіть сітку з усіх комбінацій, а потім LATERAL приєднайтесь до таблиці, наприклад:

SELECT k.kind, d.as_of_date, c.n
FROM  (SELECT DISTINCT kind FROM dates_ranges) k
CROSS  JOIN (
   SELECT d::date AS as_of_date
   FROM   generate_series(timestamp '2018-01-01', timestamp '2018-01-03', interval '1 day') d
   ) d
CROSS  JOIN LATERAL (
   SELECT count(*)::int AS n
   FROM   dates_ranges
   WHERE  kind = k.kind
   AND    d.as_of_date BETWEEN start_date AND end_date
   ) c
ORDER  BY k.kind, d.as_of_date;

Також повинні бути максимально швидкими.

У мене було LEFT JOIN LATERAL ... on trueспочатку, але в підзапиті є сукупність c, тому ми завжди отримуємо рядок і можемо використовуватиCROSS JOIN . Немає різниці у продуктивності.

Якщо у вас є таблиця з усіма релевантними видами , використовуйте її замість генерування списку з підзапитомk .

У ролях integerнеобов’язково. Ще ви отримаєтеbigint .

Індекси допоможуть, особливо багатокольоровий індекс на (kind, start_date, end_date) . Оскільки ви будуєте підзапит, цього можна зробити, а може і не зробити.

Використання функцій повернення набору, таких як generate_series()у SELECTсписку, як правило, не рекомендується у версіях Postgres до 10 (якщо ви точно не знаєте, що робите). Подивитися:

Якщо у вас багато комбінацій з кількома рядками або відсутні, ця еквівалентна форма може бути швидшою:

SELECT k.kind, d.as_of_date, count(dr.kind)::int AS n
FROM  (SELECT DISTINCT kind FROM dates_ranges) k
CROSS JOIN (
   SELECT d::date AS as_of_date
   FROM   generate_series(timestamp '2018-01-01', timestamp '2018-01-03', interval '1 day') d
   ) d
LEFT   JOIN dates_ranges dr ON dr.kind = k.kind
                           AND d.as_of_date BETWEEN dr.start_date AND dr.end_date
GROUP  BY 1, 2
ORDER  BY 1, 2;

Що стосується функцій повернення набору в SELECTсписку - я читав, що це не доцільно, однак, схоже, воно працює чудово, якщо є лише одна така функція. Якщо я впевнений, що буде лише один, може щось піти не так?
BartekCh

@BartekCh: Один SRF у SELECTсписку працює як очікувалося. Можливо, додайте коментар, щоб застерегти від додавання іншого. Або перемістіть його до FROMсписку, щоб почати з більш старих версій Postgres. Чому ризикують ускладненнями? (Це також стандартний SQL і не збиває з пантелику людей, що надходять з інших RDBMS.)
Ервін Брандстеттер

1

Використання daterangeтипу

PostgreSQL має a daterange. Використовувати його досить просто. Починаючи з ваших зразкових даних, ми переходимо до використання типу на таблиці.

BEGIN;
  ALTER TABLE dates_ranges ADD COLUMN myrange daterange;
  UPDATE dates_ranges
    SET myrange = daterange(start_date, end_date, '[]');
  ALTER TABLE dates_ranges
    DROP COLUMN start_date,
    DROP COLUMN end_date;
COMMIT;

-- Now you can create GIST index on it...
CREATE INDEX ON dates_ranges USING gist (myrange);

TABLE dates_ranges;
 kind |         myrange         
------+-------------------------
    1 | [2018-01-01,2018-02-01)
    1 | [2018-01-01,2018-01-06)
    1 | [2018-01-03,2018-01-07)
    2 | [2018-01-01,2018-01-02)
    2 | [2018-01-01,2018-01-03)
    3 | [2018-01-02,2018-01-09)
    3 | [2018-01-05,2018-01-11)
(7 rows)

Я хочу обчислити для вказаних дат і для кожного виду, у скільки рядків з дати_рангування падає кожна дата.

Тепер для запиту ми повертаємо процедуру та генеруємо ряд дат, але ось сам запит може використовувати @>оператор holdment ( ), щоб перевірити, чи є дати в діапазоні, використовуючи індекс.

Зауважте, що ми використовуємо timestamp without time zone(для припинення небезпеки DST)

SELECT d1.kind, day::date, count(d2.kind)
FROM dates_ranges AS d1
CROSS JOIN LATERAL generate_series(
  lower(myrange)::timestamp without time zone,
  upper(myrange)::timestamp without time zone,
  '1 day'
) AS gs(day)
INNER JOIN dates_ranges AS d2
  ON d2.myrange @> day::date
GROUP BY d1.kind, day;

Який є деталізованим перекриттям дня в індексі.

Як бонусний бонус, за допомогою типу діапазону дат ви можете зупинити вставки діапазонів, які перетинаються з іншими, використовуючиEXCLUDE CONSTRAINT


З вашим запитом щось не так, схоже, він рахує рядки кілька разів, JOINя думаю , занадто багато.
BartekCh

@BartekCh, якщо у вас немає рядків, що перекриваються, ви можете їх обійти, видаливши діапазони, що перекриваються (запропоновано), або скориставшисьcount(DISTINCT kind)
Еван Керролл

але я хочу перекривати рядки. Наприклад, 1дата вибору 2018-01-01знаходиться протягом перших двох рядків з dates_ranges, але Ваш запит дає 8.
BartekCh

чиcount(DISTINCT kind) ви додали DISTINCTключове слово туди?
Еван Керролл

На жаль, з DISTINCTключовим словом воно все ще не працює, як очікувалося. Він враховує різні види для кожної дати, але я хочу рахувати всі рядки кожного виду для кожної дати.
BartekCh
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.