Consulta lenta na tabela grande com GROUP BY e ORDER BY

14

Eu tenho uma tabela com 7,2 milhões de tuplas que se parece com isso:

                               table public.methods
 column |          type         |                      attributes
--------+-----------------------+----------------------------------------------------
 id     | integer               | not null DEFAULT nextval('methodkey'::regclass)
 hash   | character varying(32) | not null
 string | character varying     | not null
 method | character varying     | not null
 file   | character varying     | not null
 type   | character varying     | not null
Indexes:
    "methods_pkey" PRIMARY KEY, btree (id)
    "methodhash" btree (hash)

Agora eu quero selecionar alguns valores, mas a consulta é incrivelmente lenta:

db=# explain 
    select hash, string, count(method) 
    from methods 
    where hash not in 
          (select hash from nostring) 
    group by hash, string 
    order by count(method) desc;
                                            QUERY PLAN
----------------------------------------------------------------------------------------
 Sort  (cost=160245190041.10..160245190962.07 rows=368391 width=182)
   Sort Key: (count(methods.method))
   ->  GroupAggregate  (cost=160245017241.77..160245057764.73 rows=368391 width=182)
       ->  Sort  (cost=160245017241.77..160245026451.53 rows=3683905 width=182)
             Sort Key: methods.hash, methods.string
             ->  Seq Scan on methods  (cost=0.00..160243305942.27 rows=3683905 width=182)
                   Filter: (NOT (SubPlan 1))
                   SubPlan 1
                   ->  Materialize  (cost=0.00..41071.54 rows=970636 width=33)
                     ->  Seq Scan on nostring  (cost=0.00..28634.36 rows=970636 width=33)

A hashcoluna é o hash md5 stringe possui um índice. Então, acho que meu problema é que toda a tabela é classificada por id e não por hash, então leva um tempo para classificá-la primeiro e depois agrupá-la?

A tabela nostringcontém apenas uma lista de hashes que eu não quero ter. Mas preciso que ambas as tabelas tenham todos os valores. Portanto, não é uma opção para excluí-los.

informações adicionais: nenhuma das colunas pode ser nula (foi corrigida na definição da tabela) e estou usando o postgresql 9.2.

reox
fonte
1
Sempre forneça a versão do PostgreSQL que você usa. Qual é a porcentagem de NULLvalores na coluna method? Existem duplicatas ativadas string?
Erwin Brandstetter

Respostas:

18

A resposta doLEFT JOIN in @ dezso deve ser boa. Um índice, no entanto, dificilmente será útil (por si só), porque a consulta precisa ler toda a tabela de qualquer maneira - a exceção são as varreduras somente de índice no Postgres 9.2+ e condições favoráveis, veja abaixo.

SELECT m.hash, m.string, count(m.method) AS method_ct
FROM   methods m
LEFT   JOIN nostring n USING (hash)
WHERE  n.hash IS NULL
GROUP  BY m.hash, m.string 
ORDER  BY count(m.method) DESC;

Execute EXPLAIN ANALYZEna consulta. Várias vezes para excluir efeitos de descontar e ruídos. Compare os melhores resultados.

Crie um índice de várias colunas que corresponda à sua consulta:

CREATE INDEX methods_cluster_idx ON methods (hash, string, method);

Esperar? Depois que eu disse que um índice não ajudaria? Bem, precisamos disso para CLUSTERa mesa:

CLUSTER methods USING methods_cluster_idx;
ANALYZE methods;

Execute novamente EXPLAIN ANALYZE. Mais rápido? Deveria ser.

CLUSTERé uma operação única para reescrever a tabela inteira na ordem do índice usado. Também é efetivamente umVACUUM FULL . Se quiser ter certeza, faça um pré-teste VACUUM FULLsozinho para ver o que pode ser atribuído a isso.

Se sua tabela vir muitas operações de gravação, o efeito será degradado com o tempo. Programe CLUSTERfora do horário comercial para restaurar o efeito. O ajuste fino depende do seu caso de uso exato. O manual sobre CLUSTER.

CLUSTERé uma ferramenta bastante grosseira, precisa de um bloqueio exclusivo sobre a mesa. Se você não puder pagar, considerepg_repack que pode fazer o mesmo sem bloqueio exclusivo. Mais nesta resposta posterior:


Se a porcentagem de NULLvalores na coluna methodfor alta (mais de ~ 20%, dependendo do tamanho real das linhas), um índice parcial deverá ajudar:

CREATE INDEX methods_foo_idx ON methods (hash, string)
WHERE method IS NOT NULL;

(Sua atualização posterior mostra suas colunas NOT NULL, portanto não é aplicável.)

Se você estiver executando o PostgreSQL 9.2 ou posterior (como o @deszo comentou ), os índices apresentados podem ser úteis sem CLUSTERque o planejador possa utilizar varreduras somente de índice . Aplicável apenas sob condições favoráveis: Nenhuma operação de gravação que afete o mapa de visibilidade desde a última VACUUMe todas as colunas da consulta precisam ser cobertas pelo índice. Basicamente, as tabelas somente leitura podem usar isso a qualquer momento, enquanto as tabelas fortemente gravadas são limitadas. Mais detalhes no Wiki do Postgres.

O índice parcial acima mencionado pode ser ainda mais útil nesse caso.

Se , por outro lado, não houver NULL valores na coluna method, você deve
1.) defini-lo NOT NULLe
2.) usar em count(*)vez de count(method), isso é um pouco mais rápido e faz o mesmo na ausência de NULLvalores.

Se você precisar chamar essa consulta frequentemente e a tabela for somente leitura, crie a MATERIALIZED VIEW.


Ponto fino exótico: sua tabela é nomeada nostring, mas parece conter hashes. Ao excluir hashes em vez de cadeias, é possível que você exclua mais cadeias do que o pretendido. Extremamente improvável, mas possível.

Erwin Brandstetter
fonte
com o cluster é muito mais rápido. Ainda precisa arround 5min para a consulta, mas isso é muito melhor maneira de executá-lo durante toda a noite: D
Reox
@ reox: Desde que você executou a v9.2: você testou apenas com o índice antes de fazer o cluster? Seria interessante se você visse uma diferença. (Você não pode reproduzir a diferença após o armazenamento em cluster.) Além disso (e isso seria barato), EXPLAIN mostra uma verificação de índice ou uma verificação de tabela completa agora?
Erwin Brandstetter
5

Bem-vindo ao DBA.SE!

Você pode tentar reformular sua consulta da seguinte maneira:

SELECT m.hash, string, count(method) 
FROM 
    methods m
    LEFT JOIN nostring n ON m.hash = n.hash
WHERE n.hash IS NULL
GROUP BY hash, string 
ORDER BY count(method) DESC;

ou outra possibilidade:

SELECT m.hash, string, count(method) 
FROM 
    methods m
WHERE NOT EXISTS (SELECT hash FROM nostring WHERE hash = m.hash)
GROUP BY hash, string 
ORDER BY count(method) DESC;

NOT IN é um coletor típico de desempenho, pois é difícil usar um índice com ele.

Isso pode ser aprimorado ainda mais com índices. Um índice em nostring.hashparece útil. Mas primeiro: o que você ganha agora? (Seria melhor ver a saída, EXPLAIN ANALYZEpois os custos em si não informam o tempo que as operações levaram.)

dezso
fonte
um índice é criado no nostring.hash já, mas acho que o postgres não o usa por causa de muitas tuplas ... quando explcito desativar a varredura de sequência, ele usa o índice. se eu usar a esquerda juntar-se i obter um custo de 32 milhões, pelo que a sua melhor forma ... mas eu estou tentando otimizá-lo mais ...
Reox
3
O custo é apenas para o planejador ser capaz de planejar um plano suficientemente bom. Os tempos atuais geralmente se correlacionam com isso, mas não necessariamente. Então, se você quiser ter certeza, use EXPLAIN ANALYZE.
Dezso
1

Como o hash é um md5, você provavelmente pode tentar convertê-lo em um número: você pode armazená-lo como um número ou apenas criar um índice funcional que calcule esse número em uma função imutável.

Outras pessoas já criaram uma função pl / pgsql que converte (parte de) um valor md5 de texto em string. Consulte /programming/9809381/hashing-a-string-to-a-numeric-value-in-postgressql para obter um exemplo

Acredito que você está realmente gastando muito tempo na comparação de cadeias de caracteres enquanto verifica o índice. Se você conseguir armazenar esse valor como um número, deve ser realmente muito mais rápido.

eppesuig
fonte
1
Duvido que essa conversão acelere as coisas. Todas as consultas aqui usam igualdade para comparação. Computar representações numéricas e depois verificar a igualdade não promete grandes ganhos para mim.
Dezso
2
Eu acho que eu armazenar md5 como bytea em vez de um número para a eficiência do espaço: sqlfiddle.com/#!12/d41d8/252
Jack diz tentativa topanswers.xyz
Bem-vindo ao dba.se!
Jack diz que tente topanswers.xyz 30/11/12
@JackDouglas: Comentário interessante! 16 bytes por md5 em vez de 32 é um pouco para grandes tabelas.
Erwin Brandstetter
0

Eu me deparei muito com esse problema e descobri um truque simples em duas partes.

  1. Crie um índice de substring no valor do hash: (7 geralmente é um bom comprimento)

    create index methods_idx_hash_substring ON methods(substring(hash,1,7))

  2. Faça com que suas pesquisas / junções incluam uma correspondência de substring, para que o planejador de consultas seja sugerido para usar o índice:

    velho: WHERE hash = :kwarg

    Novo: WHERE (hash = :kwarg) AND (substring(hash,1,7) = substring(:kwarg,1,7))

Você também deve ter um índice em bruto hashtambém.

o resultado (geralmente) é que o planejador consultará primeiro o índice de substring e eliminará a maioria das linhas. em seguida, corresponde o hash completo de 32 caracteres ao índice (ou tabela) correspondente. essa abordagem reduziu as consultas de 800 ms para 4 para mim.

Jonathan Vanasco
fonte