Индекс первичного ключа не используется в простом соединении

16

У меня есть следующие таблицы и определения индекса:

CREATE TABLE munkalap (
    munkalap_id serial PRIMARY KEY,
    ...
);

CREATE TABLE munkalap_lepes (
    munkalap_lepes_id serial PRIMARY KEY,
    munkalap_id integer REFERENCES munkalap (munkalap_id),
    ...
);

CREATE INDEX idx_munkalap_lepes_munkalap_id ON munkalap_lepes (munkalap_id);

Почему ни один из индексов munkalap_id не используется в следующем запросе?

EXPLAIN ANALYZE SELECT ml.* FROM munkalap m JOIN munkalap_lepes ml USING (munkalap_id);

QUERY PLAN
Hash Join  (cost=119.17..2050.88 rows=38046 width=214) (actual time=0.824..18.011 rows=38046 loops=1)
  Hash Cond: (ml.munkalap_id = m.munkalap_id)
  ->  Seq Scan on munkalap_lepes ml  (cost=0.00..1313.46 rows=38046 width=214) (actual time=0.005..4.574 rows=38046 loops=1)
  ->  Hash  (cost=78.52..78.52 rows=3252 width=4) (actual time=0.810..0.810 rows=3253 loops=1)
        Buckets: 1024  Batches: 1  Memory Usage: 115kB
        ->  Seq Scan on munkalap m  (cost=0.00..78.52 rows=3252 width=4) (actual time=0.003..0.398 rows=3253 loops=1)
Total runtime: 19.786 ms

Это то же самое, даже если я добавлю фильтр:

EXPLAIN ANALYZE SELECT ml.* FROM munkalap m JOIN munkalap_lepes ml USING (munkalap_id) WHERE NOT lezarva;

QUERY PLAN
Hash Join  (cost=79.60..1545.79 rows=1006 width=214) (actual time=0.616..10.824 rows=964 loops=1)
  Hash Cond: (ml.munkalap_id = m.munkalap_id)
  ->  Seq Scan on munkalap_lepes ml  (cost=0.00..1313.46 rows=38046 width=214) (actual time=0.007..5.061 rows=38046 loops=1)
  ->  Hash  (cost=78.52..78.52 rows=86 width=4) (actual time=0.587..0.587 rows=87 loops=1)
        Buckets: 1024  Batches: 1  Memory Usage: 4kB
        ->  Seq Scan on munkalap m  (cost=0.00..78.52 rows=86 width=4) (actual time=0.014..0.560 rows=87 loops=1)
              Filter: (NOT lezarva)
Total runtime: 10.911 ms
Dezso
источник

Ответы:

22

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

При планировании запроса планировщик Postgres оценивает затраты на различные операции (вычислительные, последовательные и случайные операции ввода-вывода) по различным возможным схемам и выбирает план, который он оценивает как имеющий наименьшую стоимость. При выполнении операций ввода-вывода из вращающегося хранилища (дисков) случайный ввод-вывод обычно значительно медленнее, чем последовательный ввод-вывод, конфигурация pg по умолчанию для random_page_cost и seq_page_cost оценивает разницу в стоимости 4: 1.

Эти соображения вступают в игру при рассмотрении метода соединения или фильтрации, который использует индекс против индекса, который последовательно просматривает таблицу. При использовании индекса план может быстро найти строку по индексу, а затем должен учитывать случайное считывание блока для разрешения данных строки. В случае вашего второго запроса, в котором был добавлен предикат фильтрации WHERE NOT lezarva, вы можете увидеть, как это повлияло на оценки планирования в результатах EXPLAIN ANALYZE. Планировщик оценивает 1006 строк, полученных в результате объединения (что довольно близко соответствует фактическому набору результатов 964). Учитывая, что более крупная таблица munkalap_lepes содержит около 38 тыс. Строк, планировщик видит, что объединению потребуется доступ к 1006/38046 или 1/38 строкам таблицы. Он также знает, что средняя ширина строки составляет 214 байтов, а блок равен 8 КБ, поэтому имеется около 38 строк / блок.

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

В реальном мире данные часто доступны в памяти через кеш страниц ОС, поэтому не каждый блок чтения требует ввода-вывода. Может быть довольно сложно предсказать, насколько эффективным будет кэш для данного запроса, но планировщик Pg действительно использует некоторые простые эвристики. Значение конфигурации effective_cache_size информирует оценку планировщики вероятности понести фактические затраты ввода - вывод. Большее значение заставит его оценивать более низкую стоимость случайного ввода-вывода и, таким образом, может сместить его в сторону метода, управляемого индексом, при последовательном сканировании.

dbenhur
источник
Спасибо, это лучшее (и самое краткое) описание, которое я читал. Уточнил несколько ключевых моментов.
Дезсо
1
Отличное объяснение. Тем не менее, подсчет строк / данных немного сбит с толку. Вы должны учитывать заголовок страницы (24 байта) + 4 байта для каждого указателя элемента на строку + заголовок строки HeapTupleHeader(23 байта на строку) + битовая маска NULL + выравнивание в соответствии с MAXALIGN. Наконец, неизвестный объем заполнения из-за выравнивания данных в зависимости от типов данных столбцов и их последовательности. Всего на странице 8 Кбайт не более 33 строк. (Не принимая во внимание тост.)
Эрвин Брандштеттер
1
@ErwinBrandstetter Спасибо за заполнение более точных расчетов размера строки. Я всегда предполагал, что вывод оценки ширины строки с помощью объяснения будет включать в себя соображения для каждой строки, такие как заголовок и битовая маска NULL, но не накладные расходы на уровне страницы.
dbenhur
1
@dbenhur: Вы можете быстро EXPLAIN ANALYZE SELECT foo from barвыполнить проверку с помощью фиктивной таблицы. Кроме того, фактическое пространство на диске зависит от выравнивания данных, которое будет трудно учесть, когда извлекаются только некоторые строки. Ширина строки в EXPLAINпредставляет базовую потребность в пространстве для извлеченного набора столбцов.
Эрвин Брандштеттер
5

Вы извлекаете все строки из обеих таблиц, поэтому использование сканирования по индексу не приносит никакой реальной выгоды. Сканирование индекса имеет смысл, только если вы выбираете только несколько строк в таблице (обычно менее 10% -15%).

a_horse_with_no_name
источник
Да, вы правы :) Я попытался прояснить ситуацию с более конкретным случаем, см. Последний запрос.
Дезсо
@dezso: То же самое. Если у вас есть индекс, (lezarva, munkalap_id)и он достаточно избирателен, его можно использовать. Это NOTделает это менее вероятным.
ypercubeᵀᴹ
Я добавил частичный индекс, основанный на вашем предложении, и он используется, поэтому половина проблемы решена. Но я бы не ожидал, что индекс внешнего ключа будет бесполезным, учитывая, что я хочу присоединиться только к 87 значениям по сравнению с исходным 3252.
dezso
1
@dezso Строки avg 214 байтов в ширину, поэтому у вас будет чуть меньше 40 строк на блок данных 8K. Селективность индекса также составляет около 1/40 (1006/38046). Таким образом, Pg считает, что чтение всех блоков последовательно дешевле, чем вероятное чтение примерно одинакового количества блоков случайным образом при использовании индекса. На эти предполагаемые результаты обмена могут влиять значения конфигурацииffective_cache_size и random_page_cost.
dbenhur
@dbenhur: не могли бы вы сделать свой комментарий правильным ответом?
Дезсо