Class 12Computer Science · Database ManagementFull chapter

SQL: Aggregates, Grouping and Joins

The whole chapter in one place — read it, then test yourself. Clear notes, a reference sheet, a practice quiz, and worked NCERT solutions & PYQs.

Aggregate Functions: MAX, MIN, AVG, SUM, COUNT

Quick answer Aggregate functions squeeze a whole column into one value, and every one of them except COUNT(*) silently skips NULL — which is exactly where the board sets its trap.

Every SELECT you have written so far gives one output row for every row that matched. An aggregate function breaks that rule: it takes a whole column of values and returns a single value. Five are on your syllabus — MAX, MIN, AVG, SUM, COUNT.

Two tables run through this entire chapter. Build them once and every query in every section will work.

CREATE DATABASE company;
USE company;

CREATE TABLE dept (
  deptno INT PRIMARY KEY,
  dname  VARCHAR(20),
  city   VARCHAR(20)
);
INSERT INTO dept VALUES
(10, 'Sales',     'Mumbai'),
(20, 'Technical', 'Bengaluru'),
(30, 'HR',        'Delhi');

CREATE TABLE emp (
  empno  INT PRIMARY KEY,
  ename  VARCHAR(20),
  deptno INT,
  salary DECIMAL(10,2),
  bonus  DECIMAL(10,2),
  doj    DATE
);
INSERT INTO emp VALUES
(101, 'Ananya Iyer',  20,  85000, 12000, '2019-06-10'),
(102, 'Rohit Sharma', 10,  62000,  NULL, '2020-01-15'),
(103, 'Meera Nair',   20,  91000, 15000, '2018-03-01'),
(104, 'Vikram Singh', 30,  47000,  5000, '2021-07-20'),
(105, 'Priya Menon',  10,  62000,  8000, '2020-11-05'),
(106, 'Arjun Desai',  20, 120000,  NULL, '2017-02-11'),
(107, 'Kavya Reddy',  30,  54000,  6000, '2022-08-30'),
(108, 'Imran Khan', NULL,  40000,  4000, '2023-04-02');

This is what EMP actually holds. Two gaps are deliberate: Rohit and Arjun have no bonus recorded, and Imran has no department assigned. Almost every mark students lose in this unit comes from those two NULLs.

empnoenamedeptnosalarybonusdoj
101Ananya Iyer2085000.0012000.002019-06-10
102Rohit Sharma1062000.00NULL2020-01-15
103Meera Nair2091000.0015000.002018-03-01
104Vikram Singh3047000.005000.002021-07-20
105Priya Menon1062000.008000.002020-11-05
106Arjun Desai20120000.00NULL2017-02-11
107Kavya Reddy3054000.006000.002022-08-30
108Imran KhanNULL40000.004000.002023-04-02

And DEPT:

deptnodnamecity
10SalesMumbai
20TechnicalBengaluru
30HRDelhi

Worked example — all five functions in one query.

SELECT MAX(salary) AS highest,
       MIN(salary) AS lowest,
       SUM(salary) AS total,
       AVG(salary) AS average,
       COUNT(*)    AS employees
FROM emp;

Real output:

highestlowesttotalaverageemployees
120000.0040000.00561000.0070125.0000008

Eight rows went in, one row came out. That is the whole idea. Notice AVG came back as 70125.000000 — MySQL widens the result of AVG, so do not be surprised by the trailing zeros.

The NULL rule — learn this sentence. COUNT(*) counts rows. Every other aggregate, including COUNT(column), looks only at NON-NULL values. Here is the proof on our own table.

SELECT COUNT(*)      AS rows_in_table,
       COUNT(bonus)  AS bonus_values,
       COUNT(deptno) AS deptno_values,
       COUNT(DISTINCT salary) AS distinct_salaries,
       COUNT(DISTINCT deptno) AS distinct_depts
FROM emp;
rows_in_tablebonus_valuesdeptno_valuesdistinct_salariesdistinct_depts
86773

Read that row slowly. COUNT(*) says 8 because there are eight rows. COUNT(bonus) says 6 because two bonuses are NULL. COUNT(deptno) says 7 because one department is NULL. COUNT(DISTINCT salary) says 7 because 62000 appears twice (Rohit and Priya). Same table, four different numbers — and the examiner will hand you exactly one of them.

Why AVG is not what students expect. If NULLs are skipped, they are skipped in the divisor too. AVG is the sum divided by the non-null count, never by the row count.

SELECT SUM(bonus)   AS sum_bonus,
       COUNT(bonus) AS non_null_count,
       AVG(bonus)   AS avg_bonus,
       SUM(bonus)/COUNT(bonus) AS sum_over_nonnull,
       SUM(bonus)/COUNT(*)     AS sum_over_all_rows
FROM emp;
sum_bonusnon_null_countavg_bonussum_over_nonnullsum_over_all_rows
50000.0068333.3333338333.3333336250.000000

AVG(bonus) equals 8333.333333, which is 50000/6. It is not 6250, which is 50000/8. If a question asks for the average bonus per employee including those who got none, AVG is the wrong tool — you must write SUM(bonus)/COUNT(*) yourself.

An all-NULL set gives NULL, not zero. Rohit and Arjun are the only two employees here with no bonus:

SELECT SUM(bonus) AS s, AVG(bonus) AS a, COUNT(bonus) AS c, COUNT(*) AS r
FROM emp WHERE empno IN (102,106);
sacr
NULLNULL02

SUM and AVG return NULL, COUNT(bonus) returns 0, COUNT(*) returns 2. COUNT is the only aggregate that can never come back NULL.

NULLs also escape your WHERE clause. This surprises everyone:

QueryResult
SELECT COUNT(*) FROM emp WHERE bonus > 5000;4
SELECT COUNT(*) FROM emp WHERE bonus <= 5000;2
SELECT COUNT(*) FROM emp;8

4 + 2 = 6, not 8. The two NULL-bonus rows fail both conditions, because any comparison with NULL is UNKNOWN, not true and not false. To catch them you need WHERE bonus IS NULL.

MAX and MIN are not only for numbers. On text they mean alphabetical order; on dates, latest and earliest.

SELECT MAX(ename) AS last_name_alpha, MIN(doj) AS earliest_joining FROM emp;
last_name_alphaearliest_joining
Vikram Singh2017-02-11

The query you must never write. An aggregate returns one value, so it cannot sit beside an ordinary column that has eight values:

SELECT ename, MAX(salary) FROM emp;

MySQL 8 refuses it outright:

ERROR 1140 (42000): In aggregated query without GROUP BY, expression #1 of SELECT
list contains nonaggregated column 'company.emp.ename'; this is incompatible with
sql_mode=only_full_group_by

MySQL names the column in full, database.table.column, so company in that message is simply the database you are working in — you will see your own database's name there.

To get the name of the highest-paid employee you must ask twice. First find the maximum:

SELECT MAX(salary) AS highest FROM emp;
highest
120000.00

Then look up the row that carries that value:

SELECT ename, salary FROM emp WHERE salary = 120000;
enamesalary
Arjun Desai120000.00

Two ordinary queries, one after the other. That is all your syllabus asks for.

MAX / MIN SELECT MAX(col), MIN(col) FROM table; Works on numbers, text (alphabetical) and dates (latest/earliest). NULLs are skipped.
SUM / AVG SELECT SUM(col), AVG(col) FROM table; Numeric columns only. AVG = SUM(col) / COUNT(col), so NULL rows are absent from the divisor too.
COUNT(*) SELECT COUNT(*) FROM table; Counts ROWS. Skips nothing, never returns NULL, returns 0 on an empty table.
COUNT(column) SELECT COUNT(col) FROM table; Counts NON-NULL values in that column. This is the one the board tests against COUNT(*).
COUNT(DISTINCT column) SELECT COUNT(DISTINCT col) FROM table; Non-NULL and de-duplicated. DISTINCT goes INSIDE the brackets, not after SELECT.
Aggregate over filtered rows SELECT AVG(col) FROM table WHERE condition; WHERE runs first, so the aggregate only ever sees the surviving rows.
Remember
  • COUNT(*) counts rows and never skips anything; COUNT(column) counts only NON-NULL values in that column. On our EMP table the two answers are 8 and 6.
  • Every aggregate except COUNT(*) ignores NULL. AVG(bonus) = 50000/6 = 8333.333333, not 50000/8 = 6250.
  • SUM and AVG over an entirely NULL set return NULL, but COUNT returns 0. COUNT is the only aggregate that never yields NULL.
  • NULL rows fail both bonus > 5000 and bonus <= 5000, so the two counts do not add up to the table size. Use IS NULL to find them.
  • You cannot mix a plain column with an aggregate when there is no GROUP BY — MySQL raises ERROR 1140 (only_full_group_by). With a GROUP BY present, the same mistake gives ERROR 1055 instead.

GROUP BY: One Answer Per Group

Quick answer GROUP BY splits the table into buckets by the value of a column and runs the aggregate once inside each bucket, producing exactly one output row per distinct value — with all NULLs collected into a single bucket of their own.

A bare aggregate gives you one number for the whole table. Usually you want one number per department, per city, per year. That is GROUP BY.

Think of it physically. GROUP BY deptno sorts the eight employee rows into piles — one pile for deptno 10, one for 20, one for 30, and one for the rows where deptno is NULL. Then the aggregate is computed separately inside each pile, and each pile contributes one row of output.

Worked example — department-wise headcount and wage bill.

SELECT deptno, COUNT(*) AS staff, SUM(salary) AS wagebill, AVG(salary) AS avg_sal
FROM emp
GROUP BY deptno;

Real output, exactly as MySQL printed it:

deptnostaffwagebillavg_sal
203296000.0098666.666667
102124000.0062000.000000
302101000.0050500.000000
NULL140000.0040000.000000

Two things to notice, and both are examinable.

One: the NULL group is real. Imran Khan has no department, and instead of disappearing he forms a fourth group whose label is NULL. This is the one place in SQL where NULLs are treated as equal to one another — NULL = NULL is never true in a WHERE clause, but GROUP BY still puts all NULL rows in the same pile. So the answer to "how many rows will this query return?" is 4, not 3.

Two: the output is not sorted. Look at the order — 20, 10, 30, NULL. GROUP BY groups; it does not promise to sort. If the question says "display department-wise" you may get away with it, but if it says "in ascending order of department" you must add ORDER BY:

SELECT deptno, COUNT(*) FROM emp GROUP BY deptno ORDER BY deptno;
deptnoCOUNT(*)
NULL1
102
203
302

NULL sorts first in ascending order in MySQL.

The NULL rule still applies inside every group. This is the query that catches people who thought they had understood section 1:

SELECT deptno, COUNT(*) AS rows_in_group, COUNT(bonus) AS bonus_paid,
       SUM(bonus) AS bonus_total
FROM emp
GROUP BY deptno;
deptnorows_in_groupbonus_paidbonus_total
203227000.00
10218000.00
302211000.00
NULL114000.00

Department 20 has three employees but only two recorded bonuses, so AVG(bonus) for that group is 27000/2 = 13500.00, not 27000/3 = 9000. Verified:

SELECT SUM(bonus) AS s, COUNT(*) AS rows_, COUNT(bonus) AS nn, AVG(bonus) AS a
FROM emp WHERE deptno = 20;
srows_nna
27000.003213500.000000

The SELECT-list rule. Anything in your SELECT list that is not wrapped in an aggregate must appear in the GROUP BY. It has to — the database can only print one value per group, and only the grouping column is guaranteed to have exactly one value inside a group. Break the rule and MySQL 8 stops you:

SELECT deptno, ename, COUNT(*) FROM emp GROUP BY deptno;
ERROR 1055 (42000): Expression #2 of SELECT list is not in GROUP BY clause and
contains nonaggregated column 'company.emp.ename' which is not functionally
dependent on columns in GROUP BY clause; this is incompatible with
sql_mode=only_full_group_by

Note the error number carefully, because the same mistake produces two different ones. With no GROUP BY at all you get ERROR 1140 (section 1); with a GROUP BY present but the column missing from it you get ERROR 1055. Both say the same thing: department 20 holds three names and only one count, so there is no single name MySQL could print.

Grouping on two columns. Then a group is one distinct combination of the two values:

SELECT deptno, salary, COUNT(*) AS how_many
FROM emp
GROUP BY deptno, salary
ORDER BY deptno, salary;
deptnosalaryhow_many
NULL40000.001
1062000.002
2085000.001
2091000.001
20120000.001
3047000.001
3054000.001

Seven rows, because seven distinct (deptno, salary) pairs exist. Only Rohit and Priya share one — both are 62000 in department 10 — so that pair shows how_many = 2.

Grouping on an expression. You are not limited to bare columns. To count hires per year:

SELECT YEAR(doj) AS joining_year, COUNT(*) AS joined
FROM emp
GROUP BY YEAR(doj)
ORDER BY joining_year;
joining_yearjoined
20171
20181
20191
20202
20211
20221
20231

Seven output rows for eight employees — 2020 is the only year with two joinings (Rohit in January, Priya in November). Whatever you group on must be repeated in the GROUP BY clause exactly as it appears, brackets and all.

Basic grouping SELECT col, AGG(col2) FROM table GROUP BY col; Every non-aggregated column in SELECT must also be in GROUP BY.
Grouping + sorting SELECT col, COUNT(*) FROM t GROUP BY col ORDER BY col; GROUP BY never guarantees order. ORDER BY is the only way to fix the display order.
Grouping on two columns SELECT c1, c2, COUNT(*) FROM t GROUP BY c1, c2; One group per distinct COMBINATION of c1 and c2, not one per column.
Grouping on an expression SELECT YEAR(doj), COUNT(*) FROM emp GROUP BY YEAR(doj); Repeat the whole expression in GROUP BY. Do not put the alias there in an exam answer.
How many rows will it return? rows out = number of distinct values in the GROUP BY column (NULL counts as one) The fastest way to answer a 1-mark 'output of this query' question, provided there is no HAVING clause.
NULL in GROUP BY SELECT deptno, COUNT(*) FROM emp GROUP BY deptno; GROUP BY treats all NULLs as one value — the only place in SQL where NULLs are considered equal.
Remember
  • GROUP BY col produces exactly one output row per distinct value of col, and the aggregate is computed separately inside each group.
  • All NULL values form ONE group of their own. Our EMP table has three departments but GROUP BY deptno returns FOUR rows.
  • GROUP BY does not sort. The real output came out as 20, 10, 30, NULL — add ORDER BY whenever the question asks for a sorted display.
  • Any non-aggregated column in the SELECT list must also appear in the GROUP BY list, or MySQL raises ERROR 1055. (The related ERROR 1140 is what you get when there is no GROUP BY clause at all.)
  • Aggregates still ignore NULL inside each group: department 20's AVG(bonus) is 27000/2 = 13500.00, not 27000/3.

WHERE vs HAVING: Filtering Rows vs Filtering Groups

Quick answer WHERE throws away rows before grouping happens and can never contain an aggregate; HAVING throws away whole groups after the aggregate has been computed, and a question that filters on both a row property and a group property needs both clauses.

This is the single most-tested distinction in Unit 3, and it is decided entirely by when each clause runs. Memorise this order — the whole topic falls out of it:

FROM  ->  WHERE  ->  GROUP BY  ->  HAVING  ->  SELECT  ->  ORDER BY

WHERE runs before the groups exist, so it can only test one row at a time and can never contain an aggregate. HAVING runs after the groups have been formed and the aggregates computed, so its condition is about the group as a whole.

Worked example — departments with more than one employee.

SELECT deptno, COUNT(*) AS staff
FROM emp
GROUP BY deptno
HAVING COUNT(*) > 1;
deptnostaff
203
102
302

Four groups were formed; the NULL group had only Imran in it, so COUNT(*) > 1 was false for it and that group was dropped. Notice the group vanished, not the row — that is what HAVING does.

HAVING on an average.

SELECT deptno, AVG(salary) AS avg_sal
FROM emp
GROUP BY deptno
HAVING AVG(salary) > 60000;
deptnoavg_sal
2098666.666667
1062000.000000

HR (deptno 30) averages 50500.00 and the NULL group averages 40000.00, so both fail the test.

The query that needs both clauses. Read this requirement carefully: "Considering only employees earning at least Rs 50,000, display departments that still have more than one such employee, along with their total salary." There are two filters here of two different kinds — "earning at least 50000" is about a row, "more than one such employee" is about a group. So the answer needs WHERE and HAVING.

SELECT deptno, COUNT(*) AS staff, SUM(salary) AS wagebill
FROM emp
WHERE salary >= 50000
GROUP BY deptno
HAVING COUNT(*) > 1
ORDER BY deptno;
deptnostaffwagebill
102124000.00
203296000.00

Trace it. WHERE first removes Vikram (47000) and Imran (40000), leaving six rows. Those six group into 10 (two people), 20 (three people) and 30 (one person — only Kavya survives). HAVING then removes department 30 because its surviving count is 1. Here is the same query without the HAVING, so you can see department 30 sitting there before it gets removed:

SELECT deptno, COUNT(*) AS staff FROM emp
WHERE salary >= 50000 GROUP BY deptno ORDER BY deptno;
deptnostaff
102
203
301

The NULL group is gone from both — not because of HAVING, but because WHERE had already deleted Imran's row. That is the ordering doing its work.

Putting an aggregate in WHERE is an error, not a style mistake.

SELECT deptno, COUNT(*) FROM emp WHERE COUNT(*) > 1 GROUP BY deptno;
ERROR 1111 (HY000): Invalid use of group function

It cannot work: at the moment WHERE runs, no group exists, so there is nothing to count.

HAVING can test an aggregate you never display. This is perfectly legal and the board likes it:

SELECT deptno FROM emp GROUP BY deptno HAVING MAX(salary) > 90000;
deptno
20

Only Technical has anyone above 90000 (Arjun, at 120000). MAX(salary) never appears in the output, but HAVING could still use it.

HAVING without GROUP BY. Rare, but valid — the entire table is treated as one single group:

SELECT COUNT(*) AS n FROM emp HAVING COUNT(*) > 5;
n
8

Change the test to HAVING COUNT(*) > 50 and the query returns an empty set — the one group failed, so nothing is left. Do not confuse this with returning 0; there is no row at all.

Summary you can reproduce in the exam:

PointWHEREHAVING
RunsBefore GROUP BYAfter GROUP BY
FiltersIndividual rowsWhole groups
Aggregate allowed?No — ERROR 1111Yes, that is its purpose
Needs GROUP BY?NoNormally yes (else whole table is one group)
Position in queryAfter FROMAfter GROUP BY
Execution order FROM -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY Memorise this one line. Every WHERE-vs-HAVING answer is derived from it.
HAVING SELECT col, AGG(c) FROM t GROUP BY col HAVING AGG(c) condition; The condition is about the GROUP, so it may contain aggregates.
Full clause sequence SELECT ... FROM t WHERE rowcond GROUP BY col HAVING groupcond ORDER BY col; This is the only legal written order. Swapping WHERE and GROUP BY gives ERROR 1064, a plain syntax error.
Illegal: aggregate in WHERE SELECT deptno, COUNT(*) FROM emp WHERE COUNT(*) > 1 GROUP BY deptno; ERROR 1111 (HY000): Invalid use of group function. Move the test into HAVING.
HAVING on a hidden aggregate SELECT deptno FROM emp GROUP BY deptno HAVING MAX(salary) > 90000; Legal. The aggregate being tested need not appear in the SELECT list.
HAVING with no GROUP BY SELECT COUNT(*) FROM emp HAVING COUNT(*) > 5; Whole table becomes one group. Returns the single row, or an empty set if the test fails.
Remember
  • The clause order FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY explains every WHERE-vs-HAVING question. WHERE runs before groups exist; HAVING runs after.
  • WHERE filters rows, HAVING filters groups. An aggregate inside WHERE gives ERROR 1111 Invalid use of group function.
  • A requirement with a row condition AND a group condition needs both clauses — WHERE salary >= 50000 ... HAVING COUNT(*) > 1 returned departments 10 and 20 only.
  • HAVING can test an aggregate that is not in the SELECT list, e.g. HAVING MAX(salary) > 90000 returned just deptno 20.
  • HAVING without GROUP BY treats the whole table as one group; if that group fails, the query returns an empty set, not 0.

Cartesian Product: Every Row With Every Row

Quick answer A cartesian product pairs every row of the first table with every row of the second, giving rows(A) x rows(B) rows and cols(A) + cols(B) columns — and once you have printed it in full you can see exactly which rows a join condition is meant to keep.

Write two table names in the FROM clause with no condition and SQL does the only thing it can: it pairs every row of the first table with every row of the second. That is the cartesian product, also called a cross join. It is the raw material every other join is carved out of.

To see the whole thing on one page, build a small four-employee table:

CREATE TABLE emp4 (
  empno  INT PRIMARY KEY,
  ename  VARCHAR(20),
  deptno INT
);
INSERT INTO emp4 VALUES
(101, 'Ananya Iyer',  20),
(102, 'Rohit Sharma', 10),
(103, 'Meera Nair',   20),
(104, 'Vikram Singh', 30);

EMP4 — 4 rows, 3 columns:

empnoenamedeptno
101Ananya Iyer20
102Rohit Sharma10
103Meera Nair20
104Vikram Singh30

DEPT — 3 rows, 3 columns:

deptnodnamecity
10SalesMumbai
20TechnicalBengaluru
30HRDelhi

4 rows times 3 rows should be 12 rows, and 3 columns plus 3 columns should be 6 columns. Count the rows first, with three ordinary queries:

SELECT COUNT(*) AS emp4_rows    FROM emp4;
SELECT COUNT(*) AS dept_rows    FROM dept;
SELECT COUNT(*) AS product_rows FROM emp4, dept;
QueryResult
SELECT COUNT(*) FROM emp4;4
SELECT COUNT(*) FROM dept;3
SELECT COUNT(*) FROM emp4, dept;12

Worked example — the whole product, printed in full.

SELECT * FROM emp4, dept;
empnoenamedeptnodeptnodnamecity
101Ananya Iyer2030HRDelhi
101Ananya Iyer2020TechnicalBengaluru
101Ananya Iyer2010SalesMumbai
102Rohit Sharma1030HRDelhi
102Rohit Sharma1020TechnicalBengaluru
102Rohit Sharma1010SalesMumbai
103Meera Nair2030HRDelhi
103Meera Nair2020TechnicalBengaluru
103Meera Nair2010SalesMumbai
104Vikram Singh3030HRDelhi
104Vikram Singh3020TechnicalBengaluru
104Vikram Singh3010SalesMumbai

Twelve rows. Six columns — and deptno appears twice, once from each table, because a cartesian product removes nothing. Also note the department rows came back in the order 30, 20, 10; row order in a product is not guaranteed and you should not depend on it.

CROSS JOIN is the same operation written more explicitly, and gives the identical 12 rows:

SELECT COUNT(*) AS rows_from_cross_join FROM emp4 CROSS JOIN dept;
rows_from_cross_join
12

Now look at the product as data, and the join condition writes itself. Go down those twelve rows and compare the two deptno columns side by side. In only four of them are the two values equal — Ananya with Technical, Rohit with Sales, Meera with Technical, Vikram with HR. The other eight rows are nonsense like "Ananya Iyer, HR". Write exactly that comparison as a WHERE condition and the nonsense disappears:

SELECT * FROM emp4, dept WHERE emp4.deptno = dept.deptno;
empnoenamedeptnodeptnodnamecity
101Ananya Iyer2020TechnicalBengaluru
102Rohit Sharma1010SalesMumbai
103Meera Nair2020TechnicalBengaluru
104Vikram Singh3030HRDelhi

Twelve rows in, four rows out, still six columns. Exactly one surviving row per employee, and that is not luck — deptno is the primary key of DEPT, so each employee can match at most one department. An equi-join is a cartesian product with the wrong rows filtered out. That sentence is worth writing on your revision card.

Why forgetting the condition is a disaster. On the full tables:

SELECT COUNT(*) AS emp_times_dept FROM emp, dept;
emp_times_dept
24

8 x 3 = 24 rows, of which only 7 are meaningful. On a real payroll table of 5,000 employees and 40 departments the accidental product is 200,000 rows. Leaving out the join condition is the classic silent bug.

Ambiguous columns. Once both tables contribute a column called deptno, you may no longer say deptno on its own:

SELECT deptno FROM emp4, dept;
ERROR 1052 (23000): Column 'deptno' in field list is ambiguous

Qualify it — emp4.deptno or dept.deptno — or use table aliases, which is what you should do in the exam because it is shorter: SELECT E.ename, D.dname FROM emp E, dept D;. Once you declare an alias, you must use the alias everywhere; writing emp.ename after aliasing emp as E gives ERROR 1054: Unknown column 'emp.ename' in 'field list'.

Cartesian product (comma form) SELECT * FROM A, B; No condition at all. This is the form the board usually prints.
Cartesian product (explicit) SELECT * FROM A CROSS JOIN B; Identical result. Writing CROSS JOIN documents that you meant it.
Cardinality (number of rows) rows(A x B) = rows(A) * rows(B) 4 * 3 = 12. Asked directly as a 1-mark question almost every year.
Degree (number of columns) cols(A x B) = cols(A) + cols(B) 3 + 3 = 6. Nothing is merged, so a shared column name appears twice.
Qualifying a shared column SELECT emp.deptno, dept.dname FROM emp, dept; Unqualified deptno gives ERROR 1052 (23000): Column 'deptno' in field list is ambiguous.
Table alias SELECT E.ename, D.dname FROM emp E, dept D; Once aliased, the original table name can no longer be used as a prefix — that gives ERROR 1054.
Remember
  • A cartesian product pairs every row of A with every row of B. rows = rows(A) x rows(B), columns = cols(A) + cols(B). Our 4-row and 3-row tables gave exactly 12 rows and 6 columns.
  • Nothing is removed, so a column name present in both tables appears TWICE in the product.
  • FROM a, b and a CROSS JOIN b are the same operation; both returned 12 rows here.
  • Of the 12 product rows only 4 survived emp4.deptno = dept.deptno — one per employee. The equi-join condition is simply the rule that keeps those and discards the rest.
  • Referring to a column that exists in both tables without qualifying it gives ERROR 1052 ... is ambiguous. Use table.column or a table alias.

Equi-Join and Natural Join

Quick answer An equi-join filters the cartesian product with an explicit column = column condition and keeps the matching column twice, while a natural join applies that condition automatically on every same-named column and prints the shared column only once.

Both joins answer the same question — which department does each employee belong to? — and on our tables both return the same seven rows. They differ in how the condition is written and in what comes back.

Equi-join. You state the matching condition yourself, using the equality operator. There are two accepted forms and the board accepts either.

SELECT * FROM emp, dept WHERE emp.deptno = dept.deptno;
empnoenamedeptnosalarybonusdojdeptnodnamecity
101Ananya Iyer2085000.0012000.002019-06-1020TechnicalBengaluru
102Rohit Sharma1062000.00NULL2020-01-1510SalesMumbai
103Meera Nair2091000.0015000.002018-03-0120TechnicalBengaluru
104Vikram Singh3047000.005000.002021-07-2030HRDelhi
105Priya Menon1062000.008000.002020-11-0510SalesMumbai
106Arjun Desai20120000.00NULL2017-02-1120TechnicalBengaluru
107Kavya Reddy3054000.006000.002022-08-3030HRDelhi

Count the columns: 9. EMP has 6, DEPT has 3, and deptno is printed twice — the join condition proved the two values are equal but did nothing about the duplication. The second form, SELECT * FROM emp JOIN dept ON emp.deptno = dept.deptno;, gives the identical seven rows and the identical nine columns.

Natural join. Write no condition at all. SQL finds every column name common to both tables — here just deptno — joins on equality of those columns, and prints each shared column once.

SELECT * FROM emp NATURAL JOIN dept;
deptnoempnoenamesalarybonusdojdnamecity
20101Ananya Iyer85000.0012000.002019-06-10TechnicalBengaluru
10102Rohit Sharma62000.00NULL2020-01-15SalesMumbai
20103Meera Nair91000.0015000.002018-03-01TechnicalBengaluru
30104Vikram Singh47000.005000.002021-07-20HRDelhi
10105Priya Menon62000.008000.002020-11-05SalesMumbai
20106Arjun Desai120000.00NULL2017-02-11TechnicalBengaluru
30107Kavya Reddy54000.006000.002022-08-30HRDelhi

Same seven rows, but 8 columns instead of 9, and deptno has moved to the front. That is the standard behaviour: the shared columns are printed first, once each, then the rest of the left table, then the rest of the right table. What natural join removes is the duplicate copy of the common column.

PointEqui-joinNatural join
ConditionYou write it: A.col = B.colAutomatic, on all same-named columns
Columns returned by SELECT *m + n (here 6 + 3 = 9)m + n - common (here 6 + 3 - 1 = 8)
Common columnAppears twiceAppears once, printed first
Column names may differ?Yes — join eno to empid if you likeNo — names must match exactly, and if no name matches you silently get a cartesian product
ControlFullNone, which is the danger

Both joins silently drop unmatched rows. EMP has eight employees but the join returned seven:

QueryResult
SELECT COUNT(*) FROM emp;8
SELECT COUNT(*) FROM emp NATURAL JOIN dept;7

Imran Khan is missing. His deptno is NULL, and NULL is not equal to 10, 20 or 30 — it is not equal to anything, not even to another NULL. So no pairing in the cartesian product survives the condition and he disappears without any warning. If a report built on a join shows fewer people than the staff list, unmatched or NULL keys are the first thing to check.

The natural join trap the board loves. Natural join matches on every column with a shared name, not just the key you had in mind. Suppose the employee table also stored a posting city:

CREATE TABLE empc (empno INT, ename VARCHAR(20), deptno INT, city VARCHAR(20));
INSERT INTO empc VALUES
 (201,'Nikhil Rao',20,'Bengaluru'),
 (202,'Sneha Joshi',20,'Pune'),
 (203,'Tarun Bose',10,'Mumbai');

EMPC and DEPT now share two column names, deptno and city, so the natural join quietly requires both to match:

SELECT * FROM empc NATURAL JOIN dept;
deptnocityempnoenamedname
20Bengaluru201Nikhil RaoTechnical
10Mumbai203Tarun BoseSales

Sneha Joshi has vanished. She is in department 20, but she is posted in Pune while Technical's office city is Bengaluru, so the hidden second condition rejected her. The equi-join, which only tests what you asked it to test, keeps all three:

SELECT * FROM empc E JOIN dept D ON E.deptno = D.deptno;
empnoenamedeptnocitydeptnodnamecity
201Nikhil Rao20Bengaluru20TechnicalBengaluru
202Sneha Joshi20Pune20TechnicalBengaluru
203Tarun Bose10Mumbai10SalesMumbai

And when no column name matches at all, natural join gives up quietly. Suppose a staff table names its columns differently:

CREATE TABLE staff (eno INT, sname VARCHAR(20), dno INT);
INSERT INTO staff VALUES (301,'Nisha Rao',10),(302,'Farhan Ali',20);

staff and dept have no column name in common, so there is nothing for NATURAL JOIN to match on. It does not raise an error — it falls straight back to a full cartesian product:

QueryRows returned
SELECT COUNT(*) FROM staff NATURAL JOIN dept;6
SELECT COUNT(*) FROM staff, dept;6

2 x 3 = 6 both times — the natural join produced the very product it was supposed to filter. The equi-join has no such problem, because you name the columns yourself:

SELECT * FROM staff S, dept D WHERE S.dno = D.deptno;
enosnamednodeptnodnamecity
301Nisha Rao1010SalesMumbai
302Farhan Ali2020TechnicalBengaluru

That is why professionals write equi-joins even though natural joins are shorter.

Worked example — a join doing real work. "List employees earning above Rs 60,000 with their department and city, highest paid first."

SELECT E.ename, E.salary, D.dname, D.city
FROM emp E, dept D
WHERE E.deptno = D.deptno AND E.salary > 60000
ORDER BY E.salary DESC;
enamesalarydnamecity
Arjun Desai120000.00TechnicalBengaluru
Meera Nair91000.00TechnicalBengaluru
Ananya Iyer85000.00TechnicalBengaluru
Rohit Sharma62000.00SalesMumbai
Priya Menon62000.00SalesMumbai

The join condition and the row filter share one WHERE, connected by AND. Keep the join condition first — it makes the query easier to read and to mark.

Everything in this chapter, in one query. Join, then group, then filter the groups:

SELECT D.dname, D.city, COUNT(*) AS staff, AVG(E.salary) AS avg_sal
FROM emp E JOIN dept D ON E.deptno = D.deptno
GROUP BY D.dname, D.city
HAVING COUNT(*) > 1
ORDER BY avg_sal DESC;
dnamecitystaffavg_sal
TechnicalBengaluru398666.666667
SalesMumbai262000.000000
HRDelhi250500.000000

Imran is absent because the join dropped him, so the three group counts add to 7 and not 8 — a real difference between a payroll report and a headcount report, produced by nothing more than a NULL foreign key.

Equi-join (comma form) SELECT * FROM A, B WHERE A.key = B.key; The board's most common form. Join condition and row filters share one WHERE, joined by AND.
Equi-join (JOIN ... ON) SELECT * FROM A JOIN B ON A.key = B.key; Identical result. Keeps the join condition visually separate from row filters.
Natural join SELECT * FROM A NATURAL JOIN B; No condition written. Matches on EVERY column whose name appears in both tables; if no name is shared it degenerates into a cartesian product.
Columns returned by SELECT * equi-join = m + n ; natural join = m + n - (number of common columns) emp(6) and dept(3): equi-join gave 9 columns, natural join gave 8.
Position of the common column SELECT * FROM emp NATURAL JOIN dept; The shared column is printed once and FIRST, before the left table's remaining columns.
Rows lost in a join a row whose key is NULL or unmatched is dropped, with no error message 8 employees, 7 joined rows. Because deptno is the PRIMARY KEY of DEPT, each employee matched at most one department, so the join could only shrink EMP. If the second table held several rows per key the join would grow instead.
Remember
  • Equi-join: you write the condition, A.col = B.col, in WHERE or in ON. The shared column is returned TWICE, so SELECT * over emp(6) and dept(3) gave 9 columns.
  • Natural join: no condition written. SQL joins on every column with the same name and prints each shared column ONCE and FIRST, so the same join gave 8 columns.
  • Both joins return the same 7 rows here, but EMP has 8 employees. Imran Khan (deptno NULL) is dropped silently, because NULL matches nothing.
  • Natural join matches on ALL common column names. When EMPC and DEPT shared both deptno and city, Sneha Joshi disappeared because her posting city differed — and when two tables share NO column name at all, natural join silently returns the whole cartesian product. Prefer an equi-join when you want control.
  • A join can be grouped and filtered like any table: JOIN, then GROUP BY dname, then HAVING COUNT(*) > 1.

The formula sheet

Every formula in this chapter, in one place — screenshot it before your exam.

SELECT MAX(col), MIN(col) FROM table;
MAX / MIN
SELECT SUM(col), AVG(col) FROM table;
SUM / AVG
SELECT COUNT(*) FROM table;
COUNT(*)
SELECT COUNT(col) FROM table;
COUNT(column)
SELECT COUNT(DISTINCT col) FROM table;
COUNT(DISTINCT column)
SELECT AVG(col) FROM table WHERE condition;
Aggregate over filtered rows
SELECT col, AGG(col2) FROM table GROUP BY col;
Basic grouping
SELECT col, COUNT(*) FROM t GROUP BY col ORDER BY col;
Grouping + sorting
SELECT c1, c2, COUNT(*) FROM t GROUP BY c1, c2;
Grouping on two columns
SELECT YEAR(doj), COUNT(*) FROM emp GROUP BY YEAR(doj);
Grouping on an expression
rows out = number of distinct values in the GROUP BY column (NULL counts as one)
How many rows will it return?
SELECT deptno, COUNT(*) FROM emp GROUP BY deptno;
NULL in GROUP BY
FROM -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY
Execution order
SELECT col, AGG(c) FROM t GROUP BY col HAVING AGG(c) condition;
HAVING
SELECT ... FROM t WHERE rowcond GROUP BY col HAVING groupcond ORDER BY col;
Full clause sequence
SELECT deptno, COUNT(*) FROM emp WHERE COUNT(*) > 1 GROUP BY deptno;
Illegal: aggregate in WHERE
SELECT deptno FROM emp GROUP BY deptno HAVING MAX(salary) > 90000;
HAVING on a hidden aggregate
SELECT COUNT(*) FROM emp HAVING COUNT(*) > 5;
HAVING with no GROUP BY
SELECT * FROM A, B;
Cartesian product (comma form)
SELECT * FROM A CROSS JOIN B;
Cartesian product (explicit)
rows(A x B) = rows(A) * rows(B)
Cardinality (number of rows)
cols(A x B) = cols(A) + cols(B)
Degree (number of columns)
SELECT emp.deptno, dept.dname FROM emp, dept;
Qualifying a shared column
SELECT E.ename, D.dname FROM emp E, dept D;
Table alias
SELECT * FROM A, B WHERE A.key = B.key;
Equi-join (comma form)
SELECT * FROM A JOIN B ON A.key = B.key;
Equi-join (JOIN ... ON)
SELECT * FROM A NATURAL JOIN B;
Natural join
equi-join = m + n ; natural join = m + n - (number of common columns)
Columns returned by SELECT *
SELECT * FROM emp NATURAL JOIN dept;
Position of the common column
a row whose key is NULL or unmatched is dropped, with no error message
Rows lost in a join

Test yourself

Tap an answer to check it instantly — you'll see why it's right, and what to revise if it isn't.

0 correct · 0/12 answered
Q1

The EMP table has 8 rows. Two of them (Rohit Sharma and Arjun Desai) have NULL in the bonus column, and one (Imran Khan) has NULL in deptno. What does SELECT COUNT(*), COUNT(bonus), COUNT(deptno) FROM emp; return?

Q2

SUM(bonus) over the EMP table is 50000.00 and two of the eight employees have a NULL bonus. What does SELECT AVG(bonus) FROM emp; return?

Q3

EMP has employees in departments 10, 20 and 30, and one employee whose deptno is NULL. How many rows does SELECT deptno, COUNT(*) FROM emp GROUP BY deptno; return?

Q4

What is the result of running SELECT deptno, COUNT(*) FROM emp WHERE COUNT(*) > 1 GROUP BY deptno;?

Q5

Table EMP4 has 4 rows and 3 columns. Table DEPT has 3 rows and 3 columns. SELECT * FROM emp4, dept; produces a table with:

Q6

EMP has 8 rows; DEPT has 3 rows, one per department. SELECT COUNT(*) FROM emp NATURAL JOIN dept; returns 7, not 8. Why?

Q7

EMP has 6 columns and DEPT has 3, sharing only the column name deptno. If you run SELECT * with an equi-join on deptno, and then SELECT * with a natural join, how many columns does each one print?

Q8

In table ITEM, item 5004 (Eraser, dealer 103) has qty = NULL and dealer 103 supplies no other item. What does SELECT dcode, SUM(price*qty) FROM item GROUP BY dcode; show for dcode 103?

Q9

On the EMP table, SELECT COUNT(*) FROM emp WHERE bonus > 5000; returns 4 and SELECT COUNT(*) FROM emp WHERE bonus <= 5000; returns 2, but the table has 8 rows. What explains the missing 2?

Q10

On EMP (deptno 10 has 2 employees, 20 has 3, 30 has 2, and one employee has NULL deptno), what does SELECT deptno, COUNT(*) FROM emp GROUP BY deptno HAVING COUNT(*) > 2; return?

Q11

Which requirement genuinely needs BOTH a WHERE clause and a HAVING clause in the same query?

Q12

Table EMPC(empno, ename, deptno, city) and table DEPT(deptno, dname, city) share TWO column names. EMPC holds Nikhil Rao (dept 20, Bengaluru), Sneha Joshi (dept 20, Pune) and Tarun Bose (dept 10, Mumbai); DEPT lists Technical in Bengaluru and Sales in Mumbai. How many rows does SELECT * FROM empc NATURAL JOIN dept; return?

NCERT solutions & previous-year questions

Step-by-step model answers — tap a question to reveal the full solution.

NCERT questions 6

1 What is the difference between COUNT(*) and COUNT(column_name)? Explain with a suitable example.Aggregate functions and NULL

Both return a whole number, but they count different things.

  • COUNT(*) counts the number of rows in the table or group. It looks at no particular column, so it never skips anything and can never return NULL.
  • COUNT(column_name) counts the number of non-NULL values present in that one column. Every NULL in that column is ignored.

Example, on an EMP table of 8 employees in which Rohit Sharma and Arjun Desai have no bonus recorded and Imran Khan has not yet been assigned a department:

SELECT COUNT(*) AS rows_in_table,
       COUNT(bonus)  AS bonus_values,
       COUNT(deptno) AS deptno_values
FROM emp;
rows_in_tablebonus_valuesdeptno_values
867

The same eight rows give three different answers. COUNT(*) = 8 because there are eight rows; COUNT(bonus) = 6 because two bonus values are missing; COUNT(deptno) = 7 because one department is missing.

Note: all the other aggregate functions behave like COUNT(column) — SUM, AVG, MAX and MIN also ignore NULL. This is why AVG(bonus) here is 50000/6 = 8333.333333 and not 50000/8 = 6250. A third form, COUNT(DISTINCT column), counts non-NULL values after removing duplicates.

2 Consider the table TEACHER given below and write a SQL query to display the number of teachers in each department. No | Name | Age | Department | Date_of_join | Salary | Sex 1 | Jugal | 34 | Computer | 2017-01-10 | 12000 | M 2 | Sharmila | 31 | History | 2008-03-24 | 20000 | F 3 | Sandeep | 32 | Maths | 2016-12-12 | 30000 | M 4 | Sangeeta | 35 | History | 2015-07-01 | 40000 | F 5 | Rakesh | 42 | Maths | 2007-09-05 | 25000 | M 6 | Shyam | 50 | History | 2008-06-27 | 30000 | M 7 | Shiv_Om | 44 | Computer | 2017-02-25 | 21000 | M 8 | Shalakha | 33 | Maths | 2018-07-31 | 20000 | FGROUP BY with COUNT

"In each department" is the signal for GROUP BY. Group the rows by department and count the rows inside each group.

SELECT department, COUNT(*) AS teachers
FROM teacher
GROUP BY department
ORDER BY department;

Output:

departmentteachers
Computer2
History3
Maths3

Three departments, so three output rows — one per distinct value of department. ORDER BY department is added because GROUP BY by itself gives no guarantee about display order.

You could also write COUNT(name) here and get the same answer, but only because no teacher's name is NULL. COUNT(*) is the safe choice whenever the question asks for a number of records.

3 Using the TEACHER table, write a SQL query to display the maximum and the minimum salary in each department.MAX and MIN with GROUP BY

Two aggregates in the same query, each evaluated separately inside every group:

SELECT department, MAX(salary) AS highest, MIN(salary) AS lowest
FROM teacher
GROUP BY department
ORDER BY department;

Output:

departmenthighestlowest
Computer2100012000
History4000020000
Maths3000020000

Check one group by hand: History holds Sharmila (20000), Sangeeta (40000) and Shyam (30000), so the highest is 40000 and the lowest is 20000. Correct.

Common mistake: adding name to the SELECT list to show who earns the maximum. That is not allowed — name is neither aggregated nor listed in the GROUP BY, and MySQL 8 rejects the query:

ERROR 1055 (42000): Expression #2 of SELECT list is not in GROUP BY clause and
contains nonaggregated column 'teacher.name' which is not functionally dependent
on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by

(The closely related ERROR 1140 is what you get for the same mistake when there is no GROUP BY clause at all.) A group of three teachers has three names but only one maximum, so there is no single name the database could print.

4 Using the TEACHER table, write a SQL query to display the departments that have more than 2 teachers, along with the count of teachers in each.HAVING clause

The condition "more than 2 teachers" is a property of the group, not of any single row, so it must go in HAVING and not in WHERE.

SELECT department, COUNT(*) AS teachers
FROM teacher
GROUP BY department
HAVING COUNT(*) > 2;

Output:

departmentteachers
History3
Maths3

Computer has only 2 teachers, so that whole group is discarded after its count has been computed.

Why WHERE cannot be used here: WHERE is evaluated before the rows are grouped, so no counts exist yet. Writing WHERE COUNT(*) > 2 produces ERROR 1111 (HY000): Invalid use of group function.

5 Differentiate between the WHERE clause and the HAVING clause with a suitable example.WHERE vs HAVING

The difference is when each clause runs. SQL evaluates the clauses in this order:

FROM  ->  WHERE  ->  GROUP BY  ->  HAVING  ->  SELECT  ->  ORDER BY
WHEREHAVING
Runs before groupingRuns after grouping
Filters individual rowsFilters entire groups
Cannot contain an aggregate functionIs meant to contain aggregate functions
Can be used without GROUP BYNormally used with GROUP BY

Example on the TEACHER table. "Considering only teachers older than 33, display the sex-wise number of teachers and their total salary, but only where that total exceeds Rs 50,000." Age is a row condition; the salary total is a group condition. Both clauses are needed:

SELECT sex, COUNT(*) AS how_many, SUM(salary) AS total
FROM teacher
WHERE age > 33
GROUP BY sex
HAVING SUM(salary) > 50000;

Output:

sexhow_manytotal
M488000

Trace it. WHERE keeps the five teachers over 33 — Jugal (34), Sangeeta (35), Rakesh (42), Shyam (50) and Shiv_Om (44). Grouping by sex gives M with four teachers totalling 12000 + 25000 + 30000 + 21000 = 88000, and F with only Sangeeta totalling 40000. HAVING then removes the F group because 40000 is not greater than 50000.

6 Define cartesian product, equi-join and natural join. Illustrate all three using the tables EMP and DEPT.Joins

Take EMP with 8 rows and 6 columns (empno, ename, deptno, salary, bonus, doj) and DEPT with 3 rows and 3 columns (deptno, dname, city).

1. Cartesian product (cross join). Every row of the first table is paired with every row of the second, with no condition at all.

SELECT * FROM emp, dept;

Number of rows = 8 x 3 = 24; number of columns = 6 + 3 = 9. Only 7 of those 24 pairings are meaningful; the rest pair an employee with a department they do not belong to.

2. Equi-join. A cartesian product filtered by an explicit equality condition between the related columns.

SELECT * FROM emp, dept WHERE emp.deptno = dept.deptno;

This returns 7 rows and 9 columns. The condition proves the two deptno values are equal but does not merge them, so deptno is printed twice.

3. Natural join. No condition is written. SQL joins on every column whose name occurs in both tables and prints each such column only once.

SELECT * FROM emp NATURAL JOIN dept;

This returns the same 7 rows but only 8 columns, with deptno appearing once and placed first:

deptnoempnoenamesalarybonusdojdnamecity
20101Ananya Iyer85000.0012000.002019-06-10TechnicalBengaluru
10102Rohit Sharma62000.00NULL2020-01-15SalesMumbai
20103Meera Nair91000.0015000.002018-03-01TechnicalBengaluru
30104Vikram Singh47000.005000.002021-07-20HRDelhi
10105Priya Menon62000.008000.002020-11-05SalesMumbai
20106Arjun Desai120000.00NULL2017-02-11TechnicalBengaluru
30107Kavya Reddy54000.006000.002022-08-30HRDelhi

Relationship between the three: the cartesian product is the raw set of all pairings; an equi-join is that product with the non-matching pairs removed; a natural join is an equi-join that also removes the duplicated common column.

Point worth stating in the answer: EMP has 8 rows but both joins return 7. The eighth employee, Imran Khan, has a NULL deptno, and NULL is not equal to anything, so his row matches no department and is dropped without any error message.

Previous-year board questions 4

Q1 Consider the tables ITEM and DEALER given below and give the output of the SQL queries that follow. (3 marks) Table: ITEM Ino | Iname | Price | Qty | Dcode 5001 | Ball Pen | 15.00 | 200 | 101 5002 | Gel Pen | 25.00 | 150 | 102 5003 | Notebook | 48.00 | 300 | 101 5004 | Eraser | 5.00 | NULL | 103 5005 | Sharpener | 10.00 | 250 | 102 5006 | Stapler | 90.00 | 100 | 101 Table: DEALER Dcode | Dname | City 101 | Rajesh Traders | Delhi 102 | Kamal Stores | Jaipur 103 | Zenith Agency | Chennai (i) SELECT COUNT(*), COUNT(Qty), AVG(Qty) FROM ITEM; (ii) SELECT Dcode, COUNT(*), SUM(Price*Qty) FROM ITEM GROUP BY Dcode; (iii) SELECT Dcode, COUNT(*) FROM ITEM GROUP BY Dcode HAVING COUNT(*) > 2; 2023 (board pattern)

(i) SELECT COUNT(*), COUNT(Qty), AVG(Qty) FROM ITEM;

COUNT(*)COUNT(Qty)AVG(Qty)
65200.0000

Working: COUNT(*) counts rows, so 6. COUNT(Qty) counts only non-NULL quantities, and item 5004 (Eraser) has NULL, so 5. The quantities sum to 200 + 150 + 300 + 250 + 100 = 1000, and AVG divides by the non-NULL count, giving 1000/5 = 200.0000. It is not 1000/6 = 166.67.

(ii) SELECT Dcode, COUNT(*), SUM(Price*Qty) FROM ITEM GROUP BY Dcode;

DcodeCOUNT(*)SUM(Price*Qty)
101326400.00
10226250.00
1031NULL

Working: For dealer 101, (15 x 200) + (48 x 300) + (90 x 100) = 3000 + 14400 + 9000 = 26400.00. For 102, (25 x 150) + (10 x 250) = 3750 + 2500 = 6250.00. For 103 the only item has Qty = NULL, so Price*Qty is NULL and SUM over an entirely NULL group returns NULL, not 0. Note that COUNT(*) is still 1 for that group, because COUNT(*) counts the row regardless of what is in it.

(iii) SELECT Dcode, COUNT(*) FROM ITEM GROUP BY Dcode HAVING COUNT(*) > 2;

DcodeCOUNT(*)
1013

Working: Only dealer 101 supplies more than two items. Groups 102 (2 items) and 103 (1 item) are removed by HAVING after their counts have been computed.

Q2 (a) Table SALESMAN has 8 rows and 5 columns; table AREA has 4 rows and 3 columns. What will be the cardinality and the degree of the cartesian product of these two tables? (b) Why is a cartesian product rarely useful on its own? Name the operation that is normally applied to it and state what condition it uses. (2 marks) 2024 (board pattern)

(a)

  • Cardinality (number of rows) = rows(SALESMAN) x rows(AREA) = 8 x 4 = 32
  • Degree (number of columns) = columns(SALESMAN) + columns(AREA) = 5 + 3 = 8

Remember the pattern: rows multiply, columns add. Nothing is removed and nothing is merged, so if both tables contain a column with the same name, that name appears twice among the 8 columns.

(b) A cartesian product pairs every row of one table with every row of the other, so most of the resulting rows are meaningless — a salesman paired with an area he does not work in. Only a small fraction of the 32 rows describe reality.

The operation applied to it is a join, specifically an equi-join, which keeps only those rows in which the related columns of the two tables are equal:

SELECT * FROM salesman, area WHERE salesman.acode = area.acode;

Demonstration on smaller tables. A 4-row EMP4 table and a 3-row DEPT table give a cartesian product of exactly 12 rows, of which only 4 pass the condition emp4.deptno = dept.deptno — one correct department per employee, because deptno is the primary key of DEPT. So an equi-join is simply a cartesian product with the wrong rows filtered out.

Caution worth a mark: if you leave the join condition out entirely, no error is raised — you silently get all 32 rows, and every aggregate computed on them is wrong.

Q3 Consider the tables EMP(Empno, Ename, Deptno, Salary, Bonus, Doj) and DEPT(Deptno, Dname, City). EMP contains 8 employees, one of whom has a NULL Deptno; two employees have a NULL Bonus. Write SQL queries for the following. (4 marks) (i) Display each department number along with the average salary of its employees. (ii) Display the department name and the number of employees, for departments having more than one employee. (iii) Using a natural join, display the name of each employee along with the city of their department. (iv) Display the number of employees for whom a bonus has been recorded. 2022 (board pattern)

(i)

SELECT deptno, AVG(salary) AS avg_salary
FROM emp
GROUP BY deptno;
deptnoavg_salary
2098666.666667
1062000.000000
3050500.000000
NULL40000.000000

Four rows, not three — the employee with a NULL Deptno forms a group of his own, because GROUP BY collects all NULLs into a single group.

(ii) "More than one employee" is a condition on the group, so it belongs in HAVING. The department name comes from DEPT, so a join is needed.

SELECT D.dname, D.city, COUNT(*) AS staff
FROM emp E JOIN dept D ON E.deptno = D.deptno
GROUP BY D.dname, D.city
HAVING COUNT(*) > 1;
dnamecitystaff
TechnicalBengaluru3
SalesMumbai2
HRDelhi2

All three departments qualify. The NULL-department employee does not appear at all, because the join dropped his row before grouping ever happened — 3 + 2 + 2 = 7, not 8.

(iii)

SELECT ename, city
FROM emp NATURAL JOIN dept;
enamecity
Ananya IyerBengaluru
Rohit SharmaMumbai
Meera NairBengaluru
Vikram SinghDelhi
Priya MenonMumbai
Arjun DesaiBengaluru
Kavya ReddyDelhi

Seven rows from eight employees. NATURAL JOIN matches automatically on deptno, the only column name common to both tables, and the employee whose deptno is NULL matches nothing.

(iv)

SELECT COUNT(bonus) AS bonus_recorded FROM emp;
bonus_recorded
6

COUNT(bonus) counts non-NULL values only, so the two employees with no bonus are excluded: 8 - 2 = 6. Writing COUNT(*) here would be wrong — it would return 8, counting employees who have no bonus at all.

Q4 (a) Predict the output of the following two queries on table EMP, which has employees in departments 10 (2 employees), 20 (3 employees) and 30 (2 employees), plus one employee whose Deptno is NULL. Salaries are 62000 and 62000 in dept 10; 85000, 91000 and 120000 in dept 20; 47000 and 54000 in dept 30; and 40000 for the unassigned employee. Q1: SELECT Deptno, COUNT(*) FROM EMP WHERE Salary >= 50000 GROUP BY Deptno; Q2: SELECT Deptno, COUNT(*) FROM EMP WHERE Salary >= 50000 GROUP BY Deptno HAVING COUNT(*) > 1; (b) Using these two queries, explain the difference between the WHERE clause and the HAVING clause. (2 + 2 marks) 2025 (board pattern)

(a) Q1 output

DeptnoCOUNT(*)
203
102
301

Working: WHERE removes every employee earning below 50000 — that is the 47000 employee in department 30 and the 40000 unassigned employee. Six rows remain, which group into 10 with two employees, 20 with three, and 30 with only one (the 54000 employee). Note that the NULL group has disappeared entirely, and HAVING had nothing to do with it: WHERE had already deleted that row.

Q2 output

DeptnoCOUNT(*)
203
102

Working: Identical to Q1 up to the point where the groups are formed. HAVING then tests each group's count and discards department 30, whose surviving count is 1.

About the row order: MySQL actually printed these groups in the order 20, 10, 30 — GROUP BY collects rows into groups but never promises to sort them. The marks are for the correct group/count pairs, not for the sequence, so writing 10, 20, 30 is equally acceptable. If the question demands a sorted display, add ORDER BY Deptno.

(b) Difference between WHERE and HAVING

The difference is one of timing. SQL evaluates the clauses in this order:

FROM  ->  WHERE  ->  GROUP BY  ->  HAVING  ->  SELECT  ->  ORDER BY
WHEREHAVING
Executed before GROUP BYExecuted after GROUP BY
Removes individual rowsRemoves whole groups
Cannot contain an aggregate functionExists precisely to test aggregate functions
Here: removed the two low-salary employeesHere: removed the entire department 30 group

In these queries, Salary >= 50000 is a property of one employee, so it must go in WHERE, while COUNT(*) > 1 is a property of a whole department, so it must go in HAVING. Attempting WHERE COUNT(*) > 1 gives ERROR 1111 (HY000): Invalid use of group function, because when WHERE runs no groups exist yet and there is nothing to count.

Part of Priodemy for School

Interactive CBSE lessons, Class 8–12 — free with every school on Priodemy EduSuite. Explore more chapters and labs on the Priodemy for School hub.

Ask AI