Найти перекрывающиеся диапазоны дат в PostgreSQL - PullRequest
9 голосов
/ 19 декабря 2010

Это правильно?

SELECT * 
FROM   contract 
JOIN   team USING (name_team) 
JOIN   player USING(name_player) 
WHERE  name_team = ? 
AND    DATE_PART('YEAR',date_join)>= ? 
AND    DATE_PART('YEAR',date_leave)<= ?

Мой стол contract содержит имя игрока, название команды и даты, когда он вступил в клуб и покинул его.
Я хочу сделать функцию, перечисляющую всеигроки, которые были в команде в определенные годы.
Похоже, приведенный выше запрос не работает ...

Ответы [ 2 ]

60 голосов
/ 09 марта 2013

В настоящее время принятый ответ не отвечает на вопрос. И это в принципе неправильно. a BETWEEN x AND y переводится как:

<strike>a >= x AND a <b><=</b> y</strike>

Включая верхнюю границу, в то время как людям обычно необходимо исключить it:

a >= x AND a <b><</b> y

С даты вы можете легко настроить. Для 2009 года используйте «2009-12-31» в качестве верхней границы.
Но это не так просто с отметками времени , которые допускают дробные цифры. Современные версии Postgres используют внутреннее 8-байтовое целое число для хранения до 6 долей секунды (разрешение мкс). Зная это, мы могли бы все же заставить его работать, но это не интуитивно понятно и зависит от деталей реализации. Плохая идея.

Более того, a BETWEEN x AND y не находит перекрывающихся диапазонов. Нам нужно:

<b>b</b> >= x AND a <b><</b> y

А игроки, которые никогда не уходили пока не рассматриваются.

Правильный ответ

Предполагая год 2009, я перефразирую вопрос, не меняя его значения:

«Найти всех игроков данной команды, которые присоединились до 2010 года и не покидали его до 2009 года.»

Основной запрос:

SELECT p.* 
FROM   team     t
JOIN   contract c USING (name_team) 
JOIN   player   p USING (name_player) 
WHERE  t.name_team = ? 
AND    c.date_join  <  date '2010-01-01'
AND    c.date_leave >= date '2009-01-01';

Но есть еще:

Если ссылочная целостность обеспечивается с помощью ограничений FK, сама таблица team является просто шумом в запросе и может быть удалена.

Хотя один и тот же игрок может покинуть и присоединиться к той же команде, нам также необходимо сложить возможные дубликаты, например, с DISTINCT.

А нам может нужно предусмотреть особый случай: игроки, которые никогда не уходили. Предполагая, что эти игроки имеют NULL в date_leave.

«Предполагается, что игрок, который не ушел, будет играть за команду по сей день.»

Уточненный запрос:

SELECT DISTINCT p.* 
FROM   contract c
JOIN   player   p USING (name_player) 
WHERE  c.name_team = ? 
AND    c.date_join  <  date '2010-01-01'
AND   (c.date_leave >= date '2009-01-01' OR c.date_leave IS NULL);

Приоритет оператора работает против нас, AND связывается до OR. Нам нужны скобки.

Соответствующий ответ с оптимизированным DISTINCT (если встречаются дубликаты):

Как правило, имена физических лиц не являются уникальными, и используется суррогатный первичный ключ. Но, очевидно, name_player является первичным ключом player. Если вам нужны только имена игроков, нам не нужна таблица player в запросе:

SELECT DISTINCT name_player 
FROM   contract
WHERE  name_team = ? 
AND    date_join  <  date '2010-01-01'
AND   (date_leave >= date '2009-01-01' OR date_leave IS NULL);

SQL OVERLAPS оператор

Руководство:

OVERLAPS автоматически принимает более раннее значение пары в качестве Начните. Каждый период времени считается полуоткрытым интервал start <= time < end, если start и end не равны, в которых в случае, если он представляет этот единственный момент времени.

Чтобы позаботиться о потенциальных NULL значениях, COALESCE кажется самым простым:

SELECT DISTINCT name_player 
FROM   contract
WHERE  name_team = ? 
AND    (date_join, COALESCE(date_leave, CURRENT_DATE)) OVERLAPS
       (date '2009-01-01', date '2010-01-01');  -- upper bound excluded

Тип диапазона с поддержкой индекса

В Postgres 9.2 или новее вы также можете работать с фактическими типами диапазонов :

SELECT DISTINCT name_player 
FROM   contract
WHERE  name_team = ? 
AND    daterange(date_join, date_leave) &&
       daterange '[2009-01-01,2010-01-01)';  -- upper bound excluded

Типы диапазонов увеличивают накладные расходы и занимают больше места. 2 х date = 8 байт; 1 x daterange = 14 байт на диске или 17 байт в оперативной памяти. Но в сочетании с оператором перекрытия && запрос может поддерживаться индексом GiST.

Кроме того, нет необходимости в специальных значениях NULL. NULL означает «открытый диапазон» в типе диапазона - именно то, что нам нужно. Определение таблицы даже не нужно менять: мы можем создать тип диапазона на лету - и поддержать запрос с соответствующим индексом выражения:

CREATE INDEX mv_stock_dr_idx ON mv_stock USING gist (daterange(date_join, date_leave));

Связанный:

6 голосов
/ 19 декабря 2010

Почему бы не использовать между без даты часть вещи:

WHERE datefield BETWEEN '2009-10-10 00:00:00' AND '2009-10-11 00:00:00'

или что-то подобное?

...