SQL GROUP BY: Aggregation and Grouping

Learn to answer summary questions from a database table: how many, how much, on average, the smallest and the largest. Use COUNT, SUM, AVG, MIN and MAX, group rows with GROUP BY to get one result per category, and keep only the groups you need with HAVING. Along the way you learn how missing values and whole-number division change results, how to round averages and percentages, and how to check that a summary query gives the right numbers. Examples use standard SQL that runs on SQLite and PostgreSQL, with differences in MySQL, SQL Server and Oracle pointed out where they matter.

BeginnerLesson 4 of 6 in SQL Basics Course

What you will learn

  1. 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 values
      Use 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 values
      Choose between COUNT(*), COUNT(column) and COUNT(DISTINCT column), and know how each one treats repeated and missing values.
    • Predict how NULL values affect aggregates
      Know that aggregates skip NULL values, predict the results for empty inputs, and replace NULL with a default when the question calls for it.
  2. 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 BY
      Split 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 show
      Know 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 values
      Group 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.
  3. 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 condition
      Write HAVING conditions on aggregate values, including aggregates that are not displayed, to filter a grouped result.
    • Choose between WHERE and HAVING
      Use the logical order of a query to decide whether a condition filters rows or groups, and predict how that choice changes the result.
  4. 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 percentages
      Recognize when dividing aggregates loses the decimals, fix it with a decimal operand, and know how result types differ between databases.
    • Round aggregated results
      Round averages, totals and percentages with ROUND, round at the right moment, and handle the differences between SQLite and PostgreSQL.
  5. 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 CASE
      Put 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 question
      Translate a business question into a grouped query step by step, avoid mixing up levels of detail, and check that the numbers are right.

SQL GROUP BY study guide

5 chapters

1

Summarize 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_idcustomerregionamountquantitydiscountorder_date
1AnaEast1202102026-01-05
2BenWest801NULL2026-01-17
3AnaEast45352026-02-02
4ChenNorth2004NULL2026-02-14
5BenWest60102026-02-20
6DaraEast952NULL2026-03-03
7ChenNorth1505152026-03-09
8EliNULL301NULL2026-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.
  • WHERE selects the rows used by the aggregate.
  • Calculate each row’s contribution inside SUM when values must stay paired.
  • MIN and MAX follow 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
ExpressionWhat 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.
  • COUNT returns 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 COALESCE changes inputs; an outer one changes a NULL result.
  • Empty input gives zero for COUNT and NULL for the other four aggregates.
  • NULL inside an arithmetic expression can remove a row’s contribution.
2

Group 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 BY to 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 HAVING and ORDER 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 BY does not create missing combinations with zero counts.
  • The order of grouping columns does not control output order.
3

Filter 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

  • HAVING keeps or removes completed groups.
  • A HAVING aggregate need not appear in SELECT.
  • Combine group tests with AND, OR and NOT.
  • Repeat aggregate expressions in HAVING for 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:

  1. FROM supplies the rows.
  2. WHERE keeps qualifying rows.
  3. GROUP BY forms groups, and aggregates calculate their measures.
  4. HAVING keeps qualifying groups.
  5. SELECT produces the output values; DISTINCT removes duplicate output rows if requested.
  6. ORDER BY sorts the result, and LIMIT keeps the requested number of rows.

The database may arrange its internal execution differently.

Put each condition at its level
RequestClause
Orders with amount >= 60WHERE
Orders in a given date rangeWHERE
Regions with SUM(amount) > 200HAVING
Customers with more than one orderHAVING 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

  • WHERE changes aggregate inputs; HAVING filters 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.
4

Calculate 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
DatabaseBehavior to watch
SQLiteInteger division truncates; AVG returns floating point.
PostgreSQLInteger division truncates; AVG of integer or NUMERIC input returns NUMERIC.
SQL ServerInteger 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 NULLIF to handle a zero denominator.
  • Aggregate result types depend on both the engine and input type.
  • SQLite NUMERIC affinity 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 NUMERIC for two-argument ROUND.
  • Negative rounding places and integer casts differ between databases.
5

Answer 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 0 works for a conditional sum, but makes a conditional count count nonmatches.
  • A group percentage uses that group’s population as its denominator.
  • An aggregate FILTER changes only that aggregate’s inputs.
  • Use IS NULL for 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?”

  1. The measures are COUNT(*) and AVG(amount).
  2. The grain, or meaning of one result row, is one calendar month.
  3. The row condition keeps dates in 2026.
  4. The group condition requires at least three orders.
  5. 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

5 Walkthroughs

1

Turn order rows into reliable summary numbers

13 steps
Choose aggregates and counts, then trace the effects of missing values.
  1. 1
    IntroductionRead 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.

  2. 2
    ExplanationCombine the amounts

    An aggregate combines values from many rows into one summary. SUM(amount) adds the amounts, AVG(amount) finds their mean, and MIN(amount) and MAX(amount) locate the extremes. With no GROUP BY, all qualifying orders form one group. You can select several aggregates together because each one describes that same group of input rows.

  3. 3
    ExampleCalculate 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.

  4. 4
    ExampleChoose 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.

  5. 5
    ChallengeKeep 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.

  6. 6
    ExplanationCount 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.

  7. 7
    ExampleCount the missing category

    The region column has eight rows but only seven recorded values. COUNT(*) is eight, COUNT(region) is seven, and COUNT(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.

  8. 8
    ChallengeChoose the right count

    A coupon column contains NULL, 0, 8, 8 and 0. What values do COUNT(*), COUNT(coupon) and COUNT(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.

  9. 9
    ExplanationLeave 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.

  10. 10
    ExamplePut 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. COALESCE around the completed sum cannot bring omitted row contributions back.

  11. 11
    ChallengeHandle a group with no known values

    A query keeps three rows whose amounts are all NULL. What do COUNT(*), COUNT(amount) and SUM(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.

  12. 12
    ChallengeCheck 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, and COUNT(credit) for two recorded credits. The zero credit counts as recorded; replacing NULL inside the sum keeps all four charges.

  13. 13
    RecapKeep 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.

2

Build a report at the right level of detail

14 steps
Group by categories, repair ambiguous columns and create combined or calculated groups.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionDecide what a result row stands for
  2. 2
    ExplanationCollect rows by region
  3. 3
    ExampleKeep the NULL group visible
  4. 4
    TipSort before keeping the top rows
  5. 5
    ChallengePredict how many groups remain
  6. 6
    ExplanationExplain why a name is ambiguous
  7. 7
    ExampleRepair the report from its question
  8. 8
    TipRecognize a permissive result
  9. 9
    ChallengeChoose a meaningful repair
  10. 10
    ExampleSplit a group by a second value
  11. 11
    ExplanationBuild a calendar-month label
  12. 12
    ChallengeFind the grouping expression
  13. 13
    ChallengeCheck the report’s grain and labels
  14. 14
    RecapRead each row as one group
3

Place filters before or after aggregation

10 steps
Trace WHERE and HAVING through a report and predict how their placement changes the answer.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionSeparate population from selection
  2. 2
    ExplanationTest the completed group
  3. 3
    ExampleUse a count without displaying it
  4. 4
    ChallengeCheck the actual value being compared
  5. 5
    ExplanationFollow the result’s logical stages
  6. 6
    ExampleRemove small orders first
  7. 7
    ExampleThen remove small group totals
  8. 8
    ChallengeDiagnose a changed question
  9. 9
    ChallengeCombine row and group restrictions
  10. 10
    RecapLet the condition name its stage
4

Keep fractions and round the final result

11 steps
Trace numeric types, avoid zero denominators and round aggregates at the right stage.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionKeep calculation separate from display
  2. 2
    ExampleFind where the fraction disappears
  3. 3
    ExplanationIntroduce the decimal early
  4. 4
    TipProtect an empty denominator
  5. 5
    ChallengeRepair a percentage expression
  6. 6
    ExplanationRound the finished average
  7. 7
    ExampleSee early rounding change a total
  8. 8
    ChallengeCheck a dialect difference
  9. 9
    ConnectionChoose the type for exact amounts
  10. 10
    ChallengeCheck type before rounding
  11. 11
    RecapTrace the number through each stage
5

Build and check a business summary

13 steps
Create subset measures, choose a report grain and summarize grouped results in two query levels.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionKeep all orders and selected orders together
  2. 2
    ExplanationMake nonmatches contribute NULL
  3. 3
    ExampleKeep the full group available
  4. 4
    ExampleAdd the amount or add one
  5. 5
    ChallengePreserve a subset denominator
  6. 6
    ExplanationTranslate the business wording
  7. 7
    ExampleBuild the monthly result
  8. 8
    TipCheck before hiding groups
  9. 9
    ExplanationChange the input unit for an average
  10. 10
    ExampleRead the two query levels
  11. 11
    ChallengeChoose the right averaging unit
  12. 12
    ChallengeCount only qualifying customer groups
  13. 13
    RecapCheck the population and the grain

SQL GROUP BY practice questions

40 Practice Questions in 4 sets

Set 1: Read counts, groups and calculations10 Practice Questions

Question 1 of 10

A deliveries table records distances 14, 22 and 35. What value does SUM(distance) return?
  1. 35
  2. 23.67
  3. 71
  4. 3
Show answerHide answer
Answer: C
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

Five requests have status values closed, open, closed, open and open. What does COUNT(CASE WHEN status = 'closed' THEN 1 ELSE 0 END) return?
  1. 5
  2. 2
  3. 3
  4. 0
Show answerHide answer
Answer: A
Every CASE result is a recorded value: closed requests produce 1 and open requests produce 0. COUNT counts both, giving five. To count only the two closed requests, omit ELSE so nonmatches produce NULL, or add the one-and-zero indicators with SUM instead.

Question 3 of 10

Four deliveries have delays 6, NULL, 10 and 8 minutes. A missing delay means it has not been measured. What average does AVG(delay) return?
  1. 6
  2. 24
  3. 10
  4. 8
Show answerHide answer
Answer: D
The three measured delays total 24 minutes, and 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

A shifts table contains department values Sales, Support, Sales, NULL and NULL. How many rows does SELECT department, COUNT(*) FROM shifts GROUP BY department return?
  1. 5
  2. 3
  3. 2
  4. 4
Show answerHide answer
Answer: B
The query forms one group for Sales, one for Support and one for the missing department value, so it returns three rows. NULL grouping values belong together; treating each NULL as a separate group would create an extra result row that SQL does not produce.

Question 5 of 10

A parts table has several suppliers per category. The report needs one row per category and the number of its different suppliers. What SELECT list fits FROM parts GROUP BY category?
  1. category, supplier, COUNT(*)
  2. category, COUNT(DISTINCT supplier)
  3. category, COUNT(supplier)
  4. category, MIN(supplier)
Show answerHide answer
Answer: B
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

A PostgreSQL payments table has a DATE column paid_on with records from 2024 through 2026. What grouping expression keeps each calendar month separate?
  1. EXTRACT(MONTH FROM paid_on)
  2. EXTRACT(YEAR FROM paid_on)
  3. paid_on
  4. to_char(paid_on, 'YYYY-MM')
Show answerHide answer
Answer: D
The year-month expression gives each calendar month its own key, keeping June in different years separate. A month number alone would merge those Junes, while grouping by the full date would split a month into daily groups and change the requested level of detail.

Question 7 of 10

A grouped report has team totals Red 180, Blue 260 and Green 200. Which teams remain after HAVING SUM(amount) > 200?
  1. Blue
  2. Blue and Green
  3. Red and Blue
  4. Green
Show answerHide answer
Answer: A
Only Blue has a total strictly above 200. Green equals the threshold and therefore fails the greater-than test; it would pass >= 200. HAVING applies this comparison to each completed team total, so it keeps or removes each entire team result.

Question 8 of 10

A PostgreSQL query selects category and total cost, groups only by category, and uses ORDER BY supplier. Categories have several suppliers. How can it show the highest-cost categories first without changing its grain?
  1. Add supplier to GROUP BY.
  2. Use ORDER BY supplier DESC.
  3. Use ORDER BY SUM(cost) DESC.
  4. Add supplier to the SELECT list.
Show answerHide answer
Answer: C
Sorting by SUM(cost) uses one defined measure for every category group and keeps one row per category. A plain supplier cannot order a group containing several suppliers. Adding supplier to GROUP BY would make the syntax valid but split categories, changing the requested report detail.

Question 9 of 10

In SQLite, integer quantities total 23 across six rows. What does SUM(quantity) / COUNT(*) * 1.0 return?
  1. 3.0
  2. 3.8333333333333335
  3. 4.0
  4. 23.0
Show answerHide answer
Answer: A
Integer division happens first: 23 divided by six is truncated to three. Multiplying that result by 1.0 gives 3.0 and cannot recover the lost fraction. To keep the fractional mean, introduce the decimal operand before division with 1.0 * SUM(quantity) / COUNT(*).

Question 10 of 10

A SQLite table has three fees of 0.26. A query selects ROUND(SUM(fee), 1) but keeps the overall group only with HAVING SUM(fee) >= 0.8. What result does it return?
  1. One row containing 0.8.
  2. One row containing 0.9.
  3. One row containing NULL.
  4. No row.
Show answerHide answer
Answer: D
The fees total 0.78, which would display as 0.8 after rounding. HAVING compares the unrounded sum with 0.8, so the group fails and no row appears. Treating the rounded display as the tested value would accept a group whose actual total is below the threshold.
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

A ticket table has priority values 0, NULL, 2, 2 and NULL. What does COUNT(priority) return?
  1. 5
  2. 3
  3. 2
  4. 1
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

A work_log table has consultant, year and hours columns. A report needs one total hours value per consultant for 2025, keeping only totals above 120. What query shape answers it?
  1. WHERE year = 2025 AND hours > 120 GROUP BY consultant
  2. GROUP BY consultant, hours HAVING SUM(hours) > 120
  3. WHERE hours > 120 GROUP BY consultant, year
  4. WHERE year = 2025 GROUP BY consultant HAVING SUM(hours) > 120
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

A table stores delivery dates as ISO text: '2025-12-28', '2026-01-04' and '2025-02-19'. What do MIN(delivery_date) and MAX(delivery_date) return, in that order?
  1. 2025-02-19 and 2026-01-04
  2. 2025-12-28 and 2026-01-04
  3. 2026-01-04 and 2025-02-19
  4. 2026-01-04 and 2025-12-28
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

A SQLite table has (city, agent) pairs (A, Jo), (A, Jo), (A, Kai), (NULL, Lee) and (NULL, Lee). What does SELECT COUNT(*) FROM (SELECT DISTINCT city, agent FROM visits) AS pairs return?
  1. 2
  2. 4
  3. 3
  4. 5
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

Four repairs have (cost, credit) values (50, 5), (80, NULL), (40, 0) and (30, NULL). NULL means no credit. What are SUM(cost - credit) and the intended total net cost, in that order?
  1. 195 and 195
  2. 85 and 85
  3. 115 and 195
  4. 85 and 195
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

Customer totals are Ali 90, Bea 120, Cam 120 and Dev 75. What rows does ORDER BY total DESC, customer ASC LIMIT 2 keep, assuming the names sort alphabetically?
  1. Ali 90 and Bea 120
  2. Bea 120 and Cam 120
  3. Cam 120 and Bea 120
  4. Bea 120 and Ali 90
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

A fees table contains three values of 0.36. What do ROUND(SUM(fee), 1) and SUM(ROUND(fee, 1)) produce, in that order, ignoring tiny floating-point display artifacts?
  1. 1.2 and 1.1
  2. 1.1 and 1.1
  3. 1.1 and 1.2
  4. 1.2 and 1.2
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

A table contains (site, worker) pairs (A, Ren), (A, Ren), (A, Sol), (B, Ren), (NULL, Sol) and (NULL, Sol). How many groups result from grouping by site, and then by site and worker?
  1. 3 and 4
  2. 3 and 6
  3. 2 and 3
  4. 3 and 3
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

A roster has training values Safety, Safety, Tools, Tools, Tools and First Aid. What does SELECT training FROM roster GROUP BY training HAVING COUNT(*) >= 3 return?
  1. Safety and Tools
  2. Tools
  3. Tools and First Aid
  4. No rows
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

Team A has amounts 20, 80 and 100; Team B has 60 and 90. After WHERE amount >= 60 GROUP BY team HAVING AVG(amount) > 80, what remains?
  1. B with average 75.
  2. A with average about 66.67.
  3. A with average 90.
  4. A with 90 and B with 75.
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

What does SELECT 17 / 5 return in SQLite?
  1. 3.4
  2. 4
  3. 0
  4. 3
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

A query groups claims by team but selects a plain employee name. Each team has several employees. SQLite shows a name and PostgreSQL rejects the query. What explains the difference?
  1. SQLite groups by the employee automatically; PostgreSQL needs an explicit group.
  2. SQLite permits a bare column; the name does not describe the whole team.
  3. SQLite returns the first employee alphabetically; PostgreSQL needs an explicit sort.
  4. SQLite removes extra employees before grouping; PostgreSQL retains every employee.
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

Site L has amounts 30, 50 and 80; site M has 20 and 40. You need each site’s percentage of orders at least 60, including sites with none. What percentages should appear?
  1. L 100.0%; M absent.
  2. L 20.0%; M 0.0%.
  3. L 33.3%; M 0.0%.
  4. L 50.0%; M 0.0%.
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

Customer Inez has amounts 20, 30 and 50; Omar has 200. What are average order amount and average total amount per customer, in that order?
  1. 75 and 150
  2. 150 and 75
  3. 75 and 100
  4. 100 and 150
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

A stock table has (department, quantity, price) rows (A, 2, 7), (A, 3, 4) and (B, 5, 9). What does SELECT SUM(quantity * price) FROM stock WHERE department = 'A' return?
  1. 55
  2. 71
  3. 26
  4. 14
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

A region column contains NULL, West, West, East and NULL. What does COUNT(DISTINCT region) return?
  1. 2
  2. 3
  3. 5
  4. 1
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

No rows in jobs match state = 'queued'. What does SELECT team, COALESCE(SUM(hours), 0) FROM jobs WHERE state = 'queued' GROUP BY team return?
  1. One NULL-team row with zero.
  2. One NULL-team row with NULL.
  3. One zero-total row for every team.
  4. No rows.
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

A table has area values North, NULL, South, NULL and North. After WHERE area IS NOT NULL GROUP BY area, what are the group counts?
  1. North 2; South 1; NULL 2.
  2. North 2; South 1.
  3. North 1; South 1.
  4. North 3; South 2.
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

A report needs average invoice amounts per office, including only invoices dated in 2025 and displaying only offices with more than five qualifying invoices. Where do the date test and count test belong?
  1. Both tests in WHERE.
  2. Date in HAVING; count in WHERE.
  3. Both tests in HAVING.
  4. Date in WHERE; count in HAVING.
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

Orders have (date, amount) values (2025-01-10, 90), (2026-01-12, 140), (2026-01-20, 60) and (2026-02-03, 110). Group by year-month and by CASE WHEN amount >= 100 THEN 'large' ELSE 'small' END. How many groups result?
  1. 4
  2. 3
  3. 2
  4. 5
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

A report groups a bookings table by room. What HAVING condition keeps rooms with at least four bookings?
  1. HAVING COUNT(*) >= 4
  2. HAVING COUNT(*) > 4
  3. HAVING COUNT(*) = 4
  4. HAVING COUNT(*) <= 4
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

Branch A has amounts 10, 40 and 70; branch B has 30 and 50. Compare grouping after WHERE amount >= 30 with grouping all rows and using HAVING MIN(amount) >= 30. What averages appear?
  1. WHERE: A 40 and B 40; HAVING: B 40 only.
  2. WHERE: A 55 and B 40; HAVING: A 55 and B 40.
  3. WHERE: A 55 and B 40; HAVING: B 40 only.
  4. WHERE: B 40 only; HAVING: A 40 and B 40.
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

An ungrouped report can have no input rows. What expression gives the percentage of rows with a recorded value, preserves decimals, and returns NULL for an empty population in both SQLite and PostgreSQL?
  1. 100 * COUNT(value) / NULLIF(COUNT(*), 0)
  2. 100.0 * COUNT(value) / NULLIF(COUNT(*), 0)
  3. 100.0 * COUNT(value) / COALESCE(COUNT(*), 1)
  4. 100.0 * COUNT(*) / NULLIF(COUNT(value), 0)
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

In PostgreSQL, temperature is DOUBLE PRECISION. How do you round its average to two decimal places?
  1. ROUND(AVG(temperature), 2)
  2. CAST(ROUND(AVG(temperature)) AS NUMERIC)
  3. ROUND(CAST(AVG(temperature) AS INTEGER), 2)
  4. ROUND(CAST(AVG(temperature) AS NUMERIC), 2)
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

In PostgreSQL, COUNT(*) FILTER (WHERE paid = 1) appears beside COUNT(*) in a grouped query with no WHERE clause. What does the filter restrict?
  1. Every aggregate in the group.
  2. Only the final result display.
  3. Only the paid-count aggregate.
  4. Every row before groups form.
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

Customer U has amounts 40 and 70, V has 60, 60 and 10, and W has 150. How many customers have at least two orders and a total above 120?
  1. 2
  2. 3
  3. 5
  4. 1
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

A query keeps four rows with values NULL, NULL, NULL and NULL. What are COUNT(*), COUNT(value) and MAX(value), in that order?
  1. 4, 0, NULL
  2. 0, 0, NULL
  3. 4, 4, NULL
  4. 4, 0, 0
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

A visits table has (customer, zone) pairs (Jo, East), (Jo, East), (Kai, West), (Kai, North), (Lee, NULL) and (Lee, NULL). How does adding zone to grouping by customer change the number of groups?
  1. It stays at three.
  2. It rises from three to four.
  3. It rises from three to six.
  4. It rises from three to five.
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

A grouped report includes zones East, West and NULL. A row filter is WHERE zone <> 'West'. Which zone groups can remain?
  1. East and NULL.
  2. East and West.
  3. East only.
  4. NULL only.
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

In a SQLite orders table, May 2025 has amounts 20 and 100, May 2026 has 50 and 70, and June 2026 has 200. A report must show only 2026 months with at least two orders, with rounded average amount. What result fits the request?
  1. 2026-05: four orders, average 60.00.
  2. 2026-05: two orders, average 60.00.
  3. 2026-06: one order, average 200.00.
  4. 2026-05: two orders, average 120.00.
Show answerHide answer

Save this lesson to see this answer.

Stuck on your own question? Homework Help

Key Terms

58 Key Terms, each with its definition

A

Aggregate expression
An aggregate expression applies a summary function to a column or row calculation, such as adding each row’s quantity times price.
SUM(amount - discount) evaluates the subtraction per row before it adds the non-NULL results.
Aggregate FILTER
An aggregate FILTER restricts the input to one aggregate while leaving other aggregates in the same query free to use other rows.
PostgreSQL can put a filtered large-order count beside an unfiltered count of all orders in the same group.
Aggregate function
An aggregate function combines values from several input rows into one result, such as a count, sum, average or extreme.
A sales report uses SUM(amount) to turn many order amounts into one total for the selected period.
All-NULL input
An all-NULL input has rows but no recorded values for the expression being aggregated, so COUNT(x) is zero.
Four orders with unknown discounts give COUNT(*) four and COUNT(discount) zero when a filter selects only those orders.
Average per customer
An average per customer gives each customer’s total one contribution, which differs from averaging all individual order amounts.
When five customers place eight orders totaling 780, their average per customer is 156, even though the eight orders average 97.5.
AVG
AVG is an aggregate function that calculates the arithmetic mean of its non-NULL inputs, using each recorded value once.
AVG(amount) gives 97.5 for eight orders whose amounts add to 780, measuring the average order amount.

B

Bare column
A bare column is a selected column that is neither a grouping column nor inside an aggregate in an aggregate query.
SQLite may show one customer beside a region total even when several customers contributed to that region.

C

CASE category
A CASE category is a label assigned by conditions in a CASE expression, which can become the basis for a grouped report.
A threshold of amount >= 100 puts an order of exactly 100 in the category labeled “large.”
CAST
CAST converts a value to a requested SQL type, with effects that depend on the destination type and the database engine.
A SQLite cast to REAL enables decimal division, while a cast to NUMERIC may leave a whole value as an integer.
COALESCE
COALESCE is a function that returns its first non-NULL argument, allowing a chosen default for an input or a final result.
COALESCE(SUM(amount), 0) displays zero when an empty input would otherwise produce a NULL total.
Collation
A collation is a set of rules the database uses to compare and order text, including how letter case affects comparisons.
The minimum of text values can change with collation even though the rows and the MIN query stay the same.
Conditional aggregation
Conditional aggregation makes each row’s contribution depend on a condition, allowing several subset measures in one report.
A regional report can display all orders and large orders together without filtering small orders out first.
Conditional count
A conditional count counts only rows that meet a chosen condition, often by making nonmatches return NULL inside COUNT.
COUNT(CASE WHEN amount >= 100 THEN 1 END) counts large orders while skipping the NULL results of nonmatches.
Conditional total
A conditional total adds the amounts from matching rows, usually with a CASE expression that returns zero for nonmatches.
A large-order total returns the qualifying amount itself from CASE; returning 1 would count orders instead.
COUNT(*)
COUNT(*) is the aggregate form that counts every input row, including rows with missing values in one or more columns.
A table with eight orders has COUNT(*) equal to eight even if one order has no recorded region.
COUNT(column)
COUNT(column) counts rows with a non-NULL value in the named column; it includes repeated values and recorded zeros.
COUNT(discount) includes an order with discount zero but leaves out an order with discount NULL.
COUNT(DISTINCT column)
COUNT(DISTINCT column) counts the different non-NULL values of a column, with repeated recorded values counted only once.
Use COUNT(DISTINCT customer) for the number of named customers, even when some place several orders.

D

Decimal operand
A decimal operand is a numeric input that makes a calculation retain a fractional result when introduced before integer division.
Multiplying the sum by 1.0 before dividing preserves the fractional part of an average built from totals.
Derived table
A derived table is the result of a query used in FROM, allowing a second query to summarize rows created by the first query.
An inner query creates customer totals; an outer query treats those totals as its input when calculating their average.
DISTINCT
DISTINCT removes duplicate values or rows at the stage where it is applied, such as inside an aggregate or in the output list.
SUM(DISTINCT quantity) adds each quantity value once, so repeated real sales can be left out of the total.
Distinct combination
A distinct combination is a unique set of values across selected columns, treated as one row in a distinct result.
East/Ana and East/Dara are separate pairs when a report lists distinct region-and-customer combinations.
Duplicate detection
Duplicate detection finds repeated values by grouping records on those values and retaining groups with counts above one.
Grouping by customer and using HAVING COUNT(*) > 1 identifies customers with repeat order records.

E

Empty input
An empty input is a set of no rows entering a calculation, such as when a WHERE condition finds no matching records.
An ungrouped count of South orders returns zero when every order in the table belongs elsewhere.

F

Final rounding
Final rounding reduces precision after the full aggregate calculation, avoiding accumulated changes from rounding each input first.
Three fees of 0.44 total 1.32 before rounding to 1.3; rounding each fee first would produce a total of 1.2.
Floating-point number
A floating-point number stores a binary approximation of many decimal values, so some calculations can show small rounding artifacts.
A PostgreSQL DOUBLE PRECISION addition can show 0.30000000000000004 where exact decimal arithmetic gives 0.3.
Functional dependency
A functional dependency means that one value determines another, such as an order primary key determining that order’s customer.
PostgreSQL can select the customer when the grouping includes the orders table’s primary key.

G

Grain
The grain of a report is what one output row represents, such as a customer or month, and determines the needed grouping.
A report with one row per customer needs customer-level measures even when the source table records orders.
GROUP BY
GROUP BY is the clause that collects input rows by shared column or expression values so aggregates can run once per group.
GROUP BY region lets the same query calculate a separate order count and amount total for each region.
Group condition
A group condition tests a property of a completed group, such as whether its count or average meets a required threshold.
A minimum of three orders per month is a group condition because it needs the completed monthly count.
Group percentage
A group percentage measures a subset as a share of its own group, using that group’s full population as the denominator.
One large order among three East orders is about 33.3% of East orders, regardless of other regions’ counts.
Grouping column
A grouping column supplies the values that determine which input rows belong together in a grouped summary query.
Choosing customer as the grouping column makes a report about customers instead of about individual orders.
Grouping expression
A grouping expression calculates a grouping value from each row, such as a calendar month or a category assigned by CASE.
A year-month expression combines February orders while keeping them separate from February in another year.
Grouping rule
The grouping rule requires each selected value to have a defined meaning for its group, through grouping or aggregation.
A region with several customers cannot supply one plain customer name unless the query specifies how to choose it.

H

Halfway value
A halfway value lies exactly between two possible rounded results; the chosen result depends on the number type and rounding rule.
PostgreSQL exact NUMERIC rounding sends 2.5 to 3 and -2.5 to -3, moving both ties away from zero.
HAVING
HAVING is the clause that filters groups using their grouping values or calculated aggregates after those aggregates exist.
HAVING SUM(amount) > 200 keeps regions by their total, without removing small orders from surviving groups.

I

Integer division
Integer division divides whole-number operands and discards the fractional part in SQLite, PostgreSQL and SQL Server.
In SQLite, dividing 19 items by eight orders with integer operands gives 2 instead of the mean 2.375.
IS NULL
IS NULL is a SQL test that is true when a value is missing, allowing missing records to be selected or counted explicitly.
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
The logical query order describes the stages that define a query’s result, including row filtering before group calculations.
Tracing WHERE before GROUP BY explains why removing small orders changes a region’s later average.

M

Measure
A measure is the quantity a report calculates, such as a count, total or average, at the report’s chosen level of detail.
A monthly orders report can use order count and average amount as two measures of the same monthly population.
MIN and MAX
MIN and MAX are aggregates that find the smallest and largest non-NULL input values under the database’s comparison rules.
MIN(order_date) finds the earliest date when the column contains dates or consistently formatted ISO date text.
Multi-column grouping
Multi-column grouping forms a group for each combination of grouping values actually present in the surviving input rows.
Grouping by region and customer gives separate East results for Ana and Dara while keeping Ana’s orders together.

N

NULL
NULL marks a missing or unknown SQL value; arithmetic with it produces NULL, and it is different from zero or empty text.
An unknown discount can make amount - discount NULL, causing that row’s calculated value to be skipped by SUM.
NULL group
A NULL group contains the rows whose grouping value is missing; SQL puts those rows together as one result group.
Several orders with no region share one total that can be displayed with a label such as “No region.”
NULLIF
NULLIF returns NULL when its two arguments are equal and otherwise returns its first argument, making it useful for zero divisors.
NULLIF(COUNT(*), 0) makes an empty report’s denominator NULL, avoiding a PostgreSQL division-by-zero error.
NUMERIC and DECIMAL
NUMERIC and DECIMAL provide exact decimal types in PostgreSQL; in SQLite these names give affinity without exact decimal storage.
For exact SQLite money totals, integer cents avoid relying on fractional values stored as binary approximations.

O

ONLY_FULL_GROUP_BY
ONLY_FULL_GROUP_BY is a MySQL mode that rejects grouped expressions with ungrouped values not determined by the grouping.
MySQL normally rejects selecting an arbitrary customer when the query groups the orders only by region.
ORDER BY
ORDER BY is the clause that requests a defined output order, including sorting grouped rows by an aggregate or its alias.
ORDER BY total DESC lists the largest group totals first after each group has been calculated.

R

Reconciliation
Reconciliation checks that group counts and totals add to the equivalent ungrouped measures over the same filtered population.
Run without HAVING first so excluded groups do not explain a difference between the report and the input total.
ROUND
ROUND is a function that rounds a numeric value, with its second argument normally specifying digits after the decimal point.
ROUND(AVG(amount), 2) rounds the completed average to two places, keeping the earlier calculation intact.
Rounding discrepancy
A rounding discrepancy is a difference caused by reducing precision, such as rounded parts no longer summing to a rounded whole.
Three equal percentage shares displayed to one decimal add to 99.9%, although the exact shares cover 100%.
Row condition
A row condition can be evaluated for one input record, such as whether its date lies in a range or its amount meets a threshold.
The test amount >= 60 can keep one East order while removing another, before any regional summary exists.

S

SELECT alias
A SELECT alias is an output name assigned to an expression, which can be used in ORDER BY but has dialect-specific uses elsewhere.
PostgreSQL accepts ORDER BY total but needs the aggregate expression spelled out in a same-level HAVING.
SUM
SUM is an aggregate function that adds the non-NULL numeric values of a column or an expression across its input rows.
SUM(quantity * unit_price) totals row revenues after each quantity is multiplied by its matching price.

T

Tie-breaker
A tie-breaker is an additional sort key that gives rows a repeatable order when their earlier sort values are equal.
ORDER BY total DESC, customer gives tied customer totals a stable order before a limit is applied.
Truncation
Truncation removes fractional digits without rounding; PostgreSQL offers 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
An unrounded comparison tests the original calculated value, even when the selected output shows a rounded version of it.
An average displayed as 86.67 can still fail HAVING AVG(amount) >= 86.67 if its exact value is smaller.

W

WHERE
WHERE is the clause that filters individual input rows before grouping, determining which records contribute to later summaries.
A report restricted to orders from 2026 uses a date condition in WHERE before it calculates monthly totals.

Y

Year-month key
A year-month key identifies a calendar month together with its year, preventing the same month in different years from merging.
The labels “2025-01” and “2026-01” produce separate groups in a monthly report spanning both years.

About this lesson

By StudeliReport a mistake

Make your own Study Materials

Turn your notes, slides and documents into Study Guides, Practice Questions, Flashcards and 6 more tools.

Get Started