У нас есть таблица 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 не самый большой, всего несколько сотен тысяч записей, некоторые таблицы в соединении содержат миллионы записей.Извините, я думал, что на маленьких экранах легче читать





Поскольку 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-й индекс, чтобы не касаться таблицы.
Формат JSON планов EXPLAIN удобен для машинного чтения. Но вы спрашиваете людей, а не машины.