У меня есть такая таблица:
CREATE TABLE products (
id serial PRIMARY KEY,
category_ids integer[],
published boolean NOT NULL,
score integer NOT NULL,
title varchar NOT NULL);
Продукт может принадлежать нескольким категориям. category_ids
столбец содержит список идентификаторов всех категорий продуктов.
Типичный запрос выглядит так (всегда поиск одной категории):
SELECT * FROM products WHERE published
AND category_ids @> ARRAY[23465]
ORDER BY score DESC, title
LIMIT 20 OFFSET 8000;
Для ускорения я использую следующий индекс:
CREATE INDEX idx_test1 ON products
USING GIN (category_ids gin__int_ops) WHERE published;
Это очень помогает, если в одной категории слишком много товаров. Он быстро отфильтровывает продукты, относящиеся к этой категории, но затем выполняется операция сортировки, которая должна быть выполнена трудным способом (без индекса).
У меня установлено btree_gin
расширение, позволяющее мне строить многостолбцовый индекс GIN следующим образом:
CREATE INDEX idx_test2 ON products USING GIN (
category_ids gin__int_ops, score, title) WHERE published;
Но Postgres не хочет использовать это для сортировки . Даже когда я удаляю DESC
спецификатор в запросе.
Любые альтернативные подходы для оптимизации задачи приветствуются.
Дополнительная информация:
- PostgreSQL 9.4, с расширением intarray
- общее количество продуктов в настоящее время составляет 260 тыс., но ожидается значительный рост (до 10 млн., это мультитенантная платформа электронной коммерции)
- товаров в категории 1..10000 (может вырасти до 100 тыс.), в среднем ниже 100, но те категории с большим количеством товаров имеют тенденцию привлекать гораздо больше запросов
Следующий план запроса был получен из небольшой тестовой системы (4680 товаров в выбранной категории, всего 200 000 товаров в таблице):
Limit (cost=948.99..948.99 rows=1 width=72) (actual time=82.330..82.341 rows=20 loops=1)
-> Sort (cost=948.37..948.99 rows=245 width=72) (actual time=80.231..81.337 rows=4020 loops=1)
Sort Key: score, title
Sort Method: quicksort Memory: 928kB
-> Bitmap Heap Scan on products (cost=13.90..938.65 rows=245 width=72) (actual time=1.919..16.044 rows=4680 loops=1)
Recheck Cond: ((category_ids @> '{292844}'::integer[]) AND published)
Heap Blocks: exact=3441
-> Bitmap Index Scan on idx_test2 (cost=0.00..13.84 rows=245 width=0) (actual time=1.185..1.185 rows=4680 loops=1)
Index Cond: (category_ids @> '{292844}'::integer[])
Planning time: 0.202 ms
Execution time: 82.404 ms
Примечание # 1 : 82 мс может выглядеть не так страшно, но это потому, что буфер сортировки помещается в память. Как только я выбираю все столбцы из таблицы продуктов ( SELECT * FROM ...
а в реальной жизни их около 60), это Sort Method: external merge Disk: 5696kB
удваивает время выполнения. И это только для 4680 продуктов.
Точка действия № 1 (из примечания № 1): чтобы уменьшить объем памяти, необходимый для операции сортировки, и, следовательно, немного ускорить ее, было бы целесообразно сначала получить, отсортировать и ограничить идентификаторы продуктов, а затем получить полные записи:
SELECT * FROM products WHERE id IN (
SELECT id FROM products WHERE published AND category_ids @> ARRAY[23465]
ORDER BY score DESC, title LIMIT 20 OFFSET 8000
) ORDER BY score DESC, title;
Это возвращает нас к Sort Method: quicksort Memory: 903kB
~ 80 мс для 4680 продуктов. Тем не менее может быть медленным, когда количество продуктов увеличивается до 100К.
источник
score
может быть NULL, но вы все равно сортируетеscore DESC
, нетscore DESC NULLS LAST
. Один или другой, кажется, не прав ...score
на самом деле это не NULL - я исправил определение таблицы.Ответы:
Я много экспериментировал и вот мои выводы.
Джин и сортировка
Индекс GIN в настоящее время (начиная с версии 9.4) не может помочь упорядочению .
work_mem
Спасибо Крис за указание на этот параметр конфигурации . По умолчанию он равен 4 МБ, и в случае, если ваш набор записей больше, увеличение
work_mem
до нужного значения (может быть найдено изEXPLAIN ANALYSE
) может значительно ускорить операции сортировки.Перезапустите сервер, чтобы изменения вступили в силу, затем дважды проверьте:
Оригинальный запрос
Я заполнил свою базу данных продуктами из 650 тыс., А некоторые категории содержали до 40 тыс. Товаров. Я немного упростил запрос, удалив
published
предложение:Как мы видим, этого
work_mem
было недостаточно, поэтому мы имелиSort Method: external merge Disk: 29656kB
приблизительное число, для быстрой сортировки в памяти требуется чуть больше 32 МБ.Уменьшить объем памяти
Не выбирайте полные записи для сортировки, используйте идентификаторы, применяйте сортировку, смещение и ограничение, а затем загружайте только 10 нужных нам записей:
Примечание
Sort Method: quicksort Memory: 7396kB
. Результат намного лучше.JOIN и дополнительный индекс B-дерева
Как советовал Крис, я создал дополнительный индекс:
Сначала я попытался присоединиться так:
План запроса немного отличается, но результат тот же:
Играя с различными смещениями и количеством продуктов, я не мог заставить PostgreSQL использовать дополнительный индекс B-дерева.
Итак, я пошел классическим путем и создал распределительную таблицу :
Все еще не используя индекс B-дерева, набор результатов не соответствовал
work_mem
, следовательно, плохие результаты.Но при некоторых обстоятельствах, имея большое количество продуктов и небольшое смещение, PostgreSQL теперь решает использовать индекс B-дерева:
Это на самом деле вполне логично, так как индекс B-дерева здесь не дает прямого результата, он используется только как руководство для последовательного сканирования.
Давайте сравним с GIN-запросом:
Результат Джина намного лучше. Я проверил с различными комбинациями количества продуктов и смещения, ни при каких обстоятельствах подход к соединительной таблице не был лучше .
Сила реального индекса
Чтобы PostgreSQL полностью использовал индекс для сортировки, все
WHERE
параметры запроса, а такжеORDER BY
параметры должны находиться в одном индексе B-дерева. Для этого я скопировал поля сортировки из продукта в таблицу соединений:И это худший сценарий с большим количеством товаров в выбранной категории и большим смещением. При смещении = 300 время выполнения составляет всего 0,5 мс.
К сожалению, поддержание такой соединительной таблицы требует дополнительных усилий. Этого можно добиться с помощью индексированных материализованных представлений, но это полезно только в том случае, если ваши данные обновляются редко, потому что обновление такого материализованного представления является довольно сложной операцией.
Так что я до сих пор остаюсь с индексом GIN, с увеличенным
work_mem
и уменьшенным запросом памяти.источник
work_mem
настроек в postgresql.conf. Перезагрузить достаточно. И позвольте мне предостеречь отwork_mem
слишком высокого глобального значения в многопользовательской среде (не слишком низкого). Если у вас есть несколько запросов, требующих большегоwork_mem
, установите его выше для сеанса только сSET
- или только для транзакции сSET LOCAL
. См .: dba.stackexchange.com/a/48633/3684Вот несколько быстрых советов, которые могут помочь улучшить вашу производительность. Я начну с самого простого совета, который почти без усилий с вашей стороны, и перейду к более сложному совету после первого.
1.
work_mem
Итак, я сразу вижу, что сортировка, указанная в вашем плане объяснения,
Sort Method: external merge Disk: 5696kB
потребляет менее 6 МБ, но выливается на диск. Вам нужно увеличитьwork_mem
настройки в вашемpostgresql.conf
файле, чтобы они были достаточно большими, чтобы сортировка помещалась в памяти.РЕДАКТИРОВАТЬ: Кроме того, при дальнейшей проверке, я вижу, что после использования индекса, чтобы проверить,
catgory_ids
который соответствует вашим критериям, сканирование индекса растрового изображения вынуждено стать «с потерями» и должен перепроверить условие при чтении строк из соответствующих страниц кучи , Обратитесь к этому сообщению на postgresql.org для объяснения лучше, чем я дал. : P Суть в том, что выwork_mem
слишком низко. Если вы не настроили параметры по умолчанию на вашем сервере, он не будет работать хорошо.Это исправление не займет у вас практически никакого времени. Одно изменение
postgresql.conf
, и вы выключены! Обратитесь к этой странице настройки производительности для получения дополнительных советов.2. Изменение схемы
Итак, вы приняли решение в своем проекте схемы денормализовать
category_ids
в массив целых чисел, который затем заставляет вас использовать индекс GIN или GIST для получения быстрого доступа. По моему опыту, ваш выбор индекса GIN будет быстрее для чтения, чем GIST, поэтому в этом случае вы сделали правильный выбор. Тем не менее, GIN является несортированным индексом; думать о нем , как ключ-значение, где предикаты равенства являются легко проверить, но такие операции, какWHERE >
,WHERE <
илиORDER BY
не облегчило индексом.Достойным подходом было бы нормализовать ваш дизайн с помощью таблицы мостов / соединительных таблиц , используемых для указания отношений «многие ко многим» в базах данных.
В этом случае у вас есть много категорий и набор соответствующих целых чисел
category_id
, и у вас есть много продуктов и соответствующих имproduct_id
. Вместо столбца в таблице продуктов, представляющего собой целочисленный массивcategory_id
s, удалите этот столбец массива из схемы и создайте таблицу какЗатем вы можете генерировать индексы B-дерева в двух столбцах таблицы мостов,
Просто мое скромное мнение, но эти изменения могут иметь большое значение для вас. Попробуйте это
work_mem
изменение первым делом, по крайней мере.Удачи!
РЕДАКТИРОВАТЬ:
Создайте дополнительный индекс, чтобы помочь сортировке
Таким образом, если со временем ваша линейка продуктов расширяется, определенные запросы могут возвращать много результатов (тысячи, десятки тысяч?), Но которые все еще могут быть лишь небольшим подмножеством вашей общей линейки продуктов. В этих случаях сортировка может даже быть довольно дорогой, если она выполняется в памяти, но для содействия сортировке может использоваться соответствующий индекс.
Смотрите официальную документацию PostgreSQL, описывающую индексы и ORDER BY .
Если вы создаете индекс вашего
ORDER BY
reqirementsЗатем Postgres оптимизирует и решит, будет ли использование индекса или явная сортировка более экономически эффективным. Имейте в виду, что нет никаких гарантий, что Postgres будет использовать индекс; он будет стремиться оптимизировать производительность и выбирать между использованием индекса или явной сортировкой. Если вы создаете этот индекс, следите за ним, чтобы увидеть, достаточно ли он используется, чтобы оправдать его создание, и отбросьте его, если большинство ваших сортировок выполняются явно.
Тем не менее, на данный момент ваш «самый большой удар по доллару», вероятно, будет использовать больше
work_mem
, но в некоторых случаях индекс может поддерживать сортировку.источник
work_mem
конфигурации было задумано как исправление вашей проблемы «сортировки на диске», а также вашей проблемы с перепроверкой. По мере роста количества продуктов может потребоваться дополнительный индекс для сортировки. Пожалуйста, смотрите мои правки выше для уточнения.