E ExamMaster

GATE Computer Science & IT · Databases

SQL Joins, Groups and NULL

Join the campus-shop tables, group sales, and handle missing values in SQL.

The kiosk still has three tables. This lesson puts them side by side: join Sale to Item to see names and prices, group to count per student, and treat a missing phone as NULL rather than zero.

  • GATE Computer Science & IT
  • Medium level
  • 6 concepts
  • 4 practice questions

1INNER JOIN and LEFT JOIN

A join asks two tables to share a row when a condition holds. INNER JOIN Sale with Item on item_id keeps a pair only when both sides match. Sale 100 pairs with Samosa; Sale 101 pairs with Tea. Every kiosk sale has an item, so INNER JOIN Sale–Item returns all four sales, now carrying item_name and price_rs.

LEFT OUTER JOIN keeps every row of the left table even when the right side has no match, and fills the right columns with NULL. If we LEFT JOIN Student to Sale, a student who has not bought yet would still appear, with empty sale columns. INNER JOIN would drop that student. Use LEFT when "keep everyone on this side" is the question.

Figure. INNER JOIN matches sale 100 to item 10. The result row can now say "2 Samosa".

Who bought what

  1. Match keySale.item_id = Item.item_id.
  2. INNERKeep only matching pairs — four sales, each with a name and a price.
  3. LEFT from StudentKeep every student; NULL-pad sale columns if they have not bought.
INNER JOIN Sale ⋈ Item (first two)
sale_iditem_nameqtyprice_rs
100Samosa220
101Tea112

Who bought what

SELECT sale.sale_id, student.name, item.item_name, sale.qty
FROM sale
INNER JOIN student ON sale.student_id = student.student_id
INNER JOIN item ON sale.item_id = item.item_id;

Asha's spend

INNER JOIN Sale to Item. Asha's rows are sale 100 (2 × Rs 20) and sale 102 (1 × Rs 40). What did she spend?

  • sale 100: 2 × 2040
  • sale 102: 1 × 4040
  • Asha total Rs80

Pro tip. The price lives on Item. The join copies it onto each sale so qty × price is a row fact.

Coding lab. Join Sale to Item and print Asha's lines runs in the app, with checks on your output.

A LEFT OUTER JOIN from Student to Sale is used when you must
  1. Drop every student who has not bought yet
  2. Keep every student, with NULLs in Sale columns when unmatched
  3. Always duplicate every sale

LEFT OUTER preserves the left input and NULL-pads non-matches on the right.

2GROUP BY and aggregates

An aggregate is a number computed from several rows: COUNT, SUM, AVG, MIN, MAX. GROUP BY says "compute that number once per group". GROUP BY student_id on Sale makes three groups: Asha (two sales), Ravi (one), Meera (one). COUNT(*) per group is 2, 1, 1.

Without GROUP BY, an aggregate covers the whole table: SUM(qty) on Sale is 2+1+1+3 = 7 items sold. You cannot SELECT name and SUM(qty) together unless name is in the GROUP BY (or is itself an aggregate). The engine needs to know which name goes with which pile.

Figure. GROUP BY student_id piles the four sales into three groups. Aggregates then run once per pile.

Items sold per student

  1. GroupPile Sale rows by student_id: {1}, {2}, {3}.
  2. CountAsha 2 rows, Ravi 1, Meera 1.
  3. Sum qtyAsha 2+1=3 items; Ravi 1; Meera 3.
GROUP BY student_id
student_idCOUNT(*)SUM(qty)
1 Asha23
2 Ravi11
3 Meera13

Items each student bought

SELECT student_id, COUNT(*) AS sales, SUM(qty) AS items
FROM sale
GROUP BY student_id;

Whole-table SUM versus per-student SUM

What is SUM(qty) on all of Sale, and SUM(qty) in Asha's group?

  • 2 + 1 + 1 + 37 items in the shop today
  • Asha qty 2 + 13
  • check: 3 + 1 + 37

Pro tip. The per-group sums add back to the ungrouped sum when every row is in exactly one group.

SUM(qty) without GROUP BY on Sale returns
  1. One number: 7
  2. Three numbers, one per student
  3. Four numbers, one per sale

No GROUP BY means one pile — the whole table. SUM(qty) = 7.

3WHERE versus HAVING

WHERE tests a row before groups exist. HAVING tests a group after aggregates exist. "Sales of tea" is a row test: WHERE item_id = 11. "Students with at least two sales" is a group test: GROUP BY student_id HAVING COUNT(*) >= 2 — that keeps Asha and drops Ravi and Meera.

COUNT(*) >= 2 cannot live in WHERE, because a single sale row does not have a count yet. Put row filters in WHERE (they shrink the piles before you count) and put aggregate filters in HAVING.

Figure. WHERE cannot see COUNT(*). HAVING runs after GROUP BY and can.

Students with two or more sales

  1. WHEREOptional row filter first — we keep all four sales here.
  2. GROUP BYPile by student_id.
  3. HAVINGKeep groups whose COUNT(*) >= 2 — only Asha.
Which clause?
QuestionClause
item_id = 11WHERE (row)
COUNT(*) >= 2HAVING (group)
qty > 1WHERE (row)
SUM(qty) >= 3HAVING (group)

Repeat buyers

SELECT student_id, COUNT(*) AS n
FROM sale
GROUP BY student_id
HAVING COUNT(*) >= 2;
-- Asha only: student_id 1, n = 2
Which clause filters groups produced by GROUP BY, rather than individual rows?
  1. WHERE
  2. HAVING
  3. FROM

HAVING filters aggregated groups. WHERE filters rows before grouping and cannot reference aggregates.

4NULL is unknown, not zero

NULL means "we do not have this value". It is not 0 and not the empty string. Suppose Meera's phone is missing. The comparison phone = NULL is not TRUE and not FALSE — it is UNKNOWN. WHERE phone = NULL therefore drops the row. The test that keeps missing phones is WHERE phone IS NULL.

The same three-valued logic makes NULL = NULL UNKNOWN. Two missing phones are not "equal" in SQL; they are two unknowns. Use IS NULL / IS NOT NULL, never = NULL.

NULL = NULL is UNKNOWN. The IS NULL test is the one that keeps Meera when her phone is missing.

Why = NULL drops the row

  1. NULL means unknownMeera's phone is not a value we can compare.
  2. = yields UNKNOWNUNKNOWN is not TRUE, so WHERE rejects the row.
  3. IS NULLThe dedicated test for "this mark is missing".
Three-valued tests
ExpressionResult
phone = NULLUNKNOWN
NULL = NULLUNKNOWN
phone IS NULLTRUE when missing
phone IS NOT NULLTRUE when present

Find the missing phone

SELECT name FROM student
WHERE phone IS NULL;
-- not: WHERE phone = NULL
What is the result of evaluating NULL = NULL in SQL?
  1. UNKNOWN
  2. TRUE
  3. FALSE

NULL is unknown. Comparing unknown to unknown with = yields UNKNOWN. Use IS NULL.

5COUNT(*) versus COUNT(column)

COUNT(*) counts rows. COUNT(phone) counts non-NULL values of phone. If Student has three rows and Meera's phone is NULL, COUNT(*) is 3 and COUNT(phone) is 2. Every other aggregate (SUM, AVG, MIN, MAX) also skips NULLs. Only COUNT(*) treats a NULL row as a row that is still there.

That is why "how many students?" is COUNT(*) and "how many phones do we have on file?" is COUNT(phone). Mixing them is the usual trap on a column that allows missing values.

Figure. Three student rows exist. Only two phones are on file. COUNT(*) sees rows; COUNT(phone) sees values.

Three students, one missing phone

  1. RowsAsha, Ravi, Meera — COUNT(*) = 3.
  2. Phones on fileAsha and Ravi — COUNT(phone) = 2.
  3. Skip ruleSUM/AVG/MIN/MAX skip NULLs the same way COUNT(col) does.

COUNT with one NULL

Student has 3 rows; phone is NULL on 1 of them. What do COUNT(*) and COUNT(phone) return?

  • rows3
  • NULL phones1
  • COUNT(*)3
  • COUNT(phone) = 3 - 12

Pro tip. COUNT(*) includes the NULL row; COUNT(phone) does not.

phone has 10 rows, 3 NULL. COUNT(*) and COUNT(phone) are
  1. 7 and 10
  2. 10 and 7
  3. 7 and 7

COUNT(*) = 10 rows; COUNT(phone) = 7 non-NULL values.

6DISTINCT, UNION and UNION ALL

SQL SELECT keeps duplicate rows. \pi_{\text{item\_id}}(\text{Sale}) in algebra would be {10, 11, 12}. SELECT item_id FROM sale returns 10, 11, 12, 11 — Tea twice. DISTINCT asks SQL to drop the extra 11.

UNION of two queries also drops duplicates (set union). UNION ALL concatenates and keeps them. Use UNION ALL when you already know the piles are disjoint and you do not want the extra distinct pass — for example lunch sales stacked on snack sales that cannot share a sale_id.

SELECT item_id from the four sales lists 11 twice. DISTINCT or UNION (not UNION ALL) drop the extra Tea.

Duplicates
Formitem_id from Sale
Algebra \pi10, 11, 12
SQL SELECT10, 11, 12, 11
SELECT DISTINCT10, 11, 12
UNION ALL of two copieseight rows
UNION versus UNION ALL — which statement is true?
  1. UNION keeps duplicates; UNION ALL removes them
  2. UNION removes duplicates; UNION ALL keeps them
  3. Both always remove duplicates

UNION is set union; UNION ALL is multiset concatenation.

Notes

  • INNER JOIN keeps matching keys; LEFT OUTER JOIN keeps all left rows and NULL-pads the rest.
  • GROUP BY computes COUNT/SUM/AVG once per group; ungrouped aggregates cover the whole table.
  • WHERE filters rows; HAVING filters groups. Aggregates belong in HAVING.
  • NULL comparisons yield UNKNOWN; use IS NULL. COUNT(*) counts rows; COUNT(col) skips NULLs.
  • SQL SELECT keeps duplicates; DISTINCT and UNION remove them; UNION ALL keeps them.

Formulas

  • INNER JOIN ON sale.item_id = item.item_id
  • GROUP BY student_id HAVING COUNT(*) >= 2
  • COUNT(*) = rows; COUNT(col) = non-NULL values of col

Exam traps & shortcuts

  • LEFT JOIN when you must keep every left row.
  • WHERE cannot hold COUNT(*); that is HAVING.
  • NULL = NULL is UNKNOWN.

Reference tables

Asha, Ravi, Meera after the join
StudentSalesSpend Rs
AshaSamosa×2, Notebook×180
RaviTea×112
MeeraTea×336

Recap

Two tables, then a pile, then a missing value.

INNER
Keep matches only. Sale ⋈ Item gives four named lines.
LEFT
Keep every left row; NULL-pad the right when unmatched.
GROUP BY
Asha 2 sales / 3 items; whole-table SUM(qty) = 7.
HAVING
COUNT(*) >= 2 lives here, not in WHERE.
NULL
IS NULL, not = NULL. COUNT(*) vs COUNT(col).

Practise SQL Joins, Groups and NULL

Reading is free and needs no account. Practice, mocks and progress live in the app.

  • 4 exam-style questions on this topic, with explanations
  • A 3-question practice set that ends the chapter
  • Timed mocks scored with the real marking scheme
  • Readiness tracked per topic, kept on your device
Continue with Google — freeNo card, no trial. Works offline once installed.