Я много экспериментировал и вот мои выводы.
Джин и сортировка
Индекс GIN в настоящее время (начиная с версии 9.4) не может помочь упорядочению .
Из типов индексов, поддерживаемых в настоящее время PostgreSQL, только B-дерево может производить отсортированный вывод - другие типы индексов возвращают совпадающие строки в неопределенном, зависящем от реализации порядке.
work_mem
Спасибо Крис за указание на этот параметр конфигурации . По умолчанию он равен 4 МБ, и в случае, если ваш набор записей больше, увеличение work_mem
до нужного значения (может быть найдено из EXPLAIN ANALYSE
) может значительно ускорить операции сортировки.
ALTER SYSTEM SET work_mem TO '32MB';
Перезапустите сервер, чтобы изменения вступили в силу, затем дважды проверьте:
SHOW work_mem;
Оригинальный запрос
Я заполнил свою базу данных продуктами из 650 тыс., А некоторые категории содержали до 40 тыс. Товаров. Я немного упростил запрос, удалив published
предложение:
SELECT * FROM products WHERE category_ids @> ARRAY [248688]
ORDER BY score DESC, title LIMIT 10 OFFSET 30000;
Limit (cost=2435.62..2435.62 rows=1 width=1390) (actual time=1141.254..1141.256 rows=10 loops=1)
-> Sort (cost=2434.00..2435.62 rows=646 width=1390) (actual time=1115.706..1140.513 rows=30010 loops=1)
Sort Key: score, title
Sort Method: external merge Disk: 29656kB
-> Bitmap Heap Scan on products (cost=17.01..2403.85 rows=646 width=1390) (actual time=11.831..25.646 rows=41666 loops=1)
Recheck Cond: (category_ids @> '{248688}'::integer[])
Heap Blocks: exact=6471
-> Bitmap Index Scan on idx_products_category_ids_gin (cost=0.00..16.85 rows=646 width=0) (actual time=10.140..10.140 rows=41666 loops=1)
Index Cond: (category_ids @> '{248688}'::integer[])
Planning time: 0.288 ms
Execution time: 1146.322 ms
Как мы видим, этого work_mem
было недостаточно, поэтому мы имели Sort Method: external merge Disk: 29656kB
приблизительное число, для быстрой сортировки в памяти требуется чуть больше 32 МБ.
Уменьшить объем памяти
Не выбирайте полные записи для сортировки, используйте идентификаторы, применяйте сортировку, смещение и ограничение, а затем загружайте только 10 нужных нам записей:
SELECT * FROM products WHERE id in (
SELECT id FROM products WHERE category_ids @> ARRAY[248688]
ORDER BY score DESC, title LIMIT 10 OFFSET 30000
) ORDER BY score DESC, title;
Sort (cost=2444.10..2444.11 rows=1 width=1390) (actual time=707.861..707.862 rows=10 loops=1)
Sort Key: products.score, products.title
Sort Method: quicksort Memory: 35kB
-> Nested Loop (cost=2436.05..2444.09 rows=1 width=1390) (actual time=707.764..707.803 rows=10 loops=1)
-> HashAggregate (cost=2435.63..2435.64 rows=1 width=4) (actual time=707.744..707.746 rows=10 loops=1)
Group Key: products_1.id
-> Limit (cost=2435.62..2435.62 rows=1 width=72) (actual time=707.732..707.734 rows=10 loops=1)
-> Sort (cost=2434.00..2435.62 rows=646 width=72) (actual time=704.163..706.955 rows=30010 loops=1)
Sort Key: products_1.score, products_1.title
Sort Method: quicksort Memory: 7396kB
-> Bitmap Heap Scan on products products_1 (cost=17.01..2403.85 rows=646 width=72) (actual time=11.587..35.076 rows=41666 loops=1)
Recheck Cond: (category_ids @> '{248688}'::integer[])
Heap Blocks: exact=6471
-> Bitmap Index Scan on idx_products_category_ids_gin (cost=0.00..16.85 rows=646 width=0) (actual time=9.883..9.883 rows=41666 loops=1)
Index Cond: (category_ids @> '{248688}'::integer[])
-> Index Scan using products_pkey on products (cost=0.42..8.45 rows=1 width=1390) (actual time=0.004..0.004 rows=1 loops=10)
Index Cond: (id = products_1.id)
Planning time: 0.682 ms
Execution time: 707.973 ms
Примечание Sort Method: quicksort Memory: 7396kB
. Результат намного лучше.
JOIN и дополнительный индекс B-дерева
Как советовал Крис, я создал дополнительный индекс:
CREATE INDEX idx_test7 ON products (score DESC, title);
Сначала я попытался присоединиться так:
SELECT * FROM products NATURAL JOIN
(SELECT id FROM products WHERE category_ids @> ARRAY[248688]
ORDER BY score DESC, title LIMIT 10 OFFSET 30000) c
ORDER BY score DESC, title;
План запроса немного отличается, но результат тот же:
Sort (cost=2444.10..2444.11 rows=1 width=1390) (actual time=700.747..700.747 rows=10 loops=1)
Sort Key: products.score, products.title
Sort Method: quicksort Memory: 35kB
-> Nested Loop (cost=2436.05..2444.09 rows=1 width=1390) (actual time=700.651..700.690 rows=10 loops=1)
-> HashAggregate (cost=2435.63..2435.64 rows=1 width=4) (actual time=700.630..700.630 rows=10 loops=1)
Group Key: products_1.id
-> Limit (cost=2435.62..2435.62 rows=1 width=72) (actual time=700.619..700.619 rows=10 loops=1)
-> Sort (cost=2434.00..2435.62 rows=646 width=72) (actual time=697.304..699.868 rows=30010 loops=1)
Sort Key: products_1.score, products_1.title
Sort Method: quicksort Memory: 7396kB
-> Bitmap Heap Scan on products products_1 (cost=17.01..2403.85 rows=646 width=72) (actual time=10.796..32.258 rows=41666 loops=1)
Recheck Cond: (category_ids @> '{248688}'::integer[])
Heap Blocks: exact=6471
-> Bitmap Index Scan on idx_products_category_ids_gin (cost=0.00..16.85 rows=646 width=0) (actual time=9.234..9.234 rows=41666 loops=1)
Index Cond: (category_ids @> '{248688}'::integer[])
-> Index Scan using products_pkey on products (cost=0.42..8.45 rows=1 width=1390) (actual time=0.004..0.004 rows=1 loops=10)
Index Cond: (id = products_1.id)
Planning time: 1.015 ms
Execution time: 700.918 ms
Играя с различными смещениями и количеством продуктов, я не мог заставить PostgreSQL использовать дополнительный индекс B-дерева.
Итак, я пошел классическим путем и создал распределительную таблицу :
CREATE TABLE prodcats AS SELECT id AS product_id, unnest(category_ids) AS category_id FROM products;
CREATE INDEX idx_prodcats_cat_prod_id ON prodcats (category_id, product_id);
SELECT p.* FROM products p JOIN prodcats c ON (p.id=c.product_id)
WHERE c.category_id=248688
ORDER BY p.score DESC, p.title LIMIT 10 OFFSET 30000;
Limit (cost=122480.06..122480.09 rows=10 width=1390) (actual time=1290.360..1290.362 rows=10 loops=1)
-> Sort (cost=122405.06..122509.00 rows=41574 width=1390) (actual time=1264.250..1289.575 rows=30010 loops=1)
Sort Key: p.score, p.title
Sort Method: external merge Disk: 29656kB
-> Merge Join (cost=50.46..94061.13 rows=41574 width=1390) (actual time=117.746..182.048 rows=41666 loops=1)
Merge Cond: (p.id = c.product_id)
-> Index Scan using products_pkey on products p (cost=0.42..90738.43 rows=646067 width=1390) (actual time=0.034..116.313 rows=210283 loops=1)
-> Index Only Scan using idx_prodcats_cat_prod_id on prodcats c (cost=0.43..1187.98 rows=41574 width=4) (actual time=0.022..7.137 rows=41666 loops=1)
Index Cond: (category_id = 248688)
Heap Fetches: 0
Planning time: 0.873 ms
Execution time: 1294.826 ms
Все еще не используя индекс B-дерева, набор результатов не соответствовал work_mem
, следовательно, плохие результаты.
Но при некоторых обстоятельствах, имея большое количество продуктов и небольшое смещение, PostgreSQL теперь решает использовать индекс B-дерева:
SELECT p.* FROM products p JOIN prodcats c ON (p.id=c.product_id)
WHERE c.category_id=248688
ORDER BY p.score DESC, p.title LIMIT 10 OFFSET 300;
Limit (cost=3986.65..4119.51 rows=10 width=1390) (actual time=264.176..264.574 rows=10 loops=1)
-> Nested Loop (cost=0.98..552334.77 rows=41574 width=1390) (actual time=250.378..264.558 rows=310 loops=1)
-> Index Scan using idx_test7 on products p (cost=0.55..194665.62 rows=646067 width=1390) (actual time=0.030..83.026 rows=108037 loops=1)
-> Index Only Scan using idx_prodcats_cat_prod_id on prodcats c (cost=0.43..0.54 rows=1 width=4) (actual time=0.001..0.001 rows=0 loops=108037)
Index Cond: ((category_id = 248688) AND (product_id = p.id))
Heap Fetches: 0
Planning time: 0.585 ms
Execution time: 264.664 ms
Это на самом деле вполне логично, так как индекс B-дерева здесь не дает прямого результата, он используется только как руководство для последовательного сканирования.
Давайте сравним с GIN-запросом:
SELECT * FROM products WHERE id in (
SELECT id FROM products WHERE category_ids @> ARRAY[248688]
ORDER BY score DESC, title LIMIT 10 OFFSET 300
) ORDER BY score DESC, title;
Sort (cost=2519.53..2519.55 rows=10 width=1390) (actual time=143.809..143.809 rows=10 loops=1)
Sort Key: products.score, products.title
Sort Method: quicksort Memory: 35kB
-> Nested Loop (cost=2435.14..2519.36 rows=10 width=1390) (actual time=143.693..143.736 rows=10 loops=1)
-> HashAggregate (cost=2434.71..2434.81 rows=10 width=4) (actual time=143.678..143.680 rows=10 loops=1)
Group Key: products_1.id
-> Limit (cost=2434.56..2434.59 rows=10 width=72) (actual time=143.668..143.670 rows=10 loops=1)
-> Sort (cost=2433.81..2435.43 rows=646 width=72) (actual time=143.642..143.653 rows=310 loops=1)
Sort Key: products_1.score, products_1.title
Sort Method: top-N heapsort Memory: 68kB
-> Bitmap Heap Scan on products products_1 (cost=17.01..2403.85 rows=646 width=72) (actual time=11.625..31.868 rows=41666 loops=1)
Recheck Cond: (category_ids @> '{248688}'::integer[])
Heap Blocks: exact=6471
-> Bitmap Index Scan on idx_products_category_ids_gin (cost=0.00..16.85 rows=646 width=0) (actual time=9.916..9.916 rows=41666 loops=1)
Index Cond: (category_ids @> '{248688}'::integer[])
-> Index Scan using products_pkey on products (cost=0.42..8.45 rows=1 width=1390) (actual time=0.004..0.004 rows=1 loops=10)
Index Cond: (id = products_1.id)
Planning time: 0.630 ms
Execution time: 143.921 ms
Результат Джина намного лучше. Я проверил с различными комбинациями количества продуктов и смещения, ни при каких обстоятельствах подход к соединительной таблице не был лучше .
Сила реального индекса
Чтобы PostgreSQL полностью использовал индекс для сортировки, все WHERE
параметры запроса, а также ORDER BY
параметры должны находиться в одном индексе B-дерева. Для этого я скопировал поля сортировки из продукта в таблицу соединений:
CREATE TABLE prodcats AS SELECT id AS product_id, unnest(category_ids) AS category_id, score, title FROM products;
CREATE INDEX idx_prodcats_1 ON prodcats (category_id, score DESC, title, product_id);
SELECT * FROM products WHERE id in (SELECT product_id FROM prodcats WHERE category_id=248688 ORDER BY score DESC, title LIMIT 10 OFFSET 30000) ORDER BY score DESC, title;
Sort (cost=2149.65..2149.67 rows=10 width=1390) (actual time=7.011..7.011 rows=10 loops=1)
Sort Key: products.score, products.title
Sort Method: quicksort Memory: 35kB
-> Nested Loop (cost=2065.26..2149.48 rows=10 width=1390) (actual time=6.916..6.950 rows=10 loops=1)
-> HashAggregate (cost=2064.83..2064.93 rows=10 width=4) (actual time=6.902..6.904 rows=10 loops=1)
Group Key: prodcats.product_id
-> Limit (cost=2064.02..2064.71 rows=10 width=74) (actual time=6.893..6.895 rows=10 loops=1)
-> Index Only Scan using idx_prodcats_1 on prodcats (cost=0.56..2860.10 rows=41574 width=74) (actual time=0.010..6.173 rows=30010 loops=1)
Index Cond: (category_id = 248688)
Heap Fetches: 0
-> Index Scan using products_pkey on products (cost=0.42..8.45 rows=1 width=1390) (actual time=0.003..0.003 rows=1 loops=10)
Index Cond: (id = prodcats.product_id)
Planning time: 0.318 ms
Execution time: 7.066 ms
И это худший сценарий с большим количеством товаров в выбранной категории и большим смещением. При смещении = 300 время выполнения составляет всего 0,5 мс.
К сожалению, поддержание такой соединительной таблицы требует дополнительных усилий. Этого можно добиться с помощью индексированных материализованных представлений, но это полезно только в том случае, если ваши данные обновляются редко, потому что обновление такого материализованного представления является довольно сложной операцией.
Так что я до сих пор остаюсь с индексом GIN, с увеличенным work_mem
и уменьшенным запросом памяти.