Eu tenho uma tabela como esta:
CREATE TABLE products (
id serial PRIMARY KEY,
category_ids integer[],
published boolean NOT NULL,
score integer NOT NULL,
title varchar NOT NULL);
Um produto pode pertencer a várias categorias. category_ids
A coluna contém uma lista de IDs de todas as categorias de produtos.
A consulta típica fica assim (sempre procurando por categoria única):
SELECT * FROM products WHERE published
AND category_ids @> ARRAY[23465]
ORDER BY score DESC, title
LIMIT 20 OFFSET 8000;
Para acelerar, use o seguinte índice:
CREATE INDEX idx_test1 ON products
USING GIN (category_ids gin__int_ops) WHERE published;
Este ajuda muito, a menos que haja muitos produtos em uma categoria. Ele filtra rapidamente os produtos que pertencem a essa categoria, mas há uma operação de classificação que deve ser executada da maneira mais difícil (sem índice).
Uma btree_gin
extensão instalada permite que eu crie um índice GIN com várias colunas como este:
CREATE INDEX idx_test2 ON products USING GIN (
category_ids gin__int_ops, score, title) WHERE published;
Mas o Postgres não deseja usar isso para classificar . Mesmo quando eu removo o DESC
especificador na consulta.
Quaisquer abordagens alternativas para otimizar a tarefa são muito bem-vindas.
Informação adicional:
- PostgreSQL 9.4, com extensão intarray
- atualmente, o número total de produtos é de 260 mil, mas espera-se um crescimento significativo (até 10 milhões, esta é a plataforma de comércio eletrônico para vários locatários)
- produtos por categoria 1..10000 (pode crescer até 100k), a média está abaixo de 100, mas as categorias com grande número de produtos tendem a atrair muito mais solicitações
O plano de consulta a seguir foi obtido de um sistema de teste menor (4680 produtos na categoria selecionada, 200 mil produtos no total na tabela):
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
Nota # 1 : 82 ms podem não parecer assustadores, mas isso ocorre porque o buffer de classificação se encaixa na memória. Depois de selecionar todas as colunas da tabela de produtos ( SELECT * FROM ...
e na vida real existem cerca de 60 colunas), o Sort Method: external merge Disk: 5696kB
tempo de execução é dobrado. E isso é apenas para 4680 produtos.
Ponto de ação nº 1 (vem da Nota nº 1): para reduzir o espaço ocupado na memória da operação de classificação e, portanto, acelerar um pouco, seria sensato buscar, classificar e limitar os IDs de produtos primeiro e, em seguida, buscar registros completos:
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;
Isso nos leva de volta a Sort Method: quicksort Memory: 903kB
~ 80 ms para 4680 produtos. Ainda pode ser lento quando o número de produtos cresce para 100k.
fonte
score
pode ser NULL, mas você ainda classifica porscore DESC
, nãoscore DESC NULLS LAST
. Um ou outro não parece certo ...score
, de fato, NÃO é NULL - corrigi a definição da tabela.Respostas:
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 .
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 emEXPLAIN ANALYSE
) pode acelerar significativamente as operações de classificação.Reinicie o servidor para que as alterações entrem em vigor e verifique novamente:
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:Como podemos ver,
work_mem
não bastavaSort 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:
Nota
Sort Method: quicksort Memory: 7396kB
. Resultado é muito melhor.JOIN e índice adicional de árvore B
Como Chris aconselhou, criei um índice adicional:
Primeiro eu tentei entrar assim:
O plano de consulta difere um pouco, mas o resultado é o mesmo:
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 :
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:
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:
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 osORDER 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: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.fonte
work_mem
configuração geral no postgresql.conf. Recarregar é suficiente. E deixe-me alertar contra a definiçãowork_mem
muito alta globalmente em um ambiente multiusuário (também não muito baixo). Se você tiver alguma dúvida que precise de maiswork_mem
, defina-a mais alta para a sessão apenas comSET
- ou apenas a transação comSET LOCAL
. Veja: dba.stackexchange.com/a/48633/3684Aqui estão algumas dicas rápidas que podem ajudar a melhorar seu desempenho. Começarei com a dica mais fácil, que é quase sem esforço da sua parte, e passarei à dica mais difícil após a primeira.
1
work_mem
Portanto, vejo imediatamente que uma classificação relatada no seu plano de explicação
Sort Method: external merge Disk: 5696kB
está consumindo menos de 6 MB, mas está sendo derramada no disco. Você precisa aumentar suawork_mem
configuração em seupostgresql.conf
arquivo para ser grande o suficiente para que a classificação possa caber na memória.EDIT: Além disso, em uma inspeção mais aprofundada, vejo que, depois de usar o índice para verificar
catgory_ids
quais se encaixam nos seus critérios, a verificação do índice de bitmap é forçada a se tornar "com perdas" e precisa verificar novamente a condição ao ler as linhas nas páginas de heap relevantes . Consulte este post no postgresql.org para obter uma explicação melhor do que eu dei. : P O ponto principal é que vocêwork_mem
está muito baixo. Se você não ajustou as configurações padrão do seu servidor, ele não terá um bom desempenho.Essa correção levará basicamente um tempo para você fazer. Uma mudança para
postgresql.conf
, e você está fora! Consulte esta página de ajuste de desempenho para obter mais dicas.2. Mudança de esquema
Portanto, você tomou a decisão em seu design de esquema de desnormalizar
category_ids
em uma matriz inteira, o que o força a usar um índice GIN ou GIST para obter acesso rápido. Na minha experiência, sua escolha de um índice GIN será mais rápida para leituras do que um GIST, portanto, nesse caso, você fez a escolha certa. No entanto, GIN é um índice não classificado; pensar sobre isso mais como um valor-chave, onde predicados de igualdade são fáceis de verificar, mas operações comoWHERE >
,WHERE <
ouORDER BY
não são facilitados pelo índice.Uma abordagem decente seria normalizar seu design usando uma tabela de ponte / tabela de junção , usada para especificar relacionamentos muitos para muitos nos bancos de dados.
Nesse caso, você tem muitas categorias e um conjunto de números inteiros correspondentes
category_id
e muitos produtos e seusproduct_id
s correspondentes . Em vez de uma coluna na tabela do produto que é uma matriz inteira decategory_id
s, remova essa coluna da matriz do esquema e crie uma tabela comoEm seguida, você pode gerar índices da árvore B nas duas colunas da tabela de ponte,
Apenas minha humilde opinião, mas essas mudanças podem fazer uma grande diferença para você. Experimente essa
work_mem
mudança, no mínimo.Boa sorte!
EDITAR:
Crie um índice adicional para ajudar na classificação
Portanto, se com o tempo sua linha de produtos se expandir, determinadas consultas poderão retornar muitos resultados (milhares, dezenas de milhares?), Mas que ainda poderão ser apenas um pequeno subconjunto de sua linha de produtos total. Nesses casos, a classificação pode até ser bastante cara se for feita na memória, mas um índice adequadamente projetado pode ser usado para ajudar na classificação.
Veja a documentação oficial do PostgreSQL descrevendo Indexes e ORDER BY .
Se você criar um índice que corresponda às suas
ORDER BY
necessidadeso Postgres otimizará e decidirá se usar o índice ou executar uma classificação explícita será mais econômico. Lembre-se de que não há garantia de que o Postgres use o índice; procurará otimizar o desempenho e escolher entre usar o índice ou classificar explicitamente. Se você criar esse índice, monitore-o para ver se está sendo usado o suficiente para justificar sua criação e descarte-o se a maioria de suas classificações estiver sendo feita explicitamente.
Ainda assim, neste momento, sua melhoria 'maior retorno do investimento' provavelmente estará usando mais
work_mem
, mas há casos em que o índice pode suportar a classificação.fonte
work_mem
configuração foi planejada como uma correção para o problema de "classificação em disco", bem como para o problema de verificação de condição. À medida que o número de produtos aumenta, pode ser necessário ter um índice adicional para classificar. Por favor, veja minhas edições acima para esclarecimentos.