Індекс первинного ключа, який не використовується в простому з'єднанні


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

Відповіді:


22

Багато людей чули вказівки про те, що «послідовне сканування погано» і намагаються усунути їх зі своїх планів, але це не так просто. Якщо запит охоплює кожен рядок таблиці, послідовне сканування - це найшвидший спосіб отримати ці рядки. Ось чому ваш оригінальний запит приєднання використовував послідовне сканування, оскільки всі рядки в обох таблицях були обов'язковими.

Плануючи запит, планувальник Postgres оцінює витрати на різні операції (обчислювальні, послідовні та випадкові IO) за різними можливими схемами та вибирає план, який він оцінює як найменшу вартість. При виконанні IO з обертового сховища (дисків) випадковий IO, як правило, значно повільніше, ніж послідовний IO, конфігурація 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 рядків / блок.

За допомогою цієї статистики планувальник вважає ймовірним, що приєднанні доведеться читати всі або більшість блоків даних таблиці. Оскільки підшукування індексу також не є вільним, а обчислення для сканування блоку, що оцінює стан фільтра, є дуже дешевим по відношенню до IO, планувальник вирішив послідовно сканувати таблицю і уникати накладних витрат та випадкових зчитувань під час обчислення послідовного сканування. буде швидше.

У реальному світі дані часто доступні в пам'яті через кеш сторінок ОС, і тому не кожен зчитуваний блок вимагає вводу-виводу. Неможливо передбачити, наскільки ефективним буде кеш для даного запиту, але Pg-планувальник використовує просту евристику. Значення конфігурації ефективні_cache_size повідомляє планувальникам оцінки про ймовірність виникнення фактичних витрат на виведення. Більше значення призведе до того, що він оцінить нижчу вартість випадкового вводу-виводу і, таким чином, може схилити його до методу, керованого індексом, під час послідовного сканування.


Дякую, це поки що найкращий (і найкоротший) опис, який я прочитав. Уточнили кілька ключових моментів.
dezso

1
Відмінне пояснення. Однак обчислення сторінки рядків / даних трохи не вдається. Ви повинні HeapTupleHeaderрозраховувати заголовок сторінки (24 байти) + 4 байти на кожен вказівник елемента на рядок + заголовок рядка (23 байти на рядок) + NULL бітмаска + вирівнювання відповідно до MAXALIGN. Нарешті, невідома кількість прокладки через вирівнювання даних залежно від типів даних стовпців та їх послідовності. Загалом на цій сторінці не більше 33 рядків на 8-футовій сторінці. (Не беручи до уваги TOAST.)
Ервін Брандштеттер

1
@ErwinBrandstetter Дякую за заповнення більш вимогливих розрахунків розміру рядків. Я завжди вважав, що вихідна оцінка ширини рядків шляхом пояснення включатиме міркування за рядком, такі як заголовок та NULL-бітова маска, але не накладні рівні сторінки.
dbenhur

1
@dbenhur: Ви можете швидко запустити EXPLAIN ANALYZE SELECT foo from barосновну таблицю манекенів, щоб перевірити. Крім того, фактичний простір на диску залежить від вирівнювання даних, що важко буде врахувати, коли витягуються лише деякі рядки. Ширина рядка в EXPLAINявляє собою основну потребу в просторі для отриманого набору стовпців.
Ервін Брандстеттер

5

Ви витягуєте всі рядки з обох таблиць, тому реальної користі немає за допомогою сканування індексів. Сканування індексу має сенс лише якщо ви вибираєте з таблиці лише кілька рядків (як правило, менше 10% -15%)


Так, ви маєте рацію :) Я намагався уточнити ситуацію з більш конкретним випадком, подивіться останній запит.
dezso

@dezso: Те саме. Якщо індекс увімкнено (lezarva, munkalap_id)і він є досить вибірковим, він може бути використаний. Це NOTробить це менш вірогідним.
ypercubeᵀᴹ

Я додав частковий індекс на основі вашої пропозиції, і він використовується, тому половина проблеми вирішена. Але я б не очікував, що індекс зовнішнього ключа виявиться марним, враховуючи, що я хочу приєднатися до лише 87 значень порівняно з початковими 3252.
дез

1
@dezso Ряди середнього 214 байтів, тож у вас буде трохи менше 40 рядків на 8K блок даних. Селективність індексу також близько 1/40 (1006/38046). Отже, Pg показує, що читання всіх блоків послідовно дешевше, ніж ймовірне зчитування приблизно однакової кількості блоків випадковим чином при використанні індексу. На ці прогнозовані виторги можна впливати значення конфігурації ефективної_cache_size та random_page_cost.
dbenhur

@dbenhur: ви могли б зробити свій коментар правильною відповіддю?
дезсо
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.