Utilizzo dei NULL: COUNT(*) e COUNT(campo).

Sull'utilizzo dei NULL - come vi sarete accorti - c'è MOLTA confusione. Per quanto mi riguarda cerco di usarli con MOLTA cautela per una questione soprattutto di consistenza dei dati. Detto questo però c'è anche una implicazione legata alle performance che credo sia sconosciuta ai più.

In molti credono che, per contare quante righe ci sono in una tabella, utilizzare COUNT(*) o COUNT(campo) sia identico ed alcuni sono anche conviti che la seconda forma COUNT(campo) sia più performante della prima COUNT(*).

Nulla di più falso su entrambi i fronti Smile

Prima cosa da fissarsi in testa: COUNT(*) conta quante righe ci sono nella tabella, COUNT(campo) conta quanti valori non nulli ci sono per quel campo:

CREATE TABLE dbo.CountTest
(
    campo1 INT NOT NULL,
    campo2 INT NULL
)
GO

INSERT INTO dbo.CountTest VALUES (1,1)
INSERT INTO dbo.CountTest VALUES (2,NULL)
INSERT INTO dbo.CountTest VALUES (1,1)
GO

SELECT COUNT(*), COUNT(campo1), COUNT(campo2) FROM dbo.CountTest
go

Il risultato quindi è:

----------- ----------- -----------
3           3           2

 

Ci sono quindi delle differenze funzionali non indifferenti. Noi siamo cmq interessati a contare tutte le righe della tabella, quindi procediamo con questo obiettivo in mente.

E' piuttosto semplice intuire che se COUNT(*) conta il numero di righe totali, SQL Server cercherà di utilizzare l'indice più piccolo che ha a disposizione per rispondere ad una query del tipo

SELECT COUNT(*) FROM Tabella

e tipicamente sceglierà - se presente - il più piccolo indice non-cluster che trova.

Utilizziamo questo codice per generare un tabella da 100.000 righe:

IF (OBJECT_ID('dbo.fn_Nums') IS NOT NULL) DROP FUNCTION dbo.fn_Nums;
go

create function dbo.fn_Nums(@m as bigint) returns table
as
return
with
t0 as (select n = 1 union all select n = 1),
t1 as (select n = 1 from t0 as a, t0 as b),
t2 as (select n = 1 from t1 as a, t1 as b),
t3 as (select n = 1 from t2 as a, t2 as b),
t4 as (select n = 1 from t3 as a, t3 as b),
t5 as (select n = 1 from t4 as a, t4 as b),
result as (select row_number() over (order by n) as n from t5)
select n from result where n < @m+1
GO

IF (OBJECT_ID('dbo.CountTest2') IS NOT NULL) DROP TABLE dbo.ThinTable
go

create table dbo.CountTest2
(
    id int not null primary KEY NONCLUSTERED, 
    data_inizio smalldatetime null, 
    data_fine smalldatetime NOT null, 
    codice_parlante char(16) null, 
    codice_numerico smallint null, 
    regione tinyint null
);
go

insert into
    dbo.CountTest2
select
    id = n,
    data_inizio = dateadd(day, n / (1000000 / (10 * 365.25)), getdate()) + ((checksum(newid()) % (24 * 60)) / 10000.0) - CASE WHEN n % 10 = 0 THEN 1 + ABS(CHECKSUM(NEWID())) % 30 ELSE 0 END,
    data_fine = dateadd(day, n / (1000000 / (10 * 365.25)), getdate()) + ((checksum(newid()) % (24 * 60)) / 10000.0) - CASE WHEN n % 10 = 0 THEN 1 + ABS(CHECKSUM(NEWID())) % 20 ELSE 0 END,
    codice_parlante = char(65 + abs(checksum(newid()) % 25)) + RIGHT('000000000000000000000000' + CAST(1 + ABS(CHECKSUM(NEWID())) % 999999999999999 AS VARCHAR(15)), 15),
    codice_numerico = n % 10000,
    regione = n % 200
from
    dbo.fn_Nums(100000)
order by
    checksum(newid());
GO

Come potete notare l'indice sulla Primary Key è non-cluster, la tabella è priva di indice cluster e la colonna data_fine è definita con il constraint NOT NULL (cosi come, ovviamente, la colonna id che è la PK).

Guardando il piano di esecuzione per una qualsiasi di queste query:

SELECT COUNT(*) FROM dbo.CountTest2
SELECT COUNT(id) FROM dbo.CountTest2
SELECT COUNT(data_fine) FROM dbo.CountTest2

si vede un bel INDEX SCAN, segno che SQL Server sta utilizzando l'indice (e non la tabella) per contare le righe:

IndexScan

per un totale di 274 operazioni di I/O.

Se però proviamo a fare un COUNT(data_inizio), ossia lavoriamo su una colonna nullabile si ottiene questo piano di esecuzione:

TableScan

dove non viene utilizzato l'indice, per un totale di 496 operazioni di I/O, ossia l'81% in più rispetto a prima!!!!

E questo perchè la mia tabella è piccola (ha poche colonne) ed usa tipi di dati piccoli, altrimenti le prestazioni sarebbero state ancora peggiori, e peggiori saranno anche in proporzione alla quantità di dati presenti nella tabella!

Attenzione quindi:

COUNT(*) e COUNT(campo) sono equivalenti in termini di performance, a meno che non si utilizzi una colonna nullable, in questo caso le performance di COUNT(campo) - per contare quante righe ci sono nella tabella - sono invece peggiori!!!!!

Published venerdì 28 marzo 2008 18.37 by dmauri

Comments

No Comments