Descrição
PostgreSQL 9.6 no Linux, tamanho da tags_tmp
tabela ~ 30 GB (10 milhões de linhas), tags
é um text[]
e tem apenas 6 valores.
tags_tmp(id int, tags text[], maker_date timestamp, value text)
id tags maker_date value
1 {a,b,c} 2016-11-09 This is test
2 {a} 2016-11-08 This is test
3 {b,c} 2016-11-07 This is test
4 {c} 2016-11-06 This is test
5 {d} 2016-11-05 This is test
Eu preciso recuperar dados com filtro ativado tags
e também order by
em maker_date desc
. Posso criar um índice em ambas as tags & maker_date desc
colunas?
Se não, você poderia sugerir outras ideias?
Exemplo de consulta
select id, tags, maker_date, value
from tags_tmp
where tags && array['a','b']
order by maker_date desc
limit 5 offset 0
Código SQL:
create index idx1 on tags_tmp using gin (tags);
create index idx2 on tags_tmp using btree(maker_date desc);
explain (analyse on, costs on, verbose)
select id, tags, maker_date, value
from tags_tmp
where tags && array['funny','inspiration']
order by maker_date desc
limit 5 offset 0 ;
Explique o resultado:
Limit (cost=233469.63..233469.65 rows=5 width=116) (actual time=801.482..801.483 rows=5 loops=1)
Output: id, tags, maker_date, value
-> Sort (cost=233469.63..234714.22 rows=497833 width=116) (actual time=801.481..801.481 rows=5 loops=1)
Output: id, tags, maker_date, value
Sort Key: tags_tmp.maker_date DESC
Sort Method: top-N heapsort Memory: 25kB
-> Bitmap Heap Scan on public.tags_tmp (cost=6486.58..225200.81 rows=497833 width=116) (actual time=212.982..696.650 rows=366392 loops=1)
Output: id, tags, maker_date, value
Recheck Cond: (tags_tmp.tags && '{funny,inspiration}'::text[])
Heap Blocks: exact=120034
-> Bitmap Index Scan on idx1 (cost=0.00..6362.12 rows=497882 width=0) (actual time=171.742..171.742 rows=722612 loops=1)
Index Cond: (tags_tmp.tags && '{funny,inspiration}'::text[])
Planning time: 0.185 ms
Execution time: 802.128 ms
Mais Informações
Testei usando índice parcial para apenas uma tag, claro, é mais rápido. Mas eu tenho muitos tag , por exemplo: create index idx_tmp on tags_tmp using btree (maker_date desc) where (tags && array['tag1') or tags && array['tag2'] or ... or tags && array['tag6']
. E eu testei entre tags && array['tag1']
e 'tag1' = any(tags)
, o desempenho é o mesmo.
text[]
tem apenas 6 valores =a, b, c, d, e, f
. Por exemplo:tags={a,b,c}, tags={a}, tags={a,c}, tags={a,b,c,d,e,f}, tags={b,f}
e assim por diante. Mas não pode ter valorg->z, A-Z
e etc.create table tags_tmp(id int primary key not null, tags text[] not null, maker_date timestamp not null, value text)
Em termos de
distinct
valores de matriz, otags
que contéma
leva 20% das linhas da tabelawhere 'a' = any(tags)
, b=20%where 'b' = any(tags)
, c=20%where 'c' = any(tags)
, d=20%where 'd' = any(tags)
, e=10%where 'e' = any(tags)
, f=10%where 'f' = any(tags)
.Além disso,
(tags, maker_date)
não é único.Esta tabela não é somente leitura.
É
sort on timestamp
, mas meu exemplo mostra datas, desculpe por isso.
Situação atual: tags = 'a' or tags = 'b' or tags = 'c'
e mais
(1) Com GIN index
ou converter text[] to int[]
, bem como converter text[] to int
e mais, ele usará o índice de bitmap em várias tags. Finalmente, após o teste, decidi usar a solução antiga, mudar OR
para muitas UNION
cláusulas, cada UNION
uma limitando o número de dados. Claro, vou criar partial index
para cada valor de tag, assim como posso combinar com (1) acima. Em termos de OFFSET
, ele usará uma ou mais condições na WHERE
cláusula.
Exemplo
EXPLAIN (ANALYSE ON, costs ON, VERBOSE)
SELECT rs.*
FROM (
(SELECT tags,
id,
maker_date
FROM tags_tmp
WHERE 'a' = any(tags)
AND maker_date <= '2016-03-28 05:43:57.779528'::TIMESTAMP
ORDER BY maker_date DESC LIMIT 5)
UNION
(SELECT tags,
id,
maker_date
FROM tags_tmp
WHERE 'b' = any(tags)
AND maker_date <= '2016-03-28 05:43:57.779528'::TIMESTAMP
ORDER BY maker_date DESC LIMIT 5)
UNION
(SELECT tags,
id,
maker_date
FROM tags_tmp
WHERE 'c' = any(tags)
AND maker_date <= '2016-03-28 05:43:57.779528'::TIMESTAMP
ORDER BY maker_date DESC LIMIT 5)) rs
ORDER BY rs.maker_date DESC LIMIT 5 ;
Considerações gerais
A otimização do índice sempre depende da imagem completa . Tamanho da tabela, tamanho da linha, cardinalidades, frequências de valor, seletividade de consultas típicas, versão do Postgres, padrões de acesso típicos, etc.
Seu caso é particularmente difícil por dois motivos:
WHERE
eORDER BY
.O filtro na matriz é mais eficiente com o índice GIN ou GiST, mas nenhum dos tipos de índice produz uma saída classificada. O manual:
Você pode criar um índice GIN de várias colunas em
(tags, maker_date)
ou até mais colunas (a ordem das colunas do índice é irrelevante para índices GIN). Mas você precisa ter o módulo adicionalbtree_gin
instalado. Instruções:E não vai ajudar na parte
ORDER BY
do seu problema.Mais um esclarecimento:
OFFSET m LIMIT n
geralmente é quase tão caro quantoLIMIT m+n
.Solução para especificações adicionadas
Você esclareceu: apenas 6 tags distintas são possíveis. Isso é crucial.
Sua mesa é grande e sua definição de mesa deixa espaço para melhorias. O tamanho é importante para mesas grandes. Seus números (30 GB, 10 milhões de linhas) também sugerem uma grande média. tamanho da linha de ~ 3 KB. Ou você tem mais colunas do que mostra ou a tabela incha e precisa de uma
VACUUM FULL
execução (ou similar) ou suavalue
coluna é grande e TOASTada, o que tornaria minhas melhorias ainda mais eficazes, pois a relação principal é reduzida à metade de seu tamanho ou menos com isso :A ordem das colunas é relevante devido ao preenchimento de alinhamento. Detalhes:
Mais importante, isto:
tags int
. Por quê?As matrizes têm uma sobrecarga considerável de 24 bytes (semelhante a uma linha), mais itens reais.
Portanto, um
text[]
com 1-6 itens como você demonstra ('engraçado', 'inspiração', ...) ocupa ~ 56 bytes em avg . E 6 valores distintos podem ser representados por apenas 6 bits de informação (supondo que a ordem de classificação do array seja irrelevante). Poderíamos comprimir ainda mais, mas escolhi ointeger
tipo conveniente (ocupa 4 bytes ), que oferece espaço para até 31 tags distintas. Isso deixa espaço para adições posteriores sem alterações no esquema da tabela. Justificativa detalhada:Suas tags são mapeadas para bits em um bitmap,
'a'
sendo o bit menos significativo (à direita):Portanto, a matriz de tags
'{a,d,f}'
é mapeada para41
. Você pode usar qualquer string arbitrária em vez de 'a'-'f', não importa.Para encapsular a lógica sugiro duas funções auxiliares , facilmente expansíveis:
tags -> inteiro:
inteiro -> tags:
Básico aqui:
Por conveniência, você pode adicionar uma visualização para exibir tags como matriz de texto como você tinha:
Agora sua consulta básica pode ser:
Usando o operador AND binário
&
. Existem mais operadores para manipular a coluna.get_bit()
eset_bit()
dos operadores de cadeia binária também são convenientes.A consulta acima já deveria ser mais rápida, apenas para as operadoras de menor porte e mais baratas, mas nada revolucionário ainda. Para torná-lo rápido , precisamos de índices, e o acima não pode usar um índice ainda.
Um índice parcial para cada tag:
Esta consulta é equivalente à anterior, mas pode utilizar os índices:
Postgres pode combinar várias varreduras de índice de bitmap em uma
BitmapOr
etapa de forma muito eficiente, como demonstrado neste SQL Fiddle .Você pode adicionar outra condição de índice para limitar os índices a
maker_date
> algum registro de data e hora constante (e repetir a condição literal nas consultas) para reduzir seu tamanho (massivamente). Exemplo relacionado:Mais sofisticado:
Outras respostas relacionadas:
Como acelerar a classificação ORDER BY ao usar o índice GIN no PostgreSQL?
O índice espacial pode ajudar uma consulta "range - order by - limit"
Ou apenas 6
boolean
colunas ...Apenas 6 colunas booleanas podem ser uma escolha ainda melhor. Existem alguns prós e contras para qualquer solução ...
Você pode definir os sinalizadores
NOT NULL
, dependendo do seu caso de uso completo.Considerar:
Índices parciais simplesmente:
etc.
Alternativa para o seu caso especial
Pensando um pouco mais, já que todas as suas poucas tags são tão comuns , e combinar várias tags com OR é ainda menos seletivo, será mais rápido ter apenas um índice btree em
maker_date DESC
. O Postgres pode percorrer o índice e filtrar as linhas qualificadas nas tags. Isso funcionará em combinação com colunas booleanas separadas em vez da matriz ou inteiro codificado, porque o Postgres tem estatísticas de colunas mais úteis para colunas separadas.E depois:
Paginação
Você precisa de uma ordem de classificação inequívoca para conjuntos de resultados, para fazer a paginação funcionar. Não me preocupei com esta resposta, já é muito longa. Normalmente, você adicionaria mais colunas a
ORDER BY
. Como fazer a paginação funcionar de forma eficiente com isso:Vários problemas com seu caso de teste:
id
éint8
agora. Você declarou comoint
na sua pergunta original Não é uma grande diferença, mas por que a confusão para começar? É importante para o tamanho da linha e preenchimento de alinhamento. Lembre-se de declarar sua definição de tabela real, exata e completa nas perguntas.A distribuição de dados em seus dados de teste não é realista. Você tem apenas 6 combinações distintas de tags e *todas as linhas têm tag
'1'
. Presumo que você tenha todas as 63 combinações possíveis em sua mesa ao vivo, com tags distribuídas como você adicionou na pergunta.Sua tabela de teste inclui colunas de tags antigas e novas, o que anula o efeito no tamanho de armazenamento que eu procurava. Agora o tamanho da linha é ainda maior. O tamanho da sua linha é de 124 a 164 bytes, contra apenas 68 bytes no meu teste (incluindo preenchimento e identificador de item). Mais de duas vezes o tamanho faz a diferença.
Você escreve tamanho = 4163 MB . Que tamanho?
Você tem
order by random()
para os dados de teste. Sua mesa produtiva é tão aleatória assim? Normalmente, você teria dados aproximadamente classificados por carimbo de data/hora. Qual é a sua situação real ?Para ver qual plano será escolhido, teste
EXPLAIN
apenas para ver o plano de consulta antes de realmente executar a consulta. Economiza muito tempo com mesas grandes. Mas sempre forneça a saídaEXPLAIN (ANALYZE, BUFFERS)
daqui. Em sua resposta (ao contrário da pergunta),cost=
faltam estimativas. Isso torna difícil adivinhar o problema.Mas nenhum desses problemas pode explicar por que você vê uma varredura sequencial mesmo com
enable_seqscan = off
; Um teste rápido no meu laptop com Postgres 9.5 funciona . O mesmo deve ser verdade para a página 9.6.Assim como já demonstrei no SQL Fiddle .
Tem certeza de que criou todos os índices corretamente?