Робота індексів у PostgreSQL


73

У мене є пара питань щодо роботи індексів у PostgreSQL. У мене є Friendsтаблиця з таким показником:

   Friends ( user_id1 ,user_id2) 

user_id1і user_id2є сторонніми ключами до userстолу

  1. Ці еквіваленти? Якщо ні, то чому?

    Index(user_id1,user_id2) and Index(user_id2,user_id1)
  2. Якщо я створюю Первинний ключ (user_id1, user_id2), чи автоматично він створює для нього і індекси

    Якщо індекси у першому питанні не еквівалентні, то який індекс створюється над командою первинного ключа?

Відповіді:


77

Ось результати запиту таблиці на другому стовпці багатоколінного індексу .
Ефекти легко відтворити для будь-кого. Просто спробуйте вдома.

Я тестував PostgreSQL 9.0.5 на Debian, використовуючи середню таблицю бази даних реального життя з 23322 рядками. Він реалізує зв'язок n: m між таблицями adr(адресою) та att(атрибутом), але це не стосується тут. Спрощена схема:

CREATE TABLE adratt (
  adratt_id serial PRIMARY KEY
, adr_id    integer NOT NULL
, att_id    integer NOT NULL
, log_up    timestamp(0) NOT NULL DEFAULT (now())::timestamp(0)
, CONSTRAINT adratt_uni UNIQUE (adr_id, att_id)
);

UNIQUEОбмеження ефективно реалізує унікальний індекс. Я повторив тест із простим індексом, щоб бути впевненим, і отримав однакові результати, як і очікувалося.

CREATE INDEX adratt_idx ON adratt(adr_id, att_id)

Таблиця кластеризована за adratt_uniіндексом і перед тестом я запустив:

CLUSTER adratt;
ANALYZE adratt;

Послідовне сканування запитів (adr_id, att_id)настільки ж швидко, наскільки це можливо. Індекс багатокольонових ще буде використовуватися для умови запиту лише у другому стовпці індексу.

Я кілька разів запускав запити, щоб заповнити кеш, і вибрав найкраще з десяти циклів, щоб отримати порівнянні результати.

1. Запит із використанням обох стовпців

SELECT *
FROM   adratt
WHERE  att_id = 90
AND    adr_id = 10;

 adratt_id | adr_id | att_id |       log_up
-----------+--------+--------+---------------------
       123 |     10 |     90 | 2008-07-29 09:35:54
(1 row)

Вихід EXPLAIN ANALYZE:

Index Scan using adratt_uni on adratt  (cost=0.00..3.48 rows=1 width=20) (actual time=0.022..0.025 rows=1 loops=1)
  Index Cond: ((adr_id = 10) AND (att_id = 90))
Total runtime: 0.067 ms

2. Запит за допомогою першого стовпця

SELECT * FROM adratt WHERE adr_id = 10

 adratt_id | adr_id | att_id |       log_up
-----------+--------+--------+---------------------
       126 |     10 |     10 | 2008-07-29 09:35:54
       125 |     10 |     13 | 2008-07-29 09:35:54
      4711 |     10 |     21 | 2008-07-29 09:35:54
     29322 |     10 |     22 | 2011-06-06 15:50:38
     29321 |     10 |     30 | 2011-06-06 15:47:17
       124 |     10 |     62 | 2008-07-29 09:35:54
     21913 |     10 |     78 | 2008-07-29 09:35:54
       123 |     10 |     90 | 2008-07-29 09:35:54
     28352 |     10 |    106 | 2010-11-22 12:37:50
(9 rows)

Вихід EXPLAIN ANALYZE:

Index Scan using adratt_uni on adratt  (cost=0.00..8.23 rows=9 width=20) (actual time=0.007..0.023 rows=9 loops=1)
  Index Cond: (adr_id = 10)
Total runtime: 0.058 ms

3. Запит за допомогою другого стовпця

SELECT * FROM adratt WHERE att_id = 90

 adratt_id | adr_id | att_id |       log_up
-----------+--------+--------+---------------------
       123 |     10 |     90 | 2008-07-29 09:35:54
       180 |     39 |     90 | 2008-08-29 15:46:07
...
(83 rows)

Вихід EXPLAIN ANALYZE:

Index Scan using adratt_uni on adratt  (cost=0.00..818.51 rows=83 width=20) (actual time=0.014..0.694 rows=83 loops=1)
  Index Cond: (att_id = 90)
Total runtime: 0.849 ms

4. Вимкнення покажчиків індексів та бітмапскан

SET enable_indexscan = off;
SELECT * FROM adratt WHERE att_id = 90

Вихід EXPLAIN ANALYZE:

Bitmap Heap Scan on adratt  (cost=779.94..854.74 rows=83 width=20) (actual time=0.558..0.743 rows=83 loops=1)
  Recheck Cond: (att_id = 90)
  ->  Bitmap Index Scan on adratt_uni  (cost=0.00..779.86 rows=83 width=0) (actual time=0.544..0.544 rows=83 loops=1)
        Index Cond: (att_id = 90)
Total runtime: 0.894 ms
SET enable_bitmapscan = off
SELECT * FROM adratt WHERE att_id = 90

Вихід EXPLAIN ANALYZE:

Seq Scan on adratt  (cost=0.00..1323.10 rows=83 width=20) (actual time=0.009..2.429 rows=83 loops=1)
  Filter: (att_id = 90)
Total runtime: 2.680 ms

Висновок

Як очікувалося, індекс багато стовпців використовується лише для запиту на другий стовпчик.
Як і очікувалося, він менш ефективний, але запит все-таки в 3 рази швидший, ніж без індексу.
Після відключення сканування індексів, планувальник запитів вибирає скасовування бітової карти, яка виконує майже так само швидко. Тільки після відключення це також повертається до послідовного сканування.


кластеризація змінить значення, якщо кількість збігів в індексі буде достатньо високою (див. тут для доказування - зверніть увагу на подвійний пробіг, щоб отримати кешовані дані)
Джек Дуглас

1
@JackDouglas: Я ще трохи задумався над цим. Кластеризація може допомогти, як правило, тому що це ефективно також vacuum fullі a reindex. Крім цього , це допоможе індексних сканувань на першому або обох провідних колон багато , але боляче запитів на другій колонці. У щойно кластеризованій таблиці рядки з однаковим значенням у другому стовпчику розкладаються, так що потрібно буде прочитати максимум блоків.
Ервін Брандштеттер

28

повторно 1) Так і ні.

Для запиту, який використовує обидва стовпці, наприклад, where (user_id1, user_id2) = (1,2)не має значення, який індекс створено.

Для запиту, який має умову лише в одному з стовпців, наприклад, where user_id1 = 1це має значення, тому що зазвичай для порівняння оптимізатором можуть використовуватися лише "провідні" стовпці. Так where user_id1 = 1можна було б використовувати індекс (user_id1, user_id2), але він не зміг би індексувати (user_id2, user_id1) у всіх випадках.

Погравши з цим (після того, як Ервін так ласкаво показав нам налаштування, де це працює), здається, що це сильно залежить від розподілу даних другого стовпця, хоча я ще не з'ясував, яка ситуація дає можливість оптимізатору використовувати тривалі колонки для умови, де БЕЗ.

Oracle 11, який також може (іноді) використовувати стовпці, які не знаходяться на початку визначення індексу.

re 2) Так, це створить індекс

Цитата з посібника

Додавання первинного ключа автоматично створить унікальний індекс btree для стовпця або групи стовпців, які використовуються в первинному ключі.

re 2a) Primary Key (user_id1,user_id2)створить індекс на (user_id1, user_id2) (який ви можете дізнатись дуже легко, просто створивши такий первинний ключ)

Я настійно рекомендую прочитати розділ про покажчики в посібнику , він в основному відповідає на всі питання вище.

Крім того, який індекс створити? від depesz добре справляється з поясненням порядку в стовпцях індексу та інших темах, пов'язаних з індексом


11

Оголошення 1)
У PostgreSQL є такі обмеження, як @a_horse_with_no_name . До тих пір, поки багатоколірні індекси версії 8.0 не могли використовуватися лише для запитів у провідних стовпцях. Це було покращено у версії 8.1. Струму по експлуатації для Postgres 10 (оновлено) пояснює:

Багатокольоновий індекс B-дерева може використовуватися з умовами запитів, які включають будь-який підмножина стовпців індексу, але індекс є найбільш ефективним, коли на провідних (крайніх лівих) колонках є обмеження. Точне правило полягає в тому, що обмеження рівності на провідних стовпцях плюс будь-які обмеження нерівності в першому стовпчику, що не має обмеження рівності, будуть використовуватися для обмеження частини індексу, що сканується. Обмеження на стовпці праворуч від цих стовпців перевіряються в індексі, тому вони зберігають відвідування відповідної таблиці, але вони не зменшують частину індексу, яку потрібно сканувати. Наприклад, з урахуванням індексу (a, b, c)та умови запиту WHERE a = 5 AND b >= 42 AND c < 77, індекс потрібно було б сканувати з першого запису з a= 5 таb= 42 вгору через останній запис з a= 5. Записи індексу з c> = 77 будуть пропущені, але їх все одно доведеться просканувати. Цей індекс, в принципі, може бути використаний для запитів, які мають обмеження bта / або cне мають обмежень, a- але весь індекс повинен бути сканований, тому в більшості випадків планувальник вважає за краще послідовне сканування таблиці за допомогою індексу.

Наголос мій. Я можу це підтвердити з досвіду.
Також дивіться тестовий випадок, доданий сюди мою пізнішу відповідь .


11

Це у відповідь на відповідь Джека , коментар не зробив би.

Там не було ні одного , що покривають індекси в PostgreSQL до версії 9.2. Завдяки моделі MVCC, для перегляду видимості необхідно відвідати кожен кортеж у наборі результатів. Можливо, ви думаєте про Oracle.

Розробники PostgreSQL говорять про "сканування лише для індексів" . Фактично, функція була випущена разом із Postgres 9.2. Прочитайте повідомлення про фіксацію .
Депес написав дуже інформативну публікацію в блозі .

Індекси справжнього покриття (оновлення) вводяться в INCLUDEпункті з Postgres 11.

Це теж небагато:

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

Як повідомляється в коментарях до моєї іншої відповіді, я також проводив тести з таблицею з двох цілих чисел і нічого іншого. Індекс містить ті самі стовпці, що і таблиця. Розмір індексу btree становить приблизно 2/3 від таблиці. Недостатньо, щоб пояснити прискорення фактора 3. Я провів ще тест, виходячи з вашої настройки, спрощений до двох стовпців і зі 100000 рядками. У моїй установці PostgreSQL 9.0 результати були послідовними.

Якщо в таблиці є додаткові стовпці, прискорення з індексом стає більш істотним, але це, звичайно, не єдиний фактор тут .

Підсумовуючи основні моменти:

  • Багатоколонкові індекси можна використовувати із запитами на непровідних стовпцях, але прискорення становить лише фактор 3 для вибіркових критеріїв (невеликий відсоток рядків у результаті). Вищий для більших кортежів, нижчий для більших частин таблиці в наборі результатів.

  • Створіть додатковий індекс у цих стовпцях, якщо важлива продуктивність.

  • Якщо всі задіяні стовпці включені до індексу (охоплює індекс) і всі залучені рядки (на блок) видно для всіх транзакцій, ви можете отримати "сканування лише з індексом" на сторінці 9.2 або пізнішої версії.


7
  1. Ці еквіваленти? Якщо ні, то чому?

    Індекс (user_id1, user_id2) та Index (user_id2, user_id1)

Вони не є еквівалентними, і загалом кажучи, індекс (bar, baz) не буде ефективним для запитів форми select * from foo where baz=?

Ервін продемонстрував, що такі індекси дійсно можуть пришвидшити запит, але цей ефект обмежений і не є таким же порядком, як ви, як правило, очікуєте, що індекс покращить пошук - він покладається на те, що часто «повне сканування» індексу часто швидше, ніж "повне сканування" індексованої таблиці завдяки додатковим стовпцям таблиці, які не відображаються в індексі.

Підсумок: індекси можуть допомагати запитам навіть у непровідних стовпцях, але одним із двох вторинних та відносно незначних способів, а не драматичним способом, як правило, ви очікуєте, що індекс допоможе завдяки структурі btree

nb два способи, за якими може допомогти індекс, - це якщо повне сканування індексу значно дешевше, ніж повне сканування таблиці, і будь-яке: 1. Шукання таблиці є дешевими (тому що їх небагато або вони кластеризовані), або 2. індекс охоплює, щоб не було запитів на таблицю взагалі , дивіться коментарі Ервінса тут

тестова площадка:

create table foo(bar integer not null, baz integer not null, qux text not null);

insert into foo(bar, baz, qux)
select random()*100, random()*100, 'some random text '||g from generate_series(1,10000) g;

запит 1 (немає індексу, потрапляючи в 74 буфери ):

explain (buffers, analyze, verbose) select max(qux) from foo where baz=0;
                                                  QUERY PLAN
--------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=181.41..181.42 rows=1 width=32) (actual time=3.301..3.302 rows=1 loops=1)
   Output: max(qux)
   Buffers: shared hit=74
   ->  Seq Scan on stack.foo  (cost=0.00..181.30 rows=43 width=32) (actual time=0.043..3.228 rows=52 loops=1)
         Output: bar, baz, qux
         Filter: (foo.baz = 0)
         Buffers: shared hit=74
 Total runtime: 3.335 ms

запит 2 (з індексом - оптимізатор ігнорує індекс - знову потрапляє на 74 буфери ):

create index bar_baz on foo(bar, baz);

explain (buffers, analyze, verbose) select max(qux) from foo where baz=0;
                                                  QUERY PLAN
--------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=199.12..199.13 rows=1 width=32) (actual time=3.277..3.277 rows=1 loops=1)
   Output: max(qux)
   Buffers: shared hit=74
   ->  Seq Scan on stack.foo  (cost=0.00..199.00 rows=50 width=32) (actual time=0.043..3.210 rows=52 loops=1)
         Output: bar, baz, qux
         Filter: (foo.baz = 0)
         Buffers: shared hit=74
 Total runtime: 3.311 ms

запит 2 (з індексом - і ми обманюємо оптимізатор його використовувати):

explain (buffers, analyze, verbose) select max(qux) from foo where bar>-1000 and baz=0;
                                                       QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=115.56..115.57 rows=1 width=32) (actual time=1.495..1.495 rows=1 loops=1)
   Output: max(qux)
   Buffers: shared hit=36 read=30
   ->  Bitmap Heap Scan on stack.foo  (cost=73.59..115.52 rows=17 width=32) (actual time=1.370..1.428 rows=52 loops=1)
         Output: bar, baz, qux
         Recheck Cond: ((foo.bar > (-1000)) AND (foo.baz = 0))
         Buffers: shared hit=36 read=30
         ->  Bitmap Index Scan on bar_baz  (cost=0.00..73.58 rows=17 width=0) (actual time=1.356..1.356 rows=52 loops=1)
               Index Cond: ((foo.bar > (-1000)) AND (foo.baz = 0))
               Buffers: shared read=30
 Total runtime: 1.535 ms

Тож доступ через індекс у цьому випадку вдвічі швидший, потрапляючи на 30 буферів - що з точки зору індексації "трохи швидше" !, а YMMV залежно від відносного розміру таблиці та індексу, а також кількості відфільтрованих рядків та характеристик кластеризації даних у таблиці

Навпаки, запити у провідному стовпчику використовують структуру btree індексу - у цьому випадку потрапляють 2 буфери :

explain (buffers, analyze, verbose) select max(qux) from foo where bar=0;
                                                       QUERY PLAN
------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=75.70..75.71 rows=1 width=32) (actual time=0.172..0.173 rows=1 loops=1)
   Output: max(qux)
   Buffers: shared hit=38
   ->  Bitmap Heap Scan on stack.foo  (cost=4.64..75.57 rows=50 width=32) (actual time=0.036..0.097 rows=59 loops=1)
         Output: bar, baz, qux
         Recheck Cond: (foo.bar = 0)
         Buffers: shared hit=38
         ->  Bitmap Index Scan on bar_baz  (cost=0.00..4.63 rows=50 width=0) (actual time=0.024..0.024 rows=59 loops=1)
               Index Cond: (foo.bar = 0)
               Buffers: shared hit=2
 Total runtime: 0.209 ms
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.