Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Database Management Systems (DBMS)  Module 5 – SQL semantics: how queries really evaluate

GROUP BY, HAVING and aggregate semantics

How GROUP BY partitions rows, how HAVING filters groups, what aggregates return for NULLs and empty input, and which columns a grouped query may select.

  • Intermediate
  • 30 minutes
  • Examples run with sql.js 1.14.2
  • By MySmartCoPilot
Free: 30 minutes

What you will learn

  • Explain how GROUP BY partitions rows into groups and where rows with a NULL key go
  • Decide whether a condition belongs in WHERE or in HAVING
  • Decide which columns a grouped query may select in PostgreSQL and in SQLite
  • Predict what aggregates return for NULLs and for empty input, with and without GROUP BY
  • Write the classic grouped queries for above-average groups, the second highest value and zero counts

Before you start

What you’ll learn · Before you start

What you will learn

  • Explain how GROUP BY partitions rows into groups and where rows with a NULL key go
  • Decide whether a condition belongs in WHERE or in HAVING
  • Decide which columns a grouped query may select in PostgreSQL and in SQLite
  • Predict what aggregates return for NULLs and for empty input, with and without GROUP BY
  • Write the classic grouped queries for above-average groups, the second highest value and zero counts

Before you start

On this page

GROUP BY partitions the rows that survived WHERE into groups whose grouping values are equal, with all the NULLs of a column in one group of their own, and turns each group into a single output row. Aggregate functions such as count, sum and avg are computed once per group, and HAVING then keeps or drops whole groups, the way WHERE keeps or drops rows. In relational algebra this is the grouping operator, γ. Because each group becomes one row, the select list may only hold values that are single for a whole group: the grouping columns, aggregates, and in PostgreSQL the columns that depend on a grouped primary key. Two edge cases decide many exam questions: aggregates skip NULLs, and over empty input a query without GROUP BY still returns one row while a query with GROUP BY returns none.

The SQL track’s lessons on GROUP BY and HAVING cover the syntax in each engine. This lesson is about what grouping means, and about the traps that follow from it.

Grouping is a partition

Grouping takes one bag of rows and splits it into disjoint groups: every row lands in exactly one group, the one for its value of the grouping columns. Written as an algebra expression, the query below is

γ𝑑𝑒𝑝𝑡; count(∗), avg(𝑚𝑎𝑟𝑘𝑠)(𝑟𝑒𝑠𝑢𝑙𝑡𝑠)\gamma_{\mathit{dept};\ \mathrm{count}(*),\ \mathrm{avg}(\mathit{marks})}(\mathit{results})

with the grouping columns before the semicolon and the aggregates after it (the query below computes two more, count(marks) and sum(marks)). Eight course results, one of them not marked yet and one from a student whose department is missing:

Four groups from eight rows SQL · groups.sql
-- Eight course results. One student has no department on record, and one result is not marked yet.
CREATE TABLE results (roll INTEGER, dept TEXT, course TEXT, marks INTEGER);
INSERT INTO results VALUES
  (101, 'CSE', 'CS201', 78), (102, 'CSE', 'CS201', 64), (103, 'ECE', 'CS201', 55),
  (101, 'CSE', 'MA101', 91), (104, 'ME',  'MA101', NULL), (105, NULL, 'MA101', 47),
  (103, 'ECE', 'EC210', 70), (106, 'ECE', 'EC210', 82);

-- One output row per group. The rows with no department form a group of their own.
SELECT coalesce(dept, 'NULL') AS dept_group, count(*) AS rows_in_group, count(marks) AS marked,
       quote(sum(marks)) AS total, quote(round(avg(marks), 2)) AS average
FROM results
GROUP BY dept
ORDER BY dept;

Output

┌────────────┬───────────────┬────────┬───────┬─────────┐
│ dept_group │ rows_in_group │ marked │ total │ average │
├────────────┼───────────────┼────────┼───────┼─────────┤
│ NULL       │ 1             │ 1      │ 47    │ 47.0    │
│ CSE        │ 3             │ 3      │ 233   │ 77.67   │
│ ECE        │ 3             │ 3      │ 207   │ 69.0    │
│ ME         │ 1             │ 0      │ NULL  │ NULL    │
└────────────┴───────────────┴────────┴───────┴─────────┘

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < groups.sql

Eight rows are split by department into four groups, each group becomes one row with its averages, and HAVING keeps two.results: 8 rows(roll, dept, course, marks)GROUP BY dept4 groupsAggregates: one row per groupaverage CSE 77.67, ECE 69.0,ME NULL, NULL group 47.0HAVING avg(marks) >= 60CSE and ECE remainCSE3 rowsME1 row, no marksECE3 rowsNULL1 rowsplit by deptaggregate each groupkeep TRUE groups

From rows to groups to one row per group

Text description of the diagram

The diagram follows the eight results of the lesson's example from top to bottom.

  1. The input is the table results, 8 rows with the columns roll, dept, course and marks.
  2. GROUP BY dept partitions them into 4 groups by equal department: CSE with 3 rows, ECE with 3 rows, ME with 1 row that is not marked yet, and a group for the 1 row whose department is NULL.
  3. The aggregates turn each group into one row: the average marks are 77.67 for CSE, 69.0 for ECE, NULL for ME (it has no marks) and 47.0 for the NULL group.
  4. HAVING avg(marks) >= 60 keeps the groups whose condition is TRUE: CSE and ECE. The NULL group's average, 47.0, is below 60, and ME's condition is UNKNOWN because its average is NULL, so both are dropped.
  • The NULL department is a group. = would call two NULL departments UNKNOWN, but grouping compares by distinctness, under which NULLs are equal; SQLite’s manual says outright that NULLs are considered equal for grouping (SQLite: SELECT), and PostgreSQL forms the same NULL group.
  • count(*) counts rows, count(marks) counts values. ME’s group has one row and no marks, so its counts are 1 and 0, and its sum and avg are NULL: an aggregate of nothing but NULLs is NULL.
  • One row per group, whatever the group’s size. The output has 4 rows because there are 4 distinct departments, NULL included, not because of anything in the select list.

WHERE filters rows, HAVING filters groups

WHERE runs before grouping and sees one row at a time; HAVING runs after the aggregates exist and sees one group at a time (PostgreSQL 18: table expressions). The two queries below look alike and answer different questions:

A filter before grouping and a filter after it SQL · where_vs_having.sql
CREATE TABLE results (roll INTEGER, dept TEXT, course TEXT, marks INTEGER);
INSERT INTO results VALUES
  (101, 'CSE', 'CS201', 78), (102, 'CSE', 'CS201', 64), (103, 'ECE', 'CS201', 55),
  (101, 'CSE', 'MA101', 91), (104, 'ME',  'MA101', NULL), (105, NULL, 'MA101', 47),
  (103, 'ECE', 'EC210', 70), (106, 'ECE', 'EC210', 82);

-- WHERE filters rows before grouping: the average of the results of 60 or more, per department.
SELECT dept, count(*) AS results_kept, avg(marks) AS average
FROM results
WHERE marks >= 60
GROUP BY dept
ORDER BY dept;

-- HAVING filters groups after aggregation: the departments whose average is 60 or more.
SELECT dept, count(*) AS results_in_group, avg(marks) AS average
FROM results
GROUP BY dept
HAVING avg(marks) >= 60
ORDER BY dept;

Output

┌──────┬──────────────┬──────────────────┐
│ dept │ results_kept │     average      │
├──────┼──────────────┼──────────────────┤
│ CSE  │ 3            │ 77.6666666666667 │
│ ECE  │ 2            │ 76.0             │
└──────┴──────────────┴──────────────────┘
┌──────┬──────────────────┬──────────────────┐
│ dept │ results_in_group │     average      │
├──────┼──────────────────┼──────────────────┤
│ CSE  │ 3                │ 77.6666666666667 │
│ ECE  │ 3                │ 69.0             │
└──────┴──────────────────┴──────────────────┘

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < where_vs_having.sql

  • WHERE marks >= 60 removes the low results first, so ECE’s average is computed over its two good results: 76.0. ME and the NULL group vanish because none of their rows passes.
  • HAVING avg(marks) >= 60 keeps every result inside its group and judges the group as a whole: ECE stays with all three results and an average of 69.0. ME’s average is NULL, so its condition is UNKNOWN and HAVING drops it the way WHERE drops an UNKNOWN row.

Where a condition may go depends on what it reads. A condition on an aggregate can only be in HAVING, and one on a column that is not grouped can only be in WHERE. A condition on the grouping columns alone, such as dept <> 'ME', gives the same result in either clause, because it treats every row of a group alike; WHERE is the better home for it, since it removes the rows before any grouping work is done.

What the select list may contain

Each group becomes one row, so each output column needs one value per group. A grouping column has it, and so does an aggregate. A column that is neither has as many values as the group has rows, and SQL must refuse it or pick one.

PostgreSQL refuses it, unless the column is functionally dependent on the grouped columns, and the only dependency it recognises is on a table’s primary key that appears in the GROUP BY list (PostgreSQL 18: SELECT). So after GROUP BY s.roll, where roll is the primary key of students, the query may also select s.name and s.dept, because one roll number fixes them. Selecting a column of another table, such as an enrolment’s course code, still fails with an error saying that the column must appear in the GROUP BY clause or be used in an aggregate function.

SQLite picks a row. It accepts such “bare” columns, and its manual describes what they return (SQLite: SELECT, SQLite: quirks):

Bare columns in SQLite SQL · bare_columns.sql
CREATE TABLE results (roll INTEGER, dept TEXT, course TEXT, marks INTEGER);
INSERT INTO results VALUES
  (101, 'CSE', 'CS201', 78), (102, 'CSE', 'CS201', 64), (103, 'ECE', 'CS201', 55),
  (101, 'CSE', 'MA101', 91), (104, 'ME',  'MA101', NULL), (105, NULL, 'MA101', 47),
  (103, 'ECE', 'EC210', 70), (106, 'ECE', 'EC210', 82);

-- roll and course are neither grouped nor aggregated. SQLite runs the query anyway: with a single max(),
-- they come from the row that holds the maximum.
SELECT dept, roll, course, max(marks) AS best
FROM results
WHERE dept IS NOT NULL
GROUP BY dept
ORDER BY dept;

-- With avg() there is no such row, so roll comes from some row of the group.
SELECT dept, roll, avg(marks) AS average
FROM results
WHERE dept IS NOT NULL
GROUP BY dept
ORDER BY dept;

Output

┌──────┬──────┬────────┬──────┐
│ dept │ roll │ course │ best │
├──────┼──────┼────────┼──────┤
│ CSE  │ 101  │ MA101  │ 91   │
│ ECE  │ 106  │ EC210  │ 82   │
│ ME   │ 104  │ MA101  │      │
└──────┴──────┴────────┴──────┘
┌──────┬──────┬──────────────────┐
│ dept │ roll │     average      │
├──────┼──────┼──────────────────┤
│ CSE  │ 101  │ 77.6666666666667 │
│ ECE  │ 103  │ 69.0             │
│ ME   │ 104  │                  │
└──────┴──────┴──────────────────┘

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < bare_columns.sql

  • With a single min() or max(), the bare columns come from a row that holds that minimum or maximum: CSE’s best result, 91, belongs to student 101 in MA101, and the query reports exactly that. If several rows tie for the maximum, the manual says the values may come from any of them.
  • With any other aggregate, such as avg(), there is no such row, and the bare columns come from an arbitrary row of the group. Here CSE reports roll 101, but nothing in the query asks for that student: the value means nothing, and another version of SQLite could return another row.

The min()/max() rule makes SQLite’s query a convenient answer to “who has the best result in each department”, but the same text is an error in PostgreSQL. The portable form ranks the rows with a window function, or joins each row to its group’s maximum.

Aggregates over NULLs and over nothing

Aggregates ignore NULL inputs, except count(*), which counts rows (PostgreSQL 18: aggregate expressions). The most surprising results come from groups with no rows at all:

Aggregates when no row qualifies SQL · empty_input.sql
CREATE TABLE results (roll INTEGER, dept TEXT, course TEXT, marks INTEGER);
INSERT INTO results VALUES (101, 'CSE', 'CS201', 78), (102, 'CSE', 'CS201', 64);

-- No row passes the WHERE. Without GROUP BY the whole input is one group, so there is one output row.
SELECT count(*) AS n, count(marks) AS marked, quote(sum(marks)) AS total, quote(max(marks)) AS best
FROM results
WHERE course = 'PH110';

-- With GROUP BY there are no groups at all, so the query returns no rows; counting them shows it.
SELECT count(*) AS output_rows
FROM (SELECT dept, count(*) AS n FROM results WHERE course = 'PH110' GROUP BY dept);

-- HAVING without GROUP BY filters that single group: here it is removed.
SELECT count(*) AS output_rows
FROM (SELECT count(*) AS n FROM results HAVING count(*) > 5);

Output

┌───┬────────┬───────┬──────┐
│ n │ marked │ total │ best │
├───┼────────┼───────┼──────┤
│ 0 │ 0      │ NULL  │ NULL │
└───┴────────┴───────┴──────┘
┌─────────────┐
│ output_rows │
├─────────────┤
│ 0           │
└─────────────┘
┌─────────────┐
│ output_rows │
├─────────────┤
│ 0           │
└─────────────┘

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < empty_input.sql

  • Without GROUP BY, the whole input is one group, even when it is empty. The first query returns one row: count is 0, and sum and max are NULL. SQLite’s manual says such a query always returns exactly one row, and PostgreSQL’s that sum of no rows is NULL, not zero (PostgreSQL 18: aggregate functions).
  • With GROUP BY, an empty input has no groups, so the second query returns no rows at all; counting its rows shows 0.
  • HAVING without GROUP BY filters that single group, so it can remove it: PostgreSQL’s manual notes that such a query returns one row or none (PostgreSQL 18: table expressions).

Two tools give several counts in a single pass over the rows:

Distinct counts and conditional counts SQL · filter_and_distinct.sql
CREATE TABLE results (roll INTEGER, dept TEXT, course TEXT, marks INTEGER);
INSERT INTO results VALUES
  (101, 'CSE', 'CS201', 78), (102, 'CSE', 'CS201', 64), (103, 'ECE', 'CS201', 55),
  (101, 'CSE', 'MA101', 91), (104, 'ME',  'MA101', NULL), (105, NULL, 'MA101', 47),
  (103, 'ECE', 'EC210', 70), (106, 'ECE', 'EC210', 82);

-- Several conditional counts in one pass, with FILTER and with CASE.
SELECT count(*) AS results,
       count(DISTINCT roll) AS students,
       count(DISTINCT course) AS courses,
       count(*) FILTER (WHERE marks >= 60) AS sixty_plus,
       sum(CASE WHEN marks >= 60 THEN 1 ELSE 0 END) AS sixty_plus_case,
       count(*) FILTER (WHERE marks IS NULL) AS not_marked
FROM results;

Output

┌─────────┬──────────┬─────────┬────────────┬─────────────────┬────────────┐
│ results │ students │ courses │ sixty_plus │ sixty_plus_case │ not_marked │
├─────────┼──────────┼─────────┼────────────┼─────────────────┼────────────┤
│ 8       │ 6        │ 3       │ 5          │ 5               │ 1          │
└─────────┴──────────┴─────────┴────────────┴─────────────────┴────────────┘

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < filter_and_distinct.sql

count(DISTINCT roll) counts the six different students among the eight results; like other aggregates it skips NULLs. count(*) FILTER (WHERE marks >= 60) feeds the aggregate only the rows whose filter condition is TRUE, and sum(CASE WHEN marks >= 60 THEN 1 ELSE 0 END) computes the same 5 in engines without FILTER. PostgreSQL supports FILTER (PostgreSQL 18: aggregate expressions), and SQLite has since version 3.30.0 (SQLite: release history).

Three classic grouped queries

These three questions are common in exams and interviews, and each hides one of the rules above. The first two use a table of faculty salaries in which two people share the top salary:

Above-average departments and the second-highest salary SQL · classic_queries.sql
-- Faculty salaries in thousands; two people share the top salary of 150.
CREATE TABLE faculty (id INTEGER PRIMARY KEY, name TEXT, dept TEXT, salary INTEGER);
INSERT INTO faculty VALUES
  (1, 'Anita', 'CSE', 150), (2, 'Bharat', 'CSE', 120), (3, 'Chetan', 'ECE', 150),
  (4, 'Deepa', 'ECE', 95), (5, 'Elango', 'ME', 110), (6, 'Farida', 'ME', 90);

-- Departments whose average salary is above the average of all faculty (715 / 6, about 119.17).
SELECT dept, avg(salary) AS dept_average
FROM faculty
GROUP BY dept
HAVING avg(salary) > (SELECT avg(salary) FROM faculty)
ORDER BY dept;

-- The second-highest salary, without window functions: the largest value below the maximum.
SELECT max(salary) AS second_highest
FROM faculty
WHERE salary < (SELECT max(salary) FROM faculty);

-- The same with a window function: rank the distinct salaries.
SELECT salary AS second_highest
FROM (SELECT DISTINCT salary, dense_rank() OVER (ORDER BY salary DESC) AS r FROM faculty)
WHERE r = 2;

-- A common wrong answer: the second row of the sorted list, which is the tied top salary again.
SELECT salary AS second_row FROM faculty ORDER BY salary DESC LIMIT 1 OFFSET 1;

Output

┌──────┬──────────────┐
│ dept │ dept_average │
├──────┼──────────────┤
│ CSE  │ 135.0        │
│ ECE  │ 122.5        │
└──────┴──────────────┘
┌────────────────┐
│ second_highest │
├────────────────┤
│ 120            │
└────────────────┘
┌────────────────┐
│ second_highest │
├────────────────┤
│ 120            │
└────────────────┘
┌────────────┐
│ second_row │
├────────────┤
│ 150        │
└────────────┘

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < classic_queries.sql

  1. Groups above the overall average. The overall average is a single value, so it goes in a scalar subquery, and the comparison with each group’s average goes in HAVING. CSE (135.0) and ECE (122.5) are above 119.17; ME (100.0) is not.
  2. The second-highest salary. “The largest salary below the maximum” gives 120, and so does ranking the distinct salaries with dense_rank(). Taking the second row of the sorted list (LIMIT 1 OFFSET 1) gives 150 again, because two people share the top salary: rows are not values. When every salary is the same, the first two queries correctly return NULL or nothing, while the OFFSET version returns the top salary again.
  3. Counts that include zero. A course with no enrolments should show 0, and the usual mistake shows 1:
Counting enrolments per course, zero included SQL · zero_counts.sql
CREATE TABLE courses (code TEXT PRIMARY KEY, title TEXT);
CREATE TABLE enrolments (roll INTEGER, code TEXT);
INSERT INTO courses VALUES ('CS201', 'Databases'), ('MA101', 'Calculus'), ('PH110', 'Physics');
INSERT INTO enrolments VALUES (101, 'CS201'), (102, 'CS201'), (103, 'MA101');

-- The outer join keeps PH110 with NULLs for the enrolment columns.
SELECT c.code, count(*) AS count_star, count(e.roll) AS count_roll
FROM courses AS c
LEFT JOIN enrolments AS e ON e.code = c.code
GROUP BY c.code
ORDER BY c.code;

Output

┌───────┬────────────┬────────────┐
│ code  │ count_star │ count_roll │
├───────┼────────────┼────────────┤
│ CS201 │ 2          │ 2          │
│ MA101 │ 1          │ 1          │
│ PH110 │ 1          │ 0          │
└───────┴────────────┴────────────┘

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < zero_counts.sql

The LEFT JOIN keeps PH110 by pairing it with one row of NULLs, so its group has one row. count(*) counts that row and reports 1; count(e.roll) skips the NULL roll and reports 0. Count a column of the outer-joined table, or better its key, whenever empty groups must show zero.

Run SQL on CSV & Excel Files Run GROUP BY and HAVING on your own CSV file in the browser and check the group counts. SQLite Online (SQL Playground) Try the bare-column query with your own data, then rewrite it in the form PostgreSQL accepts.

Interview questions

Fresher · What is the difference between WHERE and HAVING, and when can a condition go in either?

WHERE filters rows before grouping, so it cannot use an aggregate; HAVING filters groups after the aggregates are computed, so it can. WHERE marks >= 60 changes which rows are averaged, while HAVING avg(marks) >= 60 keeps or drops whole groups by their average. A condition on the grouping columns only, such as dept <> 'ME', gives the same result in either clause, because it treats every row of a group alike, and WHERE is the cheaper place for it. A condition on an ungrouped column can only go in WHERE. Both clauses keep only TRUE, so a group whose average is NULL fails HAVING avg(marks) >= 60.

Key takeaways

  • GROUP BY partitions the rows into groups of equal grouping values, NULLs together in one group, and returns one row per group.
  • WHERE filters rows before grouping; HAVING filters groups after it. A condition on an aggregate belongs in HAVING, one on the grouping columns is best in WHERE.
  • The select list may hold grouping columns, aggregates and, in PostgreSQL, columns that depend on a grouped primary key. SQLite also accepts bare columns: from the min or max row with a single min() or max(), otherwise from an arbitrary row.
  • Aggregates skip NULLs; count(*) counts rows; over empty input, a query without GROUP BY returns one row (count 0, sum NULL) and a query with GROUP BY returns none.
  • count(*) after a LEFT JOIN counts the NULL-extended row: count the joined table’s key to get zero.
  • The second-highest value is a question about values, not rows: use max below the max, or dense_rank().

Exercise

Exercise · Easy · SQL

Count the students of every course, zero included

The table courses(code, title) lists every course on offer, and enrolments(roll, code) holds one row for each student enrolled in a course.

Return one row per course with two columns, code and students: the number of students enrolled in it. A course that nobody has chosen must appear with 0. The order of the rows does not matter.

The starter query keeps the empty courses but reports 1 student for each of them.

Starter code · course_counts.sql

-- Meant to count the students of each course, but an empty course shows 1.
SELECT c.code, count(*) AS students
FROM courses AS c
LEFT JOIN enrolments AS e ON e.code = c.code
GROUP BY c.code;
The sample tests · tests.yaml
# Sample tests: each one builds the two tables, runs your query and compares the rows it returns (in any order).
tests:
  - name: shows 0 for a course nobody chose
    setup: |
      CREATE TABLE courses (code TEXT PRIMARY KEY, title TEXT);
      CREATE TABLE enrolments (roll INTEGER, code TEXT);
      INSERT INTO courses VALUES ('CS201', 'Databases'), ('MA101', 'Calculus'), ('PH110', 'Physics');
      INSERT INTO enrolments VALUES (101, 'CS201'), (102, 'CS201'), (103, 'MA101');
    expected:
      columns: [code, students]
      rows:
        - [CS201, 2]
        - [MA101, 1]
        - [PH110, 0]
  - name: shows 0 for every course when there are no enrolments
    setup: |
      CREATE TABLE courses (code TEXT PRIMARY KEY, title TEXT);
      CREATE TABLE enrolments (roll INTEGER, code TEXT);
      INSERT INTO courses VALUES ('EC210', 'Signals'), ('ME105', 'Mechanics');
    expected:
      columns: [code, students]
      rows:
        - [EC210, 0]
        - [ME105, 0]
  - name: counts every enrolment of a busy course
    setup: |
      CREATE TABLE courses (code TEXT PRIMARY KEY, title TEXT);
      CREATE TABLE enrolments (roll INTEGER, code TEXT);
      INSERT INTO courses VALUES ('CS202', 'Algorithms');
      INSERT INTO enrolments VALUES (201, 'CS202'), (202, 'CS202'), (203, 'CS202'), (204, 'CS202');
    expected:
      columns: [code, students]
      rows:
        - [CS202, 4]
A hint

The LEFT JOIN keeps an empty course by pairing it with one row of NULLs, and count(*) counts that row. Count a column of enrolments instead, such as e.roll: count(column) skips NULLs, so an empty course counts 0.

The sample tests run on this device, in your browser (sql.js): nothing is sent to mysmartcopilot.com. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 A column city holds Pune, Goa, Pune, NULL, Goa, NULL and Delhi. How many rows does SELECT city, count(*) FROM t GROUP BY city return?

    Type a number.

    Show the answer to question 1

    Answer: 4 rows

    One row per distinct value, and all NULLs form one group: Pune, Goa, Delhi and NULL. The NULL group has a count of 2.

  2. Question 2 of 6 No row of the table matches the WHERE clause. What does SELECT count(*), sum(amount) FROM t WHERE amount > 1000 return?

    Choose one answer.

    Show the answer to question 2

    Answer: One row: 0 and NULL

    Without GROUP BY, the whole input is one group even when it is empty, so there is one output row. count(*) of no rows is 0, and sum of no rows is NULL, not zero.

  3. Question 3 of 6 The same table, the same WHERE clause, but now with GROUP BY city. What does SELECT city, count(*) FROM t WHERE amount > 1000 GROUP BY city return?

    Choose one answer.

    Show the answer to question 3

    Answer: No rows

    GROUP BY makes one group per distinct value among the rows that passed WHERE. No row passed, so there are no groups and no output rows.

  4. Question 4 of 6 Which condition can only go in HAVING, never in WHERE?

    Choose one answer.

    Show the answer to question 4

    Answer: avg(marks) >= 60

    An aggregate exists only after grouping, so a condition on it belongs in HAVING. A condition on a grouping column such as dept works in either clause, and conditions on individual rows, such as marks >= 60 or the course code, belong in WHERE.

  5. Question 5 of 6 A course has no enrolments. The report uses courses LEFT JOIN enrolments with GROUP BY the course code. What does count(*) report for that course?

    Type a number.

    Show the answer to question 5

    Answer: 1 students

    The outer join keeps the course by pairing it with one row of NULLs, and count(*) counts rows, so it reports 1. count(e.roll) would skip the NULL and report 0, which is the right answer.

  6. Question 6 of 6 In PostgreSQL, students has the primary key roll. Which select-list items are allowed in SELECT … FROM students AS s JOIN enrolments AS e ON e.roll = s.roll GROUP BY s.roll?

    Choose every answer that is right.

    Show the answer to question 6

    Answer:

    • count(e.code)
    • s.name

    s.name depends on the grouped primary key s.roll, so PostgreSQL accepts it, and an aggregate such as count(e.code) is always allowed. A student has many enrolments, so e.code and e.marks have several values per group and PostgreSQL rejects them.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.