COUNT do postgresql (DISTINCT…) muito lento

166

Eu tenho uma consulta SQL muito simples:

SELECT COUNT(DISTINCT x) FROM table;

Minha tabela possui cerca de 1,5 milhão de linhas. Esta consulta está sendo executada bem devagar; leva cerca de 7,5s, em comparação com

 SELECT COUNT(x) FROM table;

o que leva cerca de 435ms. Existe alguma maneira de alterar minha consulta para melhorar o desempenho? Tentei agrupar e fazer uma contagem regular, além de colocar um índice em x; ambos têm o mesmo tempo de execução 7.5s.

ferson2020
fonte
Acho que não. Obter os valores distintos de 1,5 milhão de linhas será lento.
Ry-
5
Eu apenas tentei em C #, pois obter os valores distintos de 1,5 milhão de números inteiros da memória leva mais de um segundo no meu computador. Então eu acho que você provavelmente está sem sorte.
Ry-
O plano de consulta dependerá muito da estrutura da tabela (índices) e da configuração das constantes de ajuste (trabalho) mem, eficaz_cache_size, random_page_cost). Com um ajuste razoável, a consulta pode ser executada em menos de um segundo.
wildplasser
Você poderia ser mais específico? Quais índices e constantes de ajuste seriam necessários para obtê-lo em menos de um segundo? Por uma questão de simplicidade, suponha que esta seja uma tabela de duas colunas com uma chave primária na primeira coluna y, e eu estou fazendo essa consulta 'distinta' em uma segunda coluna x do tipo int, com 1,5 milhão de linhas.
ferson2020
1
Por favor, inclua a definição da tabela com todos os índices (a \dsaída psqlé boa) e especifique a coluna com a qual você está com problemas. Seria bom ver as EXPLAIN ANALYZEduas consultas.
vyegorov

Respostas:

316

Você pode usar isto:

SELECT COUNT(*) FROM (SELECT DISTINCT column_name FROM table_name) AS temp;

Isso é muito mais rápido que:

COUNT(DISTINCT column_name)
Ankur
fonte
38
perguntas sagradas batman! Isso acelerou minha contagem de postgres distinta de 190 a 4,5 whoa!
Rogerdpack #
20
Encontrei este tópico em www.postgresql.org que discute a mesma coisa: link . Uma das respostas (de Jeff Janes) diz que COUNT (DISTINCT ()) classifica a tabela para fazer seu trabalho em vez de usar hash.
Ankur
5
@Ankur Posso fazer uma pergunta? Como COUNT(DISTINCT())realiza a classificação, será definitivamente útil ter um índice column_nameespecialmente com uma quantidade relativamente pequena de work_mem(onde o hash produzirá uma quantidade relativamente grande de lotes). Desde que, nem sempre é ruim de usar COUNT (DISTINCT () _, não é?
St.Antario
2
@musmahn Count(column)conta apenas valores não nulos. count(*)conta linhas. Portanto, a primeira / mais longa também contará a linha nula (uma vez). Altere para count(column_name)fazê-los se comportar da mesma maneira.
GolezTrol
1
@ankur isso não foi muito útil para mim .. não obteve nenhuma melhoria notável.
Shiwangini 9/09/19
11
-- My default settings (this is basically a single-session machine, so work_mem is pretty high)
SET effective_cache_size='2048MB';
SET work_mem='16MB';

\echo original
EXPLAIN ANALYZE
SELECT
        COUNT (distinct val) as aantal
FROM one
        ;

\echo group by+count(*)
EXPLAIN ANALYZE
SELECT
        distinct val
       -- , COUNT(*)
FROM one
GROUP BY val;

\echo with CTE
EXPLAIN ANALYZE
WITH agg AS (
    SELECT distinct val
    FROM one
    GROUP BY val
    )
SELECT COUNT (*) as aantal
FROM agg
        ;

Resultados:

original                                                      QUERY PLAN                                                      
----------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=36448.06..36448.07 rows=1 width=4) (actual time=1766.472..1766.472 rows=1 loops=1)
   ->  Seq Scan on one  (cost=0.00..32698.45 rows=1499845 width=4) (actual time=31.371..185.914 rows=1499845 loops=1)
 Total runtime: 1766.642 ms
(3 rows)

group by+count(*)
                                                         QUERY PLAN                                                         
----------------------------------------------------------------------------------------------------------------------------
 HashAggregate  (cost=36464.31..36477.31 rows=1300 width=4) (actual time=412.470..412.598 rows=1300 loops=1)
   ->  HashAggregate  (cost=36448.06..36461.06 rows=1300 width=4) (actual time=412.066..412.203 rows=1300 loops=1)
         ->  Seq Scan on one  (cost=0.00..32698.45 rows=1499845 width=4) (actual time=26.134..166.846 rows=1499845 loops=1)
 Total runtime: 412.686 ms
(4 rows)

with CTE
                                                             QUERY PLAN                                                             
------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=36506.56..36506.57 rows=1 width=0) (actual time=408.239..408.239 rows=1 loops=1)
   CTE agg
     ->  HashAggregate  (cost=36464.31..36477.31 rows=1300 width=4) (actual time=407.704..407.847 rows=1300 loops=1)
           ->  HashAggregate  (cost=36448.06..36461.06 rows=1300 width=4) (actual time=407.320..407.467 rows=1300 loops=1)
                 ->  Seq Scan on one  (cost=0.00..32698.45 rows=1499845 width=4) (actual time=24.321..165.256 rows=1499845 loops=1)
       ->  CTE Scan on agg  (cost=0.00..26.00 rows=1300 width=0) (actual time=407.707..408.154 rows=1300 loops=1)
     Total runtime: 408.300 ms
    (7 rows)

O mesmo plano do CTE provavelmente também poderia ser produzido por outros métodos (funções da janela)

wildplasser
fonte
2
Você já considerou o efeito do cache? Se executar três "explique a análise" posteriormente, o primeiro pode ser lento na busca de coisas do disco, enquanto o segundo pode ser rápido na memória.
tobixen
De fato: effective_cache_size é a primeira configuração a ser ajustada. A minha é de 2GB, IIRC.
wildplasser
Defino meu effective_cache_size como 2 GB, sem alteração no desempenho. Você sugeriria outras configurações? Se sim, para quê?
ferson2020
1) como você definiu isso? (você o copiou?) 2) Você tem realmente muita memória disponível? 3) mostre-nos o seu plano. 4) talvez minha máquina seja mais rápida ou a sua tenha uma carga mais simultânea para lidar. @ ferson2020: Ok
wildplasser
Eu o defino com a instrução: SET effective_cache_size = '2GB'; Eu tenho muita memória disponível. Tentei incluir meu plano de consulta, mas ele não caberá na caixa de comentários.
ferson2020
2

Se o seu count(distinct(x))for significativamente mais lento do que em count(x)seguida, você poderá acelerar essa consulta mantendo as contagens de valores x em uma tabela diferente, por exemplo table_name_x_counts (x integer not null, x_count int not null), usando gatilhos. Mas seu desempenho de gravação sofrerá e, se você atualizar vários xvalores em uma única transação, precisará fazer isso em alguma ordem explícita para evitar um possível impasse.

Tometzky
fonte
0

Eu também estava procurando a mesma resposta, porque em algum momento eu precisava total_count com valores distintos, juntamente com limite / deslocamento .

Porque é um pouco complicado de fazer - Obter uma contagem total com valores distintos, além de limite / deslocamento. Geralmente é difícil obter a contagem total com limite / deslocamento. Finalmente consegui o que fazer -

SELECT DISTINCT COUNT(*) OVER() as total_count, * FROM table_name limit 2 offset 0;

O desempenho da consulta também é alto.

Rana Pratap Singh
fonte