Оптимизация запросов по диапазону временных отметок (два столбца)

96

Я использую PostgreSQL 9.1 на Ubuntu 12.04.

Мне нужно выбрать записи за определенный промежуток времени: в моей таблице time_limitsесть два timestampполя и одно integerсвойство. В моей фактической таблице есть дополнительные столбцы, которые не связаны с этим запросом.

create table (
   start_date_time timestamp,
   end_date_time timestamp, 
   id_phi integer, 
   primary key(start_date_time, end_date_time,id_phi);

Эта таблица содержит примерно 2 млн записей.

Такие запросы занимали огромное количество времени:

select * from time_limits as t 
where t.id_phi=0 
and t.start_date_time <= timestamp'2010-08-08 00:00:00'
and t.end_date_time   >= timestamp'2010-08-08 00:05:00';

Поэтому я попытался добавить еще один индекс - инверсию PK:

create index idx_inversed on time_limits(id_phi, start_date_time, end_date_time);

У меня сложилось впечатление, что производительность улучшилась: время доступа к записям в середине таблицы кажется более разумным: где-то между 40 и 90 секундами.

Но это все еще несколько десятков секунд для значений в середине временного диапазона. И еще вдвое больше при наведении на конец таблицы (в хронологическом порядке).

Я впервые попытался explain analyzeполучить этот план запроса:

 Bitmap Heap Scan on time_limits  (cost=4730.38..22465.32 rows=62682 width=36) (actual time=44.446..44.446 rows=0 loops=1)
   Recheck Cond: ((id_phi = 0) AND (start_date_time <= '2011-08-08 00:00:00'::timestamp without time zone) AND (end_date_time >= '2011-08-08 00:05:00'::timestamp without time zone))
   ->  Bitmap Index Scan on idx_time_limits_phi_start_end  (cost=0.00..4714.71 rows=62682 width=0) (actual time=44.437..44.437 rows=0 loops=1)
         Index Cond: ((id_phi = 0) AND (start_date_time <= '2011-08-08 00:00:00'::timestamp without time zone) AND (end_date_time >= '2011-08-08 00:05:00'::timestamp without time zone))
 Total runtime: 44.507 ms

Смотрите результаты на depesz.com.

Что я могу сделать, чтобы оптимизировать поиск? Вы можете видеть, сколько времени тратится на сканирование двух столбцов меток времени, если для id_phiних установлено значение 0. И я не понимаю большого сканирования (60K строк!) На отметках времени. Разве они не проиндексированы по первичному ключу, а idx_inversedя добавил?

Должен ли я изменить тип отметки времени на что-то другое?

Я немного читал об индексах GIST и GIN. Я считаю, что они могут быть более эффективными при определенных условиях для пользовательских типов. Это жизнеспособный вариант для моего варианта использования?

Стефан Роллан
источник
1
ну это 45 с. Я не знаю, почему это говорит 45 мс. Я бы даже не стал жаловаться, если бы это было так быстро, как 45 мс ... :-) Возможно, ошибка в выводе объяснения анализа. Или, может быть, это время анализа, чтобы выполнить. Не знаю. Но 40/50 секунд это то, что я измеряю.
Стефан Роллан
2
Время, указанное в explain analyzeвыходных данных, - это время, необходимое для запроса на сервере . Если ваш запрос занимает 45 секунд, то дополнительное время затрачивается на передачу данных из базы данных в программу, выполняющую запрос. В конце концов, это 62682 строки, и если каждая строка большая (например, имеет длину varcharили textстолбцы), это может повлиять на время передачи коренным образом.
a_horse_with_no_name
@a_horse_with_no_name: оценкаrows=62682 rows планировщика . Запрос возвращает 0 строк. (actual time=44.446..44.446 rows=0 loops=1)
Эрвин Брандштеттер
@ErwinBrandstetter: ах, верно. Я упустил это из виду. Но, тем не менее, я никогда не видел вывод объяснения анализа ложь о времени выполнения.
a_horse_with_no_name

Ответы:

162

Для Postgres 9.1 или новее:

CREATE INDEX idx_time_limits_ts_inverse
ON time_limits (id_phi, start_date_time, end_date_time DESC);

В большинстве случаев порядок сортировки индекса вряд ли актуален. Postgres может сканировать назад практически так же быстро. Но для запросов диапазона по нескольким столбцам это может иметь огромное значение. Тесно связаны:

Рассмотрим ваш запрос:

SELECT *
FROM   time_limits
WHERE  id_phi = 0
AND    start_date_time <= '2010-08-08 00:00'
AND    end_date_time   >= '2010-08-08 00:05';

Порядок сортировки первого столбца id_phiв индексе не имеет значения. Так как он проверен на равенство ( =), он должен стоять первым. Вы получили это право. Больше в этом связанном ответе:

Postgres может быстро перейти к id_phi = 0следующему моменту и рассмотреть следующие два столбца соответствующего индекса. Они запрашиваются с условиями диапазона обратного порядка сортировки ( <=, >=). В моем индексе квалифицирующие строки идут первыми. Должен быть самый быстрый способ с индексом B-Tree 1 :

  • Вы хотите start_date_time <= something: у индекса самая ранняя метка времени.
    • Если он соответствует требованиям, также проверьте столбец 3.
      Повторяйте до тех пор, пока первая строка не будет соответствовать требованиям (супер быстро).
  • Вы хотите end_date_time >= something: индекс имеет самую последнюю метку времени первым.
    • Если это соответствует требованиям, продолжайте извлекать строки, пока первый не сработает (супер быстро)
      Продолжите со следующего значения для столбца 2 ..

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

Сколько строк соответствует первым двум столбцам?
Лишь немногие с start_date_timeблизко к началу временного диапазона таблицы. Но почти все строки id_phi = 0в хронологическом конце таблицы! Таким образом, производительность ухудшается с более поздним временем запуска.

Планировщик оценок

Планировщик оценивает rows=62682ваш пример запроса. Никто из них не подходит ( rows=0). Вы можете получить более точные оценки, если увеличите целевой показатель для таблицы. Для 2.000.000 строк ...

ALTER TABLE time_limits ALTER start_date_time SET STATISTICS 1000;
ALTER TABLE time_limits ALTER end_date_time   SET STATISTICS 1000;

... может заплатить. Или даже выше. Больше в этом связанном ответе:

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

CLUSTER / pg_repack

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

ALTER TABLE time_limits CLUSTER ON idx_time_limits_inversed;

При одновременном доступе рассмотрим pg_repack , который может делать то же самое без исключительной блокировки.

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

Индекс GiST в Postgres 9.2+

1 С pg 9.2+ есть еще один, возможно, более быстрый вариант: индекс GiST для столбца диапазона.

  • Существуют встроенные типы диапазонов для timestampи timestamp with time zone: tsrange,tstzrange . Индекс btree обычно быстрее для дополнительного integerстолбца, например id_phi. Меньше и дешевле в обслуживании тоже. Но запрос, вероятно, все еще будет быстрее в целом с объединенным индексом.

  • Измените определение таблицы или используйте индекс выражения .

  • Для многоколоночного индекса GiST вам также понадобится установить дополнительный модуль btree_gist(один раз на базу данных), который предоставляет классы операторов для включения integer.

Trifecta! Многоколончатый функциональный индекс GiST :

CREATE EXTENSION IF NOT EXISTS btree_gist;  -- if not installed, yet

CREATE INDEX idx_time_limits_funky ON time_limits USING gist
(id_phi, tsrange(start_date_time, end_date_time, '[]'));

Используйте «содержит диапазон» оператор@> в запросе Сейчас:

SELECT *
FROM   time_limits
WHERE  id_phi = 0
AND    tsrange(start_date_time, end_date_time, '[]')
    @> tsrange('2010-08-08 00:00', '2010-08-08 00:05', '[]')

SP-GiST индекс в Postgres 9,3+

SP-GiST индекс может быть даже быстрее , для такого рода запросов - за исключением того, что, цитирую инструкцию :

В настоящее время только индексы типа B-tree, GiST, GIN и BRIN поддерживают многоколоночные индексы.

По-прежнему верно в Postgres 12.
Вы должны объединить spgistиндекс только (tsrange(...))со вторым btreeиндексом (id_phi). С добавленными накладными расходами я не уверен, что это может конкурировать.
Связанный ответ с эталоном только для tsrangeстолбца:

Эрвин Брандштеттер
источник
78
Я должен сказать это, по крайней мере, только один раз, что каждый из ваших ответов по SO и DBA имеет действительно высокую добавленную стоимость / научную ценность и большую часть времени является наиболее полным. Просто чтобы сказать это один раз: респект!
Стефан Роллан
1
Merci Bien! :) Так вы получили более быстрые результаты?
Эрвин Брандштеттер,
Я должен позволить закончить большую массовую копию, сгенерированную из моего очень неловкого запроса, поэтому, делая процесс очень медленным, он вращался в течение нескольких часов, прежде чем я задал вопрос. Но я рассчитал, и я решил позволить ему повернуть до завтрашнего утра, он будет закончен, и новый стол готов к заполнению завтра. Я пытался создать ваш индекс одновременно во время работы, но из-за слишком большого доступа (я думаю), создание индекса должно быть заблокировано. Я повторю этот же тест снова завтра с вашим решением. Я также посмотрел, как выполнить обновление до 9.2 ;-) для Debian / Ubuntu.
Стефан Роллан
2
@StephaneRolland: было бы все еще интересно, почему вывод объяснения анализа показывает 45 миллисекунд, в то время как запрос занимает более 40 секунд.
a_horse_with_no_name
1
@John: Postgres может перемещаться по индексу вперед или назад, но он не может изменить направление при одном и том же сканировании. В идеале у вас должны быть все подходящие строки на узел первый (или последний), но для достижения наилучших результатов это должно быть одинаковое выравнивание (соответствующие предикаты запроса) для всех столбцов.
Эрвин Брандштеттер
5

Ответ Эрвина уже исчерпывающий, однако:

Типы диапазонов для временных меток доступны в PostgreSQL 9.1 с расширением Temporal от Джеффа Дэвиса: https://github.com/jeff-davis/PostgreSQL-Temporal

Примечание: имеет ограниченные возможности (использует Timestamptz, и вы можете иметь только стиль '[)', перекрывающий друг друга). Кроме того, есть много других веских причин для обновления до PostgreSQL 9.2.

Nathan-м
источник
3

Вы можете попытаться создать многоколонный индекс в другом порядке:

primary key(id_phi, start_date_time,end_date_time);

Я однажды опубликовал аналогичный вопрос, также связанный с упорядочением индексов по многоколонному индексу. Ключ пытается использовать в первую очередь самые строгие условия, чтобы уменьшить пространство поиска.

Изменить : моя ошибка. Теперь я вижу, что у вас уже есть этот индекс.

jap1968
источник
У меня уже есть оба индекса. За исключением первичного ключа другого, но предлагаемый вами индекс уже существует, и именно он используется, если вы посмотрите на объяснение:Bitmap Index Scan on idx_time_limits_phi_start_end
Стефан Роллан,
1

Мне удалось быстро увеличить (с 1 сек до 70мс)

У меня есть таблица с агрегацией многих измерений и многих уровней ( lстолбец) (30 с, 1 м, 1 ч и т. Д.), Есть два столбца с привязкой к диапазону: $sдля начала и $eдля конца.

Я создал два многоколоночных индекса: один для начала и один для конца.

Я настроил запрос выбора: выберите диапазоны, где их начальная граница находится в заданном диапазоне. дополнительно выберите диапазоны, где их конечная граница находится в заданном диапазоне.

Explain показывает два потока строк, эффективно использующих наши индексы.

Индексы:

drop index if exists agg_search_a;
CREATE INDEX agg_search_a
ON agg (measurement_id, l, "$s");

drop index if exists agg_search_b;
CREATE INDEX agg_search_b
ON agg (measurement_id, l, "$e");

Выберите запрос:

select "$s", "$e", a, t, b, c from agg
where 
    measurement_id=0 
    and l =  '30s'
    and (
        (
            "$s" > '2013-05-01 02:05:05'
            and "$s" < '2013-05-01 02:18:15'
        )
        or 
        (
             "$e" > '2013-05-01 02:00:05'
            and "$e" < '2013-05-01 02:18:05'
        )
    )

;

Объясните:

[
  {
    "Execution Time": 0.058,
    "Planning Time": 0.112,
    "Plan": {
      "Startup Cost": 10.18,
      "Rows Removed by Index Recheck": 0,
      "Actual Rows": 37,
      "Plans": [
    {
      "Startup Cost": 10.18,
      "Actual Rows": 0,
      "Plans": [
        {
          "Startup Cost": 0,
          "Plan Width": 0,
          "Actual Rows": 26,
          "Node Type": "Bitmap Index Scan",
          "Index Cond": "((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$s\" > '2013-05-01 02:05:05'::timestamp without time zone) AND (\"$s\" < '2013-05-01 02:18:15'::timestamp without time zone))",
          "Plan Rows": 29,
          "Parallel Aware": false,
          "Actual Total Time": 0.016,
          "Parent Relationship": "Member",
          "Actual Startup Time": 0.016,
          "Total Cost": 5,
          "Actual Loops": 1,
          "Index Name": "agg_search_a"
        },
        {
          "Startup Cost": 0,
          "Plan Width": 0,
          "Actual Rows": 36,
          "Node Type": "Bitmap Index Scan",
          "Index Cond": "((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$e\" > '2013-05-01 02:00:05'::timestamp without time zone) AND (\"$e\" < '2013-05-01 02:18:05'::timestamp without time zone))",
          "Plan Rows": 39,
          "Parallel Aware": false,
          "Actual Total Time": 0.011,
          "Parent Relationship": "Member",
          "Actual Startup Time": 0.011,
          "Total Cost": 5.15,
          "Actual Loops": 1,
          "Index Name": "agg_search_b"
        }
      ],
      "Node Type": "BitmapOr",
      "Plan Rows": 68,
      "Parallel Aware": false,
      "Actual Total Time": 0.027,
      "Parent Relationship": "Outer",
      "Actual Startup Time": 0.027,
      "Plan Width": 0,
      "Actual Loops": 1,
      "Total Cost": 10.18
    }
      ],
      "Exact Heap Blocks": 1,
      "Node Type": "Bitmap Heap Scan",
      "Plan Rows": 68,
      "Relation Name": "agg",
      "Alias": "agg",
      "Parallel Aware": false,
      "Actual Total Time": 0.037,
      "Recheck Cond": "(((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$s\" > '2013-05-01 02:05:05'::timestamp without time zone) AND (\"$s\" < '2013-05-01 02:18:15'::timestamp without time zone)) OR ((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$e\" > '2013-05-01 02:00:05'::timestamp without time zone) AND (\"$e\" < '2013-05-01 02:18:05'::timestamp without time zone)))",
      "Lossy Heap Blocks": 0,
      "Actual Startup Time": 0.033,
      "Plan Width": 44,
      "Actual Loops": 1,
      "Total Cost": 280.95
    },
    "Triggers": []
  }
]

Хитрость в том, что узлы вашего плана содержат только нужные строки. Ранее мы получили тысячи строк в узле плана, потому что он выбран all points from some point in time to the very end, затем следующий узел удалил ненужные строки.

Боровский
источник