Myten om COUNT(*)
Förord
En vanligt förekommande 'myt' i SQL Server världen är den om prestandaskillnader mellan COUNT(*) och COUNT(kolumnnamn), där kolumnnamn är en kolumn i den tabell man vill räkna rader i. Det är ett av de vanligaste tipsen om prestanda man ser på forum och i maillistor. Det är dock inte alltid ett korrekt tips, och i de flesta fall är det rentav fel. Aven om det finns situationer när man kan (eller t o m ska) använda COUNT(kolumnnamn) ska man definitivt inte alltid göra det. Den felaktiga idén grundas troligen på felaktiga uppfattningar om hur SQL Server hanterar data internt. Artikeln finns även publicerad på Hedgate.NETInnehåll
»»
»
»
Beskrivning av COUNT()
För det första måste man vara medveten om att det är skillnad mellan de olika varianterna av COUNT(), och vad denna skillnad är. Den fullständiga syntaxen för COUNT() är följande:
COUNT ( { [ ALL | DISTINCT ] expression ] | * } )
expression är vilket uttryck som helst (förutom uniqueidentifer, text, image eller ntext, samt aggregerande funktioner eller subqueries), och brukar oftast vara en kolumn i tabellen. ALL är standard och används om inget anges (COUNT(kolumnnamn) och COUNT(ALL kolumnnamn) är alltså likvärdiga).
COUNT(*) returnerar antalet rader i tabellen, medan COUNT(kolumnnamn) returnerar antal rader där värdet i kolumnnamn (eller resultatet av det uttryck som skrivs om det inte är en enskild kolumn) inte är NULL. Om man använder COUNT(DISTINCT kolumnnamn) räknas dublettrader dessutom endast en gång. COUNT() kan alltså returnera olika värde beroende på hur man kör den.
Myten och fakta
Många tror att COUNT(kolumnnamn) skulle vara snabbare än COUNT(*), därför att COUNT(*) skulle läsa in alla kolumner på varje rad (precis som SELECT * FROM tabellnamn) för att räkna antalet rader, medan COUNT(kolumnnamn) endast skulle behöva kontrollera den angivna kolumnen. Detta stämmer naturligtvis inte, av flera anledningar. För det första så kan SQL Server inte läsa in endast innehållet i en enskild kolumn. SQL Server lagrar raderna med datan på 8Kb stora datasidor på hårddisken. Sidorna innehåller en eller flera rader (en rad kan innehålla upp till 8060 bytes, med vissa undantag), och dessa sidor läses upp till minnet när SQL Server behöver datan på en eller flera rader på sidan. För att kontrollera en kolumn måste alltså en hel sida (med en eller flera rader) läsas in för att kunna behandlas. Om sidorna läses från disk eller redan finns i minnet gör egentligen ingen skillnad för den här diskutionen, förutom att prestandaskillnaderna blir mycket mindre tidsmässigt sett, även om den proportionerliga skillnaden är den samma.
För att undvika att behöva läsa in hela rader när man egentligen bara är intresserad av en kolumn kommer SQL Server att utnyttja ett index om det finns något tillgängligt. Ett index lagras på liknande sätt som datan, på sidor som är 8Kb stora. Men eftersom ett index troligen är betydligt smalare än en datarad (eftersom indexet endast består av en eller flera av de kolumner som dataraden innehåller) får man plats med fler (normalt sett betydligt fler) rader per sida i ett index än i själva datan. Alltså behöver SQL Server inte läsa in lika många sidor för att kontrollera raderna i ett index som den måste med själva datan.
Dock gäller detta naturligtvis inte bara när man kör COUNT(kolumnnamn), utan även COUNT(*) kommer att använda indexet för att räkna raderna. I vissa fall kanske det dessutom inte finns ett index som täcker den angivna kolumnen, men tabellen har ett (eller flera) annat index. I detta fall skulle COUNT(*) gå mycket snabbare än COUNT(kolumnnamn), eftersom det i det första fallet skulle räcka att räkna antalet rader i vilket index som helst, medan COUNT(kolumnnamn) skulle behöva hämta datasidorna för att kunna kontrollera värdet i kolumnen.
För att testa detta så kör följande i SQL Server Query Analyzer (ställ in den på att visa resultatet i textform om den inte redan gör det, Ctrl-T):
USE Northwind
GO
SET STATISTICS IO ON
SELECT COUNT(*) FROM Orders
SELECT COUNT(CustomerId) FROM Orders
SELECT * FROM Orders
SET STATISTICS IO OFF
Kommandot SET STATISTICS IO ON gör att man efter resultatet får information om hur mycket IO SQL Server fått utföra för att köra frågan, och det kan vara bra att använda om man vill jämföra två frågor mot varandra. Det som är intressant här är logical reads och/eller physical reads (logical reads visar hur många sidor som lästes in från minnet och physical reads hur ånga som lästes in från disk). På min dator blir resultatet av COUNT() (bägge varianterna) 830 rader, vilket ni också bör ha om ni inte ändrat i er Northwinddatabas. Notera dock att för den första frågan krävs det 3 logical reads (kör koden några gånger i rad om ni får physical reads för att läsa in allt i minnet), medan det för den andra frågan krävs 21 logical reads. Notera även att det är lika många logical reads som det krävs för den tredje frågan (som alltså inte gör någon count alls utan helt enkelt hämtar hela datan).
Vilken ska användas?
Som jag har visat här så finns det inga prestandaproblem med COUNT(*), utan snarare tvärtom, i vissa fall kan man få dålig prestanda med COUNT(kolumnnamn). Normalt sett kommer man väl dock inte få prestandaproblem med COUNT(kolumnnamn), eftersom man antagligen har ett vettigt index på kolumnen. Värre är kanske att man kan få ett annat resultat än man väntar sig. Tänk exempelvis om man har en funktion som kör COUNT(kolumnnamn) på en tabell där kolumnnamn inte tillåter NULL-värden för att räkna antal rader i tabellen. I ett senare skede ändrar någon kolumndefinitionen till att tillåta NULL-värden, och plötsligt visar din funktion inte längre antalet rader i tabellen utan istället antal rader där kolumnnamn inte är NULL. Normalt sett finns det alltså ingen anledning att inte köra COUNT(*). Som jag nämnde i början finns det dock situationer när man vill (eller snarare ska) köra COUNT(kolumnnamn), där ett självklart exempel är om man endast är intresserad av de rader där värdet i kolumnen ej är NULL. Här kan man annars få ett oväntat resultat om man kör COUNT(*) på tabellen och räknar alla rader. Ett klassiskt sådant problem är när man använder COUNT() tillsammans med någon annan aggregerande funktion. Ponera att vi har en tabell med någon slags insamlade värden lagrade, där värdet dessutom är NULL i på vissa rader. Nu vill vi ha ett medelvärde av dessa värden, men vi kan inte använda funktionen AVG() pga att vi vill utesluta vissa värden (de kanske samlades in för tidigt eller något). Om man då jämför nedanstående SQL-frågor ser man förhoppningsvis direkt ett stort problem med den första frågan:
SELECT SUM(kolumn) / COUNT(*) FROM tabell WHERE något_uttryck_som_utesluter_vissa_rader
SELECT SUM(kolumn) / COUNT(kolumn) FROM tabell WHERE något_uttryck_som_utesluter_vissa_rader
Frågorna kommer att ge olika resultat, pga att SUM() inte räknar NULL-värden (vare sig som NULL eller 0). Om summan då blir exempelvis 1500, och antalet rader (exklusive de som tas bort av WHERE-klausulen) är 150 varav 50 har NULL i kolumnen, så blir resultatet av den första frågan 10 (1500/150), medan den andra frågan ger resultatet 15 (1500/100). Detta är faktiskt ett vanligt fel jag ibland ser i mitt arbete som konsult inom databasteknik, och beror oftast på att den som skrivit frågan inte var medveten om hur NULL hanteras i de olika aggregerande funktionerna (SUM() och COUNT() i detta exemplet).
/Christoffer Hedgate
0 Kommentarer