Работа индексов в 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), автоматически ли он создает индексы для него и

    Если индексы в первом вопросе не эквивалентны, то какой индекс создается по вышеуказанной команде первичного ключа?

codecool
источник

Ответы:

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: я подумал об этом еще немного. Кластеризация может помочь в целом, потому что это эффективно также a 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, который также может (иногда) использовать столбцы, которые не находятся в начале определения индекса.

ре 2) Да, это создаст индекс

Цитата из руководства

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

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

Я настоятельно рекомендую вам прочитать главу об индексах в руководстве , она в основном отвечает на все вопросы выше.

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

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

  • Создайте дополнительный индекс для этих столбцов, если важна производительность.

  • Если все вовлеченные столбцы включены в индекс (охватывающий индекс) и все задействованные строки (на блок) видны для всех транзакций, вы можете получить «сканирование только индекса» в pg 9.2 или новее.

Эрвин Брандштеттер
источник
7
  1. Это эквивалентно? Если нет, то почему?

    Индекс (user_id1, user_id2) и Индекс (user_id2, user_id1)

Они не эквивалентны и, вообще говоря, индекс (bar, baz) не будет эффективен для запросов вида select * from foo where baz=?

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

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

Обратите внимание на то, что индекс может помочь в двух случаях: если полное сканирование индекса значительно дешевле, чем полное сканирование таблицы, и либо: 1. поиск в таблице дешев (потому что их мало или они кластеризованы), либо 2. индекс покрывает, поэтому нет поиска таблиц вообще , смотрите Erwins комментарии здесь

обкатки:

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
Джек Дуглас
источник