SQL GROUP BY Explained
Learn how SQL GROUP BY groups rows and works with aggregate functions such as COUNT, SUM, AVG, MIN, and MAX.
SQL GROUP BY is used to divide rows into groups that share the same values in one or more columns. It is most useful when you want to calculate a result for each group instead of returning every individual row. For example, you can use GROUP BY to count customers by country, calculate total sales for each product, find the average order value for each customer, or determine how many employees belong to each department.
GROUP BY is usually combined with aggregate functions such as COUNT(), SUM(), AVG(), MIN(), and MAX(). Understanding how these functions interact with GROUP BY is essential for writing reports, dashboards, analytics queries, and many other SQL statements.
What Does GROUP BY Do in SQL?
The GROUP BY clause tells SQL to combine rows that have identical values in the specified grouping columns. Instead of producing one result row for every source row, the query can produce one result row for each distinct group.
Suppose a customers table contains country and name columns. Several customers may come from the same country. If you want to know how many customers belong to each country, you do not need the individual customer rows. You need to group them by country and count the rows in every group.
SELECT country, COUNT(*) AS customer_count
FROM customers
GROUP BY country;The result contains one row for each distinct country. COUNT(*) calculates how many source rows belong to each group.
Conceptually, GROUP BY changes the question from 'What are all the customers?' to 'What is the result for each country represented in the customers table?'
The grouping itself does not automatically calculate a count, sum, or average. GROUP BY defines the groups, while aggregate functions calculate values from the rows inside those groups.
A Simple GROUP BY Example
Consider a sales table with product, category, quantity, and price columns. If the table contains many sales records, selecting all rows gives you individual transactions. GROUP BY can instead summarize those transactions by category.
SELECT category, COUNT(*) AS sales_count
FROM sales
GROUP BY category;If the table contains the categories Electronics, Books, and Clothing, the query returns three result rows, assuming all three categories exist in the filtered data.
The important point is that SQL creates a separate group for every distinct value of category. All rows with category = 'Electronics' belong to one group, all rows with category = 'Books' belong to another, and so on.
Interactive GROUP BY Example
The following example demonstrates how changing the grouping column changes the result. You can explore how customer rows are grouped and how COUNT(*) produces one result for each distinct group.
GROUP BY Syntax
The basic syntax places GROUP BY after FROM and any WHERE clause. A common pattern is SELECT, FROM, WHERE, GROUP BY, HAVING, and ORDER BY.
SELECT column_name, aggregate_function(column_name)
FROM table_name
WHERE condition
GROUP BY column_name
HAVING group_condition
ORDER BY column_name;Not every query needs every clause. For example, WHERE and HAVING are optional, and ORDER BY is only needed when you want to control the order of the result.
GROUP BY with COUNT()
COUNT() is one of the most common aggregate functions used with GROUP BY. It can be used to count rows or count non-NULL values in a particular column.
SELECT department, COUNT(*) AS employee_count
FROM employees
GROUP BY department;COUNT(*) counts every row in each department group. If a department contains 25 employees, its count is 25.
COUNT(column) behaves differently because it ignores NULL values in that column.
SELECT department, COUNT(email) AS employees_with_email
FROM employees
GROUP BY department;Here, employees whose email is NULL are not included in the COUNT(email) result. This distinction is important when NULL values are possible.
GROUP BY with SUM()
SUM() calculates the total of a numeric expression within each group. It is commonly used for sales, quantities, costs, revenue, and other additive values.
SELECT product_id, SUM(quantity) AS total_quantity
FROM order_items
GROUP BY product_id;This query returns one row per product and calculates the total quantity across all matching order items.
You can also calculate a monetary total by summing an expression rather than a single stored column.
SELECT product_id,
SUM(quantity * price) AS total_revenue
FROM order_items
GROUP BY product_id;GROUP BY with AVG()
AVG() calculates the average value for each group. It is useful for metrics such as average order value, average salary, average rating, or average delivery time.
SELECT department,
AVG(salary) AS average_salary
FROM employees
GROUP BY department;The result contains one average salary for each department. AVG() generally ignores NULL values when calculating the average.
GROUP BY with MIN() and MAX()
MIN() returns the smallest value in each group, while MAX() returns the largest. These functions can be used with numbers, dates, and other comparable values depending on the SQL dialect.
SELECT department,
MIN(salary) AS minimum_salary,
MAX(salary) AS maximum_salary
FROM employees
GROUP BY department;This produces one row per department with the minimum and maximum salary found in that group.
Grouping by Multiple Columns
GROUP BY can contain more than one column. In that case, SQL creates groups based on the unique combination of values from those columns.
SELECT country, city, COUNT(*) AS customer_count
FROM customers
GROUP BY country, city;This query creates a group for each unique country-and-city combination. Customers in the same city and country belong to the same group.
Grouping by multiple columns is different from grouping by either column separately. For example, grouping by country alone produces one group per country, while grouping by country and city produces multiple groups inside countries when several cities are present.
GROUP BY and SELECT
One of the most important GROUP BY rules is that selected columns generally need to be either included in GROUP BY or used inside an aggregate function. This rule exists because a grouped result contains multiple source rows, and SQL needs to know which value should represent the group.
SELECT country, COUNT(*) AS customer_count
FROM customers
GROUP BY country;country is included in GROUP BY, while COUNT(*) is an aggregate expression. The query therefore has a well-defined value for every result column.
By contrast, selecting an unrelated non-grouped column can cause an error in many SQL systems because there may be several different values for that column inside the same group.
SELECT country, name, COUNT(*)
FROM customers
GROUP BY country;If a country contains many customers, which name should SQL return? There is no single correct answer. That is why strict SQL implementations reject this query.
GROUP BY with WHERE
WHERE filters individual rows before the grouping operation. This makes WHERE useful when you want to restrict which source records participate in the groups.
SELECT country, COUNT(*) AS customer_count
FROM customers
WHERE status = 'active'
GROUP BY country;Only active customers participate in the grouping. Inactive customers are removed by WHERE before the country groups are calculated.
This distinction becomes particularly important when you compare WHERE with HAVING. WHERE works with source rows, while HAVING filters the groups produced by GROUP BY.
GROUP BY with HAVING
HAVING filters groups after the grouping and aggregation step. It is commonly used when the filtering condition depends on an aggregate result such as COUNT(), SUM(), or AVG().
SELECT country, COUNT(*) AS customer_count
FROM customers
GROUP BY country
HAVING COUNT(*) >= 10;This query first creates groups by country and counts the customers in each group. HAVING then keeps only countries whose customer count is at least 10.
WHERE vs HAVING
| Clause | Filters | Typical use |
|---|---|---|
| WHERE | Individual rows | Remove rows before grouping |
| GROUP BY | Rows into groups | Create groups for aggregation |
| HAVING | Groups | Filter aggregate results |
| ORDER BY | Final result | Sort the returned rows |
For example, suppose you want countries with at least 10 active customers. WHERE should filter inactive customers, GROUP BY should create country groups, and HAVING should filter groups based on their counts.
SELECT country, COUNT(*) AS customer_count
FROM customers
WHERE status = 'active'
GROUP BY country
HAVING COUNT(*) >= 10
ORDER BY customer_count DESC;GROUP BY with ORDER BY
ORDER BY can sort grouped results just like ordinary query results. A common pattern is to sort by an aggregate value.
SELECT category,
SUM(amount) AS total_sales
FROM sales
GROUP BY category
ORDER BY total_sales DESC;The result is sorted from the largest total sales value to the smallest. In many SQL dialects, an alias defined in SELECT can be used in ORDER BY.
You can also order directly by the aggregate expression when appropriate.
SELECT category,
SUM(amount) AS total_sales
FROM sales
GROUP BY category
ORDER BY SUM(amount) DESC;GROUP BY with DISTINCT Values
GROUP BY and DISTINCT can sometimes produce similar-looking results when no aggregate function is involved. For example, both of these queries can return unique countries.
SELECT DISTINCT country
FROM customers;SELECT country
FROM customers
GROUP BY country;However, their purposes are different. DISTINCT removes duplicate result rows, while GROUP BY creates groups that can be used with aggregate calculations. When you need counts, sums, averages, or other group-level calculations, GROUP BY is the appropriate tool.
GROUP BY and NULL Values
NULL requires special attention in grouped queries. Rows with NULL in a grouping column are generally placed into the same group for GROUP BY purposes.
SELECT department, COUNT(*) AS employee_count
FROM employees
GROUP BY department;If some employees have department = NULL, those rows form a NULL group. The result can therefore contain a row whose department value is NULL.
This is different from COUNT(column), which does not count NULL values in the aggregated column. GROUP BY and aggregate functions therefore need to be considered separately when NULL values are present.
GROUP BY with Expressions
You can group by expressions rather than only raw column names. This is useful when the grouping category needs to be calculated from existing data.
SELECT
EXTRACT(YEAR FROM order_date) AS order_year,
COUNT(*) AS order_count
FROM orders
GROUP BY EXTRACT(YEAR FROM order_date);The exact date functions vary between database systems, but the general idea is the same: calculate a grouping value and aggregate rows that share it.
GROUP BY with CASE
CASE expressions can be used to create custom categories before aggregation. This is useful when you want to group numeric values into ranges or classify records according to business rules.
SELECT
CASE
WHEN amount < 100 THEN 'Small'
WHEN amount < 500 THEN 'Medium'
ELSE 'Large'
END AS order_size,
COUNT(*) AS order_count
FROM orders
GROUP BY
CASE
WHEN amount < 100 THEN 'Small'
WHEN amount < 500 THEN 'Medium'
ELSE 'Large'
END;This groups orders into Small, Medium, and Large categories. Some database systems support features or syntax that can make repeating expressions easier, so always check the SQL dialect you are using.
GROUP BY with JOIN
GROUP BY is frequently used after joining related tables. A JOIN can provide the columns needed for grouping, while an aggregate function summarizes the matching records.
SELECT
customers.country,
COUNT(orders.id) AS order_count
FROM customers
JOIN orders
ON orders.customer_id = customers.id
GROUP BY customers.country;The query joins customers with their orders, groups the matching records by customer country, and counts orders for each country.
GROUP BY with Multiple Aggregate Functions
A single GROUP BY query can calculate several aggregate values at once. This is useful for reports where each group needs multiple metrics.
SELECT
category,
COUNT(*) AS product_count,
AVG(price) AS average_price,
MIN(price) AS minimum_price,
MAX(price) AS maximum_price
FROM products
GROUP BY category;Each category produces one result row containing several calculated statistics. This is often more efficient and easier to maintain than writing separate queries for every metric.
GROUP BY Without an Aggregate Function
GROUP BY can technically be used without an aggregate function in many SQL systems, but this is often unnecessary if your only goal is to return unique combinations of values.
SELECT country, city
FROM customers
GROUP BY country, city;If you only need unique country-and-city combinations, DISTINCT usually communicates the intent more clearly.
SELECT DISTINCT country, city
FROM customers;Common GROUP BY Errors
Many GROUP BY problems are caused by a mismatch between the columns in SELECT and the columns used for grouping. Understanding the reason behind these errors is more useful than memorizing individual database error messages.
Selecting a Non-Grouped Column
A common mistake is selecting a regular column that is neither included in GROUP BY nor wrapped in an aggregate function.
SELECT department, employee_name, AVG(salary)
FROM employees
GROUP BY department;If a department contains multiple employees, there is no single employee_name that naturally represents the entire department. The query therefore has an ambiguous result and may be rejected.
Using WHERE for an Aggregate Condition
Another common mistake is trying to use an aggregate function in WHERE.
SELECT country, COUNT(*)
FROM customers
WHERE COUNT(*) >= 10
GROUP BY country;The aggregate value does not exist at the row-filtering stage represented by WHERE. Use HAVING for conditions that depend on aggregate results.
SELECT country, COUNT(*) AS customer_count
FROM customers
GROUP BY country
HAVING COUNT(*) >= 10;Forgetting a Grouping Column
When grouping by multiple dimensions, forgetting one column can produce a valid query with incorrect business results. For example, grouping sales only by product when the report actually needs product and year will combine records from different years into the same group.
SELECT product_id, order_year, SUM(amount)
FROM sales
GROUP BY product_id, order_year;The grouping columns should match the dimensions at which you want to analyze the data.
Accidentally Multiplying Rows with JOIN
A GROUP BY query can be syntactically correct while still returning the wrong totals because a JOIN duplicated rows before aggregation.
For example, joining orders to another one-to-many table can cause one order to appear several times. SUM(order_amount) can then count the same order multiple times.
GROUP BY Query Evaluation Order
The written order of SQL clauses is not the same as the conceptual order in which a database processes a query. A simplified model is useful for understanding GROUP BY.
- FROM and JOIN determine the source row set.
- WHERE removes rows that do not satisfy row-level conditions.
- GROUP BY divides the remaining rows into groups.
- Aggregate functions calculate values for each group.
- HAVING removes groups that do not satisfy group-level conditions.
- SELECT produces the requested result columns.
- ORDER BY sorts the final result.
This model explains why WHERE and HAVING have different purposes. WHERE operates before grouping, while HAVING operates on grouped results.
GROUP BY Performance Considerations
GROUP BY can require significant database work when the source table contains a large number of rows or when there are many distinct grouping values. The database may need to sort or otherwise organize rows before calculating the aggregates.
Performance depends on the database engine, indexes, query structure, data distribution, and execution plan. There is no universal rule that adding an index to every GROUP BY column will automatically make the query faster.
Filtering unnecessary rows before aggregation can often reduce the amount of data that must be grouped. For example, if a report only covers recent orders, applying an appropriate WHERE condition can prevent older rows from participating in the grouping.
SELECT customer_id, SUM(amount) AS total_spent
FROM orders
WHERE order_date >= '2026-01-01'
GROUP BY customer_id;For performance-sensitive queries, use the database's query plan tools to understand how the GROUP BY is actually executed instead of relying only on assumptions.
GROUP BY and Large Reports
For dashboards and reporting systems, GROUP BY is often used to calculate metrics at different levels of detail. A report might group by country, department, product, month, or a combination of several dimensions.
When designing these queries, first identify the level of detail required in the final result. Every grouping column represents a dimension of that result. Adding another grouping column generally creates more specific groups and therefore more result rows.
How to Debug a GROUP BY Query
- Start by checking the source rows produced by FROM and JOIN.
- Add WHERE conditions and verify that the correct rows remain.
- Identify exactly which columns define the desired groups.
- Add GROUP BY using those columns.
- Add one aggregate function and verify its result.
- Add additional aggregate functions one at a time.
- Use HAVING only for conditions on grouped results.
- Check for NULL values that may affect grouping or aggregation.
- Inspect JOINs if counts or sums appear unexpectedly large.
- Use ORDER BY to make the grouped result easier to inspect.
A useful debugging technique is to temporarily remove the aggregation and inspect the underlying rows. This can reveal duplicate rows, unexpected NULL values, incorrect JOIN conditions, or filters that are not doing what you expected.
GROUP BY Best Practices
- Use GROUP BY when you need one result per logical group.
- Choose grouping columns based on the level of detail required by the report.
- Use aggregate functions to calculate meaningful values for each group.
- Use WHERE to filter source rows before grouping.
- Use HAVING to filter groups based on aggregate conditions.
- Use aliases for calculated columns to make results easier to understand.
- Be careful when combining GROUP BY with JOINs that can multiply rows.
- Consider NULL behavior when choosing aggregate functions.
- Use DISTINCT instead of GROUP BY when you only need unique values.
- Format complex grouped queries consistently so the grouping logic is easy to inspect.
- Use query execution plans when investigating performance problems.
Frequently Asked Questions
What is GROUP BY in SQL?
GROUP BY divides rows into groups based on identical values in one or more columns. It is commonly combined with aggregate functions such as COUNT(), SUM(), AVG(), MIN(), and MAX() to calculate one result for each group.
What is the difference between GROUP BY and ORDER BY?
GROUP BY creates groups of rows and is commonly used for aggregation. ORDER BY sorts the rows in the final result. They solve different problems and can be used together in the same query.
What is the difference between WHERE and HAVING?
WHERE filters individual source rows before grouping, while HAVING filters groups after aggregation. Use HAVING when the condition depends on an aggregate such as COUNT(), SUM(), or AVG().
Can GROUP BY use multiple columns?
Yes. GROUP BY can contain multiple columns. SQL creates groups based on the unique combination of values in those columns.
Why does SQL say a column must appear in GROUP BY?
A selected non-aggregate column generally needs to be included in GROUP BY because a group can contain multiple source values for that column. Without grouping or aggregation, SQL may not have a single unambiguous value to return.
What is the difference between COUNT(*) and COUNT(column)?
COUNT(*) counts rows, including rows where individual columns contain NULL. COUNT(column) counts non-NULL values in the specified column.
Can GROUP BY be used without aggregate functions?
Yes, many SQL systems allow it. However, if the goal is simply to return unique combinations of columns, SELECT DISTINCT is often clearer and communicates the intent more directly.
Helpful SQL Grouping Tools
Several types of SQL development tools can make grouped queries easier to write and troubleshoot. SQL formatters can improve the readability of queries with multiple clauses and aggregate expressions. Syntax highlighters make GROUP BY, HAVING, and aggregate functions easier to distinguish visually. Query validators can help detect syntax and structural problems, while query explainers can help clarify what a grouped query is doing. Column extraction tools can also be useful when working with large SELECT statements and checking which fields participate in a query.
Conclusion
SQL GROUP BY is the main mechanism for turning individual rows into grouped summaries. It lets you calculate counts, totals, averages, minimums, maximums, and other aggregate values for each distinct group.
The most important concepts are straightforward once their roles are separated: GROUP BY defines the groups, aggregate functions calculate values inside those groups, WHERE filters rows before grouping, and HAVING filters groups after aggregation. GROUP BY can also be combined with JOIN, ORDER BY, CASE expressions, multiple columns, and several aggregate functions to build detailed analytical queries.
When a grouped query produces an unexpected result, check the source rows first, then the filters, grouping columns, aggregate functions, NULL values, and JOIN relationships. This approach makes GROUP BY queries much easier to understand, debug, and maintain.