Note COUNT(*) counts all rows including NULLs. COUNT(column) skips NULLs. COUNT(DISTINCT column) counts unique non-NULL values. This distinction is a common source of bugs.
count rowscount distinctnumber of recordstotal count
SUM and AVG
syntax
SELECT SUM(column), AVG(column) FROM table;
example
SELECT
SUM(total_amount) AS revenue,
AVG(total_amount) AS avg_order_value
FROM orders
WHERE order_date >= '2025-01-01';
Note Both SUM and AVG ignore NULL values. If all values are NULL, SUM returns NULL (not 0). Wrap with COALESCE(SUM(col), 0) if you need a zero default.
sum valuesaveragetotal amountmean value
MIN and MAX
syntax
SELECT MIN(column), MAX(column) FROM table;
example
SELECT
MIN(price) AS cheapest,
MAX(price) AS most_expensive,
MIN(created_at) AS first_product,
MAX(created_at) AS latest_product
FROM products;
Note Every non-aggregated column in SELECT must appear in GROUP BY (ANSI rule). MySQL historically allowed non-grouped columns with unpredictable results; enable ONLY_FULL_GROUP_BY mode to catch this.
group rowsgroup by columnaggregate per groupsummarize by
HAVING
syntax
SELECT column, aggregate(col)
FROM table
GROUP BY column
HAVING aggregate_condition;
example
SELECT
user_id,
COUNT(*) AS order_count,
SUM(total_amount) AS total_spent
FROM orders
GROUP BY user_id
HAVING COUNT(*) >= 5;
output
-- Only users with 5 or more orders
Note HAVING filters groups after aggregation. WHERE filters rows before aggregation. A common mistake is using WHERE with aggregate functions — that is a syntax error.
filter groupshaving clausefilter after group byaggregate condition
GROUP BY Multiple Columns
syntax
SELECT col1, col2, aggregate(col)
FROM table
GROUP BY col1, col2;
example
SELECT
EXTRACT(YEAR FROM order_date) AS order_year,
status,
COUNT(*) AS order_count
FROM orders
GROUP BY EXTRACT(YEAR FROM order_date), status
ORDER BY order_year, status;
Note Grouping by multiple columns creates one group for each unique combination of values. The more columns you group by, the more granular (and more numerous) the groups become.
group by two columnsmulti-column groupgroup by multiple
GROUP BY with ROLLUP
syntax
SELECT col1, col2, aggregate(col)
FROM table
GROUP BY ROLLUP(col1, col2);
example
SELECT
category_id,
brand,
SUM(price * stock_qty) AS inventory_value
FROM products
GROUP BY ROLLUP(category_id, brand);
output
-- Includes subtotals per category and a grand total row
-- category_id | brand | inventory_value
-- 1 | Acme | 5000
-- 1 | NULL | 12000 (subtotal for category 1)
-- NULL | NULL | 45000 (grand total)
Note ROLLUP generates subtotals in a hierarchy. Use GROUPING() function to distinguish real NULLs from subtotal NULLs. MySQL supports WITH ROLLUP syntax; PostgreSQL uses ROLLUP().
SELECT col1, col2, aggregate(col)
FROM table
GROUP BY GROUPING SETS ((col1), (col2), ());
example
SELECT
category_id,
region,
SUM(revenue) AS total_revenue
FROM sales
GROUP BY GROUPING SETS (
(category_id, region),
(category_id),
(region),
()
);
output
-- Returns aggregations at every specified level
-- including per-category, per-region, and grand total
Note GROUPING SETS let you define exactly which grouping combinations you want, unlike ROLLUP which follows a strict hierarchy. Supported in PostgreSQL and SQL Server; MySQL uses ROLLUP only.
SELECT
COUNT(*) AS total_orders,
COUNT(*) FILTER (WHERE status = 'completed') AS completed,
COUNT(*) FILTER (WHERE status = 'cancelled') AS cancelled,
SUM(total_amount) FILTER (WHERE status = 'completed') AS completed_revenue
FROM orders;
-- PostgreSQL
SELECT
order_id,
STRING_AGG(product_name, ', ' ORDER BY product_name) AS products
FROM order_items oi
JOIN products p ON p.id = oi.product_id
GROUP BY order_id;
Note STRING_AGG is standard SQL. MySQL uses GROUP_CONCAT with a default max length of 1024 characters — increase group_concat_max_len if results get truncated.