Индексы

Индексы являются распространенным способом повышения производительности базы данных. Индекс позволяет серверу баз данных находить и извлекать определенные строки гораздо быстрее, чем тот делал бы это без индекса. Но индексы также увеличивают нагрузку на СУБД в целом, поэтому использовать их следует разумно.



Введение

Предположим, у нас есть таблица, подобная этой:

CREATE TABLE test1 (
    id integer,
    content varchar
);

и приложение делает много запросов вида

SELECT content FROM test1 WHERE id = константа;

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

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

Следующая команда может использоваться для создания индекса для вышеупомянутого столбца id:

CREATE INDEX test1_id_index ON test1 (id);

Имя test1_id_index можно выбрать любое, но лучше подобрать что-то, напоминающее о назначении индекса.

Чтобы удалить индекс, воспользуйтесь командой DROP INDEX. Индексы можно добавлять и удалять из таблиц в любое время.

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

Индексы также могут использоваться командами UPDATE и DELETE с условиями поиска. Более того, индексы могут использоваться в поиске с соединением, поэтому индекс, определенный для столбца, являющегося частью условия соединения, также может значительно ускорить запросы с соединениями.

Создание индекса для большой таблицы может занять много времени. По умолчанию QHB позволяет выполнять чтение (операторы SELECT) из таблицы параллельно с созданием индекса, но операции записи (INSERT, UPDATE, DELETE) блокируются до завершения построения индекса. В производственной среде это зачастую недопустимо. Можно разрешить запись параллельно с созданием индекса, но следует учитывать несколько моментов — дополнительную информацию см. в разделе Неблокирующее построение индексов.

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



Типы индексов

QHB предоставляет несколько типов индексов: B-дерево, хэш, GiST, SP-GiST, GIN и BRIN. Для каждого типа индекса применяется свой алгоритм, который наиболее пригоден для определенного типа запросов. По умолчанию команда CREATE INDEX создает индексы B-деревья, потому что они подходит в большинстве ситуаций. Другие типы индексов можно выбрать, написав ключевое слово USING, а за ним — название типа индекса. Например, создать хэш-индекс можно так:

CREATE INDEX имя ON таблица USING HASH (столбец);

B-дерево

B-деревья могут обрабатывать запросы с условиями на равенство и проверками диапазонов с данными, которые можно отсортировать в некотором порядке. В частности, планировщик запросов QHB будет рассматривать возможность использования индекса B-дерева всякий раз, когда индексированный столбец участвует в условии с одним из этих операторов:

<   <=   =   >=   >

Конструкции, равнозначные комбинациям этих операторов, такие как BETWEEN и IN, также могут быть реализованы с помощью поиска по индексу B-дереву. Кроме того, B-дерево можно использовать с условием IS NULL или IS NOT NULL на столбец индекса.

Оптимизатор также может использовать индексы B-деревья для запросов, включающих операторы сопоставления с шаблоном LIKE и ~, если шаблон является константой и привязан к началу строки; например, col LIKE 'foo%' или col ~ '^foo', но не col LIKE '%bar'. Однако если в базе данных не используется локаль C, нужно будет создать индекс со специальным классом операторов для поддержки индексации запросов на сопоставление с шаблоном; см. раздел Классы операторов и семейства операторов ниже. Индексы B-деревья можно использовать и для ILIKE и ~*, но только если шаблон начинается с символов, отличных от алфавитных, т. е. символов, не подверженных преобразованию регистра.

Индексы B-деревья также можно использовать для извлечения данных, отсортированных по порядку. Это не всегда быстрее, чем простое сканирование и сортировка, но зачастую полезно.


Хэш

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

=

GiST

Индексы GiST — это не единая категория индексов, а скорее инфраструктура, в которой можно реализовать множество различных стратегий индексации. Соответственно, конкретные операторы, с которыми может использоваться индекс GiST, варьируются в зависимости от стратегии индексации (класса операторов). Например, стандартный дистрибутив QHB включает классы операторов GiST для нескольких двумерных геометрических типов данных, которые поддерживают индексированные запросы с использованием следующих операторов:

<<   &<   &>   >>   <<|   &<|   |&>   |>>   @>   <@   ~=   &&

(Значение этих операторов см. в разделе Геометрические функции и операторы). Классы операторов GiST, включенные в стандартный дистрибутив, приведены в таблице Встроенные классы операторов GiST. Многие другие классы операторов GiST доступны в коллекции share/extension или в виде отдельных проектов. Подробную информацию см. в главе Индексы GiST.

Индексы GiST также могут оптимизировать поиск «ближайшего соседа», например, такой запрос:

SELECT * FROM places ORDER BY location <-> point '(101,456)' LIMIT 10;

находит десять мест, ближайших к заданной целевой точке. Возможность сделать это зависит от конкретного класса операторов, использованного при построении индекса. Операторы, которые можно использовать таким образом, перечислены в столбце «Операторы сортировки» таблицы Встроенные классы операторов GiST.


SP-GiST

Индексы SP-GiST, так же, как и индексы GiST, предлагают инфраструктуру, которая поддерживает различные виды поиска. SP-GiST позволяет реализовать широкий спектр различных несбалансированных дисковых структур данных, таких как дерево квадрантов, k-мерные и префиксные деревья (Tries). Например, стандартный дистрибутив QHB включает классы операторов SP-GiST для точек в двумерном пространстве, поддерживающие индексные запросы с использованием следующих операторов:

<<   >>   ~=   <@   <<|   |>>

(Значение этих операторов см. в разделе Геометрические функции и операторы). Классы операторов SP-GiST, включенные в стандартный дистрибутив, приведены в таблице Встроенные классы операторов SP-GiST. Подробную информации см. в главе Индексы SP-GiST.

Как и GiST, SP-GiST поддерживает поиск «ближайшего соседа». Для классов операторов SP-GiST, которые поддерживают дистанционное упорядочение, соответствующие операторы перечислены в столбце «Операторы сортировки» таблицы Встроенные классы операторов SP-GiST.


GIN

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

Подобно GiST и SP-GiST, GIN может поддерживать множество различных пользовательских стратегий индексирования, и конкретные операторы, с которыми может использоваться индекс GIN, различаются в зависимости от стратегии индексирования. Например, в стандартный дистрибутив QHB включен класс операторов GIN для массивов, который поддерживает индексированные запросы с использованием следующих операторов:

<@   @>   =   &&

(Значение этих операторов см. в разделе Функции и операторы для массивов.) Классы операторов GIN, включенные в стандартный дистрибутив, перечислены в таблице Встроенные классы операторов GIN. Многие другие классы операторов GIN доступны в коллекции share/extension или в виде отдельных проектов. Подробную информацию см. в главе Индексы GIN.


BRIN

Индексы BRIN (сокращение от Block Range Indexes, индекс диапазона блоков) хранят сводные данные о значениях, хранящихся в последовательных диапазонах физических блоков таблицы. Поэтому они наиболее эффективны для столбцов, значения в которых хорошо коррелируют с физическим порядком строк таблицы. Как и GiST, SP-GiST и GIN, BRIN могут поддерживать множество различных стратегий индексирования, и конкретные операторы, с которыми может использоваться индекс BRIN, различаются в зависимости от стратегии индексирования. Для типов данных, имеющих линейный порядок сортировки, индекс хранит минимальные и максимальные значения данных в столбце для каждого диапазона блоков. Это позволяет поддерживать запросы, использующие следующие операторы:

<   <=   =   >=   >

Классы операторов BRIN, включенные в стандартный дистрибутив, перечислены в таблице Встроенные классы операторов BRIN. Подробную информацию см. в главе Индексы BRIN.



Многостолбцовые индексы

Индекс может быть определен для более чем одного столбца таблицы. Например, если у вас есть такая таблица:

CREATE TABLE test2 (
  major int,
  minor int,
  name varchar
);

(предположим, что вы так храните каталог /dev в базе данных), и вы часто делаете запросы вида:

SELECT name FROM test2 WHERE major = константа AND minor = константа;

то, возможно, целесообразно создать индекс по двум столбцам (major и minor), например:

CREATE INDEX test2_mm_idx ON test2 (major, minor);

В настоящее время только индексы типов B-дерево, GiST, GIN и BRIN можно делать многостолбцовыми. Возможность создания индекса по нескольким ключевым столбцам не зависит от возможности добавления в этот индекс дополнительных столбцов (INCLUDE). В индексах может быть до 32 столбцов, включая столбцы INCLUDE.

Многостолбцовый индекс B-дерево можно использовать с условиями запроса, включающими любое подмножество столбцов индекса, но наиболее эффективен этот индекс, когда заданы ограничения на ведущие (крайние левые) столбцы. Точное правило заключается в том, что условия равенства на ведущие столбцы вместе с условиями неравенства на первый столбец, у которого нет условия равенства, будут использоваться для ограничения сканируемой области индекса. Условия на столбцы правее них тоже проверяются по индексу, избавляя от необходимости посещать таблицу, но область индекса, которую нужно просканировать, они не уменьшают. Например, если есть индекс по (a, b, c) и условие запроса WHERE a = 5 AND b <= 50 AND c = 100, то индекс будет сканироваться от первой записи с a = 5 и b = 42 до последней записи с a = 5. Записи индекса с c >= 77 будут пропускаться, но все равно будут просканированы. Этот индекс, в принципе, можно использовать и для запросов которые имеют ограничения на b и/или c без ограничения на a — но в этом случае будет сканироваться весь индекс, поэтому в большинстве случаев планировщик предпочтет использованию этого индекса последовательное сканирование таблицы.

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

Многостолбцовый индекс GIN можно использовать с условиями запроса, включающими любое подмножество столбцов индекса. В отличие от B-дерева или GiST, эффективность поиска по индексу одинакова независимо от того, какие столбцы индекса входят в условие запроса.

Многостолбцовый индекс BRIN можно использовать с условиями запроса, включающими любое подмножество столбцов индекса. Подобно GIN и в отличие от B-дерева или GiST, эффективность поиска по индексу одинакова независимо от того, какие столбцы индекса входят в условие запроса. Единственная причина иметь несколько индексов BRIN в одной таблице вместо одного многостолбцового заключается в возможности задавать разное значение параметра хранения pages_per_range.

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

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



Индексы и ORDER BY

В дополнение к простому поиску строк, удовлетворяющих запросу, индекс может выдать их отсортированными в определенном порядке. Это позволяет учесть в запросе указание ORDER BY без отдельного этапа сортировки. Из всех типов индексов, поддерживаемых в настоящее время QHB, только B-дерево умеет выдавать отсортированные данные — другие типы индексов возвращают строки в неопределенном, зависящем от реализации порядке.

Планировщик удовлетворит указание ORDER BY, либо просканировав доступный индекс, соответствующий этому указанию, либо просканировав таблицу в физическом порядке и выполнив явную сортировку. Для запроса, требующего сканирования большей части таблицы, явная сортировка скорее всего будет быстрее использования индекса, потому что для следования шаблону последовательного обращения ей требуется меньше дисковых операций ввода/вывода. Индексы более полезны, когда нужно извлечь только несколько строк. Важный особый случай представляет ORDER BY в сочетании с LIMIT n: при явной сортировке для определения первых n строк придется обработать все данные, но если есть индекс, соответствующий ORDER BY, первые n строк можно получить напрямую, вовсе не сканируя остальные.

По умолчанию индексы B-деревья хранят свои записи в порядке возрастания, причем значения NULL идут после всех остальных (при прочем равенстве записей они упорядочиваются по столбцу TID таблицы). Это означает, что при прямом сканировании индекса по столбцу x выдается результат, удовлетворяющий ORDER BY x (или точнее, ORDER BY x ASC NULLS LAST). Этот индекс можно просканировать и в обратном направлении, получив выходные данные, удовлетворяющие ORDER BY x DESC (или точнее, ORDER BY x DESC NULLS FIRST, поскольку NULLS FIRST подразумевается для ORDER BY DESC по умолчанию).

Можно настроить порядок индекса B-дерева, добавив параметры ASC, DESC, NULLS FIRST и/или NULLS LAST при создании индекса, например:

CREATE INDEX test2_info_nulls_low ON test2 (info NULLS FIRST);
CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST);

Индекс, хранящий элементы в порядке возрастания, в котором NULL идут первыми, может удовлетворить либо предложению ORDER BY x ASC NULLS FIRST, либо предложению ORDER BY x DESC NULLS LAST, в зависимости от направления его сканирования.

Вы можете поинтересоваться, зачем предлагать все четыре параметра, когда два параметра вместе с возможностью обратного сканирования будут охватывать все варианты ORDER BY. Для одностолбцовых индексов этих параметров действительно многовато, но в многостолбцовых индексах они могут быть полезны. Рассмотрим индекс по двум столбцам (x, y): он удовлетворит ORDER BY x, y если сканировать вперед, или ORDER BY x DESC, y DESC, если сканировать назад. Но может случиться так, что приложению часто понадобится применять ORDER BY x ASC, y DESC. С простым индексом такой порядок получить никак не удастся, но он возможен, если индекс определен как (x ASC, y DESC) или (x DESC, y ASC).

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



Объединение нескольких индексов

При простом сканировании индекса могут применяться только те предложения запроса, которые используют столбцы индекса с операторами его класса операторов и соединяются AND. Например, для данного индекса (a, b) условие запроса WHERE a = 5 AND b = 6 может использовать этот индекс напрямую, но запрос вроде WHERE a = 5 OR b = 6 — не может.

К счастью, QHB имеет возможность объединять несколько индексов (включая многократное использование одного индекса) для обработки случаев, которые невозможно реализовать при простых сканированиях индекса. Система может сформировать условия AND и OR за несколько сканирований индекса. Например, запрос типа WHERE x = 42 OR x = 47 OR x = 53 OR x = 99 можно разбить на четыре отдельных сканирования индекса по x, по одному для каждого предложения. Результаты этих сканирований затем логически складываются посредством OR для получения итогового результата. Другой пример: если у нас есть отдельные индексы по x и y, одна из возможных реализаций запроса WHERE x = 5 AND y = 6 состоит в том, чтобы использовать каждый индекс для соответствующего предложения этого запроса, а затем логически умножить результаты индекса посредством AND для определения результирующих строк.

Чтобы объединить несколько индексов, система сканирует каждый необходимый индекс и подготавливает в памяти битовую карту, сохраняя в той местоположения строк таблицы, о которых известно, что они удовлетворяют условиям этого индекса. Затем битовые карты объединяются посредством AND и OR в соответствии с требованиями запроса. Наконец просматриваются и возвращаются фактические строки таблицы. Строки таблицы просматриваются в физическом порядке, потому что так они располагаются в битовой карте; это означает, что любой порядок исходных индексов потерян, и поэтому если в запросе есть предложение ORDER BY, то потребуется отдельный этап сортировки. По этой причине, а также потому, что на каждое дополнительное сканирование индекса тратится дополнительное время, планировщик иногда предпочитает использовать простое сканирование индекса, даже если доступны дополнительные индексы, которые тоже можно было бы использовать.

Во всех приложениях, кроме самых простых, могут быть полезны различные комбинации индексов, и, принимая решение о том, какие индексы вводить, разработчик базы данных должен найти компромисс. Иногда лучше использовать многостолбцовые индексы, а иногда лучше создавать отдельные индексы и полагаться на возможность объединения индексов. Например, если рабочая нагрузка включает в себя набор запросов, которые иногда содержат условие только по столбцу x, иногда только по y, а иногда по обоим столбцам, можно создать два отдельных индекса по x и y, полагаясь при обработке запросов, использующих оба столбца, на объединение этих индексов. Еще можно создать многостолбцовый индекс по (x, y). Обычно для запросов с условиями по обоим столбцам такой индекс эффективнее объединения индексов, но, как говорилось в подразделе Многостолбцовые индексы, он почти бесполезен для запросов с условиями только по y, поэтому он не должен быть единственным индексом. В таком случае успешным будет сочетание многостолбцового индекса и отдельного индекса по y. Для запросов с условием только по x можно использовать многостолбцовый индекс, хотя он будет больше и, следовательно, медленнее, чем индекс только по x. Последний вариант заключается в создании всех трех индексов, но скорее всего это разумно, только если поиск в таблице происходит гораздо чаще, чем изменения, и частота всех трех типов запросов одинакова. Если один из типов запросов встречается значительно реже, чем другие, лучше создать только два индекса, которые наиболее соответствуют двум более частым запросам.



Уникальные индексы

Также индексы можно использовать для обеспечения уникальности значения столбца или комбинации значений из нескольких столбцов.

CREATE UNIQUE INDEX имя ON таблица (столбец [, ...]);

В настоящее время только индексы типа B-дерево могут быть объявлены уникальными.

Когда индекс объявляется уникальным, наличие нескольких строк таблицы с одинаковыми индексируемыми значениями не допускаются. При этом значения NULL одинаковыми не считаются. Уникальный многостолбцовый индекс будет отклонять только те случаи, когда все индексируемые столбцы одинаковы в нескольких строках.

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

Примечание
Не надо вручную создавать индексы по уникальным столбцам; это лишь продублирует автоматически созданный индекс.



Индексы по выражениям

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

Например, распространенный способ сравнения без учета регистра заключается в использовании функции lower:

SELECT * FROM test1 WHERE lower(col1) = 'value';

Этот запрос может использовать индекс, если таковой был определен по результату функции lower(col1):

CREATE INDEX test1_lower_col1_idx ON test1 (lower(col1));

Если объявить этот индекс уникальным (UNIQUE), это не позволит создать строки, в которых значения col1 отличаются только регистром, равно как и строки, в которых значения col1 действительно идентичны. Таким образом, индексы по выражениям можно применять для обеспечения выполнения ограничений, которые нельзя определить как простые ограничения уникальности.

Другой пример: если часто выполняются запросы вроде

SELECT * FROM people WHERE (first_name || ' ' || last_name) = 'John Smith';

тогда, возможно, стоит создать такой индекс:

CREATE INDEX people_names ON people ((first_name || ' ' || last_name));

Синтаксис команды CREATE INDEX обычно требует заключать индексные выражения в круглые скобки, как показано во втором примере. Скобки можно опустить, если выражение представляет собой просто вызов функции, как в первом примере.

Индексы по выражениям относительно дороги в обслуживании, поскольку производные выражения должны вычисляться после каждого добавления строки и каждого изменения без оптимизации HOT. Однако выражения индекса не вычисляются повторно во время поиска по индексу, поскольку результаты вычисления уже хранятся в индексе. В обоих приведенных выше примерах система видит запрос как просто WHERE индексированный_столбец = ’константа’, поэтому скорость поиска такая же, как для любого другого простого запроса индекса. Таким образом, индексы по выражениям полезны, когда скорость поиска важнее скорости добавления и изменения.



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

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

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

Пример 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';

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

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

Пример 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 может находиться как среди оплаченных, так и среди неоплаченных заказов.

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

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

Пример 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.

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

Имейте в виду, что настройка частичного индекса подразумевает, что вы знаете о данных как минимум столько же, сколько планировщик запросов; в частности, вы знаете, когда использование индекса может быть выгодным. Формирование этого знания требует опыта и понимания того, как работают индексы в QHB. В большинстве случаев преимущество частичного индекса над обычным будет минимальным. Бывают случаи, когда они даже контрпродуктивны, как показано в Примере 4.

Пример 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);

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

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

Более подробную информацию о частичных индексах можно найти в следующих источниках: «The case for partial indexes» и «Generalized Partial Indexes».



Сканирование только по индексу и покрывающие индексы

Все индексы в QHB являются вторичными, то есть каждый индекс хранится отдельно от основной области данных таблицы (которая в терминологии QHB называется кучей таблицы). Это означает, что при обычном сканировании индекса извлечение каждой строки требует выполнения выборки данных как из индекса, так и из кучи. Более того, хотя записи индекса, соответствующие заданному индексируемому условию WHERE, обычно близки друг к другу в индексе, строки таблицы, на которые они ссылаются, могут находиться в этой куче где угодно. Таким образом, обращение к куче при сканировании индекса включает в себя много операций произвольного чтения данных в куче, которые могут быть медленными, особенно на традиционных вращающихся носителях. (Как описано в разделе Объединение нескольких индексов, сканирование по битовой карте пытается снизить эти затраты, делая обращения к куче упорядоченными, но не более того.)

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

  1. Тип индекса должен поддерживать сканирование только по индексу. Индексы B-дерева поддерживают его всегда. Индексы GiST и SP-GiST поддерживают сканирование только по индексу для некоторых классов операторов, но не для всех. Остальные типы индексов это сканирование не поддерживают. Основным требованием является то, что индекс должен физически хранить или иметь возможность восстановить исходное значение данных для каждой записи индекса. В качестве контрпримера, индексы GIN не могут поддерживать сканирование только по индексу, поскольку обычно каждая запись индекса содержит только часть исходного значения данных.

  2. Запрос должен ссылаться только на столбцы, хранящиеся в индексе. Например, если в таблице имеется индекс по столбцам x и y, и в ней также есть столбец z, сканирование только по индексу могут использовать следующие запросы:

    SELECT x, y FROM tab WHERE x = 'key';
    SELECT x FROM tab WHERE x = 'key' AND y < 42;
    

    а такие запросы не могут:

    SELECT x, z FROM tab WHERE x = 'key';
    SELECT x FROM tab WHERE x = 'key' AND z < 42;
    

    (Индексы по выражению и частичные индексы усложняет это правило, как рассказывается ниже.)

Если эти два фундаментальных требования выполнены, то все значения данных, требуемые запросу, доступны из индекса, поэтому сканирование только по индексу физически возможно. Но для любого сканирования таблицы в QHB есть дополнительное требование: система должна убедиться, что каждая извлеченная строка «видима» для MVCC-снимка запроса, как описано в главе Управление параллельным доступом. Информация о видимости хранится не в записях индекса, а только в записях кучи, так что на первый взгляд может показаться, что для любого извлечения строки в любом случае потребуется обращение к куче. И это действительно так, если строка таблицы была недавно изменена. Однако для редко меняющихся данных есть способ обойти эту проблему. QHB отслеживает для каждой страницы в куче таблицы, все ли строки, хранящиеся на этой странице, достаточно стары, чтобы быть видимыми для всех текущих и будущих транзакций. Эта информация сохраняется в битах в карте видимости таблицы. Сканирование только по индексу после нахождения подходящей записи индекса проверяет бит карты видимости для соответствующей страницы кучи. Если он установлен, строка определенно видимая, и поэтому данные могут быть возвращены без дополнительных операций. Если он не установлен, запись кучи придется посетить, чтобы выяснить, видима ли строка, поэтому никакого преимущества в производительности по сравнению со стандартным сканированием индекса не достигается. Даже в положительном случае этот подход заменяет обращение к куче на обращение к карте видимости; но поскольку карта видимости на четыре порядка меньше описываемой ей кучи, для доступа к ней требуется гораздо меньше операций ввода/вывода. В большинстве случаев карта видимости постоянно кэширована в памяти.

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

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

SELECT y FROM tab WHERE x = 'key';

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

CREATE INDEX tab_x_y ON tab(x) INCLUDE (y);

может обрабатывать эти запросы как сканирование только по индексу, потому что y можно получить из индекса, не посещая кучу.

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

CREATE UNIQUE INDEX tab_x_y ON tab(x) INCLUDE (y);

условие уникальности применяется только к столбцу x, но не к комбинации x и y. (Также в ограничениях UNIQUE и PRIMARY KEY можно задать предложение INCLUDE, предоставив альтернативный синтаксис для настройки такого индекса.)

Добавлять в индекс неключевые столбцы дополнительной нагрузки следует обдуманно, особенно если эти столбцы широкие. Если кортеж индекса превысит максимально допустимый размер для данного типа индекса, добавление данных завершится неудачно. В любом случае неключевые столбцы дублируют данные из таблицы и раздувают размер индекса, что потенциально замедляет поиск. И помните, что нет никакого смысла включать в индекс столбцы дополнительной нагрузки, если таблица изменяется часто, поскольку все равно потребуется обращаться к куче. Если обращение к кортежу кучи все равно необходимо, получение значений всех столбцов столбца оттуда не увеличит затраты. Другие ограничения заключаются в том, что в качестве дополнительных столбцов нельзя включать выражения, и что в настоящее время только индексы B-деревья, GiST и SP-GiST поддерживают дополнительные столбцы.

До того, как в QHB появилось свойство INCLUDE, люди иногда создавали покрывающие индексы, записывая столбцы дополнительной нагрузки как обычные столбцы индекса, то есть

CREATE INDEX tab_x_y ON tab(x, y);

даже если они не собирались использовать y как часть предложения WHERE. Это прекрасно работает, пока лишние столбцы являются конечными; делать их начальными неразумно по причинам, изложенным в разделе Многостолбцовые индексы. Однако этот метод не подходит, если нужно, чтобы индекс обеспечил уникальность ключевого столбца (или столбцов).

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

В принципе сканирование только по индексу можно использовать с индексами по выражениям. Например, имея индекс по f(x), где x — это столбец таблицы, можно выполнить запрос

SELECT f(x) FROM tab WHERE f(x) < 1;

как сканирование только по индексу; и это очень привлекательно, если функция f() затрата для вычисления. Однако в настоящее время планировщик QHB не очень разбирается в таких случаях. Он считает, что запрос потенциально может быть выполнен при сканировании только по индексу, только когда из индекса доступны все столбцы, необходимые для запроса. В этом примере x требуется исключительно в контексте f(x), но планировщик этого не замечает и приходит к выводу, что сканирование только по индексу невозможно. Если сканирование только по индексу кажется достаточно целесообразным, то это ограничение можно обойти, добавив x в качестве дополнительного столбца, например:

CREATE INDEX tab_f_x ON tab (f(x)) INCLUDE (x);

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

Интересны также взаимодействия частичных индексов со сканированием только по индексу. Рассмотрим частичный индекс, показанный в Примере 3:

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

В принципе, с этим индексом можно провести сканирование только по индексу для удовлетворения запроса вроде

SELECT target FROM tests WHERE subject = 'some-subject' AND success;

Но есть проблема: предложение WHERE ссылается на столбец success, который недоступен в качестве результирующего столбца индекса. Несмотря на это, сканирование только по индексу возможно, потому что плану не нужно перепроверять эту часть предложения WHERE во время выполнения: у всех записей, найденных в индексе, неизбежно будет значение success = true, так что это не нужно явно проверять в плане. QHB распознает такие случаи и позволит осуществить сканирование только по индексу.



Классы операторов и семейства операторов

В определении индекса можно указать класс операторов для каждого столбца индекса.

CREATE INDEX имя ON таблица (столбец класс_оп [ ( параметры_класса_оп ) ] [параметры сортировки] [, ...]);

Класс операторов определяет операторы, которые будут использоваться индексом для этого столбца. Например, индекс B-дерево по типу int4 будет использовать класс int4_ops; этот класс операторов включает функции сравнения для значений типа int4. На практике обычно бывает достаточно класса операторов по умолчанию для типа данных столбца. Основная причина указания классов операторов заключается в том, что для некоторых типов данных может быть более одного значимого варианта поведения индекса. Например, нам может понадобиться отсортировать тип данных комплексного числа по модулю или по действительной части. Это можно сделать, определив два класса операторов для этого типа данных, а затем выбрав подходящий класс при создании индекса. Класс оператора определяет основной порядок сортировки (который затем можно изменить, добавив параметры сортировки COLLATE, ASC/ DESC и/или NULLS FIRST/NULLS LAST).

Помимо классов операторов по умолчанию существует также несколько встроенных классов операторов:

  • Классы операторов text_pattern_ops, varchar_pattern_ops и bpchar_pattern_ops поддерживают индексы B-деревья для типов text, varchar и char соответственно. Отличие от классов операторов по умолчанию состоит в том, что значения сравниваются строго символ за символом, а не в соответствии с правилами сортировки, специфичными для локали. Это делает данные классы операторов пригодными для использования в запросах, включающих выражения сопоставления с шаблоном (регулярные выражения LIKE или POSIX), когда база данных использует локаль, отличную от стандартной «C». Например, можно проиндексировать столбец varchar следующим образом:

    CREATE INDEX test_index ON test_table (col varchar_pattern_ops);
    

    Обратите внимание, что если вы хотите, чтобы индекс применялся в запросах, включающих обычные сравнения <, <=, > или >=, придется также создать индекс с классом операторов по умолчанию. Такие запросы не могут использовать классы операторов xxx_pattern_ops. (Однако же обычные сравнения на равенство могут использовать эти классы операторов). Можно создать несколько индексов по одному столбцу, но с разными классами операторов. Если вы используете локаль C, вам не нужен индекс с классами операторов xxx_pattern_ops, поскольку в локали C индекс с операторами по умолчанию можно использовать и для запросов сопоставления с шаблоном.

Следующий запрос показывает все определенные классы операторов:

SELECT am.amname AS index_method,
       opc.opcname AS opclass_name,
       opc.opcintype::regtype AS indexed_type,
       opc.opcdefault AS is_default
    FROM pg_am am, pg_opclass opc
    WHERE opc.opcmethod = am.oid
    ORDER BY index_method, opclass_name;

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

Эта расширенная версия предыдущего запроса показывает семейство операторов, к которому принадлежит каждый класс операторов:

SELECT am.amname AS index_method,
       opc.opcname AS opclass_name,
       opf.opfname AS opfamily_name,
       opc.opcintype::regtype AS indexed_type,
       opc.opcdefault AS is_default
    FROM pg_am am, pg_opclass opc, pg_opfamily opf
    WHERE opc.opcmethod = am.oid AND
          opc.opcfamily = opf.oid
    ORDER BY index_method, opclass_name;

Этот запрос показывает все определенные семейства операторов и все операторы, включенные в каждое семейство:

SELECT am.amname AS index_method,
       opf.opfname AS opfamily_name,
       amop.amopopr::regoperator AS opfamily_operator
    FROM pg_am am, pg_opfamily opf, pg_amop amop
    WHERE opf.opfmethod = am.oid AND
          amop.amopfamily = opf.oid
    ORDER BY index_method, opfamily_name, opfamily_operator;

Совет
В psql имеются команды \dAc, \dAf и \dAo, которые предоставляют несколько более усовершенствованные версии этих запросов.



Индексы и правила сортировки

Индекс может поддерживать только одно правило сортировки на столбец индекса. Если есть необходимость в разных правилах сортировки, может потребоваться несколько индексов.

Рассмотрим следующие операторы:

CREATE TABLE test1c (
    id integer,
    content varchar COLLATE "x"
);

CREATE INDEX test1c_content_index ON test1c (content);

Индекс автоматически использует правило сортировки нижележащего столбца. Поэтому запрос вида

SELECT * FROM test1c WHERE content > константа;

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

SELECT * FROM test1c WHERE content > константа COLLATE "y";

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

CREATE INDEX test1c_content_y_index ON test1c (content COLLATE "y");


Анализ использования индексов

Хотя индексы в QHB не нуждаются в обслуживании или настройке, все же важно проверить, какие индексы действительно используются реальной рабочей нагрузкой запросов. Изучение использования индекса для отдельного запроса выполняется командой EXPLAIN; ее применение для этой цели иллюстрируется в разделе Использование EXPLAIN. Также можно собрать общую статистику об использовании индекса на работающем сервере, как описано в разделе Сборщик статистики.

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

  • Сначала всегда запускайте ANALYZE. Эта команда собирает статистику о распределении значений в таблице. Эта информация требуется для оценки количества строк, возвращаемых запросом, которое необходимо планировщику для реалистичной оценки затрат для каждого возможного плана выполнения запроса. При отсутствии какой-либо реальной статистики будут приняты некоторые значения по умолчанию, которые почти наверняка будут неточными. Поэтому изучить применение индекса приложением без запуска ANALYZE заведомо невозможно. Дополнительную информацию см. в подразделах Обновление статистики планировщика и Процесс «Автовакуум».

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

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

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

  • Когда индексы не используются, для тестирования может быть полезно форсировать их применение. Существуют параметры времени выполнения, которые могут выключать различные типы планов (см. подраздел Конфигурация метода планирования). Например, выключение последовательного сканирования (enable_seqscan) и соединений с вложенными циклами (enable_nestloop), являющихся наиболее простыми планами, заставит систему использовать другой план. Если система по-прежнему выбирает последовательное сканирование или соединение с вложенными циклами, то, вероятно, существует более фундаментальная причина не использовать индекс; например, условие запроса не соответствует индексу. (В каких видах запросов могут работать те или иные индексы, рассказывается в предыдущих разделах.)

  • Если все же удалось заставить систему использовать индекс, то есть два варианта: либо система права, и использование индекса действительно не подходит, либо оценки стоимости планов запросов не отражают реальность. Поэтому необходимо замерить время выполнения запроса с индексами и без них. Здесь может пригодиться команда EXPLAIN ANALYZE.

  • Если окажется, что оценки стоимости ошибочны, опять-таки есть два варианта. Общая стоимость вычисляется умножением оценки стоимости каждого узла плана для одной строки на оценку избирательности узла плана. Стоимость, оцениваемую для узлов плана, можно скорректировать с помощью параметров времени выполнения (описанных в подразделе Константы стоимости для планировщика). Неточная оценка избирательности обычно связана с неудовлетворительной статистикой. Это можно исправить, настроив параметры сбора статистики (см. ALTER TABLE).

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