Já escutei algumas pessoas afirmarem, inclusive DBAs, que usar COUNT(1) é mais performático que COUNT(*). Além disso, também ouço programadores dizerem que fazer um COUNT numa tabela é sempre uma operação “tranquila”, ou em outras palavras, sempre “rápida”.
Neste artigo vou demonstrar que, para o SQL Server, COUNT(1) é puro mito. Também vou explicar como o COUNT funciona internamente, de modo a comprovar que fazer um COUNT numa tabela nem sempre é uma operação “tranquila”, pelo contrário, pode demorar e gerar overhead no sistema (servidor de banco, tráfego na rede, etc).
Vamos criar uma tabela para os nossos exemplos e, em seguida, comparar planos de execução. Mãos à obra:
-- Cria uma tabela de vendas sem primary key CREATE TABLE dbo.Sales ( SalesId int NOT NULL, SalesDate datetime NOT NULL, CustomerId int NOT NULL, TerritoryId int NULL, -- note que esta é uma coluna nulável TotalDue money NOT NULL, ) go -- Insere 31465 linhas INSERT INTO dbo.Sales SELECT h.SalesOrderID, h.OrderDate, h.CustomerID, h.TerritoryID, h.TotalDue FROM AdventureWorks.Sales.SalesOrderHeader h GO -- Estou habilitando o Actual Execution Plan (CTRL+M) -- e executando as queries abaixo SELECT COUNT(*) FROM dbo.Sales; SELECT COUNT(1) FROM dbo.Sales;
Vejamos os planos gráficos das queries:
Os planos são estritamente iguais. Para contar as linhas o SQL Server varreu a tabela inteira (Table Scan) e, em seguida, computou a agregação referente ao COUNT (Stream Aggregate). Checando as propriedades do operador Stream Aggregate, referente à query que faz o COUNT(1), constatamos que COUNT(1) é igual a COUNT(*).
Um caso curioso
A tabela que criamos não possui nenhum índice; sobretudo, a tabela é uma heap (sem índice clustered). Veja o custo comparativo entre SELECT COUNT(*) e SELECT * FROM.
-- Limpando o data cache e o plan cache CHECKPOINT DBCC dropcleanbuffers DBCC freeproccache GO -- Comparando custo COUNT(*) vs SELECT * FROM SELECT COUNT(*) AS Qt FROM dbo.Sales; SELECT * FROM dbo.Sales;
Incrível! Neste exemplo o SELECT COUNT(*) foi mais caro que o SELECT * FROM.
Vejamos o que acontece quando criamos um índice nonclustered sobre uma coluna obrigatória e fazemos novamente um COUNT.
-- Cria um índice nonclustered sobre uma coluna obrigatória CREATE nonclustered INDEX ak_sales_customer ON dbo.Sales (CustomerId); -- Fazendo o COUNT novamente SELECT COUNT(*) FROM dbo.Sales
O otimizador “inteligentemente” resolveu utilizar o único índice existente para fazer a contagem. O motivo?! Ora, é muito mais rápido e barato varrer uma estrutura de índice do que varrer páginas de dados (tabela). No exemplo dado, o otimizador precisava responder a uma simples questão: qual a quantidade de linhas para a query disparada? Para atender a essa requisição é suficiente contar o número de valores existentes no índice, uma vez que essa quantidade corresponde à quantidade de linhas na tabela.
Se compararmos novamente os custos SELECT COUNT(*) versus SELECT * FROM, veremos que o COUNT(*) passou a ser mais rápido e barato:
Neste ponto depreendemos uma importante lição: o COUNT, assim como outras agregações, pode beneficiar-se, e muito bem, de índices, acelerando as operações. Além disso, dependendo da query e dos índices envolvidos, vale realçar que o COUNT pode ser executado de maneira bem diferente do que um SELECT * FROM.
Vejamos agora um exemplo onde, mesmo com índices, o COUNT(*) acaba sendo mais custoso que o SELECT * FROM.
-- criando um índice clustered CREATE UNIQUE clustered INDEX AK_sales_id ON dbo.sales (salesid); -- Comparando novamente os custos SELECT COUNT(*) FROM dbo.Sales s WHERE s.SalesId < 50000; SELECT * FROM dbo.Sales s WHERE s.SalesId < 50000;
Novamente o custo do COUNT(*) foi mais caro.
Agora vamos analisar um COUNT(*) contra uma tabela que tem mais de 12 milhões de registros. Vou usar o banco ContosoRetailDW. Você pode baixa-lo gratuitamente neste link: http://www.microsoft.com/en-us/download/details.aspx?id=18279.
USE ContosoRetailDW GO SET statistics TIME ON SELECT COUNT(*) FROM dbo.FactOnlineSales SET statistics TIME off
Vejamos o resultado do statistics time:
SQL Server Execution Times: CPU time = 3183 ms, elapsed time = 7484 ms.
A contagem das linhas da tabela demorou, em meu notebook, mais de 7 segundos (estou usando um INTEL i3 com 4GB RAM). Isso comprova que COUNT(*) não é tão tranquilo assim. Tudo dependerá do tamanho das tabelas envolvidas, bem como se existem ou não índices nonclustered.
Uma alternativa ao COUNT(*)
Uma boa alternativa é consultarmos os metadados ao invés de usarmos o COUNT(*). Segue abaixo, duas alternativas que utilizo com frequência quando preciso levantar a quantidade de registros.
SELECT i.rows FROM sys.sysindexes i WHERE i.id = object_id('FactOnlineSales') AND i.indid <= 1; SELECT SUM(row_count) Qt FROM sys.dm_db_partition_stats p WHERE p.object_id = object_id('dimcurrency') AND p.index_id <= 1;
Conclusão
Quando trabalhamos com tabelas largas é necessário sermos mais cuidados com as nossas queries. Coisas simples de serem escritas, como o COUNT(*), podem ter performance inesperadas quando não temos um índice que propicie um INDEX SEEK ou INDEX SCAN. Por isso, sempre analise o plano de execução das suas queries antes de colocá-las em produção.