11.8. Частичные индексы#

11.8. Частичные индексы

11.8. Частичные индексы #

Частичный индекс - это индекс, построенный над подмножеством таблицы; подмножество определяется условным выражением (называемым предикатом частичного индекса). Индекс содержит записи только для тех строк таблицы, которые удовлетворяют предикату. Частичные индексы являются специализированной функцией, но существует несколько ситуаций, в которых они полезны.

Одна из основных причин использования частичного индекса - избежать индексации общих значений. Поскольку запрос, ищущий общее значение (которое составляет более нескольких процентов от всех строк таблицы), все равно не будет использовать индекс, нет смысла включать эти строки в индекс. Это уменьшает размер индекса, что ускоряет выполнение запросов, которые действительно используют индекс. Это также ускоряет многие операции обновления таблицы, потому что индекс не нужно обновлять во всех случаях. Пример возможного применения этой идеи показан в Пример 11.1.

Пример 11.1. Настройка частичного индекса для исключения общих значений

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

Предположим, что у нас есть таблица следующего вида:

CREATE TABLE access_log (
    url varchar,
    client_ip inet,
    ...
);

Для создания частичного индекса, подходящего для нашего примера, используйте команду такого вида:

CREATE INDEX access_log_client_ip_ix ON access_log (client_ip)
WHERE NOT (client_ip > inet '192.168.100.0' AND
           client_ip < inet '192.168.100.255');

Типичный запрос, который может использовать этот индекс, будет:

SELECT *
FROM access_log
WHERE url = '/index.html' AND client_ip = inet '212.78.10.32';

Здесь IP-адрес запроса скрыт частичным индексом. Следующий запрос не может использовать частичный индекс, так как он использует IP-адрес, исключенный из индекса:

SELECT *
FROM access_log
WHERE url = '/index.html' AND client_ip = inet '192.168.100.23';

Обратите внимание, что для такого частичного индекса требуется заранее определить общие значения, поэтому такие частичные индексы лучше использовать для распределений данных, которые не изменяются. Такие индексы могут быть пересозданы время от времени для корректировки новых распределений данных, но это требует дополнительных усилий по обслуживанию.


Еще одно возможное использование частичного индекса - исключение значений из индекса, которые не интересуют типичную рабочую нагрузку запросов; это показано в Пример 11.2. Это приводит к тем же преимуществам, перечисленным выше, но предотвращает доступ к "неинтересным" значениям через этот индекс, даже если индексное сканирование может быть прибыльным в этом случае. Очевидно, настройка частичных индексов для такого сценария потребует много внимания и экспериментов.

Пример 11.2. Настройка частичного индекса для исключения неинтересующих значений

Если у вас есть таблица, которая содержит как оплаченные, так и неоплаченные заказы, где неоплаченные заказы занимают небольшую долю от общего количества строк, но при этом являются наиболее часто запрашиваемыми, вы можете улучшить производительность, создав индекс только на неоплаченные строки. Команда для создания индекса будет выглядеть следующим образом:

CREATE INDEX orders_unbilled_index ON orders (order_nr)
    WHERE billed is not true;

Возможный запрос для использования этого индекса может быть:

SELECT * FROM orders WHERE billed is not true AND order_nr < 10000;

Однако индекс также может использоваться в запросах, которые вообще не связаны с order_nr, например:

SELECT * FROM orders WHERE billed is not true AND amount > 5000.00;

Это не так эффективно, как частичный индекс на столбце amount, так как системе приходится сканировать весь индекс. Однако, если незафиксированных заказов относительно немного, использование этого частичного индекса только для поиска незафиксированных заказов может быть выгодным.

Обратите внимание, что этот запрос не может использовать данный индекс:

SELECT * FROM orders WHERE order_nr = 3501;

Порядковый номер 3501 может быть среди оплаченных или неоплаченных заказов.


Пример 11.2 также показывает, что индексируемый столбец и столбец, используемый в предикате, не обязательно должны совпадать. Tantor BE поддерживает частичные индексы с произвольными предикатами, при условии, что в них участвуют только столбцы индексируемой таблицы. Однако имейте в виду, что предикат должен соответствовать условиям, используемым в запросах, которые должны получить выгоду от индекса. Чтобы быть точным, частичный индекс может использоваться в запросе только в том случае, если система может распознать, что математический предикат индекса математически подразумевается условием WHERE запроса. Tantor BE не имеет сложного теоремного доказателя, который может распознавать математически эквивалентные выражения, записанные в различных формах. (Создание такого общего теоремного доказателя крайне сложно и, вероятно, будет слишком медленным, чтобы быть действительно полезным). Система может распознавать простые неравенства, например, x < 1 подразумевает x < 2; в противном случае условие предиката должно точно соответствовать части условия WHERE запроса, иначе индекс не будет распознан как используемый. Сопоставление происходит на этапе планирования запроса, а не во время выполнения. В результате параметризованные предложения запроса не работают с частичным индексом. Например, подготовленный запрос с параметром может указывать x < ?, что никогда не подразумевает x < 2 для всех возможных значений параметра.

Третье возможное использование частичных индексов не требует использования индекса в запросах вообще. Идея здесь заключается в создании уникального индекса над подмножеством таблицы, как в Пример 11.3. Это обеспечивает уникальность среди строк, которые удовлетворяют предикату индекса, без ограничения тех, которые этому предикату не удовлетворяют.

Пример 11.3. Настройка частичного уникального индекса

Предположим, у нас есть таблица, описывающая результаты тестов. Мы хотим убедиться, что для заданной комбинации субъекта и цели есть только одна запись с результатом "успешно", но может быть любое количество записей с результатом "неуспешно". Вот один из способов сделать это:

CREATE TABLE tests (
    subject text,
    target text,
    success boolean,
    ...
);

CREATE UNIQUE INDEX tests_success_constraint ON tests (subject, target)
    WHERE success;

Это особенно эффективный подход, когда есть немного успешных тестов и много неудачных. Также возможно разрешить только одно значение NULL в столбце, создав уникальный частичный индекс с ограничением IS NULL.


Наконец, частичный индекс также может использоваться для переопределения выбора плана запроса системой. Кроме того, наборы данных с особенными распределениями могут привести к использованию индекса, когда это действительно необходимо. В этом случае индекс может быть настроен таким образом, чтобы он не был доступен для проблемного запроса. Обычно Tantor BE делает разумные выборы в отношении использования индексов (например, он избегает их при извлечении общих значений, поэтому предыдущий пример действительно только экономит место индекса, и не требуется избегать использования индекса), и грубо неверные выборы плана являются причиной для сообщения об ошибке.

Имейте в виду, что настройка частичного индекса указывает на то, что вы знаете не меньше, чем знает планировщик запросов, в частности, вы знаете, когда индекс может быть выгодным. Формирование этого знания требует опыта и понимания того, как работают индексы в Tantor BE. В большинстве случаев преимущество частичного индекса перед обычным индексом будет минимальным. Есть случаи, когда они являются довольно контрпродуктивными, как в Пример 11.4.

Пример 11.4. Не используйте частичные индексы в качестве замены для разделения.

Вам может показаться соблазнительным создать большой набор неперекрывающихся частичных индексов, например

CREATE INDEX mytable_cat_1 ON mytable (data) WHERE category = 1;
CREATE INDEX mytable_cat_2 ON mytable (data) WHERE category = 2;
CREATE INDEX mytable_cat_3 ON mytable (data) WHERE category = 3;
...
CREATE INDEX mytable_cat_N ON mytable (data) WHERE category = N;

Это плохая идея! Почти наверняка вам будет лучше использовать единственный неполный индекс, объявленный как

CREATE INDEX mytable_cat_data ON mytable (category, data);

(Переместите столбец категории вперед, по причинам, описанным в Раздел 11.3). В то время как поиск в этом большем индексе может потребовать спуска на несколько уровней дерева больше, чем поиск в меньшем индексе, это почти наверняка будет дешевле, чем усилия планировщика, необходимые для выбора соответствующего из частичных индексов. Суть проблемы заключается в том, что система не понимает отношений между частичными индексами и будет тщательно проверять каждый из них, чтобы увидеть, применим ли он к текущему запросу.

Если ваша таблица настолько большая, что один индекс действительно является плохой идеей, вам следует рассмотреть возможность использования секционирования вместо этого (см. Раздел 5.11). С помощью этого механизма система понимает, что таблицы и индексы не перекрываются, поэтому возможна гораздо более эффективная работа.


Более подробную информацию о частичных индексах можно найти в [ston89b], [olson93] и [seshadri95].