2013-03-21 3 views
3

У меня есть таблица, содержащая пару временных меток, которые представляют временной интервал. Эти строки ограничены идентификатором пользователя, и каждый пользователь может иметь одну или несколько строк, связанных с ними.PostgreSQL SELECT должен соответствовать нескольким строкам

Эти данные генерируются из абстрактной формы «доступности», которая отображается, когда пользователь доступен в течение недели. Мне нужно ввести ряд временных диапазонов в качестве запроса и вернуть все идентификаторы пользователей, для которых все строки в таблице соответствуют.

Учитывая эту таблицу:

CREATE TABLE "public"."availability" (
    "id" int4 NOT NULL, 
    "user_id" int4, 
    "starts_at" timestamp(6), 
    "ends_at" timestamp(6), 
    PRIMARY KEY ("id") 
) WITH (OIDS=FALSE) 

и эти данные:

User #1 is available Mon-Tue between 08:00 and 17:00 

+----+---------+---------------------+---------------------+ 
| id | user_id | starts_at   | ends_at    | 
+----+---------+---------------------+---------------------+ 
| 1 | 1  | 2013-03-18 08:00:00 | 2013-03-18 17:00:00 | 
+----+---------+---------------------+---------------------+ 
| 2 | 1  | 2013-03-19 08:00:00 | 2013-03-19 17:00:00 | 
+----+---------+---------------------+---------------------+ 

User #2 is available Sun-Sat all day 

+----+---------+---------------------+---------------------+ 
| 3 | 2  | 2013-03-17 00:00:00 | 2013-03-23 23:59:59 | 
+----+---------+---------------------+---------------------+ 

User #3 is available Wed between 06:00 and 18:00 

+----+---------+---------------------+---------------------+ 
| 4 | 3  | 2013-03-20 06:00:00 | 2013-03-20 18:00:00 | 
+----+---------+---------------------+---------------------+ 

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

SELECT * FROM "public"."availability" 
    WHERE ('2013-03-19 08:35:00' BETWEEN starts_at AND ends_at 
    AND '2013-03-19 18:25:00' BETWEEN starts_at AND ends_at) 
    OR ('2013-03-20 12:00:00' BETWEEN starts_at AND ends_at 
    AND '2013-03-20 18:00:00' BETWEEN starts_at AND ends_at); 

+----+---------+---------------------+---------------------+ 
| id | user_id | starts_at   | ends_at    | 
+----+---------+---------------------+---------------------+ 
| 3 | 2  | 2013-03-17 00:00:00 | 2013-03-23 23:59:59 | 
+----+---------+---------------------+---------------------+ 
| 4 | 3  | 2013-03-20 06:00:00 | 2013-03-20 18:00:00 | 
+----+---------+---------------------+---------------------+ 

Но мне действительно нужно иметь возможность запрашивать несколько временных интервалов и повторно поверните только user_id s, которые соответствуют всем условий.

Запрос: 2013-03-17 10:00:00 - 2013-03-17 16:00:00, 2013-03-23 10:00:00 - 2013-03-23 16:00:00 должен вернуться:

+----+---------+---------------------+---------------------+ 
| id | user_id | starts_at   | ends_at    | 
+----+---------+---------------------+---------------------+ 
| 3 | 2  | 2013-03-17 00:00:00 | 2013-03-23 23:59:59 | 
+----+---------+---------------------+---------------------+ 

Запрос: 2013-03-18 09:00:00 - 2013-03-18 16:00:00, 2013-03-19 08:00:00 - 2013-03-19 15:45:00 должен вернуться:

+----+---------+---------------------+---------------------+ 
| id | user_id | starts_at   | ends_at    | 
+----+---------+---------------------+---------------------+ 
| 1 | 1  | 2013-03-18 08:00:00 | 2013-03-18 17:00:00 | 
+----+---------+---------------------+---------------------+ 
| 2 | 1  | 2013-03-19 08:00:00 | 2013-03-19 17:00:00 | 
+----+---------+---------------------+---------------------+ 
| 3 | 2  | 2013-03-17 00:00:00 | 2013-03-23 23:59:59 | 
+----+---------+---------------------+---------------------+ 

Запрос: 2013-03-18 07:00:00 - 2013-03-18 18:00:00 не должен возвращать ничего ,

SQLFiddle example

+1

SQLFiddle: http://sqlfiddle.com/#!12/b0fcf/4 –

+0

@CraigRinger спасибо! Я не знал о SQLfiddle, это полезно. –

+0

Спасибо, что пересмотрели вопрос. Я уберу свой теперь нерелевантный ответ и дам другим на него, поскольку у меня нет времени. –

ответ

2

SQL Fiddle

Это эксплуатирует булево приведение к целому числу, 0 или 1.

select a.* 
from 
    availability a 
    inner join 
    (
     select 
      user_id, 
      sum (
       ('2013-03-18 09:00:00' between starts_at and ends_at 
       and 
       '2013-03-18 16:00:00' between starts_at and ends_at 
       )::integer 
       + 
       ('2013-03-19 08:00:00' between starts_at and ends_at 
       and 
       '2013-03-19 15:45:00' between starts_at and ends_at 
       )::integer 
      ) period 
     from availability 
     group by user_id 
    ) s on a.user_id = s.user_id 
where period >= 2 

Измените where условие к числу периодов, чтобы соответствовать.

+0

Очень умный ! Благодарю. –

2

Для такого приложения, если вы используете PostgreSQL версии 9.2 или новее, вы можете попробовать range type. Вот пример создания, загрузки и отображения данных:

CREATE TABLE availability (
    id  int4 NOT NULL, 
    user_id int4, 
    avail tstzrange, 
    PRIMARY KEY (id) 
); 
INSERT INTO availability VALUES 
    (1, 1, '[2013-03-18 08:00:00, 2013-03-18 17:00:00)'), 
    (2, 1, '[2013-03-19 08:00:00, 2013-03-19 17:00:00)'), 
    (3, 2, '[2013-03-17 00:00:00, 2013-03-23 24:00:00)'), 
    (4, 3, '[2013-03-20 06:00:00, 2013-03-20 18:00:00)'); 
SELECT * FROM availability ; 
 
id | user_id |      avail       
----+---------+----------------------------------------------------- 
    1 |  1 | ["2013-03-18 08:00:00-05","2013-03-18 17:00:00-05") 
    2 |  1 | ["2013-03-19 08:00:00-05","2013-03-19 17:00:00-05") 
    3 |  2 | ["2013-03-17 00:00:00-05","2013-03-24 00:00:00-05") 
    4 |  3 | ["2013-03-20 06:00:00-05","2013-03-20 18:00:00-05") 
(4 rows) 

Тогда вы можете запросить с различными операторами.Если вы хотите, чтобы все диапазоны доступности, которые содержат любой указанного запроса диапазонов:

SELECT * FROM availability 
    WHERE avail @> '[2013-03-19 08:35:00, 2013-03-19 18:25:00)' 
    OR avail @> '[2013-03-20 12:00:00, 2013-03-20 18:00:00)'; 

или:

SELECT * FROM availability 
    WHERE avail @> ANY 
      (ARRAY ['[2013-03-19 08:35:00, 2013-03-19 18:25:00)'::tstzrange, 
        '[2013-03-20 12:00:00, 2013-03-20 18:00:00)'::tstzrange]); 
 
id | user_id |      avail       
----+---------+----------------------------------------------------- 
    3 |  2 | ["2013-03-17 00:00:00-05","2013-03-24 00:00:00-05") 
    4 |  3 | ["2013-03-20 06:00:00-05","2013-03-20 18:00:00-05") 
(2 rows) 

Если вы хотите, чтобы все диапазоны доступности, которые содержат все из указанного диапазоны запросов в одном диапазоне:

SELECT * FROM availability 
    WHERE avail @> '[2013-03-17 10:00:00, 2013-03-17 16:00:00)' 
    AND avail @> '[2013-03-23 10:00:00, 2013-03-23 16:00:00)'; 

или:

SELECT * FROM availability 
    WHERE avail @> ALL 
      (ARRAY ['[2013-03-17 10:00:00, 2013-03-17 16:00:00)'::tstzrange, 
        '[2013-03-23 10:00:00, 2013-03-23 16:00:00)'::tstzrange]); 
 
id | user_id |      avail       
----+---------+----------------------------------------------------- 
    3 |  2 | ["2013-03-17 00:00:00-05","2013-03-24 00:00:00-05") 
(1 row) 

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

WITH s(ts) AS 
(
    VALUES 
    ('[2013-03-18 09:00:00, 2013-03-18 16:00:00)'::tstzrange), 
    ('[2013-03-19 08:00:00, 2013-03-19 15:45:00)'::tstzrange) 
) 
SELECT DISTINCT a1.* 
    FROM s s1 
    JOIN availability a1 ON a1.avail @> s1.ts 
    AND NOT EXISTS 
     (
      SELECT * FROM s s2 
      WHERE NOT EXISTS 
        (
        SELECT * FROM availability a2 
         WHERE a2.user_id = a1.user_id 
         AND a2.avail @> s2.ts 
       ) 
     ); 

или (адаптация C запрос lodoaldo Нет на использование диапазонов):

SELECT a.* 
    FROM availability a 
    JOIN (
     SELECT 
      user_id, 
      sum(('[2013-03-18 09:00:00, 2013-03-18 16:00:00)'::tstzrange 
        <@ avail)::integer 
       + 
       ('[2013-03-19 08:00:00, 2013-03-19 15:45:00)'::tstzrange 
        <@ avail)::integer 
       ) period 
      FROM availability 
      GROUP BY user_id 
     ) s ON a.user_id = s.user_id 
    WHERE period >= 2; 

Вы можете создать индекс, чтобы сделать такие запросы очень быстро на больших столах, как это:

CREATE INDEX availability_avail ON availability USING gist (avail); 

Примечание:

  • я оставил от схемы и цитаты для удобства чтения.
  • Индекс вряд ли будет использоваться с четырьмя строками, поскольку все данные будут доступны быстрее, непосредственно прочитав одну страницу данных. С большими столами это может иметь большое значение.
  • Я использовал диапазоны TIMESTAMP WITH TIME ZONE, потому что по умолчанию (голый) TIMESTAMP часы двигаются назад каждый год в конце летнего времени. Чтобы зафиксировать моменты времени, используйте TIMESTAMP WITH TIME ZONE (timestamptz).
  • При использовании непосредственно литералы не обязательно должны быть явно лишены; при использовании формы запроса ANY или ALL необходимы явные приведения.
  • Квадратные скобки в диапазоне означают, что диапазон включает в себя смежное время, а круглые круглые скобки означают, что диапазон не включает в смежное время. Временные метки обычно указываются с использованием [), так что диапазон, заканчивающийся заданным временем и другим диапазоном, начинающимся с того же времени, считается смежным, а не , перекрывающим.
  • '24:00:00' на одну дату и '00:00:00' на следующую дату в тот же момент.
  • Два предыдущих пункта позволяют упростить спецификацию временной метки, заканчивающейся в полночь. Нет никакого риска «потерянной секунды» или другой странности.
+0

Возможно, это скорее правильный ответ, так как тип данных диапазона, очевидно, хорошо подходит для этой проблемы. Мы находимся в процессе работы над обновлением до 9.2, так что это, вероятно, тот подход, в котором я закончу. –

Смежные вопросы