For accountants & finance pros

Aggregate functions: SUM, AVG, COUNT & friends

Totals, averages and counts are the bread and butter of accounting work — and they're where SQL starts paying for itself. If you can write =SUM() in Excel, you can write this. Every example below runs against real cash-receipts data you can query yourself.

Snippets rewrite themselves for the engine you pick.

Everything here uses the payments and orders tables of the classicmodels sample database — 273 real payments from 98 customers. Paste any snippet straight into the SQL Editor or DBeaver and you'll get the same numbers shown.

On this page

What is an aggregate function?

Normally a query gives you back one row per record — ask for payments and you get every payment. An aggregate function collapses many rows into a single summarising number.

If you use Excel, you already know this idea. =SUM(B2:B274) takes 273 cells and gives you one total. SQL's SUM() does exactly the same job — it just works on a column of a table instead of a range of cells.

Without an aggregate — 273 rows

Every payment the company has ever received, one row each.


    
Result
customerNumberpaymentDateamount
1032004-10-196066.78
1122003-06-061676.14

273 rows returned — accurate, but you still have to add them up yourself.

With an aggregate — 1 row

The same 273 payments, added up by the database.


    
Result
total_cash_received
8853839.23

One row. One number. That is the whole idea of an aggregate.

The rule: an aggregate takes many rows in and gives one row out. Everything else on this page is a variation on that.

The five you'll actually use

There are only five aggregate functions worth memorising, and you already know all of them from spreadsheets:

SQLExcelWhat it doesIn accounting terms
COUNT(*)COUNTAHow many rows How many invoices / payments / transactions
SUM(x)SUMAdds the values Total cash received, total revenue, account balance
AVG(x)AVERAGEMean of the values Average invoice size, average days to pay
MIN(x)MINSmallest value Earliest date, smallest payment
MAX(x)MAXLargest value Latest date, largest exposure

Three of them at once, over the cash receipts.


    
Result
average_paymentsmallest_paymentlargest_payment
32431.653114615.45120166.58

Note the ugly precision on the average — we fix that next.

A one-line cash-receipts summary

You can ask for several aggregates in one statement, and wrap them in ROUND(x, 2) to get sensible money values instead of floating-point noise.

The kind of summary you would normally build by hand in Excel.


    
Result
paymentstotal_receivedaverage_paymentsmallestlargest
2738853839.2332431.65615.45120166.58

The whole cash-receipts picture, from one statement.

GROUP BY — subtotal by anything

An aggregate on its own gives you the grand total. Add GROUP BY and you get one total per group — the SQL equivalent of a pivot table or a subtotalled report.

By period (fiscal year)

Cash received per year — a revenue-by-period report.


    
Result
fiscal_yearpaymentscash_received
20031003250217.70
20041364313328.25
2005371290293.28

Three groups in, three rows out — one per year.

By customer (an AR subledger)

Group by the customer instead and the same data becomes a receivables summary — who has paid us the most.

Top 10 customers by cash paid.


    
The rule: whatever you put in GROUP BY becomes the "per" in your question. Total per year. Total per customer. Total per product line. Anything not aggregated must appear in the GROUP BY.

WHERE vs HAVING — the one everybody trips on

Both filter. The difference is when they run:

That's why you can't say WHERE SUM(amount) > 100000 — at the time WHERE runs, the sum doesn't exist yet.

Customers who have paid us more than $100,000 in total.


    

Both together

Use them in the same query and each does its own job: WHERE narrows to 2004 payments, then HAVING keeps only the big spenders within that year.

Customers who paid us over $100,000 during 2004 alone.


    

COUNT(*) vs COUNT(column) — the NULL trap

This one bites accountants in particular, because it silently changes your numbers.

An unshipped order has no ship date — it's NULL — so the two counts disagree. You can turn that into a useful figure:

How many orders exist, how many actually shipped, and the gap.


    
Result
all_ordershave_shippednot_yet_shipped
32631214

The 14-row gap IS the answer: 14 orders have no ship date.

Watch out: SUM() and AVG() ignore NULLs too. An average over a column with blanks divides by the number of non-blank values — which is usually what you want, but rarely what you expected. If NULL should count as zero, say so explicitly.

COUNT(DISTINCT …) — count the unique ones

273 payments arrived, but not from 273 different customers. DISTINCT counts each customer once:

Payments received vs. customers who actually paid.


    
Result
paymentspaying_customers
27398

273 payments from 98 customers — so the average customer paid us about 2.8 times.

Aggregating maths, not just columns

You can aggregate an expression, not only a plain column. This is how invoice maths works: extend each line (quantity × price), then sum the extensions.

Line count and invoice total for the 10 largest orders.


    

The same trick, grouped by product line, gives you a revenue breakdown:

Revenue by product line.


    
The rule: SUM(quantityOrdered * priceEach) multiplies first, row by row, then adds the results — exactly like extending an invoice in a spreadsheet and totalling the column. SUM(a) * SUM(b) is a different (and almost always wrong) number.

Where to next