PostgreSQL: ПОЛНОЕ ВНЕШНЕЕ СОЕДИНЕНИЕ для типов данных RANGE

У меня есть несколько таблиц с частично перекрывающимися значениями TSTZRANGE, и мне нужно JOIN их так, чтобы "точки останова" создавались для каждой границы UPPER и LOWER для каждого диапазона.

Некоторые примеры данных:

CREATE TABLE foo (
    foo_id SERIAL PRIMARY KEY,
    foo TEXT,
    active_timespan TSTZRANGE
);
INSERT INTO 
    foo (foo, active_timespan)
VALUES
    ('One',TSTZRANGE('2015-01-01 00:00:00-00','2015-01-01 06:00:00-00')),
    ('Two',TSTZRANGE('2015-01-01 12:00:00-00','2015-01-01 18:00:00-00'));

CREATE TABLE bar (
    bar_id SERIAL PRIMARY KEY,
    bar TEXT,
    active_timespan TSTZRANGE
);
INSERT INTO 
    bar (bar, active_timespan)
VALUES
    ('Alpha',TSTZRANGE('2015-01-01 03:00:00-00','2015-01-01 09:00:00-00')),
    ('Bravo',TSTZRANGE('2015-01-01 15:00:00-00','2015-01-01 21:00:00-00'));

Желаемый результат:

active_timespan                                       | foo  | bar
------------------------------------------------------|------|------
'["2015-01-01 00:00:00-00","2015-01-01 03:00:00-00"]' | One  | Null
'["2015-01-01 03:00:00-00","2015-01-01 06:00:00-00"]' | One  | Alpha
'["2015-01-01 06:00:00-00","2015-01-01 09:00:00-00"]' | Null | Alpha
'["2015-01-01 12:00:00-00","2015-01-01 15:00:00-00"]' | Two  | Null
'["2015-01-01 15:00:00-00","2015-01-01 18:00:00-00"]' | Two  | Bravo
'["2015-01-01 18:00:00-00","2015-01-01 21:00:00-00"]' | Null | Bravo

Текущий подход:

В настоящее время я использую сложную систему cte, в которой я выбираю нужные мне данные из каждой таблицы в ее собственную cte, а затем UNION все отдельные значения диапазона UPPER и LOWER из каждого запроса, оставляя мне список TIMESTAMPTZ значений. Затем я использую оконную функцию LEAD для создания новых значений TSTZRANGE. Как только я закончу все это, я присоединю исходный cte к новому списку значений TSTZRANGE.

Обновлять

Текущий шаблон запроса ниже:

WITH
    cte_foo AS (
        SELECT
            foo_id,
            foo,
            active_timespan
        FROM
            foo
        WHERE
            active_timespan && $1
    )
    , cte_bar AS (
        SELECT
            bar_id,
            bar,
            active_timespan
        FROM
            bar
        WHERE
            active_timespan && $1
    )
--  continue for each table to be joined... 
--  , cte_baz AS (
--      SELECT
--          baz_id,
--          baz,
--          active_timespan
--      FROM
--          baz
--      WHERE
--          active_timespan && $1
--  )
    , cte_times AS (
        SELECT DISTINCT
            UNNEST(
                ARRAY[
                    LOWER(cte_foo.active_timespan),
                    UPPER(cte_foo.active_timespan)
                ]
            ) AS breakpoint
        FROM
            cte_foo

        UNION
        SELECT DISTINCT
            UNNEST(
                ARRAY[
                    LOWER(cte_bar.active_timespan),
                    UPPER(cte_bar.active_timespan)
                ]
            ) AS breakpoint
        FROM
            cte_bar

--      continue for each table to be joined... 
--      UNION
--      SELECT DISTINCT
--          UNNEST(
--              ARRAY[
--                  LOWER(cte_baz.active_timespan),
--                  UPPER(cte_baz.active_timespan)
--              ]
--          ) AS breakpoint
--      FROM
--          cte_baz
    )
    , cte_timespans AS (
        SELECT
            TSTZRANGE(
                sub_times.breakpoint,
                LEAD(sub_times.breakpoint, 1, 'infinity'::TIMESTAMPTZ) OVER (ORDER BY sub_times.breakpoint)
            ) AS timespan
        FROM
            (
                SELECT DISTINCT
                    cte_times.breakpoint
                FROM
                    cte_times
            ) AS sub_times
        ORDER BY
            timespan
    )
SELECT
    cte_timespans.timespan,
    cte_foo.foo,
    cte_bar.bar
FROM
    cte_timespans
    LEFT OUTER JOIN cte_foo ON cte_timespans.timespan && cte_foo.active_timespan
    LEFT OUTER JOIN cte_bar ON cte_timespans.timespan && cte_bar.active_timespan

--  continue for each table to be joined...
--  LEFT OUTER JOIN cte_baz ON cte_timespans.timespan && cte_baz.active_timespan
ORDER BY
    cte_timelines.timespan

Производительность явно низкая, особенно когда я присоединяюсь к нескольким таблицам (в некоторых случаях более десяти). Это лучший способ подойти к этой ситуации?

Принятие желаемого за действительное:

Было бы здорово, если бы у PostgreSQL был какой-то RANGE JOIN — что-то вроде:

SELECT
    *
FROM
    foo
    FULL OUTER RANGE JOIN bar ON foo.active_timespan && bar.active_timespan

person losthorse    schedule 22.09.2015    source источник
comment
Производительность явно низкая, я не вижу никаких доказательств этого в вашем вопросе. Прочтите: wiki.postgresql.org/wiki/Slow_Query_Questions и предоставьте план выполнения (используйте explain analyze ) на ваших реальных данных. Тип данных диапазона также может быть проиндексирован: postgresql.org/docs /current/static/ Вы пробовали это?   -  person a_horse_with_no_name    schedule 22.09.2015
comment
@a_horse_with_no_name — производительность снижается из-за характера cte (описанного в документации как ограничения оптимизации). Они также удаляют все индексы после начального SELECT.   -  person losthorse    schedule 22.09.2015
comment
В вашем вопросе нет CTE. Кроме того, full outer join вернет все строки из обеих таблиц, что невозможно оптимизировать. База данных должна прочитать все строки из foo и все строки из bar.   -  person a_horse_with_no_name    schedule 22.09.2015
comment
@a_horse_with_no_name — CTE указан в моем текущем подходе; мой вопрос: есть ли лучший способ объединить типы диапазонов, чтобы вернуть результаты, соответствующие шаблону в желаемом наборе результатов?   -  person losthorse    schedule 22.09.2015
comment
Если вам нужна помощь с медленным запросом, опубликуйте реальный запрос, который вы используете.   -  person a_horse_with_no_name    schedule 22.09.2015
comment
@a_horse_with_no_name - вопрос обновлен запросом   -  person losthorse    schedule 22.09.2015
comment
Я не вижу никаких индексов в вашем вопросе? Это недостаток в вашем вопросе или в вашей настройке? Я также не вижу плана запроса. Ознакомьтесь с инструкциями здесь: stackoverflow.com/tags/postgresql-performance/info   -  person Erwin Brandstetter    schedule 22.09.2015
comment
@ErwinBrandstetter - я исключил это из примера ... индекс gist присутствует на каждом TSTZRANGE   -  person losthorse    schedule 22.09.2015
comment
То, что вы хотите, в основном невозможно: декартово произведение foo(N=2) и bar(N=2) имеет размер 4. Результатом range_a NOT IN range_b может быть 0,1 или 2 диапазона, а результатом tuple*tuple может быть 0 или 1 кортеж. Таким образом, либо вам придется рассматривать разделенные диапазоны отдельно, либо вам придется пройти через границы, как это делает @byMike.   -  person joop    schedule 22.09.2015
comment
Что-то подобное этому может помочь (но обратите внимание, это не будет работать с диапазонами) . И да, оператор перекрытий (&&) нельзя использовать для full join прямо сейчас FULL JOIN is only supported with merge-joinable or hash-joinable join conditions, что оставляет мало места для оптимизации.   -  person pozs    schedule 22.09.2015


Ответы (1)


предположения

  • изменение структуры данных не вариант
  • промежутки времени имеют правильный индекс

цели оптимизации

  • меньше данных в запросах cte
  • упростить запрос
  • «ленивая» загрузка данных

этапы процесса

  • создать список (уникальных) точек останова
  • составить список диапазонов между точками останова
  • загрузить данные, соответствующие диапазонам

результирующий запрос

WITH
  eventlist AS (
    SELECT DISTINCT
      UNNEST(
          ARRAY [
          LOWER( active_timespan ),
          UPPER( active_timespan )
          ]
      ) AS timestamp
    FROM foo WHERE active_timespan && $1
    UNION DISTINCT
    SELECT
      UNNEST(
          ARRAY [
          LOWER( active_timespan ),
          UPPER( active_timespan )
          ]
      ) AS timestamp
    FROM bar WHERE bar.active_timespan && $2
  ),
  durations AS (
    SELECT
      TSTZRANGE(
          timestamp,
          LEAD( timestamp, 1, 'infinity' ) OVER (ORDER BY timestamp ASC)
      ) AS active_span
    FROM eventlist
  )
SELECT
  durations.active_span,
  foo.foo,
  bar.bar
FROM durations
  LEFT JOIN foo ON active_span && foo.active_timespan
  LEFT JOIN bar ON active_span && bar.active_timespan
WHERE (foo.foo IS NOT NULL OR bar.bar IS NOT NULL)
  AND foo.active_timespan && $1
  AND bar.active_timespan && $2
;
person byMike    schedule 22.09.2015
comment
Похоже, что он делает то же самое, что и моя текущая конфигурация, за исключением того, что он дважды попадает в базовые таблицы (и окончательные соединения таблиц записываются по-другому)... я что-то упустил? - person losthorse; 22.09.2015
comment
разница в том, что сначала в память загружаются только временные метки списка, а не все таблицы --> требуется меньше памяти. как я вижу сейчас, последние два утверждения where являются излишними. Если есть правильные индексы для всех диапазонов, это должно работать довольно быстро. - person byMike; 22.09.2015