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
SELECTSUM(column),AVG(column)FROMtable;
Example
SELECTSUM(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.
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
SELECT
user_id,COUNT(*)AS order_count,SUM(total_amount)AS total_spent
FROM orders
GROUPBY user_id
HAVINGCOUNT(*)>=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
SELECTEXTRACT(YEAR FROM order_date)AS order_year,
status,COUNT(*)AS order_count
FROM orders
GROUPBYEXTRACT(YEAR FROM order_date), status
ORDERBY 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
SELECT
category_id,
brand,SUM(price * stock_qty)AS inventory_value
FROM products
GROUPBYROLLUP(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
category_id,
region,SUM(revenue)AS total_revenue
FROM sales
GROUPBY 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.
SELECTCOUNT(*)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;
-- PostgreSQLSELECT
order_id,STRING_AGG(product_name,', 'ORDERBY product_name)AS products
FROM order_items oi
JOIN products p ON p.id= oi.product_idGROUPBY 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.