Eu fiz muitas experiências e aqui estão minhas descobertas.
GIN e classificação
O índice GIN atualmente (a partir da versão 9.4) não pode ajudar no pedido .
Dos tipos de índice atualmente suportados pelo PostgreSQL, apenas a B-tree pode produzir saída classificada - os outros tipos de índice retornam linhas correspondentes em uma ordem não especificada e dependente da implementação.
work_mem
Obrigado Chris por apontar para este parâmetro de configuração . O padrão é 4 MB e, se o seu conjunto de registros for maior, aumentar work_mem
para o valor adequado (pode ser encontrado em EXPLAIN ANALYSE
) pode acelerar significativamente as operações de classificação.
ALTER SYSTEM SET work_mem TO '32MB';
Reinicie o servidor para que as alterações entrem em vigor e verifique novamente:
SHOW work_mem;
Consulta original
Eu preenchi meu banco de dados com produtos de 650k com algumas categorias com até 40k produtos. Simplifiquei um pouco a consulta removendo a published
cláusula:
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
Como podemos ver, work_mem
não bastava Sort Method: external merge Disk: 29656kB
(o número aqui é aproximado, ele precisa de um pouco mais de 32 MB para a classificação rápida na memória).
Reduzir a pegada de memória
Não selecione registros completos para classificação, use IDs, aplique classificação, deslocamento e limite e carregue apenas 10 registros necessários:
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
Nota Sort Method: quicksort Memory: 7396kB
. Resultado é muito melhor.
JOIN e índice adicional de árvore B
Como Chris aconselhou, criei um índice adicional:
CREATE INDEX idx_test7 ON products (score DESC, title);
Primeiro eu tentei entrar assim:
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;
O plano de consulta difere um pouco, mas o resultado é o mesmo:
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
Jogando com várias compensações e contagens de produtos, não consegui fazer o PostgreSQL usar um índice adicional de árvore B.
Então eu segui o caminho clássico e criei a tabela de junção :
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
Ainda sem usar o índice da árvore B, o conjunto de resultados não se encaixava work_mem
, portanto, resultados ruins.
Mas, em algumas circunstâncias, ter um grande número de produtos e um pequeno deslocamento do PostgreSQL agora decide usar o índice da árvore 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
Isso é de fato bastante lógico, pois o índice da árvore B aqui não produz resultado direto, é usado apenas como um guia para varredura seqüencial.
Vamos comparar com a consulta 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
O resultado do GIN é muito melhor. Eu verifiquei com várias combinações de número de produtos e offset, em nenhuma circunstância a abordagem da tabela de junções era melhor .
O poder do índice real
Para que o PostgreSQL utilize totalmente o índice para classificação, todos os WHERE
parâmetros de consulta e os ORDER BY
parâmetros devem residir no índice de árvore B único. Para fazer isso, copiei os campos de classificação do produto para a tabela de junção:
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
E este é o pior cenário, com grande número de produtos na categoria escolhida e grande deslocamento. Quando offset = 300, o tempo de execução é de apenas 0,5 ms.
Infelizmente, a manutenção dessa tabela de junção exige um esforço extra. Isso pode ser realizado por meio de visualizações materializadas indexadas, mas isso só é útil quando os dados são atualizados raramente, pois a atualização dessa visualização materializada é uma operação bastante pesada.
Então, eu estou ficando com o índice GIN até agora, com aumento work_mem
e redução da consulta de pegada de memória.