Learn with DataTug · English

SQL GROUP BY and HAVING: Aggregation Made Simple

Turn invoice rows into useful summaries with GROUP BY, COUNT, SUM, AVG and HAVING. Choose the right grouping keys, handle NULLs and practice on tested Chinook SQLite examples.

By DataTug · AI-assisted writing · Published . SQL examples are checked against the downloadable Chinook SQLite database.

First decide what one result row means

A sales report starts with a question: do you need one row per invoice, per customer, or per billing country? This is the output grain. GROUP BY collects eligible input rows with matching keys and produces one summary row per group.

Our source contains 412 invoices belonging to 59 customers. A customer summary has 59 rows; each row describes all invoices for one customer. A country summary groups different customers together. Choose the grain before adding totals.

Prerequisite: recognize SELECT, FROM and WHERE. No earlier lesson is required. If your report combines tables, read JOINs vs Subqueries and check row multiplicity before aggregating. You can practice with checked exercises after the examples.

The Chinook sample database

These examples use the complete Chinook music-store database available in the DataTug demo: 59 customers and 412 invoices, with every original table and value retained. All 59 customers have invoices. Queries that focus on one customer or qualifying invoices show that condition explicitly in the SQL.

The relationship is Customer.CustomerId → Invoice.CustomerId: one customer can have many invoices; each invoice belongs to one customer. InvoiceId is the invoice’s unique key. InvoiceDate is ISO-sortable text; Total is a numeric amount in the dataset’s units. This source uses dates from 2021–2025; other Chinook releases differ. Invoice IDs are not necessarily chronological.

Results are shown as a preview for clarity. Tables with more than 10 records initially show the first 10. Choose Show all to reveal every matching record, or Run to execute the exact displayed SQL in the DataTug editor against the same Chinook database. No query is saved to the project. The preview does not add a WHERE or LIMIT clause. Synthetic examples define their own rows with VALUES in the displayed query.

Download the complete Chinook SQLite database and open it with SQLite 3.25.0 or later, then run any query below. No account or network database is required. Query downloads contain every displayed example and exercise solution. Frozen result tables are checked against this database; each code header states its minimum SQLite version.

Chinook upstream and schema · Chinook licence · Source attribution. SQLite uses binary floating-point for many decimal operations; examples round money for display. Production finance needs an explicit exact-decimal or integer-minor-unit policy.

COUNT, SUM and AVG summarize the input

One summary for all invoices

What are the invoice count, revenue and average invoice amount?

SQLite 3+ · Chinook
SELECT COUNT(*) AS InvoiceCount, ROUND(SUM(Total), 2) AS Revenue,
       ROUND(AVG(Total), 2) AS AverageInvoice
FROM Invoice;
One summary for all invoices: expected result · 1 record
InvoiceCountRevenueAverageInvoice
4122328.65.65

COUNT(*) counts invoices. SUM(Total) adds their amounts. AVG(Total) calculates the average invoice amount. Without GROUP BY, this aggregate query produces one summary for the entire eligible input. ROUND controls the displayed precision; it does not change which rows enter the calculation.

GROUP BY output

Find total spending per customer.

SQLite 3+ · Chinook
SELECT CustomerId, ROUND(SUM(Total), 2) AS CustomerTotal
FROM Invoice GROUP BY CustomerId ORDER BY CustomerId;
Show all 59 recordsShow first 10 records
GROUP BY output: expected result · Showing 10 of 59 records for clarity59 records
CustomerIdCustomerTotal
139.62
237.62
339.62
439.62
540.62
649.62
742.62
837.62
937.62
1037.62
1137.62
1237.62
1337.62
1437.62
1538.62
1637.62
1739.62
1837.62
1938.62
2039.62
2137.62
2239.62
2337.62
2443.62
2542.62
2647.62
2737.62
2843.62
2937.62
3037.62
3137.62
3237.62
3337.62
3439.62
3537.62
3637.62
3743.62
3837.62
3938.62
4038.62
4137.62
4239.62
4340.62
4441.62
4545.62
4645.62
4737.62
4840.62
4937.62
5037.62
5138.62
5237.62
5337.62
5437.62
5537.62
5637.62
5746.62
5838.62
5936.64

412 invoice rows become 59 customer summary rows. Every customer appears because all 59 have invoices.

Adding GROUP BY CustomerId creates a separate sum per customer. Customer 1 totals 39.62 across seven invoices; their summary is one row. GROUP BY does not promise a display order, so every multirow example specifies ORDER BY.

AVG(Total) weights each non-NULL invoice equally. Averaging customer averages instead would weight customers equally, which can produce a different result when invoice counts differ. Define the denominator your question needs.

Grouping keys define which rows belong together

One summary per billing country

How many invoices and how much revenue belong to each billing country?

SQLite 3+ · Chinook
SELECT BillingCountry, COUNT(*) AS InvoiceCount,
       ROUND(SUM(Total), 2) AS Revenue, ROUND(AVG(Total), 2) AS AverageInvoice
FROM Invoice
GROUP BY BillingCountry
ORDER BY BillingCountry;
Show all 24 recordsShow first 10 records
One summary per billing country: expected result · Showing 10 of 24 records for clarity24 records
BillingCountryInvoiceCountRevenueAverageInvoice
Argentina737.625.37
Australia737.625.37
Austria742.626.09
Belgium737.625.37
Brazil35190.15.43
Canada56303.965.43
Chile746.626.66
Czech Republic1490.246.45
Denmark737.625.37
Finland741.625.95
France35195.15.57
Germany28156.485.59
Hungary745.626.52
India1375.265.79
Ireland745.626.52
Italy737.625.37
Netherlands740.625.8
Norway739.625.66
Poland737.625.37
Portugal1477.245.52
Spain737.625.37
Sweden738.625.52
USA91523.065.75
United Kingdom21112.865.37

BillingCountry answers where invoices were billed, rather than the customer's current profile country. Use the field that matches your business question. Select each grouping key or an aggregate; an ungrouped invoice ID beside a country total cannot identify all contributing invoices.

One summary per country and city

How does a second grouping key change the output grain?

SQLite 3+ · Chinook
SELECT BillingCountry, BillingCity, COUNT(*) AS InvoiceCount,
       ROUND(SUM(Total), 2) AS Revenue
FROM Invoice
GROUP BY BillingCountry, BillingCity
ORDER BY BillingCountry, BillingCity;
Show all 53 recordsShow first 10 records
One summary per country and city: expected result · Showing 10 of 53 records for clarity53 records
BillingCountryBillingCityInvoiceCountRevenue
ArgentinaBuenos Aires737.62
AustraliaSidney737.62
AustriaVienne742.62
BelgiumBrussels737.62
BrazilBrasília737.62
BrazilRio de Janeiro737.62
BrazilSão José dos Campos739.62
BrazilSão Paulo1475.24
CanadaEdmonton737.62
CanadaHalifax737.62
CanadaMontréal739.62
CanadaOttawa737.62
CanadaToronto737.62
CanadaVancouver738.62
CanadaWinnipeg737.62
CanadaYellowknife737.62
ChileSantiago746.62
Czech RepublicPrague1490.24
DenmarkCopenhagen737.62
FinlandHelsinki741.62
FranceBordeaux739.62
FranceDijon740.62
FranceLyon737.62
FranceParis1477.24
GermanyBerlin1475.24
GermanyFrankfurt743.62
GermanyStuttgart737.62
HungaryBudapest745.62
IndiaBangalore636.64
IndiaDelhi738.62
IrelandDublin745.62
ItalyRome737.62
NetherlandsAmsterdam740.62
NorwayOslo739.62
PolandWarsaw737.62
PortugalLisbon739.62
PortugalPorto737.62
SpainMadrid737.62
SwedenStockholm738.62
USABoston737.62
USAChicago743.62
USACupertino738.62
USAFort Worth747.62
USAMadison742.62
USAMountain View1477.24
USANew York737.62
USAOrlando739.62
USARedmond739.62
USAReno737.62
USASalt Lake City743.62
USATucson737.62
United KingdomEdinburgh 737.62
United KingdomLondon1475.24

Two keys define one group for each distinct country/city pair. Including the country avoids accidentally combining cities with the same name in different countries. Adding a key makes the grain more detailed; removing one combines groups. For real customer reporting, use a stable CustomerId rather than assuming names are unique.

Grouping depends on the engine's equality and collation rules. Different spellings and letter case can form different groups. Normalize deliberately when the report needs that behavior; do not silently merge business identities.

HAVING keeps groups that meet a condition

Keep customers whose total exceeds 45

Which customers spent more than 45 across all invoices?

SQLite 3+ · Chinook
SELECT CustomerId, COUNT(*) AS InvoiceCount, ROUND(SUM(Total), 2) AS CustomerTotal
FROM Invoice
GROUP BY CustomerId
HAVING SUM(Total) > 45
ORDER BY CustomerId;
Keep customers whose total exceeds 45: expected result · 5 records
CustomerIdInvoiceCountCustomerTotal
6749.62
26747.62
45745.62
46745.62
57746.62

Each customer gets a total, then HAVING keeps only totals greater than 45. The result has five customers. This condition belongs on the aggregate at this SELECT level: WHERE cannot evaluate that same-level SUM before its group exists.

For a fuller comparison—including cases where WHERE and HAVING return identical rows and what that means for cost—continue to WHERE vs HAVING. Repeating SUM(Total) in HAVING also avoids relying on engine-specific alias support.

WHERE chooses the population; HAVING chooses the summaries

Combine WHERE and HAVING

Which customers spent more than 10 during 2025?

SQLite 3+ · Chinook
SELECT CustomerId, COUNT(*) AS InvoiceCount, ROUND(SUM(Total), 2) AS CustomerTotal
FROM Invoice
WHERE InvoiceDate >= '2025-01-01' AND InvoiceDate < '2026-01-01'
GROUP BY CustomerId
HAVING SUM(Total) > 10
ORDER BY CustomerId;
Show all 18 recordsShow first 10 records
Combine WHERE and HAVING: expected result · Showing 10 of 18 records for clarity18 records
CustomerIdInvoiceCountCustomerTotal
6227.84
10215.84
12311.88
14215.84
18222.77
27215.84
29311.88
31215.84
33311.88
35324.75
39222.77
44215.84
48215.84
50311.88
52215.84
54311.88
56324.75
58311.89

WHERE first limits input to invoices in 2025. GROUP BY forms customer summaries from that input. HAVING keeps the 18 customers with more than 10 in eligible spending. Their totals mean 2025 spending, not all-time spending.

The half-open date interval includes the start of 2025 and excludes the start of 2026. Comparing the unrounded SUM with the threshold preserves the stated condition; rounding is only for display.

Indexes and selective row predicates can reduce work before grouping. GROUP BY may still require sorting or temporary state. Inspect your engine's plan and measure representative data before claiming that a particular spelling is faster.

COUNT, NULLs and missing groups

Count rows versus known values

How many customers have a company value?

SQLite 3+ · Chinook
SELECT COUNT(*) AS CustomerCount, COUNT(Company) AS KnownCompanies,
       COUNT(*) - COUNT(Company) AS MissingCompanies
FROM Customer;
Count rows versus known values: expected result · 1 record
CustomerCountKnownCompaniesMissingCompanies
591049

COUNT(*) counts all 59 customer rows. COUNT(Company) counts only non-NULL company values. An empty string is still a non-NULL value; decide whether your data model treats it as missing.

SUM and AVG ignore NULL inputs. If every value in a group is NULL, SUM and AVG return NULL, while COUNT(*) still counts its rows. A comparison against NULL is unknown, so it does not pass HAVING. SQLite groups NULL keys together; NULL grouping is not the same as the expression NULL = NULL being true.

An aggregate query without GROUP BY still returns one row on empty input: COUNT(*) is zero and SUM/AVG are NULL. A grouped query with no eligible rows returns no groups. It does not invent customers or countries with zero invoices.

Intermediate solution

Count invoices over 20 for every customer, including zero.

SQLite 3+ · Chinook
SELECT c.CustomerId, COUNT(i.InvoiceId) AS InvoiceCount
FROM Customer AS c
LEFT JOIN Invoice AS i ON i.CustomerId = c.CustomerId AND i.Total > 20
GROUP BY c.CustomerId
ORDER BY c.CustomerId;
Show all 59 recordsShow first 10 records
Intermediate solution: expected result · Showing 10 of 59 records for clarity59 records
CustomerIdInvoiceCount
10
20
30
40
50
61
70
80
90
100
110
120
130
140
150
160
170
180
190
200
210
220
230
240
250
261
270
280
290
300
310
320
330
340
350
360
370
380
390
400
410
420
430
440
451
461
470
480
490
500
510
520
530
540
550
560
570
580
590

Only four customers have a qualifying invoice. COUNT(i.InvoiceId) correctly returns zero for each null-extended row; COUNT(*) would incorrectly return one for the other 55 customers.

This existing lesson query starts with Customer and LEFT JOINs invoices above 20 to retain customers with no matching invoice. COUNT(i.InvoiceId) counts matches; COUNT(*) would also count the preserved customer row and give at least one. The invoice condition stays in ON so unmatched customers survive. See JOIN exercises for that distinction.

Joining invoices to invoice lines can repeat invoice amounts. Summing the repeated invoice Total would overcount revenue. Check the grain after every join, and aggregate at the grain required by your question.

Practice: choose keys, population and threshold

Beginner · country summaries

Return billing countries with at least 20 invoices, showing count and total revenue.

Hint

Group by BillingCountry, count invoices, and test COUNT(*) in HAVING.

Solution and checked result

Exercise solution: countries with at least 20 invoices

Return billing countries with at least 20 invoices, showing count and total revenue.

SQLite 3+ · Chinook
SELECT BillingCountry, COUNT(*) AS InvoiceCount, ROUND(SUM(Total), 2) AS Revenue
FROM Invoice
GROUP BY BillingCountry
HAVING COUNT(*) >= 20
ORDER BY BillingCountry;
Exercise solution: countries with at least 20 invoices: expected result · 6 records
BillingCountryInvoiceCountRevenue
Brazil35190.1
Canada56303.96
France35195.1
Germany28156.48
USA91523.06
United Kingdom21112.86

Intermediate · two keys and a date range

Return country/city groups with more than 15 revenue from invoices dated in 2025.

Hint

Use WHERE for the 2025 date range, GROUP BY both location keys, and HAVING for SUM(Total).

Solution and checked result

Exercise solution: city groups in 2025

Return country/city groups with more than 15 revenue from invoices dated in 2025.

SQLite 3+ · Chinook
SELECT BillingCountry, BillingCity, ROUND(SUM(Total), 2) AS Revenue
FROM Invoice
WHERE InvoiceDate >= '2025-01-01' AND InvoiceDate < '2026-01-01'
GROUP BY BillingCountry, BillingCity
HAVING SUM(Total) > 15
ORDER BY BillingCountry, BillingCity;
Show all 13 recordsShow first 10 records
Exercise solution: city groups in 2025: expected result · Showing 10 of 13 records for clarity13 records
BillingCountryBillingCityRevenue
ArgentinaBuenos Aires24.75
BrazilSão Paulo16.83
CanadaEdmonton15.84
CanadaHalifax15.84
Czech RepublicPrague36.75
FinlandHelsinki15.84
FranceParis22.77
NetherlandsAmsterdam15.84
PortugalPorto24.75
USAMountain View16.83
USANew York22.77
USATucson15.84
United KingdomLondon16.83

References and next steps

Download every example and solution · SQL Learning Hub and learning path.

Next: compare WHERE and HAVING, then preserve detail with window functions. Related: JOINs vs Subqueries. Practice: aggregation exercises and row-versus-group filtering exercises.

Corrections: report an issue with the query, SQLite version and expected result. AI-assisted writing with automated SQL checks and independent technical review.