Data

5 jun, 2012

Mito T-SQL: COUNT(1) é mais rápido que COUNT(*)

Publicidade

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.