PostgreSQL. Почему этот индекс сканируется только медленнее, чем сканирование индекса?

У нас есть таблица foo_tbl (имя скрыто, тот же тип данных и DDL):

CREATE TABLE public.foo_tbl (
    id int8 NOT NULL,
    foo_id varchar(11) NOT NULL,
    foo_date timestamptz NULL,
    -- ... other unrelated columns ...

    CONSTRAINT pk_footbl PRIMARY KEY (id)
);
CREATE INDEX idx_1_2cols ON public.foo_tbl USING btree (foo_date, foo_id); -- initial index
CREATE INDEX idx_2_1col ON public.foo_tbl USING btree (foo_id); -- added later, when the query is slow

У нас есть большой запрос, который соединяет 7 таблиц с этой таблицей, используя foo_id и получая foo_date. Пример (реальный запрос намного больше):

select b.bar_code, f.foo_date from bar_tbl b join foo_tbl f on b.bar_id = f.foo_id limit 100;

Без объединения с foo_tbl запрос выполняется быстро (< 2 с).

После добавления соединения с foo_tbl запрос выполняется намного медленнее (> 15 с), несмотря на выполнение «сканирования только индекса» для foo_tbl с использованием индекса idx_1_2cols (в запросе используются только эти 2 столбца этой таблицы). Это EXPLAIN ANALYZE результат для таблицы:

{
  "Node Type": "Index Only Scan",
  "Parent Relationship": "Inner",
  "Parallel Aware": false,
  "Scan Direction": "Forward",
  "Index Name": "idx_1_2cols",
  "Relation Name": "foo_tbl",
  "Schema": "public",
  "Alias": "f",
  "Startup Cost": 0.42,
  "Total Cost": 2886.11,
  "Plan Rows": 1,
  "Plan Width": 20,
  "Actual Startup Time": 12.843,
  "Actual Total Time": 13.068,
  "Actual Rows": 1,
  "Actual Loops": 1200,
  "Output": ["f.foo_date", "f.foo_id"],
  "Index Cond": "(f.foo_id = (b.bar_id)::text)",
  "Rows Removed by Index Recheck": 0,
  "Heap Fetches": 0,
  "Shared Hit Blocks": 2284772,
  "Shared Read Blocks": 0,
  "Shared Dirtied Blocks": 0,
  "Shared Written Blocks": 0,
  "Local Hit Blocks": 0,
  "Local Read Blocks": 0,
  "Local Dirtied Blocks": 0,
  "Local Written Blocks": 0,
  "Temp Read Blocks": 0,
  "Temp Written Blocks": 0,
  "I/O Read Time": 0.0,
  "I/O Write Time": 0.0
}

Для исследования мы создали индекс с одним столбцом idx_2_1col, и запрос снова стал быстрым (< 3 с). Когда EXPLAIN, планировщик выбирает новый индекс вместо старого индекса для «сканирования индекса»:

{
  "Node Type": "Index Scan",
  "Parent Relationship": "Inner",
  "Parallel Aware": false,
  "Scan Direction": "Forward",
  "Index Name": "idx_2_1col",
  "Relation Name": "foo_tbl",
  "Schema": "public",
  "Alias": "f",
  "Startup Cost": 0.42,
  "Total Cost": 0.46,
  "Plan Rows": 1,
  "Plan Width": 20,
  "Actual Startup Time": 0.007,
  "Actual Total Time": 0.007,
  "Actual Rows": 1,
  "Actual Loops": 1200,
  "Output": ["f.foo_date", "f.foo_id"],
  "Index Cond": "((f.foo_id)::text = (b.bar_id)::text)",
  "Rows Removed by Index Recheck": 0,
  "Shared Hit Blocks": 4800,
  "Shared Read Blocks": 0,
  "Shared Dirtied Blocks": 0,
  "Shared Written Blocks": 0,
  "Local Hit Blocks": 0,
  "Local Read Blocks": 0,
  "Local Dirtied Blocks": 0,
  "Local Written Blocks": 0,
  "Temp Read Blocks": 0,
  "Temp Written Blocks": 0,
  "I/O Read Time": 0.0,
  "I/O Write Time": 0.0
}

Итак, почему сканирование индекса в этом случае выполняется быстрее, чем сканирование только индекса? И почему Index так медленно сканирует?

Примечания:

  • Уже VACUUM ANALYZE до EXPLAIN ANALYZE запроса
  • foo_tbl не самый большой, всего несколько сотен тысяч записей, некоторые таблицы в соединении содержат миллионы записей.
  • DBS совместим с Amazon Aurora PostgreSQL 13.5 (не бессерверный)

Формат JSON планов EXPLAIN удобен для машинного чтения. Но вы спрашиваете людей, а не машины.

jjanes 13.04.2023 21:24

Извините, я думал, что на маленьких экранах легче читать

Tr1et 14.04.2023 05:02
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
2
71
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Поскольку DDL для bar_tbl не был предоставлен, нижеследующее является просто обоснованным предположением.

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

Проблема в том, что соединение находится на foo_id, но префикс индекса foo_date. Измените порядок индекса на (foo_id, foo_date), и запрос будет выполняться намного быстрее.

Добавление idx_2_1col повысило производительность, поскольку соединение между bar_id и foo_id можно было эффективно выполнить с помощью индекса, хотя для выполнения запроса также требовалось чтение из базовой таблицы.

Ответ принят как подходящий

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

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

Индекс B-дерева с несколькими столбцами можно использовать с условиями запроса, которые включать любое подмножество столбцов индекса, но индекс эффективен, когда есть ограничения на ведущий (самый левый) столбцы. Точное правило состоит в том, что ограничения равенства на ведущие столбцы плюс любые ограничения неравенства в первом столбце, который делает не имеет ограничения равенства, будет использоваться для ограничения части индекс, который сканируется. Ограничения для столбцов справа от эти столбцы проверяются в индексе, поэтому они экономят посещения собственно таблицы, но они не уменьшают часть индекса, которая для сканирования.

Вы можете попробовать переключить столбцы в индексе или создать покрывающий индекс, включив дату во 2-й индекс, чтобы не касаться таблицы.

Другие вопросы по теме