Ускорьте запрос PostgreSQL с помощью временной метки OVERLAPS и «PARTITION BY» - PullRequest
4 голосов
/ 21 сентября 2011

У меня в PostgreSQL 9.0 довольно большая таблица (500K - 1M строк), которая содержит общую информацию о временном интервале, то есть она определяет, когда строка в другой таблице («функция») является допустимой. Определение выглядит так (слегка упрощенно):

CREATE TABLE feature_timeslice
(
  timeslice_id int NOT NULL,
  feature_id int NOT NULL,
  valid_time_begin timestamp NOT NULL,
  valid_time_end timestamp,
  sequence_number smallint,
  -- Some other columns
  CONSTRAINT pk_feature_timeslice PRIMARY KEY (timeslice_id)
  -- Some other constraints
)

CREATE INDEX ix_feature_timeslice_feature_id
ON feature_timeslice USING btree (feature_id);

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

CREATE TABLE specific_feature_timeslice
(
  timeslice_id int NOT NULL,
  -- Other columns
  CONSTRAINT pk_specific_feature_timeslice PRIMARY KEY (timeslice_id),
  CONSTRAINT fk_specific_feature_timeslice_feature_timeslice FOREIGN KEY (timeslice_id) REFERENCES feature_timeslice (timeslice_id)
)

Может быть несколько временных интервалов с перекрывающимися действительными периодами (время начала / окончания), но тот, который имеет самый высокий sequence_number, имеет приоритет (опять же, небольшое упрощение, но достаточно близкое). Я хотел бы эффективно найти действующую на данный момент строку для каждого feature_id, поэтому у меня определено представление, например:

CREATE VIEW feature_timeslice_id_now
AS
    SELECT timeslice_id
    FROM
    (
        SELECT timeslice_id, rank() OVER
        (
            PARTITION BY feature_id
            ORDER BY sequence_number DESC, timeslice_id DESC
        )
        FROM feature_timeslice
        WHERE (current_timestamp AT TIME ZONE 'UTC', '0'::interval) OVERLAPS (valid_time_begin, COALESCE(valid_time_end, 'infinity'::timestamp))
    ) subq 
    WHERE subq.rank = 1

Обычно запрашивается так:

SELECT *
FROM specific_feature_timeslice sf
JOIN feature_timeslice_id_now n USING (timeslice_id)
WHERE sf.name = 'SOMETHING'

Это работает, но все еще слишком медленно - занимает 1-2 секунды, хотя может быть возвращено только 1-5 строк, потому что критерий specific_feature_timeslice обычно сильно его сужает. (Более сложные запросы, объединяющие несколько представлений функций, очень быстро замедляются.) Я не могу понять, как заставить PostgreSQL сделать это более эффективно. План запроса выглядит следующим образом:

   Join Filter: ((r.timeslice_id)::integer = (subq.timeslice_id)::integer)
  ->  Subquery Scan on subq  (cost=32034.36..37876.98 rows=835 width=4) (actual time=2086.125..5243.467 rows=250918 loops=1)
        Filter: (subq.rank = 1)
        ->  WindowAgg  (cost=32034.36..35790.33 rows=166932 width=10) (actual time=2086.110..4066.351 rows=250918 loops=1)
              ->  Sort  (cost=32034.36..32451.69 rows=166932 width=10) (actual time=2086.065..2654.971 rows=250918 loops=1)
                    Sort Key: feature_timeslice.feature_id, feature_timeslice.sequence_number, feature_timeslice.timeslice_id
                    Sort Method:  quicksort  Memory: 13898kB
                    ->  Seq Scan on feature_timeslice  (cost=0.00..17553.93 rows=166932 width=10) (actual time=287.270..1225.595 rows=250918 loops=1)
                          Filter: overlaps(timezone('UTC'::text, now()), (timezone('UTC'::text, now()) + '00:00:00'::interval), (valid_time_begin)::timestamp without time zone, COALESCE((valid_time_end)::timestamp without time zone, 'infinity'::timestamp without time zone))
  ->  Materialize  (cost=0.00..1093.85 rows=2 width=139) (actual time=0.002..0.007 rows=2 loops=250918)
        ->  Seq Scan on specific_feature_timeslice sf  (cost=0.00..1093.84 rows=2 width=139) (actual time=1.958..7.674 rows=2 loops=1)
              Filter: ((name)::text = 'SOMETHING'::text)
Total runtime: 10319.875 ms

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

== Редактировать ==

ОК, я попытался нормализовать таблицу, как было предложено в обоих ответах, то есть я переместил valid_time_begin и valid_time_end в отдельную таблицу, time_period. Я также заменил оконную функцию на WHERE NOT EXISTS ([better candidate time slice]). В процессе я также обновил до PostgreSQL 9.1. При этом некоторые запросы теперь в два раза быстрее. План запроса выглядит так же, как в ответе wildplasser . Это хорошо, но не так хорошо, как я надеялся - для выбора из одной таблицы характеристик все равно требуется больше секунды.

В идеале, я хотел бы воспользоваться преимуществом избирательности условия ГДЕ, как говорит Эрвин Брандштеттер . Если я создаю запрос вручную, время, которое я получаю, составляет 15-30 мс. Теперь это больше похоже на это! Ручной запрос выглядит примерно так:

WITH filtered_feature AS
(
    SELECT *
    FROM specific_feature_timeslice sf
    JOIN feature_timeslice ft USING (timeslice_id)
    WHERE sf.name = 'SOMETHING'
)
SELECT *
FROM filtered_feature ff
JOIN
(
    SELECT timeslice_id
    FROM filtered_feature candidate
    JOIN time_period candidate_time ON candidate.valid_time_period_id = candidate_time.id
    WHERE ('2011-09-26', '0'::interval) OVERLAPS (candidate_time.valid_time_begin, COALESCE(candidate_time.valid_time_end, 'infinity'::timestamp))
        AND NOT EXISTS
        (
            SELECT *
            FROM filtered_feature better
            JOIN time_period better_time ON better.valid_time_period_id = better_time.id
            WHERE ('2011-09-26', '0'::interval) OVERLAPS (better_time.valid_time_begin, COALESCE(better_time.valid_time_end, 'infinity'::timestamp))
                AND better.feature_id = candidate.feature_id AND better.timeslice_id != candidate.timeslice_id
                AND better.sequence_number > candidate.sequence_number
        )
) AS ft ON ff.timeslice_id = ft.timeslice_id

К сожалению, это слишком большой и сложный для использования в обычных запросах, которые могут объединить многие другие таблицы. Мне нужен какой-то способ инкапсулировать эту логику в функцию (для произвольного времени) или, по крайней мере, в представление (для текущего времени), но я не могу понять, как это сделать, все еще заставляя планировщик запросов сначала выполнить фильтрацию по определенной функции. Если бы я только мог передать набор строк в функцию - но, насколько я знаю, PostgreSQL не позволяет этого. Есть идеи?

== Заключение ==

Я решил использовать наследование PostgreSQL для решения этой проблемы (см. Мой ответ), но я не смог бы придумать эту идею, если бы не ответ Эрвин Брандштеттер , поэтому награда достается ему , wildplasser * Ответ 1045 * также был очень полезен, потому что он позволил мне убрать ненужную оконную функцию, что ускорило ее. Большое спасибо вам обоим!

Ответы [ 3 ]

2 голосов
/ 30 сентября 2011

Я решил использовать наследование PostgreSQL для решения этой проблемы, поэтому каждая таблица specific_feature_timeslice наследуется от feature_timeslice (а не ссылается на нее, как раньше). Это позволяет «выборочность функции может вступить в силу первой» - план запроса начинается с сужения его до нескольких нужных мне строк. Теперь схема выглядит следующим образом:

CREATE TABLE feature_timeslice
(
  timeslice_id int NOT NULL,
  feature_id int NOT NULL,
  valid_time_begin timestamp NOT NULL,
  valid_time_end timestamp,
  sequence_number smallint,
  -- Some other columns
  CONSTRAINT pk_feature_timeslice PRIMARY KEY (timeslice_id)
  -- Some other constraints
)

CREATE TABLE specific_feature_timeslice
(
  -- Feature-specific columns only, eg.
  name character varying(100),

  CONSTRAINT pk_specific_feature_timeslice PRIMARY KEY (timeslice_id)
)
INHERITS (feature_timeslice);

CREATE INDEX ix_specific_feature_timeslice_feature_id
ON specific_feature_timeslice (feature_id);

Каждая такая производная таблица имеет свою функцию для выбора тока строки в указанное время:

CREATE FUNCTION specific_feature_asof(effective_time timestamp)
RETURNS SETOF specific_feature_timeslice
AS $BODY$
    SELECT candidate.*
    FROM specific_feature_timeslice candidate
    WHERE ($1, '0'::interval) OVERLAPS (candidate.valid_time_begin, COALESCE(candidate.valid_time_end, 'infinity'::timestamp))
        AND NOT EXISTS
        (
            SELECT *
            FROM specific_feature_timeslice better
            WHERE ($1, '0'::interval) OVERLAPS (better.valid_time_begin, COALESCE(better.valid_time_end, 'infinity'::timestamp))
                AND better.feature_id = candidate.feature_id AND better.timeslice_id != candidate.timeslice_id AND better.sequence_number > candidate.sequence_number
        )
$BODY$ LANGUAGE SQL STABLE;

Конечно, я автоматически генерирую эти функции - они одинаковы, за исключением имени таблицы. Типичный запрос затем становится:

SELECT *
FROM specific_feature_asof('2011-09-30')
WHERE name = 'SOMETHING'

и план запроса выглядит следующим образом:

Nested Loop Anti Join  (cost=0.00..412.84 rows=3 width=177) (actual time=0.044..7.038 rows=10 loops=1)
  Join Filter: (((better.timeslice_id)::integer <> (candidate.timeslice_id)::integer) AND ((better.sequence_number)::smallint > (candidate.sequence_number)::smallint))
  ->  Seq Scan on specific_feature_timeslice candidate  (cost=0.00..379.66 rows=3 width=177) (actual time=0.018..6.688 rows=10 loops=1)
        Filter: (((name)::text = 'SOMETHING'::text) AND overlaps(('2011-09-30 00:00:00'::timestamp without time zone)::timestamp without time zone, (('2011-09-30 00:00:00'::timestamp without time zone)::timestamp without time zone + '00:00:00'::interval), (valid_time_begin)::timestamp without time zone, COALESCE((valid_time_end)::timestamp without time zone, 'infinity'::timestamp without time zone)))
  ->  Index Scan using ix_specific_feature_timeslice_feature_id on specific_feature_timeslice better  (cost=0.00..8.28 rows=1 width=14) (actual time=0.008..0.011 rows=1 loops=10)
        Index Cond: ((feature_id)::integer = (candidate.feature_id)::integer)
        Filter: overlaps(('2011-09-30 00:00:00'::timestamp without time zone)::timestamp without time zone, (('2011-09-30 00:00:00'::timestamp without time zone)::timestamp without time zone + '00:00:00'::interval), (valid_time_begin)::timestamp without time zone, COALESCE((valid_time_end)::timestamp without time zone, 'infinity'::timestamp without time zone))
Total runtime: 7.150 ms

Разница в производительности довольно существенна: простой выбор, такой как запрос выше, занимает 30-60 мс. Объединение двух таких функций занимает до 300-400 мс, что немного больше, чем я ожидал, но все же вполне приемлемо.

С этими изменениями я думаю, что больше не нужно нормализовать feature_timeslice, т.е. Извлеките действительное время начала / окончания в отдельную таблицу, поэтому я этого не делал.

1 голос
/ 21 сентября 2011

Сначала нормализуй свои сущности. Ваша установка может выглядеть так:

CREATE TABLE feature
( feature_id int primary key,
  name text
  -- Some other columns
);

CREATE TABLE timeslice
( timeslice_id int primary key,
  valid_begin timestamp NOT NULL,
  valid_end timestamp
  -- Some other columns?
);

CREATE TABLE feature_timeslice
( feature_id int references feature (feature_id),
  timeslice_id int references timeslice (timeslice_id),
  sequence_number smallint,             -- guess it should live here?
  -- Some other columns?
  CONSTRAINT pk_feature_timeslice PRIMARY KEY (feature_id, timeslice_id)
);

Затем попробуйте объединить два SELECT в один. Таким образом, избирательность функции может вступить в силу в первую очередь. СЕЙЧАС, избавься от вида!

SELECT DISTINCT ON (1) ft.feature_id, first_value(ft.timeslice_id) OVER (PARTITION BY ft.feature_id ORDER BY ft.sequence_number DESC, ft.timeslice_id DESC) AS timeslice_id 
  FROM feature f
  JOIN feature_timeslice ft USING (feature_id)
  JOIN timeslice t USING (timeslice_id)
 WHERE f.name = 'SOMETHING'
AND t.valid_begin <= now()::timestamp
AND (t.valid_end >= now()::timestamp OR t.valid_end IS NULL);

Если функция является настолько избирательной, насколько вы предполагали (макс. 10 временных интервалов на функцию), тогда индексы для valid_begin или sequence_number не слишком широко используются.
Индекс на feature.name может помочь, хотя!
Наиболее заметной особенностью здесь является объединение DISTINCT с функцией WINDOW.

1 голос
/ 21 сентября 2011

У вас проблема с нормализацией.

  • timeslice_id - это суррогатный ключ.
  • (feature_id, sequence_number} являются ключом-кандидатом
  • (feature_id, valid_time_begin (valid_time_end)) также является ключом-кандидатом.

Вы неправильно используете оконную функцию, просто чтобы выбрать кандидата с рангом = 1. Самостоятельное присоединение, вероятно, дешевле.

EDIT:

CREATE index feature_timeslice_alt2 ON feature_timeslice
  ( feature_id,valid_time_begin);
CREATE UNIQUE index feature_timeslice_alt ON feature_timeslice
  ( feature_id,sequence_number);


CREATE VIEW feature_timeslice_id_encore AS
   SELECT timeslice_id FROM feature_timeslice t0
   WHERE (current_timestamp AT TIME ZONE 'UTC', '0'::interval)
          OVERLAPS (t0.valid_time_begin, COALESCE(t0.valid_time_end, 'infinity'::timestamp))
   AND NOT EXISTS ( 
      SELECT timeslice_id FROM feature_timeslice t1
      WHERE (current_timestamp AT TIME ZONE 'UTC', '0'::interval)
             OVERLAPS (t1.valid_time_begin, COALESCE(t1.valid_time_end, 'infinity'::timestamp))
      -- EDIT: forgot this
      AND t1.feature_id = t0.feature_id
      AND t1.sequence_number < t0.sequence_number
  );      

РЕДАКТИРОВАТЬ: результирующий план запроса:

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Hash Anti Join  (cost=9090.62..18428.34 rows=45971 width=4) (actual time=110.053..222.897 rows=9030 loops=1)
   Hash Cond: (t0.feature_id = t1.feature_id)
   Join Filter: (t1.sequence_number < t0.sequence_number)
   ->  Seq Scan on feature_timeslice t0  (cost=0.00..8228.67 rows=68956 width=12) (actual time=0.031..106.646 rows=9030 loops=1)
         Filter: "overlaps"(timezone('UTC'::text, now()), (timezone('UTC'::text, now()) + '00:00:00'::interval), valid_time_begin, COALESCE(valid_time_end, 'infinity'::timestamp without time zone))
   ->  Hash  (cost=8228.67..8228.67 rows=68956 width=8) (actual time=109.979..109.979 rows=9030 loops=1)
         Buckets: 8192  Batches: 1  Memory Usage: 353kB
         ->  Seq Scan on feature_timeslice t1  (cost=0.00..8228.67 rows=68956 width=8) (actual time=0.016..106.995 rows=9030 loops=1)
               Filter: "overlaps"(timezone('UTC'::text, now()), (timezone('UTC'::text, now()) + '00:00:00'::interval), valid_time_begin, COALESCE(valid_time_end, 'infinity'::timestamp without time zone))
 Total runtime: 223.488 ms

План запроса для запроса OP был похож на его план и имел «Общее время выполнения: 1404,092 мс». msgstr "(но, вероятно, масштаб будет хуже, из-за шага сортировки)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...