Maneira rápida de descobrir a contagem de linhas de uma tabela no PostgreSQL

107

Preciso saber o número de linhas em uma tabela para calcular uma porcentagem. Se a contagem total for maior do que alguma constante predefinida, usarei o valor da constante. Caso contrário, usarei o número real de linhas.

Eu posso usar SELECT count(*) FROM table. Mas se meu valor constante for 500.000 e eu tiver 5.000.000.000 de linhas em minha tabela, contar todas as linhas levará muito tempo.

É possível parar de contar assim que meu valor constante for ultrapassado?

Preciso do número exato de linhas apenas enquanto estiver abaixo do limite fornecido. Caso contrário, se a contagem estiver acima do limite, uso o valor limite e quero a resposta o mais rápido possível.

Algo assim:

SELECT text,count(*), percentual_calculus()  
FROM token  
GROUP BY text  
ORDER BY count DESC;
Renato Dinhani
fonte
5
Você não poderia simplesmente tentar selecionar as primeiras n linhas, onde n = constante + 1 ? Se retornar mais do que sua constante, você sabe que deve usar sua constante e, se não retornar, você é bom?
gddc
Você tem um campo de identidade ou incremento automático na tabela
Sparky
1
@Sparky: PKs baseados em sequência não são garantidos como contíguos, linhas podem ser excluídas ou pode haver lacunas causadas por transações abortadas.
mu é muito curto
Sua atualização parece contradizer sua pergunta original ... você precisa saber o número exato de linhas ou você só precisa saber o número exato se estiver abaixo de um limite?
Flimzy
1
@ RenatoDinhaniConceição: Você pode explicar o problema exato que você está tentando resolver? Acho que minha resposta abaixo resolve o que você disse inicialmente ser seu problema. A atualização faz com que pareça que você deseja contar (*), bem como muitos outros campos. Ajudaria se você pudesse explicar exatamente o que está tentando fazer. Obrigado.
Ritesh

Respostas:

224

A contagem de linhas em tabelas grandes é conhecida por ser lenta no PostgreSQL. Para obter um número preciso, é necessário fazer uma contagem completa de linhas devido à natureza do MVCC . Há uma maneira de acelerar isso drasticamente se a contagem não precisar ser exata como parece ser no seu caso.

Em vez de obter a contagem exata ( lento com tabelas grandes):

SELECT count(*) AS exact_count FROM myschema.mytable;

Você obtém uma estimativa aproximada como esta ( extremamente rápido ):

SELECT reltuples::bigint AS estimate FROM pg_class where relname='mytable';

A proximidade da estimativa depende de você correr o ANALYZEsuficiente. Geralmente é muito próximo.
Veja o FAQ do PostgreSQL Wiki .
Ou a página wiki dedicada para contagem (*) de desempenho .

Melhor ainda

O artigo do PostgreSQL Wiki é foi um pouco desleixado . Ele ignorou a possibilidade de haver várias tabelas com o mesmo nome em um banco de dados - em esquemas diferentes. Para explicar isso:

SELECT c.reltuples::bigint AS estimate
FROM   pg_class c
JOIN   pg_namespace n ON n.oid = c.relnamespace
WHERE  c.relname = 'mytable'
AND    n.nspname = 'myschema'

Ou melhor ainda

SELECT reltuples::bigint AS estimate
FROM   pg_class
WHERE  oid = 'myschema.mytable'::regclass;

Mais rápido, simples, seguro e elegante. Veja o manual sobre Tipos de Identificadores de Objetos .

Use to_regclass('myschema.mytable')no Postgres 9.4+ para evitar exceções para nomes de tabela inválidos:


TABLESAMPLE SYSTEM (n) no Postgres 9.5+

SELECT 100 * count(*) AS estimate FROM mytable TABLESAMPLE SYSTEM (1);

Como @a_horse comentou , a cláusula recém-adicionada para o SELECTcomando pode ser útil se as estatísticas em pg_classnão forem atualizadas o suficiente por algum motivo. Por exemplo:

  • Sem autovacuumcorrer.
  • Imediatamente após um grande INSERTou DELETE.
  • TEMPORARYtabelas (que não são cobertas por autovacuum).

Isso só olha para uma seleção aleatória de n % ( 1no exemplo) de blocos e conta as linhas nela. Uma amostra maior aumenta o custo e reduz o erro, sua escolha. A precisão depende de mais fatores:

  • Distribuição do tamanho da linha. Se um determinado bloco tiver linhas mais largas do que o normal, a contagem será menor do que o normal, etc.
  • Tuplas mortas ou um FILLFACTORespaço de ocupação por bloco. Se distribuída de forma desigual pela tabela, a estimativa pode estar errada.
  • Erros gerais de arredondamento.

Na maioria dos casos, a estimativa de pg_classserá mais rápida e precisa.

Resposta à pergunta real

Primeiro, preciso saber o número de linhas dessa tabela, se a contagem total for maior do que alguma constante predefinida,

E se isso ...

... é possível no momento em que a contagem passar do meu valor constante, ele irá parar a contagem (e não esperar terminar a contagem para informar que a contagem de linhas é maior).

Sim. Você pode usar uma subconsulta comLIMIT :

SELECT count(*) FROM (SELECT 1 FROM token LIMIT 500000) t;

Na verdade, o Postgres para de contar além do limite fornecido, você obtém uma contagem exata e atual de até n linhas (500.000 no exemplo) e n caso contrário. Não tão rápido quanto a estimativa pg_class, no entanto.

Erwin Brandstetter
fonte
8
Acabei atualizando a página Wiki do Postgres com a consulta aprimorada.
Erwin Brandstetter
5
Com 9.5 obter uma estimativa rápida deve ser possível usando a tablesamplecláusula: egselect count(*) * 100 as cnt from mytable tablesample system (1);
a_horse_with_no_name
1
@JeffWidman: Todas essas estimativas podem ser maiores do que a contagem real de linhas por vários motivos. Não menos importante, as exclusões podem ter acontecido nesse meio tempo.
Erwin Brandstetter
2
@ErwinBrandstetter percebe que esta questão é antiga, mas se você empacotou a consulta em uma subconsulta, o limite ainda seria eficiente ou toda a subconsulta seria executada e então limitada na consulta externa. SELECT count(*) FROM (Select * from (SELECT 1 FROM token) query) LIMIT 500000) limited_query;(Eu pergunto porque estou tentando obter uma contagem de uma consulta arbitrária que pode já ter uma cláusula de limite)
Nicholas Erdenberger
1
@NicholasErdenberger: Isso depende da subconsulta. O Postgres pode precisar considerar mais linhas do que o limite de qualquer maneira (como com ORDER BY somethingwhile não pode usar um índice ou com funções de agregação). Além disso, apenas o número limitado de linhas da subconsulta é processado.
Erwin Brandstetter
12

Fiz isso uma vez em um aplicativo postgres executando:

EXPLAIN SELECT * FROM foo;

Em seguida, examinar a saída com um regex ou lógica semelhante. Para um SELECT * simples, a primeira linha de saída deve ser semelhante a esta:

Seq Scan on uids  (cost=0.00..1.21 rows=8 width=75)

Você pode usar o rows=(\d+)valor como uma estimativa aproximada do número de linhas que seriam retornadas e, em seguida, faça o valor real SELECT COUNT(*)se a estimativa for, digamos, menos de 1,5x o seu limite (ou qualquer número que você considere adequado para sua aplicação).

Dependendo da complexidade da sua consulta, esse número pode se tornar cada vez menos preciso. Na verdade, em meu aplicativo, à medida que adicionamos junções e condições complexas, ele se tornou tão impreciso que era completamente inútil, mesmo saber quantas linhas teríamos retornado com uma potência de 100, então tivemos que abandonar essa estratégia.

Mas se sua consulta for simples o suficiente para que o Pg possa prever com alguma margem de erro razoável quantas linhas ela retornará, pode funcionar para você.

Flimzy
fonte
2

Referência retirada deste blog.

Você pode usar a seguir para consultar a contagem de linhas.

Usando pg_class:

 SELECT reltuples::bigint AS EstimatedCount
    FROM   pg_class
    WHERE  oid = 'public.TableName'::regclass;

Usando pg_stat_user_tables:

SELECT 
    schemaname
    ,relname
    ,n_live_tup AS EstimatedCount 
FROM pg_stat_user_tables 
ORDER BY n_live_tup DESC;
Anvesh
fonte
Apenas observe rapidamente que você precisa ANALISAR VÁCUO suas tabelas para que este método funcione.
William Abma
1

No Oracle, você pode usar rownumpara limitar o número de linhas retornadas. Eu estou supondo que uma construção semelhante existe em outros SQLs também. Portanto, para o exemplo que você deu, você poderia limitar o número de linhas retornadas a 500001 e aplicar um count(*)então:

SELECT (case when cnt > 500000 then 500000 else cnt end) myCnt
FROM (SELECT count(*) cnt FROM table WHERE rownum<=500001)
Ritesh
fonte
1
SELECT count (*) cnt FROM table sempre retornará uma única linha. Não tenho certeza de como LIMIT adicionará qualquer benefício lá.
Chris Bednarski
@ChrisBednarski: Eu verifiquei a versão do oráculo da minha resposta em um banco de dados Oracle. Funciona muito bem e resolve o que eu pensava ser o problema de OP (0,05 s count(*)com rownum, 1 s sem o uso de rownum). Sim, SELECT count(*) cnt FROM tablesempre retornará 1 linha, mas com a condição LIMIT, retornará "500001" quando o tamanho da mesa for maior que 500000 e <tamanho> quando o tamanho da mesa for <= 500000.
Ritesh em
2
Sua consulta PostgreSQL é um absurdo completo. Sintaticamente e logicamente errado. Corrija ou remova-o.
Erwin Brandstetter,
@ErwinBrandstetter: Removido, não percebia que o PostgreSQL era tão diferente.
Ritesh
@allrite: sem dúvida, sua consulta Oracle funciona bem. LIMIT funciona de maneira diferente. Em um nível básico, ele limita o número de linhas retornadas ao cliente, não o número de linhas consultadas pelo mecanismo de banco de dados.
Chris Bednarski
0

Qual é a largura da coluna de texto?

Com um GROUP BY, não há muito o que fazer para evitar uma varredura de dados (pelo menos uma varredura de índice).

Eu recomendo:

  1. Se possível, altere o esquema para remover a duplicação de dados de texto. Desta forma, a contagem acontecerá em um campo estreito de chave estrangeira na tabela 'muitos'.

  2. Como alternativa, crie uma coluna gerada com um HASH do texto e, em seguida, GROUP BY com a coluna hash. Novamente, isso é para diminuir a carga de trabalho (faça uma varredura em um índice de coluna estreito)

Editar:

Sua pergunta original não corresponde exatamente à sua edição. Não tenho certeza se você está ciente de que COUNT, quando usado com um GROUP BY, retornará a contagem de itens por grupo e não a contagem de itens em toda a tabela.

Chris Bednarski
fonte
0

Você pode obter a contagem pela consulta abaixo (sem * ou quaisquer nomes de coluna).

select from table_name;
Super Nova
fonte
2
Isso não parece ser mais rápido do que count(*).
Ensolarado
-3

Para SQL Server (2005 ou superior), um método rápido e confiável é:

SELECT SUM (row_count)
FROM sys.dm_db_partition_stats
WHERE object_id=OBJECT_ID('MyTableName')   
AND (index_id=0 or index_id=1);

Detalhes sobre sys.dm_db_partition_stats são explicados no MSDN

A consulta adiciona linhas de todas as partes de uma tabela particionada (possivelmente).

index_id = 0 é uma tabela não ordenada (Heap) e index_id = 1 é uma tabela ordenada (índice clusterizado)

Métodos ainda mais rápidos (mas não confiáveis) são detalhados aqui.

DrKoch
fonte