- Learn
- Computer Science & IT
- Programming
- SQL Basics
- SQL GROUP BY
SQL GROUP BY: Aggregation and Grouping
BeginnerLesson 4 of 6 in SQL Basics Course
What you will learn
- Use COUNT, SUM, AVG, MIN and MAX to turn many rows into summary values, choose the right kind of count, and predict how missing values change a result.
- Find totals, averages, smallest and largest valuesUse SUM, AVG, MIN and MAX on a whole table or on the rows a WHERE clause keeps, and combine several of them in one query.
- Count rows, values and distinct valuesChoose between COUNT(*), COUNT(column) and COUNT(DISTINCT column), and know how each one treats repeated and missing values.
- Predict how NULL values affect aggregatesKnow that aggregates skip NULL values, predict the results for empty inputs, and replace NULL with a default when the question calls for it.
- Find totals, averages, smallest and largest values
- Use GROUP BY to get one summary row per category, per combination of values or per calculated value, and follow the rule for what a grouped query can show.
- Summarize each category with GROUP BYSplit a table into groups that share a value, compute aggregates per group, and sort and limit the grouped result.
- Follow the rule for what a grouped query can showKnow which columns may appear in the SELECT list of a grouped query, fix the error that appears when the rule is broken, and recognize how databases differ.
- Group by several columns and by calculated valuesGroup rows by combinations of columns and by expressions such as the month of a date or a CASE category, and read the more detailed result.
- Summarize each category with GROUP BY
- Keep only the groups whose totals, counts or averages meet a condition, and decide whether a condition belongs in WHERE or in HAVING.
- Keep the groups that meet a conditionWrite HAVING conditions on aggregate values, including aggregates that are not displayed, to filter a grouped result.
- Choose between WHERE and HAVINGUse the logical order of a query to decide whether a condition filters rows or groups, and predict how that choice changes the result.
- Keep the groups that meet a condition
- Avoid whole-number division, know what type of number each aggregate returns, and round averages, totals and percentages at the right moment.
- Avoid integer division in averages and percentagesRecognize when dividing aggregates loses the decimals, fix it with a decimal operand, and know how result types differ between databases.
- Round aggregated resultsRound averages, totals and percentages with ROUND, round at the right moment, and handle the differences between SQLite and PostgreSQL.
- Avoid integer division in averages and percentages
- Combine CASE with aggregates for conditional counts, totals and percentages, and turn a question into a grouped query whose numbers you can check.
- Count and add up by condition with CASEPut a CASE expression inside an aggregate to compute several conditional counts, totals and percentages in one grouped query.
- Build and check a summary query from a questionTranslate a business question into a grouped query step by step, avoid mixing up levels of detail, and check that the numbers are right.
- Count and add up by condition with CASE
SQL GROUP BY study guide
1Summarize data with aggregate functions
A table records individual events; a report often needs a few numbers that describe them together. Aggregate functions turn rows into counts, totals, averages and extremes. Choosing the function is only part of the job: you also need to decide which rows belong in the calculation and what missing values mean. We will use a small orders table to check each answer by hand before relying on the query.
1.1 Find totals, averages, smallest and largest values
An aggregate function combines values from many rows into one result. Oracle also calls it a group function. Without GROUP BY, an aggregate query treats all rows that pass WHERE as one group and returns one summary row.
Start with data you can check
Each row in orders is one order. The numeric columns below contain whole numbers; order_id is the primary key. Dates use ISO text in SQLite and the DATE type in PostgreSQL.
| order_id | customer | region | amount | quantity | discount | order_date |
|---|---|---|---|---|---|---|
| 1 | Ana | East | 120 | 2 | 10 | 2026-01-05 |
| 2 | Ben | West | 80 | 1 | NULL | 2026-01-17 |
| 3 | Ana | East | 45 | 3 | 5 | 2026-02-02 |
| 4 | Chen | North | 200 | 4 | NULL | 2026-02-14 |
| 5 | Ben | West | 60 | 1 | 0 | 2026-02-20 |
| 6 | Dara | East | 95 | 2 | NULL | 2026-03-03 |
| 7 | Chen | North | 150 | 5 | 15 | 2026-03-09 |
| 8 | Eli | NULL | 30 | 1 | NULL | 2026-03-21 |
Choose the measure
SUM(amount) adds the amounts. AVG(amount) gives their arithmetic mean. MIN(amount) finds the smallest amount and MAX(amount) finds the largest. You can request all four in one query:
SELECT SUM(amount) AS total, AVG(amount) AS avg_amount, MIN(amount) AS smallest, MAX(amount) AS largest FROM orders;
The result is one row: total 780, average 97.5, smallest 30 and largest 200. Add the eight amounts to get 780, then divide by eight to get 97.5. Each function uses the same eight input rows. Clear aliases after AS make the output headings useful; a generated heading such as sum says little about what was added.
Filter before calculating
To summarize East orders, write SELECT COUNT(*) AS orders, SUM(amount) AS total FROM orders WHERE region = 'East';. The filter keeps amounts 120, 45 and 95. The query returns three orders and a total of 260. WHERE chooses the input rows before any aggregate runs.
An aggregate can also take a calculated expression. In a table with quantity and unit_price, SUM(quantity * unit_price) first finds each row's revenue and then adds it. Two rows, two items at 5 and three items at 10, contribute 10 and 30, for a total of 40. SUM(quantity) * SUM(unit_price) instead multiplies 5 by 15 and gives 75. Keeping the calculation inside SUM preserves each quantity's matching price.
Read extremes correctly
MIN(order_date) is 2026-01-05 and MAX(order_date) is 2026-03-21. Date values and ISO date text sort chronologically. Text such as '03/21/2026' follows character order, which does not reliably match date order. Text extremes also follow the database's collation, its rules for comparing text. Here the customer extremes are 'Ana' and 'Eli'; mixed letter case can change text ordering.
MAX(amount) returns an amount, not the whole order that contains it. To show a customer with a largest order, use SELECT customer, amount FROM orders ORDER BY amount DESC LIMIT 1;. PostgreSQL rejects a plain customer beside MAX(amount) without a suitable grouping. SQLite has a special rule for a single MIN or MAX that can return a neighboring column from the extreme row, but tied extremes still need a tie rule.
Key Takeaways
- An aggregate returns one value for a set of rows.
WHEREselects the rows used by the aggregate.- Calculate each row’s contribution inside
SUMwhen values must stay paired. MINandMAXfollow numeric, date or text ordering.- An extreme value does not identify an entire row.
1.2 Count rows, values and distinct values
“How many?” can ask about rows, recorded values or different values. These are different counts, even when they happen to give the same number in a small table.
Three forms of COUNT
| Expression | What it counts |
|---|---|
COUNT(*) | Every input row |
COUNT(region) | Rows with a non-NULL region |
COUNT(DISTINCT region) | Different non-NULL region values |
In the orders table, the regions are East, West, East, North, West, East, North and NULL. SELECT COUNT(*) AS order_rows, COUNT(region) AS with_region, COUNT(DISTINCT region) AS regions FROM orders; returns 8, 7 and 3. The repeated region names count separately in COUNT(region), but each name contributes only once to the distinct count.
For “How many different customers ordered?”, use COUNT(DISTINCT customer). Ana, Ben and Chen each appear twice, while Dara and Eli appear once, so the answer is five customers. COUNT(customer) returns eight because every order names a customer. It measures named order rows, not people.
A recorded zero still counts
The discounts are 10, NULL, 5, NULL, 0, NULL, 15 and NULL. COUNT(discount) returns four. The zero records a known value and counts just like 10, 5 and 15. An empty string is also a value in SQLite and PostgreSQL; Oracle treats an empty string as NULL.
Subtract the non-NULL count from the row count to measure missing records: SELECT COUNT(*) - COUNT(region) AS missing_region FROM orders; returns one. This works because both counts use exactly the same input rows.
DISTINCT lists and distinct counts
SELECT DISTINCT region FROM orders; lists East, West, North and one NULL row. Its job is to return different values, and NULL remains in that list. COUNT(DISTINCT region) returns three because the count ignores NULL. Do not infer a distinct count by counting a list unless you have decided whether its NULL row belongs.
DISTINCT can also appear inside SUM or AVG. The quantities are 2, 1, 3, 4, 1, 2, 5 and 1. Their ordinary sum is 19. SUM(DISTINCT quantity) keeps only 1, 2, 3, 4 and 5, giving 15. Repeated quantities usually describe real additional sales. Removing them changes the measure; it does not repair duplicate records.
Count combinations in two steps
For different region-and-customer pairs, make a distinct list first:
SELECT COUNT(*) FROM (SELECT DISTINCT region, customer FROM orders) AS pairs;
The inner query returns five pairs: East/Ana, East/Dara, North/Chen, West/Ben and NULL/Eli. The outer COUNT(*) counts those five rows, including NULL/Eli. This form works in SQLite and PostgreSQL. MySQL also accepts COUNT(DISTINCT region, customer), but that form excludes combinations containing NULL, so it can answer a different question. In SQLite and PostgreSQL the comma-separated form is invalid.
Finally, a count never returns NULL. SELECT COUNT(*) FROM orders WHERE region = 'South'; returns one summary row containing zero, because there are no matching orders.
Key Takeaways
- Choose between rows, recorded values and different recorded values.
- Zero is a recorded value, so
COUNT(column)includes it. - A distinct list can include NULL;
COUNT(DISTINCT column)skips it. SUM(DISTINCT x)removes repeated values, including legitimate repeats.- Count distinct combinations by counting a distinct inner result.
COUNTreturns zero for an empty input.
1.3 Predict how NULL values affect aggregates
NULL means that a value is missing or unknown. It changes which values an aggregate sees, so a total or average can be valid SQL while measuring something different from what you intended.
Work out the denominator
Suppose scores contains Ana 80, Ben NULL, Chen 70 and Dara 90. SUM(score) is 240, COUNT(score) is three and COUNT(*) is four. AVG(score) is 80: the three recorded scores add to 240 and are divided by three.
The five standard aggregates ignore NULL arguments. COUNT(*) counts every row because it does not evaluate a column argument. MIN(score) is 70 and MAX(score) is 90; NULL does not become the smallest or largest score. This differs from sorting, where NULL values occupy an end of the result: first in an ascending SQLite sort, last in an ascending PostgreSQL sort.
A manual average needs the non-NULL count: 1.0 * SUM(score) / NULLIF(COUNT(score), 0). The decimal operand keeps fractional results, and NULLIF makes a zero divisor NULL. Dividing by COUNT(*) would give 60 and would effectively treat the missing score as zero.
Choose what a default means
COALESCE returns its first non-NULL argument. If Ben has not been graded, leave the score missing and average the known results. If a missed exam is recorded as NULL but must count as zero, AVG(COALESCE(score, 0)) averages 80, 0, 70 and 90, giving 60. The data's meaning decides whether that replacement is justified.
Compare putting COALESCE inside and outside a function. AVG(COALESCE(score, 0)) changes the values entering the average. COALESCE(AVG(score), 0) changes only a NULL final result; it still gives 80 for these scores. Oracle's NVL(x, 0) is another two-argument form for replacing NULL.
Separate no rows from no recorded values
For WHERE region = 'South', no orders remain. An aggregate query without GROUP BY still returns one row: COUNT(*) is zero, while SUM(amount), AVG(amount), MIN(amount) and MAX(amount) are NULL. To display an empty total as zero, use COALESCE(SUM(amount), 0). SQLite also offers TOTAL(amount), which returns 0.0 for empty input.
Now keep the four orders whose discount is NULL. There are rows, but no recorded discount values: COUNT(*) is four, COUNT(discount) is zero, and SUM(discount) is NULL. Zero recorded values and four missing values are different observations.
Check expressions before they enter SUM
Arithmetic with NULL produces NULL. In SUM(amount - discount), the four orders without a recorded discount contribute no value at all. Only the known-discount rows contribute: 110, 40, 60 and 135, totaling 345.
If a missing discount means no discount, write SUM(amount - COALESCE(discount, 0)). Every order then contributes; total amount 780 minus recorded discounts 30 gives 750. Wrapping the entire sum in COALESCE would not restore the omitted orders because 345 is already non-NULL.
To locate missing values, use IS NULL or IS NOT NULL. WHERE discount IS NULL finds four orders. discount = NULL is never true and therefore cannot select them.
Key Takeaways
AVG(x)divides by the number of non-NULL values.- An inner
COALESCEchanges inputs; an outer one changes a NULL result. - Empty input gives zero for
COUNTand NULL for the other four aggregates. - NULL inside an arithmetic expression can remove a row’s contribution.
2Group rows and summarize each group
A whole-table total answers one question, but many reports ask for a result per customer, region or month. Grouping sets that level of detail before the aggregates run. You will trace how rows become groups, choose columns that can describe a whole group, and see how adding a grouping column changes the report. Sorting then decides how to present the results; grouping alone does not give them an order.
2.1 Summarize each category with GROUP BY
GROUP BY collects rows that share a grouping value. Each aggregate runs separately within each group, and the output contains one row for every group that exists after filtering.
Decide what one result row means
“Total sales per region” asks for one output row per region. Start by writing that sentence, then use region as the grouping column:
SELECT region, COUNT(*) AS orders, SUM(amount) AS total FROM orders GROUP BY region;
The three East amounts, 120, 45 and 95, become one group. Its count is three and its total is 260. North has two orders totaling 350; West has two totaling 140. Eli's order forms a NULL-region group with one order totaling 30. There are four result rows, not eight.
Read the query as a sequence: FROM supplies orders, any WHERE removes rows, GROUP BY collects the remaining rows by region, and the aggregates summarize each collection. SELECT displays its label and measures. Grouping does not discard the smaller orders inside a group; they all contribute unless a row filter has removed them.
Keep missing categories visible
Rows with NULL in the grouping column belong to one NULL group. This is useful: a report can show how much activity has no recorded region. It does not treat every missing region as a different category.
To show a readable label, select COALESCE(region, 'No region') AS region_label and keep GROUP BY region. The label changes the displayed value. To exclude those rows from the calculations, add WHERE region IS NOT NULL before GROUP BY. The resulting totals add to 750, because Eli's amount of 30 has been left out.
A grouped query over an input with no rows returns no groups. For example, filtering the table to South and then grouping by region returns no result rows. That differs from a whole-table aggregate without GROUP BY, which produces one summary row even on empty input.
Sort the groups explicitly
GROUP BY makes no promise about output order. To list the biggest regions first, add ORDER BY total DESC. The alias total is allowed in ORDER BY; ORDER BY SUM(amount) DESC gives the same sort.
A customer report can select the top three totals:
SELECT customer, SUM(amount) AS total FROM orders GROUP BY customer ORDER BY total DESC, customer LIMIT 3;
It returns Chen 350, Ana 165 and Ben 140. The second sort key, customer, gives tied totals a repeatable order. LIMIT 3 returns only three rows; it does not keep every tie at the boundary. SQL Server expresses this limit with SELECT TOP 3, while Oracle supports FETCH FIRST 3 ROWS ONLY.
When sorting category labels ascending, SQLite normally places NULL first and PostgreSQL places it last. Specify the intended ordering when the position matters.
Check the grouping column
SELECT DISTINCT region FROM orders and SELECT region FROM orders GROUP BY region return the same distinct region labels, including NULL. Use grouping when you also need a measure for each label.
Grouping by the unique order_id gives eight groups of one row each. Every count is one, and each amount total equals its original amount. For a regional report, that result signals that the grouping column is too detailed.
Key Takeaways
- State what one output row represents before choosing
GROUP BY. - All NULL grouping values form one group.
- Use
ORDER BYto set result order and a second key to settle ties. - Grouping by a unique row key leaves one-row groups.
2.2 Follow the rule for what a grouped query can show
A grouped report has to fit many input rows into one result row. Every displayed value therefore needs a clear meaning for the whole group. This is why SQL restricts the columns you can select.
Follow the portable grouping rule
An item in a grouped SELECT should be a grouping column, an expression built from grouping columns, an aggregate or a constant. Consider:
SELECT region, customer, COUNT(*) FROM orders GROUP BY region;
East contains Ana, Ana and Dara. The group has one region and one count, but two different customer names. The query has not said which customer a single East result should display. PostgreSQL rejects the ungrouped customer column.
The same issue arises without GROUP BY: SELECT customer, SUM(amount) FROM orders combines a table-wide total with an individual name. The total has no single customer label.
Repair the query to match the question
There are three useful repairs, each with a different meaning:
- For one row per region, remove
customer:SELECT region, COUNT(*) FROM orders GROUP BY region;. - For one row per region and customer, group by both:
SELECT region, customer, COUNT(*) FROM orders GROUP BY region, customer;. - For the number of customers per region, aggregate the names:
SELECT region, COUNT(DISTINCT customer) AS customers FROM orders GROUP BY region;.
The last query reports two customers in East and one in each other region group. Simply adding a column to GROUP BY can change the report's level of detail, so choose the repair from the question, not just from the error message.
A grouping column may be omitted from SELECT. SELECT COUNT(*) FROM orders GROUP BY region; is valid and returns four counts: three, two, two and one, in no promised order. Those numbers are difficult to use without their region labels.
Apply the rule beyond SELECT
A grouped ORDER BY customer has the same problem when grouping only by region: which customer should determine East's position? Grouped HAVING conditions must also describe a whole group. Use grouped columns or aggregate expressions there.
Recognize permissive database behavior
SQLite accepts a bare column, a selected column that is neither grouped nor aggregated. It takes a value from one input row of the group. An apparently successful result such as 'Ana' beside East does not mean Ana represents every order in that region.
With exactly one built-in MIN or MAX aggregate, SQLite takes bare values from a row holding the extreme. Ties and multiple extreme aggregates make the selected row uncertain. This shortcut is database-specific. Sorting the original rows is clearer when you want an order with an extreme amount.
MySQL's default ONLY_FULL_GROUP_BY mode rejects ungrouped values that are not determined by the group. ANY_VALUE(customer) explicitly permits an arbitrary value, but does not find the customer associated with a chosen total. SQL Server and Oracle also reject ordinary ungrouped columns.
PostgreSQL and MySQL recognize a primary-key exception: grouping by a table's primary key identifies a single row, so its other columns are determined. SELECT order_id, customer, SUM(amount) FROM orders GROUP BY order_id works in PostgreSQL. Listing every displayed plain column in GROUP BY remains the portable approach.
Key Takeaways
- Each displayed value must describe the whole group.
- Removing, grouping or aggregating a column answers different questions.
- The grouping rule also applies to grouped
HAVINGandORDER BY. - SQLite’s accepted bare columns can hide an incorrect report.
- A grouped primary key determines the other columns of its row.
2.3 Group by several columns and by calculated values
Some reports need a combination of categories, or a category calculated from each row. SQL can group by several columns or by an expression. The important choice is still what one output row stands for.
Group by a combination
GROUP BY region, customer puts rows together only when both values match. On the orders table, run:
SELECT region, customer, COUNT(*) AS orders, SUM(amount) AS total FROM orders GROUP BY region, customer ORDER BY region, customer;
The results are East/Ana: two orders, 165; East/Dara: one, 95; North/Chen: two, 350; West/Ben: two, 140; and NULL/Eli: one, 30. The two East results add to the regional total of 260.
Grouping by customer, region forms the same combinations. The column order in GROUP BY does not sort the output. The separate ORDER BY above chooses how to display it, subject to each database's default position for NULL.
Only combinations present in the input appear. There is no West/Ana row with a count of zero. Grouping summarizes existing rows; it does not generate every possible pairing of category labels.
Add detail only when needed
Adding a column splits a group only if its rows contain different values in that column. Grouping by customer alone already gives five groups here. Adding region still gives five, because each customer has just one region value. Adding order_date to a regional report creates smaller groups for the separate dates and changes the question being answered.
Group dates by year and month
In SQLite, use strftime('%Y-%m', order_date) to create a label such as '2026-02':
SELECT strftime('%Y-%m', order_date) AS month, COUNT(*) AS orders, SUM(amount) AS total FROM orders GROUP BY strftime('%Y-%m', order_date) ORDER BY month;
January has two orders totaling 200, February has three totaling 305, and March has three totaling 275. In PostgreSQL, replace both occurrences of strftime('%Y-%m', order_date) with to_char(order_date, 'YYYY-MM') for a DATE column.
Grouping by month number alone combines the same month across years. January 2025 and January 2026 would become one group. Keeping the year in the grouping expression preserves separate calendar months.
Repeating the expression in GROUP BY makes the grouping explicit. SQLite, PostgreSQL and MySQL also accept the output alias, such as GROUP BY month; SQL Server does not accept a same-level SELECT alias there.
Build categories with CASE
A CASE expression can label each order before grouping:
SELECT CASE WHEN amount >= 100 THEN 'large' ELSE 'small' END AS size, COUNT(*) AS orders, SUM(amount) AS total FROM orders GROUP BY CASE WHEN amount >= 100 THEN 'large' ELSE 'small' END;
The large category contains 120, 200 and 150: three orders totaling 470. The small category contains five orders totaling 310. The threshold is inclusive, so an amount of exactly 100 belongs to 'large'. The same CASE expression appears in the display and the grouping so both use the same category definition.
Key Takeaways
- Several grouping columns identify combinations that occur in the data.
- Adding a column splits only groups where that column varies.
- Include year as well as month for separate calendar months.
- Use the same category expression in the selected label and grouping.
GROUP BYdoes not create missing combinations with zero counts.- The order of grouping columns does not control output order.
3Filter grouped results with HAVING
Filtering can change the rows used in a calculation or decide which finished summaries appear in a report. Those choices answer different questions. You will use HAVING to keep groups by count, total or average, then combine it with WHERE without changing the intended meaning. Following the logical order of a query makes it easier to place each condition and predict the result before running SQL.
3.1 Keep the groups that meet a condition
HAVING tests groups after their aggregates have been computed. Use it for a request such as “regions with total sales above 200.” The condition describes each regional total, not any single order.
Calculate first, keep groups second
SELECT region, COUNT(*) AS orders, SUM(amount) AS total FROM orders GROUP BY region HAVING SUM(amount) > 200;
Before HAVING, the regional totals are East 260, North 350, West 140 and NULL 30. The condition keeps East with three orders and North with two. It removes the other two result groups; it does not remove selected orders from inside East or North.
In written SQL, place HAVING after GROUP BY and before ORDER BY. Sorting then applies to the groups that survived the condition.
Filter on measures you do not display
SELECT region FROM orders GROUP BY region HAVING COUNT(*) >= 2; returns East, North and West. The database can count a group's rows to decide whether it qualifies without displaying that count. HAVING and SELECT do not need identical aggregate lists.
You can combine group conditions. HAVING COUNT(*) >= 2 AND AVG(amount) > 80 keeps East and North. East has three orders and an average of about 86.67; North has two and an average of 175. West has enough orders but its average is only 70. Both tests must pass because they are joined with AND; OR would allow either test to pass.
Find repeated values
Grouping and counting also finds duplicates:
SELECT customer, COUNT(*) AS orders FROM orders GROUP BY customer HAVING COUNT(*) > 1;
Ana, Ben and Chen each have two orders and qualify. Dara and Eli have one and do not. To find repeated email values in a customer table, the same pattern groups by email and keeps counts above one. The count is the number of records sharing that value.
Use the expression the condition needs
Repeat the aggregate in HAVING: HAVING SUM(amount) > 200. PostgreSQL and standard SQL do not allow the output alias total in that same-level condition, although SQLite and MySQL do. An alias is safe in a later ORDER BY total DESC.
A displayed rounded value and the value tested can differ. East's exact average is 260 divided by three. If you select ROUND(AVG(amount), 2) but write HAVING AVG(amount) > 86.67, East is removed: its unrounded average is below 86.67. North remains. The condition uses exactly the expression written in HAVING. Only HAVING ROUND(AVG(amount), 2) ... would test the rounded value.
Filter the single overall group
HAVING can also operate without an explicit GROUP BY. SELECT COUNT(*) FROM orders HAVING COUNT(*) > 5; treats all eight orders as one group, passes the test and returns a row containing eight. Replacing the condition with COUNT(*) > 10 returns no rows, not a row containing zero.
PostgreSQL supports this pattern, as does SQLite from version 3.39.0. In an older SQLite installation, a query using HAVING needs an explicit GROUP BY.
Key Takeaways
HAVINGkeeps or removes completed groups.- A
HAVINGaggregate need not appear inSELECT. - Combine group tests with
AND,ORandNOT. - Repeat aggregate expressions in
HAVINGfor portable queries. - A condition tests its own expression, which may be unrounded.
3.2 Choose between WHERE and HAVING
A condition belongs where the thing it describes exists. Individual orders exist before grouping. A regional total exists only after the orders have been grouped and added.
Trace the logical order
SQL is written as SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT. To reason about the result, follow this logical order:
FROMsupplies the rows.WHEREkeeps qualifying rows.GROUP BYforms groups, and aggregates calculate their measures.HAVINGkeeps qualifying groups.SELECTproduces the output values;DISTINCTremoves duplicate output rows if requested.ORDER BYsorts the result, andLIMITkeeps the requested number of rows.
The database may arrange its internal execution differently.
Put each condition at its level
| Request | Clause |
|---|---|
Orders with amount >= 60 | WHERE |
| Orders in a given date range | WHERE |
Regions with SUM(amount) > 200 | HAVING |
| Customers with more than one order | HAVING COUNT(*) > 1 |
WHERE SUM(amount) > 200 fails because the total does not yet exist at the row-filtering stage. A row condition such as region = 'East' can be decided before grouping and belongs in WHERE.
Work through both filters
SELECT region, COUNT(*) AS orders, SUM(amount) AS total FROM orders WHERE amount >= 60 GROUP BY region HAVING SUM(amount) > 200;
First remove the orders of 45 and 30. East now contains 120 and 95, North contains 200 and 150, and West contains 80 and 60. Next calculate totals of 215, 350 and 140. Finally keep totals above 200. The output is East with two orders and 215, and North with two and 350. The NULL region has no remaining rows, so it never forms a group.
Do not change the question while moving a filter
“For each region, average only orders of 60 or more” uses WHERE amount >= 60. East's average becomes 107.5 from 120 and 95.
HAVING MIN(amount) >= 60 asks for regions where every non-NULL amount meets the threshold. East is removed because its minimum is 45; North and West remain with their original orders. Moving the test changed the meaning from selecting individual orders to accepting or rejecting whole regions.
For this regional report, region <> 'West' gives the same result in WHERE or HAVING: East and North remain, and the NULL group also fails the comparison. WHERE states the row restriction directly and applies it before grouping. To keep missing regions as well, the condition needs an explicit OR region IS NULL.
SELECT aliases are unavailable in earlier WHERE and HAVING clauses in PostgreSQL. SQLite permits more alias use, but spelling out the input column or expression keeps the intended stage clear.
Key Takeaways
WHEREchanges aggregate inputs;HAVINGfilters aggregate results.- Place conditions about a group’s count, total or average in
HAVING. - Trace row filtering before grouping when predicting a result.
- A test on the group minimum can remove a whole group.
4Calculate exact and well-rounded results
A query can choose the right rows and still lose the fractional part of its answer. Number types control division and aggregate results, while rounding controls how much precision you show. You will keep decimals through calculations, choose sensible handling for an empty denominator, and round only the final measure. The same short expression can behave differently across databases, so each relevant difference is identified.
4.1 Avoid integer division in averages and percentages
Dividing two whole-number values can discard the fraction before you ever display the answer. That matters when you build a mean or percentage from totals and counts.
Keep a decimal operand before division
In SQLite and PostgreSQL, 7 / 2 returns 3. With integer input, SUM(quantity) / COUNT(*) on the orders table returns 2: 19 items divided by eight orders loses its fractional part. PostgreSQL's SUM of an INTEGER column returns BIGINT, which still uses integer division here.
Write 1.0 * SUM(quantity) / COUNT(*) to obtain 2.375 in both engines. Multiplying by 1.0 creates a decimal operand before the division. SUM(quantity) / COUNT(*) * 1.0 gives 2.0 because the division has already discarded the fraction. Operations of the same precedence are evaluated from left to right.
For this table, AVG(quantity) directly gives 2.375 in SQLite and PostgreSQL. The manual formula matters when you need a different denominator or a percentage.
Calculate a percentage
Three of the eight orders have amounts of at least 100. A count of those orders divided by the count of all orders measures their share. Scale by 100.0:
SELECT 100.0 * COUNT(CASE WHEN amount >= 100 THEN 1 END) / COUNT(*) AS pct_large FROM orders;
The result is 37.5. Using the integer 100 instead produces 37 in SQLite and for integer count division in PostgreSQL. Rounding that later would only change how 37 is displayed.
A whole-table query may have no matching rows. NULLIF(COUNT(*), 0) returns NULL when the count is zero, so 100.0 * COUNT(discount) / NULLIF(COUNT(*), 0) returns NULL for an empty input. An unguarded zero denominator raises an error in PostgreSQL; SQLite returns NULL. A NULL share can clearly represent “no orders to measure.”
Check the database and input type
| Database | Behavior to watch |
|---|---|
| SQLite | Integer division truncates; AVG returns floating point. |
| PostgreSQL | Integer division truncates; AVG of integer or NUMERIC input returns NUMERIC. |
| SQL Server | Integer division truncates; AVG of integer input returns an integer. |
| MySQL and Oracle | / keeps the fractional result for integer operands. |
In SQL Server, use AVG(quantity * 1.0) or cast inputs to DECIMAL before averaging. In PostgreSQL, SUM of a BIGINT column returns NUMERIC, so its division need not behave like SUM of an INTEGER column. MySQL returns DECIMAL for sums of integer inputs.
Casting can also create the needed decimal operand: use CAST(SUM(quantity) AS REAL) in SQLite or CAST(SUM(quantity) AS NUMERIC) in PostgreSQL. SQLite's NUMERIC cast can retain an integer, so that cast alone does not ensure decimal division.
Separate exact decimals from approximations
Floating-point types store binary approximations. PostgreSQL NUMERIC and DECIMAL support exact decimal values; floating-point additions can display small artifacts such as 0.30000000000000004. SQLite's NUMERIC affinity does not provide exact decimal storage. For exact money totals there, store integer cents, sum them, and divide by 100.0 for display.
Key Takeaways
- Create a decimal operand before dividing integer totals or counts.
- Multiplying after integer division cannot restore lost digits.
- Use
NULLIFto handle a zero denominator. - Aggregate result types depend on both the engine and input type.
- SQLite
NUMERICaffinity does not provide exact decimal storage. - SQL Server needs decimal inputs for a fractional average of integers.
4.2 Round aggregated results
Rounding chooses how many digits a result shows. Keep the full calculation until the final step so that display choices do not change the measure you are calculating.
Round an aggregate result
ROUND(x, n) rounds to n digits after the decimal point. ROUND(x) rounds to a whole number. For example, ROUND(3.14159, 2) is 3.14.
SELECT region, ROUND(AVG(amount), 2) AS avg_amount FROM orders GROUP BY region; gives East 86.67, North 175, West 70 and NULL-region 30. SQLite may show 175.0 while PostgreSQL's NUMERIC result shows 175.00. These represent the same number. Rounding a number and formatting a fixed-width text label are different jobs.
Round after adding or averaging
For three fees of 0.44, add first to get 1.32, then round to one decimal to get 1.3. ROUND(SUM(fee), 1) follows that sequence. SUM(ROUND(fee, 1)) rounds each fee to 0.4 and then adds them, giving 1.2. Early rounding changed the total.
Likewise, ROUND(100 * 1 / 3, 1) gives 33.0 in SQLite and PostgreSQL because integer division has already produced 33. ROUND(100.0 * 1 / 3, 1) preserves the fraction and gives 33.3. Choose the calculation type before rounding.
Rounded shares may not add to 100%. Three equal shares can each display as 33.3%, totaling 99.9%. The underlying shares still cover the whole. Recalculating one arbitrary share to force the display total changes what that displayed number means.
Use a supported input type
PostgreSQL supports two-argument ROUND(value, n) for NUMERIC. If temp is DOUBLE PRECISION, write ROUND(CAST(AVG(temp) AS NUMERIC), 2). Averaging 20.5, 21.25 and 19.0 gives 20.25, and the cast lets you use two-argument rounding. With integer amount, PostgreSQL's average is already NUMERIC, so no cast is needed.
For exact PostgreSQL NUMERIC values, halfway cases round away from zero: 2.5 becomes 3 and -2.5 becomes -3. SQLite also gives 3.0 and -3.0 for these examples. PostgreSQL floating-point rounding is platform-dependent; nearest-even behavior is common. Binary approximation can also place a decimal just below an intended halfway point. Do not use a particular floating-point tie as a general rule for exact decimal arithmetic.
Notice two other dialect differences
PostgreSQL, MySQL, SQL Server and Oracle use a negative number of decimal places to round to the left of the decimal point. ROUND(1234.5, -2) gives 1200. SQLite treats a negative second argument as zero and gives 1235.0. To round to hundreds in SQLite, use ROUND(x / 100.0) * 100.
Casting to an integer is also database-specific: SQLite cuts off the fractional part, so 2.9 becomes 2; PostgreSQL rounds it to 3. Use ROUND when rounding is the purpose. PostgreSQL's TRUNC(x, n) cuts off digits instead: TRUNC(2.97, 1) is 2.9, while rounding it to one decimal gives 3.0.
Key Takeaways
- Round the aggregate result after completing its calculation.
- Rounding cannot repair earlier integer division.
- Rounded percentages can total 99.9% or 100.1%.
- Cast a PostgreSQL floating-point average to
NUMERICfor two-argumentROUND. - Negative rounding places and integer casts differ between databases.
5Answer business questions with summary queries
Business reports often need several measures of the same population: all orders, large orders and the share that are large. Conditional aggregation keeps those measures side by side. You will then assemble a complete report from its question, check totals and missing categories, and use a second aggregation step when the question is about customers or other groups instead of individual orders.
5.1 Count and add up by condition with CASE
Conditional aggregation lets different measures use different contributions while keeping the same overall set of rows. A CASE expression decides what each row contributes to a count or sum.
Count matching rows
COUNT(CASE WHEN amount >= 100 THEN 1 END) counts orders of at least 100. The expression returns 1 for a match and NULL for other rows because it has no ELSE. COUNT skips those NULLs, leaving three matches in the orders table.
SUM(CASE WHEN amount >= 100 THEN 1 ELSE 0 END) counts matches by adding ones and zeros. With existing input rows it gives the same count. COUNT(CASE WHEN amount >= 100 THEN 1 ELSE 0 END) does not: both 1 and 0 are non-NULL, so it counts all eight orders.
Add matching amounts
For a conditional total, return the amount itself:
SELECT region, COUNT(*) AS orders, COUNT(CASE WHEN amount >= 100 THEN 1 END) AS large_orders, SUM(CASE WHEN amount >= 100 THEN amount ELSE 0 END) AS large_total FROM orders GROUP BY region;
East has three orders, one large order and a large total of 120. North has two orders, both large, totaling 350. West has two orders, no large orders and a large total of zero. The NULL-region group has one order and zeros for both large measures.
The output keeps West because its input rows still exist. Adding WHERE amount >= 100 would remove West's rows before grouping, so West would disappear. That filter would also make COUNT(*) count only large orders, losing the all-order comparison.
Use the right denominator
Within each region, calculate the percentage of large orders with ROUND(100.0 * COUNT(CASE WHEN amount >= 100 THEN 1 END) / COUNT(*), 1) AS pct_large. East gives 33.3, North 100.0, West 0.0 and the NULL group 0.0. Each denominator is that group's order count, not the table-wide count of eight.
For an ungrouped report that can have no rows, guard the denominator with NULLIF(COUNT(*), 0). A count on an empty input is zero, but a percentage of an empty population has no denominator.
Make several category columns
A single result can show one count per region:
SELECT COUNT(CASE WHEN region = 'East' THEN 1 END) AS east, COUNT(CASE WHEN region = 'West' THEN 1 END) AS west, COUNT(CASE WHEN region = 'North' THEN 1 END) AS north FROM orders;
The counts are three, two and two. Eli's order belongs to none of these named-region tests. Add COUNT(CASE WHEN region IS NULL THEN 1 END) to count the one missing-region order. Comparing a NULL region to a name does not produce a true condition.
Recognize alternative syntax
PostgreSQL and SQLite from version 3.30.0 support COUNT(*) FILTER (WHERE amount >= 100). The filter applies to that aggregate, so an adjacent unfiltered COUNT(*) still counts all orders. MySQL and SQL Server do not support this aggregate FILTER syntax; the CASE form works on both.
SQLite and MySQL also allow SUM(amount >= 100) because comparisons produce numeric truth values. PostgreSQL rejects SUM of booleans. Use the CASE form when a query should work across these engines.
Key Takeaways
- Put conditions inside aggregates to keep measures for different subsets together.
COUNT(CASE ... THEN 1 END)skips nonmatches.ELSE 0works for a conditional sum, but makes a conditional count count nonmatches.- A group percentage uses that group’s population as its denominator.
- An aggregate
FILTERchanges only that aggregate’s inputs. - Use
IS NULLfor a separate count of missing categories.
5.2 Build and check a summary query from a question
A summary query starts with a question about a population at a particular level of detail. Translate those choices before writing SQL, then check that the output still answers the original question.
Build the report in order
Ask: “For each month of 2026 with at least three orders, how many orders were placed and what was the average amount, to two decimals?”
- The measures are
COUNT(*)andAVG(amount). - The grain, or meaning of one result row, is one calendar month.
- The row condition keeps dates in 2026.
- The group condition requires at least three orders.
- The output should be sorted by month, with the average rounded for display.
In SQLite:
SELECT strftime('%Y-%m', order_date) AS month, COUNT(*) AS orders, ROUND(AVG(amount), 2) AS avg_amount FROM orders WHERE order_date >= '2026-01-01' AND order_date < '2027-01-01' GROUP BY strftime('%Y-%m', order_date) HAVING COUNT(*) >= 3 ORDER BY month;
For PostgreSQL date values, replace the month expression in both places with to_char(order_date, 'YYYY-MM'). The date range includes the start of 2026 and excludes the start of 2027.
January has two orders and fails the group condition. February has three orders totaling 305, with average 101.67. March has three totaling 275, with average 91.67. The surviving groups account for six orders, not eight, because HAVING removed January.
Check before filtering groups away
Run the report without HAVING first. Counts across all groups should add to the row count under the same WHERE filter, and totals should add to the ungrouped total. Here those checks give eight and 780. Do not compare retained-group totals with the whole input after some groups have been excluded.
Look for a NULL group, inspect the order of results, and recompute a group by filtering the raw table. For East, WHERE region = 'East' should give three orders totaling 260. A discrepancy can reveal an extra grouping column, a misplaced row filter or an expression that became NULL.
Distinguish an average order from an average customer
AVG(amount) is 97.5 per order. Average spending per customer asks for a different unit: the five customer totals are 165, 140, 350, 95 and 30. Their average is 156.
Calculate that in two stages:
SELECT AVG(total) AS avg_customer_total FROM (SELECT customer, SUM(amount) AS total FROM orders GROUP BY customer) AS per_customer;
The inner query creates one row per customer. The outer query averages those rows, giving each customer one contribution. With all customers named, 1.0 * SUM(amount) / COUNT(DISTINCT customer) gives the same 156. Unequal order counts make averaging order amounts a different calculation.
Summarize the groups themselves
To count customers with more than one order, first list them and then count their rows:
SELECT COUNT(*) FROM (SELECT customer FROM orders GROUP BY customer HAVING COUNT(*) > 1) AS repeat_customers;
The answer is three. Directly nesting aggregates, as in MAX(AVG(amount)) at one query level, fails in SQLite and PostgreSQL. To find the region with the highest average, sort the grouped averages: ORDER BY AVG(amount) DESC LIMIT 1 returns North with 175. Oracle also supports nested group functions, but a separate query level makes the two calculations explicit. Check ties when a report asks for a top result.
Key Takeaways
- Choose measures, grain, row conditions and group conditions from the question.
- Check group counts and totals before removing groups with
HAVING. - An average per customer needs customer totals as its inputs.
- Use an inner grouped query and an outer aggregate to summarize groups.
SQL GROUP BY examples
1Turn order rows into reliable summary numbers
13 steps
- 1IntroductionRead the report question
A manager wants an order total, a customer count and an average discount. Each sounds like a simple number, but each uses the input differently. Each is an aggregate, which turns many rows into one value, and the wrong choice gives a misleading number. We will build those measures from the eight orders: first combine amounts, then choose what to count, then decide how NULL changes the average. Keep asking what one input row contributes to the result.
- 2ExplanationCombine the amounts
An aggregate combines values from many rows into one summary.
SUM(amount)adds the amounts,AVG(amount)finds their mean, andMIN(amount)andMAX(amount)locate the extremes. With noGROUP BY, all qualifying orders form one group. You can select several aggregates together because each one describes that same group of input rows. - 3ExampleCalculate one complete summary
The order amounts are 120, 80, 45, 200, 60, 95, 150 and 30. They total 780. Dividing that total by eight gives an average of 97.5; the smallest is 30 and the largest is 200.
SELECT SUM(amount) AS total, AVG(amount) AS mean_amount, MIN(amount) AS low, MAX(amount) AS high FROM orders;The output is one row with four labeled measures.
- 4ExampleChoose the input before adding
Now ask only about East.
WHERE region = 'East'keeps amounts 120, 45 and 95 before the aggregate runs.SELECT SUM(amount) AS total FROM orders WHERE region = 'East';The output is 260. The other regions contribute nothing because their orders failed the row condition. A filter on input rows changes the total itself, not just its displayed label.
- 5ChallengeKeep a quantity with its price
A sales table has two rows: quantity 4 at unit price 6, and quantity 2 at unit price 9. What does
SUM(quantity * unit_price)return, and why is multiplying the two sums different?---reveal---
The row revenues are 24 and 18, so the sum is 42. Multiplying the sums gives six times 15, or 90, because it no longer pairs each quantity with its own row’s price.
- 6ExplanationCount records or customers
The orders name Ana, Ben, Ana, Chen, Ben, Dara, Chen and Eli.
COUNT(customer)counts eight recorded names, including repeat appearances.COUNT(DISTINCT customer)counts five different names.The wording decides which fits: an order count needs rows, while a customer count needs distinct customers. Equal names on separate orders are expected repeats, not extra customers.
- 7ExampleCount the missing category
The region column has eight rows but only seven recorded values.
COUNT(*)is eight,COUNT(region)is seven, andCOUNT(DISTINCT region)is three.Subtracting the recorded-value count from the row count gives one missing region. Notice that a distinct region list would also include a NULL row; the distinct count ignores that NULL instead of treating it as a fourth named region.
- 8ChallengeChoose the right count
A coupon column contains NULL, 0, 8, 8 and 0. What values do
COUNT(*),COUNT(coupon)andCOUNT(DISTINCT coupon)return, in that order?---reveal---
They return five, four and two. There are five rows, four recorded values and two different recorded values, 0 and 8. Recorded zero values count; NULL does not enter either column count.
- 9ExplanationLeave an unknown score out
Consider scores 80, NULL, 70 and 90. The recorded scores total 240.
AVG(score)divides by the three recorded scores and returns 80.It does not divide by all four rows: a missing score is not a known zero. If the missing value represents a missed exam that should score zero, the intended calculation changes to
AVG(COALESCE(score, 0)), which gives 60. - 10ExamplePut the default at the right stage
The discount is missing on four orders. In
SUM(amount - discount), each of those row expressions becomes NULL, so only the other four contribute. Their net amounts total 345.If missing means no discount, use
SUM(amount - COALESCE(discount, 0)). All eight orders then contribute, and the total is 750.COALESCEaround the completed sum cannot bring omitted row contributions back. - 11ChallengeHandle a group with no known values
A query keeps three rows whose amounts are all NULL. What do
COUNT(*),COUNT(amount)andSUM(amount)return?---reveal---
They return three, zero and NULL. Rows exist, but there are no recorded amounts to count or add. An empty input would also give a NULL sum, but its row count would be zero. Use
COALESCE(SUM(amount), 0)only when the report should show that missing total as zero. - 12ChallengeCheck a calculation and its population
Four service records have charges 20, 30, 40 and 50, with credits 2, NULL, 0 and NULL. A missing credit means no credit. What total net charge and count of recorded credits should the report show?
---reveal---
Use
SUM(charge - COALESCE(credit, 0))for a net total of 138, andCOUNT(credit)for two recorded credits. The zero credit counts as recorded; replacing NULL inside the sum keeps all four charges. - 13RecapKeep the measure tied to its inputs
Choose the aggregate from the question, then trace the values entering it. Counts distinguish rows, recorded entries and different entries. For averages and row arithmetic, check what NULL removes and whether a replacement has a real meaning. When an answer looks surprising, inspect the surviving rows and their contributions before changing the SQL.
2Build a report at the right level of detail
14 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionDecide what a result row stands for
- 2ExplanationCollect rows by region
- 3ExampleKeep the NULL group visible
- 4TipSort before keeping the top rows
- 5ChallengePredict how many groups remain
- 6ExplanationExplain why a name is ambiguous
- 7ExampleRepair the report from its question
- 8TipRecognize a permissive result
- 9ChallengeChoose a meaningful repair
- 10ExampleSplit a group by a second value
- 11ExplanationBuild a calendar-month label
- 12ChallengeFind the grouping expression
- 13ChallengeCheck the report’s grain and labels
- 14RecapRead each row as one group
3Place filters before or after aggregation
10 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionSeparate population from selection
- 2ExplanationTest the completed group
- 3ExampleUse a count without displaying it
- 4ChallengeCheck the actual value being compared
- 5ExplanationFollow the result’s logical stages
- 6ExampleRemove small orders first
- 7ExampleThen remove small group totals
- 8ChallengeDiagnose a changed question
- 9ChallengeCombine row and group restrictions
- 10RecapLet the condition name its stage
4Keep fractions and round the final result
11 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionKeep calculation separate from display
- 2ExampleFind where the fraction disappears
- 3ExplanationIntroduce the decimal early
- 4TipProtect an empty denominator
- 5ChallengeRepair a percentage expression
- 6ExplanationRound the finished average
- 7ExampleSee early rounding change a total
- 8ChallengeCheck a dialect difference
- 9ConnectionChoose the type for exact amounts
- 10ChallengeCheck type before rounding
- 11RecapTrace the number through each stage
5Build and check a business summary
13 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionKeep all orders and selected orders together
- 2ExplanationMake nonmatches contribute NULL
- 3ExampleKeep the full group available
- 4ExampleAdd the amount or add one
- 5ChallengePreserve a subset denominator
- 6ExplanationTranslate the business wording
- 7ExampleBuild the monthly result
- 8TipCheck before hiding groups
- 9ExplanationChange the input unit for an average
- 10ExampleRead the two query levels
- 11ChallengeChoose the right averaging unit
- 12ChallengeCount only qualifying customer groups
- 13RecapCheck the population and the grain
SQL GROUP BY practice questions
Set 1: Read counts, groups and calculations10 Practice Questions
Question 1 of 10
SUM(distance) return?Show answerHide answer
SUM adds the three distances, giving 71. The largest distance, 35, would be the result of MAX(distance), while an average would divide the combined distance by three; neither answers the request for a total.Question 2 of 10
COUNT(CASE WHEN status = 'closed' THEN 1 ELSE 0 END) return?Show answerHide answer
Question 3 of 10
AVG(delay) return?Show answerHide answer
AVG divides by their count of three, giving eight minutes. Dividing by all four rows would give six minutes, effectively replacing the unknown delay with zero even though no such measurement was recorded.Question 4 of 10
SELECT department, COUNT(*) FROM shifts GROUP BY department return?Show answerHide answer
Question 5 of 10
FROM parts GROUP BY category?Show answerHide answer
COUNT(DISTINCT supplier) turns all recorded supplier values in a category into one count of different suppliers. A plain supplier value is ambiguous because the category can have several. COUNT(supplier) would count records with a supplier, including repeat appearances.Question 6 of 10
paid_on with records from 2024 through 2026. What grouping expression keeps each calendar month separate?Show answerHide answer
Question 7 of 10
HAVING SUM(amount) > 200?Show answerHide answer
>= 200. HAVING applies this comparison to each completed team total, so it keeps or removes each entire team result.Question 8 of 10
ORDER BY supplier. Categories have several suppliers. How can it show the highest-cost categories first without changing its grain?Show answerHide answer
Question 9 of 10
SUM(quantity) / COUNT(*) * 1.0 return?Show answerHide answer
1.0 * SUM(quantity) / COUNT(*).Question 10 of 10
ROUND(SUM(fee), 1) but keeps the overall group only with HAVING SUM(fee) >= 0.8. What result does it return?Show answerHide answer
Set 2: Choose the report’s population and detail10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
COUNT(priority) return?Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
MIN(delivery_date) and MAX(delivery_date) return, in that order?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
SELECT COUNT(*) FROM (SELECT DISTINCT city, agent FROM visits) AS pairs return?Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
SUM(cost - credit) and the intended total net cost, in that order?Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
ORDER BY total DESC, customer ASC LIMIT 2 keep, assuming the names sort alphabetically?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
ROUND(SUM(fee), 1) and SUM(ROUND(fee, 1)) produce, in that order, ignoring tiny floating-point display artifacts?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
SELECT training FROM roster GROUP BY training HAVING COUNT(*) >= 3 return?Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
WHERE amount >= 60 GROUP BY team HAVING AVG(amount) > 80, what remains?Show answerHide answer
Save this lesson to see this answer.
Set 3: Combine missing values, types and groups10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
SELECT 17 / 5 return in SQLite?Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
SELECT SUM(quantity * price) FROM stock WHERE department = 'A' return?Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
COUNT(DISTINCT region) return?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
jobs match state = 'queued'. What does SELECT team, COALESCE(SUM(hours), 0) FROM jobs WHERE state = 'queued' GROUP BY team return?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
WHERE area IS NOT NULL GROUP BY area, what are the group counts?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
CASE WHEN amount >= 100 THEN 'large' ELSE 'small' END. How many groups result?Show answerHide answer
Save this lesson to see this answer.
Set 4: Diagnose and finish complete reports10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
WHERE amount >= 30 with grouping all rows and using HAVING MIN(amount) >= 30. What averages appear?Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
temperature is DOUBLE PRECISION. How do you round its average to two decimal places?Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
COUNT(*) FILTER (WHERE paid = 1) appears beside COUNT(*) in a grouped query with no WHERE clause. What does the filter restrict?Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
COUNT(*), COUNT(value) and MAX(value), in that order?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
WHERE zone <> 'West'. Which zone groups can remain?Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
Show answerHide answer
Save this lesson to see this answer.
Stuck on your own question? Homework Help
Key Terms
A
Aggregate expression
SUM(amount - discount) evaluates the subtraction per row before it adds the non-NULL results.Aggregate FILTER
Aggregate function
SUM(amount) to turn many order amounts into one total for the selected period.All-NULL input
COUNT(x) is zero.COUNT(*) four and COUNT(discount) zero when a filter selects only those orders.Average per customer
AVG
AVG(amount) gives 97.5 for eight orders whose amounts add to 780, measuring the average order amount.B
Bare column
C
CASE category
CASE expression, which can become the basis for a grouped report.amount >= 100 puts an order of exactly 100 in the category labeled “large.”CAST
REAL enables decimal division, while a cast to NUMERIC may leave a whole value as an integer.COALESCE
COALESCE(SUM(amount), 0) displays zero when an empty input would otherwise produce a NULL total.Collation
MIN query stay the same.Conditional aggregation
Conditional count
COUNT.COUNT(CASE WHEN amount >= 100 THEN 1 END) counts large orders while skipping the NULL results of nonmatches.Conditional total
CASE expression that returns zero for nonmatches.CASE; returning 1 would count orders instead.COUNT(*)
COUNT(*) equal to eight even if one order has no recorded region.COUNT(column)
COUNT(discount) includes an order with discount zero but leaves out an order with discount NULL.COUNT(DISTINCT column)
COUNT(DISTINCT customer) for the number of named customers, even when some place several orders.D
Decimal operand
Derived table
FROM, allowing a second query to summarize rows created by the first query.DISTINCT
SUM(DISTINCT quantity) adds each quantity value once, so repeated real sales can be left out of the total.Distinct combination
Duplicate detection
HAVING COUNT(*) > 1 identifies customers with repeat order records.E
Empty input
WHERE condition finds no matching records.F
Final rounding
Floating-point number
DOUBLE PRECISION addition can show 0.30000000000000004 where exact decimal arithmetic gives 0.3.Functional dependency
G
Grain
GROUP BY
GROUP BY region lets the same query calculate a separate order count and amount total for each region.Group condition
Group percentage
Grouping column
Grouping expression
CASE.Grouping rule
H
Halfway value
NUMERIC rounding sends 2.5 to 3 and -2.5 to -3, moving both ties away from zero.HAVING
HAVING SUM(amount) > 200 keeps regions by their total, without removing small orders from surviving groups.I
Integer division
IS NULL
WHERE region IS NULL selects orders whose region has not been recorded, such as an order from a customer named Eli.L
Logical query order
WHERE before GROUP BY explains why removing small orders changes a region’s later average.M
Measure
MIN and MAX
MIN(order_date) finds the earliest date when the column contains dates or consistently formatted ISO date text.Multi-column grouping
N
NULL
amount - discount NULL, causing that row’s calculated value to be skipped by SUM.NULL group
NULLIF
NULLIF(COUNT(*), 0) makes an empty report’s denominator NULL, avoiding a PostgreSQL division-by-zero error.NUMERIC and DECIMAL
O
ONLY_FULL_GROUP_BY
ORDER BY
ORDER BY total DESC lists the largest group totals first after each group has been calculated.R
Reconciliation
HAVING first so excluded groups do not explain a difference between the report and the input total.ROUND
ROUND(AVG(amount), 2) rounds the completed average to two places, keeping the earlier calculation intact.Rounding discrepancy
Row condition
amount >= 60 can keep one East order while removing another, before any regional summary exists.S
SELECT alias
ORDER BY but has dialect-specific uses elsewhere.ORDER BY total but needs the aggregate expression spelled out in a same-level HAVING.SUM
SUM(quantity * unit_price) totals row revenues after each quantity is multiplied by its matching price.T
Tie-breaker
ORDER BY total DESC, customer gives tied customer totals a stable order before a limit is applied.Truncation
TRUNC and SQLite uses it when casting to integer.TRUNC(2.97, 1) gives 2.9 in PostgreSQL, while ROUND(2.97, 1) gives 3.0 for that exact decimal.U
Unrounded comparison
HAVING AVG(amount) >= 86.67 if its exact value is smaller.W
WHERE
WHERE before it calculates monthly totals.Y
Year-month key
About this lesson
Make your own Study Materials
Turn your notes, slides and documents into Study Guides, Practice Questions, Flashcards and 6 more tools.