Aggregates: COUNT, SUM, AVG, MIN and MAX
Turn many rows into one answer: the five everyday aggregate functions, what COUNT(*) and COUNT(column) really count, how NULLs are skipped, the integer AVG trap, and STRING_AGG for building lists.
SELECT COUNT(*) AS Products, SUM(Stock) AS UnitsInStock, MIN(Price) AS Cheapest, MAX(Price) AS Dearest, AVG(Price) AS AveragePriceFROM dbo.Products;Products UnitsInStock Cheapest Dearest AveragePrice
-------- ------------ -------- ------- ------------
12 1422 4.10 89.00 22.254166| Function | Returns | Works on |
|---|---|---|
COUNT(*) | ||
COUNT(col) | ||
SUM(col) | ||
AVG(col) | ||
MIN(col) / MAX(col) |
What COUNT counts
COUNT(*) counts rows. COUNT(column) counts the rows where that column has a value, so it skips NULLs. COUNT(DISTINCT column) counts the different values:
SELECT COUNT(*) AS Customers, COUNT(City) AS WithCity, COUNT(DISTINCT City) AS Cities, COUNT(Email) AS WithEmail, COUNT(Phone) AS WithPhoneFROM dbo.Customers;Warning: Null value is eliminated by an aggregate or other SET operation.
Customers WithCity Cities WithEmail WithPhone
--------- -------- ------ --------- ---------
8 7 4 6 5Every aggregate except COUNT(*) skips NULLs
CREATE TABLE #Charges (OrderId int, Charge decimal(6,2) NULL);INSERT INTO #Charges VALUES (1, 5.00), (2, 10.00), (3, NULL), (4, 9.00); SELECT AVG(Charge) AS AvgKnown, SUM(Charge) / COUNT(*) AS AvgOverAll, AVG(ISNULL(Charge, 0)) AS AvgNullAsZeroFROM #Charges;Warning: Null value is eliminated by an aggregate or other SET operation. AvgKnown AvgOverAll AvgNullAsZero -------- ---------- ------------- 8.000000 6.000000 6.000000
AVG(Charge) divides 24 by the 3 known charges. Dividing by all 4 rows, or treating the missing charge as 0, gives 6. Decide which question you are asking: “the average of the charges we know” or “the average per order”.
The integer AVG trap
int, so the average is an int too, and the fraction is thrown away:
SELECT SUM(Stock) AS Total, COUNT(*) AS Products, AVG(Stock) AS IntAverage, AVG(CAST(Stock AS decimal(9,2))) AS RealAverageFROM dbo.Products;Total Products IntAverage RealAverage ----- -------- ---------- ----------- 1422 12 118 118.500000
Aggregates over a filter or a join
SELECT COUNT(*) AS Lines, COUNT(DISTINCT o.OrderId) AS Orders, SUM(oi.Quantity * oi.UnitPrice) AS RevenueFROM dbo.Orders AS oJOIN dbo.OrderItems AS oi ON oi.OrderId = o.OrderIdWHERE o.Status = 'Shipped';Lines Orders Revenue ----- ------ ------- 14 7 1199.15
SELECT MIN(OrderDate) AS FirstOrder, MAX(OrderDate) AS LastOrder, DATEDIFF(day, MIN(OrderDate), MAX(OrderDate)) AS DaysTrading, MIN(Status) AS FirstStatusAlphabeticallyFROM dbo.Orders;FirstOrder LastOrder DaysTrading FirstStatusAlphabetically ---------- ---------- ----------- ------------------------- 2024-02-03 2024-06-12 130 Cancelled
When there are no rows
SELECT COUNT(*) AS Products, SUM(Stock) AS Units, MAX(Price) AS DearestFROM dbo.ProductsWHERE CategoryId = 7;Products Units Dearest
-------- ----- -------
0 NULL NULLBuilding a list with STRING_AGG
STRING_AGG (SQL Server 2017+) joins the values of many rows into one string, with a separator. WITHIN GROUP (ORDER BY …) sets the order of the list:
SELECT STRING_AGG(Name, N', ') WITHIN GROUP (ORDER BY Name) AS OutOfStockFROM dbo.ProductsWHERE Stock = 0;OutOfStock -------------------------------- Safety goggles, Wall plugs (100)
Aggregates and plain columns do not mix
SELECT Name, MAX(Price) AS DearestFROM dbo.Products;Msg 8120, Level 16, State 1, Line 1 Column 'dbo.Products.Name' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.
Show a hint
Show the solution
SELECT COUNT(*) AS Lines, SUM(oi.Quantity) AS Items, SUM(oi.Quantity * oi.UnitPrice) AS OrderValue, STRING_AGG(p.Name, N', ') WITHIN GROUP (ORDER BY p.Name) AS ProductsFROM dbo.OrderItems AS oiJOIN dbo.Products AS p ON p.ProductId = oi.ProductIdWHERE oi.OrderId = 4;Lines Items OrderValue Products
----- ----- ---------- -------------------------------------
2 46 524.00 Plywood sheet 18 mm, Timber 2x4 2.4 mRemember
An aggregate turns many rows into one value: COUNT, SUM, AVG, MIN, MAX. COUNT(*) counts rows; COUNT(col) skips NULLs; COUNT(DISTINCT col) counts different values. SUM, AVG, MIN and MAX ignore NULLs, which changes what AVG means. AVG of an integer column returns an integer: convert to decimal first. Over zero rows, COUNT returns 0 and the others return NULL. STRING_AGG joins values into a list; WITHIN GROUP sets the order. A plain column cannot sit next to an aggregate without GROUP BY (Msg 8120).