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 4 – Relational algebra and relational calculus

Division, grouping and extended relational algebra

Division for "for all" questions, built from the core operators and written in SQL, then grouping, aggregates and bags, with the empty-divisor trap run.

  • Intermediate
  • 30 minutes
  • Examples run with sql.js 1.14.2, Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free: 30 minutes

What you will learn

  • Write 'for all' queries with division and derive division from the basic operators
  • Use extended operators such as grouping with aggregation, generalised projection and duplicate elimination
  • Translate division into SQL with a double NOT EXISTS and check that the results match

Before you start

What you’ll learn · Before you start

What you will learn

  • Write 'for all' queries with division and derive division from the basic operators
  • Use extended operators such as grouping with aggregation, generalised projection and duplicate elimination
  • Translate division into SQL with a double NOT EXISTS and check that the results match

Before you start

On this page

Division answers “for all” questions. For relations R(A, B) and S(B), R ÷ S returns the values of A that R pairs with every tuple of S: with Enrolled(sid, cid) and a list of core courses Core(cid), Enrolled ÷ Core is the set of students who took every core course. Division is not a core operator, because it can be written as π A (R) − π A ((π A (R) × S) − R), a formula this lesson takes apart step by step. Extended relational algebra adds what the core lacks for everyday questions: grouping with aggregation (γ), which counts, sums and averages per group, generalised projection, which computes new values such as credits times grade points, duplicate elimination (δ), sorting (τ), and versions of union, intersection and difference that work on bags, as SQL’s do. Along the way, three SQL forms of division meet an empty divisor, and they return three different answers.

What division answers

Take the enrolments without their grades, R(sid, cid), and the three core courses of the college in S(cid): CS201, CS202 and MA101. A student belongs to R ÷ S when R holds a tuple (sid, c) for every course c in S:

R÷S={a∈πA(R)|for every b∈S,(a,b)∈R}\begin{aligned} R \div S = \{\, a \in \pi_{A}(R) \mid {} &\text{for every } b \in S, \\ &(a, b) \in R \,\} \end{aligned}

The result’s heading is A, the attributes of R that are not in S. Asha (sid 1) took CS201, CS202 and MA101, and Esha (sid 5) took them too, plus HS101: extra courses do not matter. Chitra took two of the three, Bilal and Dev one each, so Enrolled ÷ Core holds sids 1 and 5.

The questions division answers have “every”, “all” or “each” in them: the suppliers who supply every part, the customers who bought every product of a category, the employees who worked on all the projects of their department. The divisor S is the “every” list, and the dividend R holds the pairs that may cover it.

Division from the core operators

The definition talks about “every”, which none of the six core operators says. The formula turns “every” into “not missing any”: find the candidates that miss at least one tuple of S, and remove them.

R÷S=πA(R)−πA((πA(R)×S)−R)R \div S = \pi_{A}(R) - \pi_{A}\big((\pi_{A}(R) \times S) - R\big)

Read from the inside: π A (R) lists the candidates. Pairing every candidate with every tuple of S gives all the pairs that R would have to contain. Subtracting R leaves the pairs that are missing, and projecting those on A gives the candidates that miss something. The candidates that remain are the answer. E. F. Codd gave this reduction, in his own notation, in the 1972 paper that defined his relational algebra, and noted there that division is the algebra’s counterpart of the universal quantifier.

Enrolled ÷ Core step by step, and the formula against the definition Python · division.py
"""Division R(A, B) ÷ S(B), by its definition and by the formula built from the core operators."""
from itertools import chain, combinations, product

# Enrolled reduced to (sid, cid), and the core courses every student is meant to take.
ENROLLED = {(1, "CS201"), (1, "CS202"), (1, "MA101"), (2, "EC210"), (2, "MA101"), (3, "CS201"),
            (3, "MA101"), (4, "MA101"), (5, "CS201"), (5, "CS202"), (5, "MA101"), (5, "HS101")}
CORE = {"CS201", "CS202", "MA101"}


def by_definition(R, S):
    """The a values of R that are paired in R with every b of S."""
    return {a for (a, _) in R if all((a, b) in R for b in S)}


def by_formula(R, S, show=False):
    """π A (R) − π A ((π A (R) × S) − R)"""
    candidates = {a for (a, _) in R}                         # π A (R)
    every_pair = {(a, b) for a in candidates for b in S}     # π A (R) × S
    missing = every_pair - R                                 # ... − R
    short = {a for (a, _) in missing}                        # π A (...)
    if show:
        print(f"  π sid (Enrolled)              {len(candidates):>2} tuples  {sorted(candidates)}")
        print(f"  that × Core                   {len(every_pair):>2} tuples")
        print(f"  that − Enrolled               {len(missing):>2} tuples  {sorted(missing)}")
        print(f"  π sid of the missing pairs    {len(short):>2} tuples  {sorted(short)}")
        print(f"  π sid (Enrolled) − that       {len(candidates - short):>2} tuples  {sorted(candidates - short)}")
    return candidates - short


print("Enrolled ÷ Core, step by step:")
by_formula(ENROLLED, CORE, show=True)

# Every R over A × B and every S over B, for small A and B: the formula never disagrees with the definition.
A, B = (1, 2, 3), ("p", "q", "r")
pairs = list(product(A, B))
all_r = list(chain.from_iterable(combinations(pairs, k) for k in range(len(pairs) + 1)))
all_s = list(chain.from_iterable(combinations(B, k) for k in range(len(B) + 1)))
checked = disagreements = 0
for R in map(set, all_r):
    for S in map(set, all_s):
        checked += 1
        disagreements += by_definition(R, S) != by_formula(R, S)
print(f"\n{checked:,} pairs (R, S) checked, {disagreements} disagreements")

print("\nEdge cases:")
print("  Core empty: Enrolled ÷ {} =", sorted(by_formula(ENROLLED, set())))
print("  Enrolled empty: {} ÷ Core =", sorted(by_formula(set(), CORE)))

Output

Enrolled ÷ Core, step by step:
  π sid (Enrolled)               5 tuples  [1, 2, 3, 4, 5]
  that × Core                   15 tuples
  that − Enrolled                5 tuples  [(2, 'CS201'), (2, 'CS202'), (3, 'CS202'), (4, 'CS201'), (4, 'CS202')]
  π sid of the missing pairs     3 tuples  [2, 3, 4]
  π sid (Enrolled) − that        2 tuples  [1, 5]

4,096 pairs (R, S) checked, 0 disagreements

Edge cases:
  Core empty: Enrolled ÷ {} = [1, 2, 3, 4, 5]
  Enrolled empty: {} ÷ Core = []

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 division.py

Enrolled ÷ Core in five steps: 5 candidates, 15 pairs, 5 missing pairs, 3 candidates short of a course, answer 1 and 5.π sid (Enrolled)the candidates: 1, 2, 3, 4, 5× Coreevery candidate with every core course:15 pairs− Enrolledthe pairs nobody enrolled in: 5(2, CS201) (2, CS202) (3, CS202)(4, CS201) (4, CS202)π sidthe candidates missing a course: 2, 3, 4candidates − those1 and 5: Asha and Esha

Enrolled ÷ Core, step by step

Text description of the diagram

Five boxes from top to bottom, each the result of one operator applied to the box above it.

  1. π sid (Enrolled): the candidates 1, 2, 3, 4 and 5.
  2. Times Core: every candidate with every core course, 15 pairs.
  3. Minus Enrolled: the 5 pairs nobody enrolled in, (2, CS201), (2, CS202), (3, CS202), (4, CS201) and (4, CS202).
  4. π sid of those: the candidates missing a course, 2, 3 and 4.
  5. The candidates minus those: 1 and 5, Asha and Esha, the answer.

The five candidates and the three core courses make 15 pairs that would all have to be enrolments. Five of them are not: Bilal missed two core courses, Chitra one and Dev two. Removing sids 2, 3 and 4 leaves 1 and 5. The program then tries every R over three values of a and three of b against every S over the same b values, 4,096 pairs of relations, and the formula never disagrees with the definition.

Two edge cases come out of the run. If S is empty, “for every b in S” is true of everything, so R ÷ S is all of π A (R): students 1 to 5. If R is empty, there are no candidates, and Codd’s paper says so explicitly: when R is empty, R divided by S is empty, even if S is empty too.

Note which candidates division considers: only the values of A that appear in R. Farhan, who has no enrolment, is not in π sid (Enrolled), so he can never be in Enrolled ÷ Core, not even when Core is empty. To divide over all students, put π sid (Student) in place of π A (R) in both places of the formula.

Division in SQL, and the empty divisor

SQL has no division operator. The standard way to write “every” is a double negation: a student qualifies when no core course exists that the student has not taken. Each “no … exists” is a NOT EXISTS subquery, and since an EXISTS test only asks whether a row exists, the nesting carries no risk of duplicate rows. A second common way counts: a student qualifies when the number of different core courses they took equals the number of core courses. The program writes three versions as one view, runs it, empties the core list and runs it again:

Three SQL forms of division, before and after the core list is emptied SQL · every_core.sql
CREATE TABLE student (sid INTEGER PRIMARY KEY, name TEXT NOT NULL, branch TEXT NOT NULL);
CREATE TABLE enrolled (sid INTEGER NOT NULL, cid TEXT NOT NULL, grade TEXT, PRIMARY KEY (sid, cid));
CREATE TABLE core (cid TEXT PRIMARY KEY);
INSERT INTO student VALUES
  (1, 'Asha', 'CSE'), (2, 'Bilal', 'ECE'), (3, 'Chitra', 'CSE'),
  (4, 'Dev', 'ME'), (5, 'Esha', 'CSE'), (6, 'Farhan', 'ECE');
INSERT INTO enrolled VALUES
  (1, 'CS201', 'A'), (1, 'CS202', 'B'), (1, 'MA101', 'A'), (2, 'EC210', 'A'),
  (2, 'MA101', 'B'), (3, 'CS201', 'B'), (3, 'MA101', 'A'), (4, 'MA101', 'C'),
  (5, 'CS201', 'A'), (5, 'CS202', 'A'), (5, 'MA101', 'B'), (5, 'HS101', 'A');
INSERT INTO core VALUES ('CS201'), ('CS202'), ('MA101');

-- Three ways to ask for the students who took every core course.
CREATE VIEW answers AS
-- 1. Double NOT EXISTS over all students: no core course exists that the student has not taken.
SELECT 'NOT EXISTS, candidates from student' AS method, count(*) AS answers,
       coalesce(group_concat(sid, ', ' ORDER BY sid), '(none)') AS sids
FROM student s
WHERE NOT EXISTS (SELECT 1 FROM core c
                  WHERE NOT EXISTS (SELECT 1 FROM enrolled e WHERE e.sid = s.sid AND e.cid = c.cid))
UNION ALL
-- 2. The same test with the candidates taken from enrolled, as R ÷ S takes them from R.
SELECT 'NOT EXISTS, candidates from enrolled', count(*),
       coalesce(group_concat(sid, ', ' ORDER BY sid), '(none)')
FROM (SELECT DISTINCT sid FROM enrolled) r
WHERE NOT EXISTS (SELECT 1 FROM core c
                  WHERE NOT EXISTS (SELECT 1 FROM enrolled e WHERE e.sid = r.sid AND e.cid = c.cid))
UNION ALL
-- 3. Counting: as many different core courses taken as there are core courses.
SELECT 'GROUP BY and count', count(*), coalesce(group_concat(sid, ', ' ORDER BY sid), '(none)')
FROM (SELECT e.sid FROM enrolled e
      WHERE e.cid IN (SELECT cid FROM core)
      GROUP BY e.sid
      HAVING count(DISTINCT e.cid) = (SELECT count(*) FROM core));

SELECT * FROM answers;

-- The college drops its core list for a term: "every core course" is now true of every student.
DELETE FROM core;
SELECT * FROM answers;

Output

┌──────────────────────────────────────┬─────────┬──────┐
│                method                │ answers │ sids │
├──────────────────────────────────────┼─────────┼──────┤
│ NOT EXISTS, candidates from student  │ 2       │ 1, 5 │
│ NOT EXISTS, candidates from enrolled │ 2       │ 1, 5 │
│ GROUP BY and count                   │ 2       │ 1, 5 │
└──────────────────────────────────────┴─────────┴──────┘
┌──────────────────────────────────────┬─────────┬──────────────────┐
│                method                │ answers │       sids       │
├──────────────────────────────────────┼─────────┼──────────────────┤
│ NOT EXISTS, candidates from student  │ 6       │ 1, 2, 3, 4, 5, 6 │
│ NOT EXISTS, candidates from enrolled │ 5       │ 1, 2, 3, 4, 5    │
│ GROUP BY and count                   │ 0       │ (none)           │
└──────────────────────────────────────┴─────────┴──────────────────┘

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

With three core courses, all three versions agree on sids 1 and 5. With an empty core list they give three answers:

  • NOT EXISTS over student returns all 6 students. No core course exists, so none is missing for anyone, Farhan included. This is the plain reading of “took every core course” when there are none.
  • NOT EXISTS over the sids of enrolled returns the 5 students who have any enrolment. It is exactly Enrolled ÷ Core, because its candidates come from R, as the algebra’s do.
  • GROUP BY with count returns nobody. It first keeps the enrolments in core courses, of which there are none, so no group is left to compare with a count of 0.

None of the three is wrong in itself, but only the first two agree with the definition of “every”. The counting form also needs care in normal use: it must keep only enrolments in divisor courses (an extra course would otherwise make up for a missing one) and count distinct courses (a duplicated enrolment would otherwise count twice). Prefer the double NOT EXISTS, and choose its outer table by deciding who the candidates are.

Extended operators: grouping, computing and sorting

The core algebra can find the courses with the most credits, but it cannot count or add up anything. Extended relational algebra adds operators for that, each with a direct SQL counterpart:

  • Grouping and aggregation, written γ grouping attributes; aggregates (R): one tuple per group, with an aggregate function such as count, sum, avg, min or max computed over the group. γ branch; count(sid) → students (Student) gives one tuple per branch. With no grouping attributes, the whole relation is one group.
  • Generalised projection, π with expressions: π sid, credits × 10 → hours (R) computes a new attribute from old ones, the way a SELECT list can.
  • Duplicate elimination, δ (R), which removes repeated tuples from a bag; on sets it changes nothing.
  • Sorting, τ attributes (R), which orders the result. A relation has no order, so τ only makes sense at the root of an expression, like ORDER BY at the end of a query.
  • Outer joins, from the previous lesson, which need NULL and so are not part of the core either.
Grouping, a generalised projection, sorting, and aggregates over no rows SQL · extended.sql
CREATE TABLE student (sid INTEGER PRIMARY KEY, name TEXT NOT NULL, branch TEXT NOT NULL);
CREATE TABLE course (cid TEXT PRIMARY KEY, title TEXT NOT NULL, dept TEXT NOT NULL, credits INTEGER NOT NULL);
CREATE TABLE enrolled (sid INTEGER NOT NULL, cid TEXT NOT NULL, grade TEXT, PRIMARY KEY (sid, cid));
INSERT INTO student VALUES
  (1, 'Asha', 'CSE'), (2, 'Bilal', 'ECE'), (3, 'Chitra', 'CSE'),
  (4, 'Dev', 'ME'), (5, 'Esha', 'CSE'), (6, 'Farhan', 'ECE');
INSERT INTO course VALUES
  ('CS201', 'Databases', 'CSE', 4), ('CS202', 'Operating Systems', 'CSE', 4),
  ('CS301', 'Compilers', 'CSE', 4), ('MA101', 'Discrete Mathematics', 'MATH', 3),
  ('EC210', 'Signals and Systems', 'ECE', 3), ('HS101', 'Technical Writing', 'HSS', 2);
INSERT INTO enrolled VALUES
  (1, 'CS201', 'A'), (1, 'CS202', 'B'), (1, 'MA101', 'A'), (2, 'EC210', 'A'),
  (2, 'MA101', 'B'), (3, 'CS201', 'B'), (3, 'MA101', 'A'), (4, 'MA101', 'C'),
  (5, 'CS201', 'A'), (5, 'CS202', 'A'), (5, 'MA101', 'B'), (5, 'HS101', 'A');

-- γ branch; count(sid) → students (Student): one tuple per group.
SELECT branch, count(sid) AS students FROM student GROUP BY branch;

-- Generalised projection (grade points, on a 10-point scale with A = 10, B = 8, C = 6), then
-- γ sid; sum(credits), sum(points × credits) / sum(credits) → the credit-weighted average, sorted (τ).
SELECT e.sid, sum(c.credits) AS credits,
       round(sum(CASE e.grade WHEN 'A' THEN 10 WHEN 'B' THEN 8 WHEN 'C' THEN 6 END * c.credits) * 1.0
             / sum(c.credits), 2) AS weighted_points
FROM enrolled e JOIN course c ON c.cid = e.cid
GROUP BY e.sid
ORDER BY weighted_points DESC;

-- Aggregates over no rows: count gives 0 and sum gives NULL, while grouping gives no groups at all.
SELECT count(*) AS f_grades, quote(sum(c.credits)) AS f_credits,
       (SELECT count(*) FROM (SELECT grade FROM enrolled WHERE grade = 'F' GROUP BY grade)) AS f_groups
FROM enrolled e JOIN course c ON c.cid = e.cid WHERE e.grade = 'F';

Output

┌────────┬──────────┐
│ branch │ students │
├────────┼──────────┤
│ CSE    │ 3        │
│ ECE    │ 2        │
│ ME     │ 1        │
└────────┴──────────┘
┌─────┬─────────┬─────────────────┐
│ sid │ credits │ weighted_points │
├─────┼─────────┼─────────────────┤
│ 5   │ 13      │ 9.54            │
│ 1   │ 11      │ 9.27            │
│ 2   │ 6       │ 9.0             │
│ 3   │ 7       │ 8.86            │
│ 4   │ 3       │ 6.0             │
└─────┴─────────┴─────────────────┘
┌──────────┬───────────┬──────────┐
│ f_grades │ f_credits │ f_groups │
├──────────┼───────────┼──────────┤
│ 0        │ NULL      │ 0        │
└──────────┴───────────┴──────────┘

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

The second query combines all three. A generalised projection turns each grade into points on a 10-point scale (A is 10, B is 8 and C is 6 in this example) and multiplies them by the course’s credits; γ sid adds those up per student and divides by the student’s credits; τ sorts the result. Esha’s 124 points over 13 credits give 9.54. Farhan has no row, because he has no enrolment to group: as with division, an aggregate only sees the tuples its input has.

The last query shows aggregates over no rows. count returns 0, but sum returns NULL: PostgreSQL’s manual makes the same point about its own aggregates, all of which give NULL over an empty input except count, so that the sum of nothing is NULL rather than 0. Grouped by grade, the same empty input gives no group at all, so a GROUP BY query returns no rows where the ungrouped query returned one. That is the same effect that made the counting form of division return nobody.

Bags: the algebra that SQL uses

An SQL table can hold the same row twice, so SQL works with bags (multisets), in which each tuple has a multiplicity. The bag versions of the set operators count copies: a tuple that appears m times in R and n times in S appears m + n times in the bag union R ⊎ S, min(m, n) times in the bag intersection and max(m − n, 0) times in the bag difference. These are the rules of SQL’s ALL operators; PostgreSQL’s manual gives the last two for INTERSECT ALL (“min(m,n) times”) and EXCEPT ALL (“max(m-n,0) times”). SQLite has UNION ALL, but not INTERSECT ALL or EXCEPT ALL.

Bag union, intersection and difference with Counter Python · bags.py
"""Bag (multiset) versions of union, intersection and difference, the way SQL's ALL operators count."""
from collections import Counter
import sqlite3

# The branches of the students in two tutorial groups: a bag, because a branch can appear more than once.
group_1 = Counter(["CSE", "CSE", "ECE", "ME"])
group_2 = Counter(["CSE", "ECE", "ECE"])


def show(label, bag):
    print(f"  {label:40} {sorted(bag.elements())}")


show("group 1", group_1)
show("group 2", group_2)
show("union, m + n copies (UNION ALL)", group_1 + group_2)
show("intersection, min(m, n) (INTERSECT ALL)", group_1 & group_2)
show("difference, max(m − n, 0) (EXCEPT ALL)", group_1 - group_2)
show("set union (UNION)", Counter(set(group_1) | set(group_2)))

# SQLite has UNION ALL, so the first bag operation can be checked against a real engine.
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE g1 (branch TEXT)")
db.execute("CREATE TABLE g2 (branch TEXT)")
db.executemany("INSERT INTO g1 VALUES (?)", [(b,) for b in group_1.elements()])
db.executemany("INSERT INTO g2 VALUES (?)", [(b,) for b in group_2.elements()])
rows = Counter(b for (b,) in db.execute("SELECT branch FROM g1 UNION ALL SELECT branch FROM g2"))
print("  SQLite's UNION ALL gives the same bag:", rows == group_1 + group_2)

# A law of sets that bags break: R ∪ R = R.
print("  R ∪ R = R for sets:", set(group_1) | set(group_1) == set(group_1),
      "| for bags:", group_1 + group_1 == group_1)

Output

  group 1                                  ['CSE', 'CSE', 'ECE', 'ME']
  group 2                                  ['CSE', 'ECE', 'ECE']
  union, m + n copies (UNION ALL)          ['CSE', 'CSE', 'CSE', 'ECE', 'ECE', 'ECE', 'ME']
  intersection, min(m, n) (INTERSECT ALL)  ['CSE', 'ECE']
  difference, max(m − n, 0) (EXCEPT ALL)   ['CSE', 'ME']
  set union (UNION)                        ['CSE', 'ECE', 'ME']
  SQLite's UNION ALL gives the same bag: True
  R ∪ R = R for sets: True | for bags: False

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bags.py

Python’s Counter implements exactly these rules with +, & and -, and SQLite’s UNION ALL gives the same bag as +. Most algebraic laws survive the move to bags, such as selection distributing over union, but some do not: R ∪ R = R holds for sets, while R ⊎ R has every tuple twice. An optimiser that rewrites SQL must use the bag laws, because duplicates change counts and sums.

What relational algebra cannot express

Some questions are beyond every expression of the algebra, extended or not. A classic one is the transitive closure of a relation: with a relation Prereq(course, needs), list every course with all its prerequisites, direct and indirect, to any depth. A. V. Aho and J. D. Ullman proved in 1979 that relational algebra cannot express it. The intuition is that an expression has a fixed number of joins, so it can follow chains of prerequisites only up to a fixed length, and a chain one step longer escapes it. SQL closes the gap with recursive queries: in PostgreSQL’s words, RECURSIVE lets a WITH query “refer to its own output”, which is what following a chain of any length needs. A later lesson of this track covers recursive queries and Datalog.

Division in GATE CS

Relational algebra is part of Section 9, Databases, of the GATE 2027 Computer Science syllabus. When a question says “all” or “every”, look for a division, written as ÷ or spelled out as π A (R) − π A ((π A (R) × S) − R). Check the heading of the result, remember that the candidates come from R, work out what happens when S is empty, and use the fact that a value of A needs at least |S| tuples of R to qualify when a question asks for a size.

SQLite Online (SQL Playground) Paste every_core.sql, then delete a single core course instead of all three and predict the three answers before you run it. Run SQL on CSV & Excel Files Load enrolments and a course list as two CSV files and write the double NOT EXISTS query against them.

Exercises

Exercise · Medium · SQL

Students who took every course of one department

The tables are the lesson's student(sid, name, branch), course(cid, title, dept, credits) and enrolled(sid, cid, grade), plus a one-row table target(dept) that names a department.

Write one query that returns a single column, name: the students who are enrolled in every course that the department in target offers. Every student in student is a candidate, including students with no enrolments. If the department offers no courses at all, every student qualifies, because there is no course they have missed.

The sample tests use three departments: one with a single course, one with two, and one with none.

Starter code · every_course.sql

-- The students enrolled in every course of the department named in target.
SELECT s.name
FROM student s;
The sample tests · tests.yaml
# Sample tests: each one builds the tables, runs your query on them and compares the rows it returns (in any order).
tests:
  - name: "a department with one course: everyone who took MA101"
    setup: |
      CREATE TABLE student (sid INTEGER PRIMARY KEY, name TEXT NOT NULL, branch TEXT NOT NULL);
      CREATE TABLE course (cid TEXT PRIMARY KEY, title TEXT NOT NULL, dept TEXT NOT NULL, credits INTEGER NOT NULL);
      CREATE TABLE enrolled (sid INTEGER NOT NULL, cid TEXT NOT NULL, grade TEXT, PRIMARY KEY (sid, cid));
      CREATE TABLE target (dept TEXT NOT NULL);
      INSERT INTO target VALUES ('MATH');
      INSERT INTO student VALUES
        (1, 'Asha', 'CSE'), (2, 'Bilal', 'ECE'), (3, 'Chitra', 'CSE'),
        (4, 'Dev', 'ME'), (5, 'Esha', 'CSE'), (6, 'Farhan', 'ECE');
      INSERT INTO course VALUES
        ('CS201', 'Databases', 'CSE', 4), ('CS202', 'Operating Systems', 'CSE', 4),
        ('CS301', 'Compilers', 'CSE', 4), ('MA101', 'Discrete Mathematics', 'MATH', 3),
        ('EC210', 'Signals and Systems', 'ECE', 3), ('HS101', 'Technical Writing', 'HSS', 2);
      INSERT INTO enrolled VALUES
        (1, 'CS201', 'A'), (1, 'CS202', 'B'), (1, 'MA101', 'A'), (2, 'EC210', 'A'),
        (2, 'MA101', 'B'), (3, 'CS201', 'B'), (3, 'MA101', 'A'), (4, 'MA101', 'C'),
        (5, 'CS201', 'A'), (5, 'CS202', 'A'), (5, 'MA101', 'B'), (5, 'HS101', 'A');
    expected:
      columns: [name]
      rows:
        - [Asha]
        - [Bilal]
        - [Chitra]
        - [Dev]
        - [Esha]
  - name: "a department with two courses: only the students who took both"
    setup: |
      CREATE TABLE student (sid INTEGER PRIMARY KEY, name TEXT NOT NULL, branch TEXT NOT NULL);
      CREATE TABLE course (cid TEXT PRIMARY KEY, title TEXT NOT NULL, dept TEXT NOT NULL, credits INTEGER NOT NULL);
      CREATE TABLE enrolled (sid INTEGER NOT NULL, cid TEXT NOT NULL, grade TEXT, PRIMARY KEY (sid, cid));
      CREATE TABLE target (dept TEXT NOT NULL);
      INSERT INTO target VALUES ('CSE');
      INSERT INTO student VALUES (10, 'Ravi', 'ECE'), (11, 'Meena', 'CSE'), (12, 'Kiran', 'ME'), (13, 'Lata', 'ME');
      INSERT INTO course VALUES
        ('CS201', 'Databases', 'CSE', 4), ('CS250', 'Computer Networks', 'CSE', 4),
        ('ME110', 'Mechanics', 'ME', 3), ('HS102', 'Ethics', 'HSS', 2);
      INSERT INTO enrolled VALUES
        (10, 'CS250', 'B'), (11, 'CS201', 'A'), (11, 'CS250', 'A'),
        (12, 'CS201', 'B'), (12, 'CS250', 'C'), (12, 'ME110', 'A');
    expected:
      columns: [name]
      rows:
        - [Meena]
        - [Kiran]
  - name: "a department with no courses: every student, also those with no enrolment"
    setup: |
      CREATE TABLE student (sid INTEGER PRIMARY KEY, name TEXT NOT NULL, branch TEXT NOT NULL);
      CREATE TABLE course (cid TEXT PRIMARY KEY, title TEXT NOT NULL, dept TEXT NOT NULL, credits INTEGER NOT NULL);
      CREATE TABLE enrolled (sid INTEGER NOT NULL, cid TEXT NOT NULL, grade TEXT, PRIMARY KEY (sid, cid));
      CREATE TABLE target (dept TEXT NOT NULL);
      INSERT INTO target VALUES ('LAW');
      INSERT INTO student VALUES
        (1, 'Asha', 'CSE'), (2, 'Bilal', 'ECE'), (3, 'Chitra', 'CSE'),
        (4, 'Dev', 'ME'), (5, 'Esha', 'CSE'), (6, 'Farhan', 'ECE');
      INSERT INTO course VALUES
        ('CS201', 'Databases', 'CSE', 4), ('CS202', 'Operating Systems', 'CSE', 4),
        ('CS301', 'Compilers', 'CSE', 4), ('MA101', 'Discrete Mathematics', 'MATH', 3),
        ('EC210', 'Signals and Systems', 'ECE', 3), ('HS101', 'Technical Writing', 'HSS', 2);
      INSERT INTO enrolled VALUES
        (1, 'CS201', 'A'), (1, 'CS202', 'B'), (1, 'MA101', 'A'), (2, 'EC210', 'A'),
        (2, 'MA101', 'B'), (3, 'CS201', 'B'), (3, 'MA101', 'A'), (4, 'MA101', 'C'),
        (5, 'CS201', 'A'), (5, 'CS202', 'A'), (5, 'MA101', 'B'), (5, 'HS101', 'A');
    expected:
      columns: [name]
      rows:
        - [Asha]
        - [Bilal]
        - [Chitra]
        - [Dev]
        - [Esha]
        - [Farhan]
A hint

Turn "every course" around: a student qualifies when there is no course of the department that the student has not taken. That is two NOT EXISTS, one inside the other. A version that counts courses has to be checked against the department with no courses.

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.

Exercise · Medium · SQL

Students who took every course one student took

The tables are the lesson's student(sid, name, branch), course(cid, title, dept, credits) and enrolled(sid, cid, grade), plus a one-row table target(sid) that names one student.

Write one query that returns a single column, name: every other student who is enrolled in all the courses that the target student is enrolled in. Extra courses do not matter, and the target student is not part of the answer. If the target student has no enrolments, every other student qualifies.

This is a division whose divisor is computed from the data: the courses of one student.

Starter code · same_courses.sql

-- Every other student enrolled in all the courses the target student is enrolled in.
SELECT s.name
FROM student s
WHERE s.sid <> (SELECT sid FROM target);
The sample tests · tests.yaml
# Sample tests: each one builds the tables, runs your query on them and compares the rows it returns (in any order).
tests:
  - name: the students who took all of Asha's courses, Asha left out
    setup: |
      CREATE TABLE student (sid INTEGER PRIMARY KEY, name TEXT NOT NULL, branch TEXT NOT NULL);
      CREATE TABLE course (cid TEXT PRIMARY KEY, title TEXT NOT NULL, dept TEXT NOT NULL, credits INTEGER NOT NULL);
      CREATE TABLE enrolled (sid INTEGER NOT NULL, cid TEXT NOT NULL, grade TEXT, PRIMARY KEY (sid, cid));
      CREATE TABLE target (sid INTEGER NOT NULL);
      INSERT INTO target VALUES (1);
      INSERT INTO student VALUES
        (1, 'Asha', 'CSE'), (2, 'Bilal', 'ECE'), (3, 'Chitra', 'CSE'),
        (4, 'Dev', 'ME'), (5, 'Esha', 'CSE'), (6, 'Farhan', 'ECE');
      INSERT INTO course VALUES
        ('CS201', 'Databases', 'CSE', 4), ('CS202', 'Operating Systems', 'CSE', 4),
        ('CS301', 'Compilers', 'CSE', 4), ('MA101', 'Discrete Mathematics', 'MATH', 3),
        ('EC210', 'Signals and Systems', 'ECE', 3), ('HS101', 'Technical Writing', 'HSS', 2);
      INSERT INTO enrolled VALUES
        (1, 'CS201', 'A'), (1, 'CS202', 'B'), (1, 'MA101', 'A'), (2, 'EC210', 'A'),
        (2, 'MA101', 'B'), (3, 'CS201', 'B'), (3, 'MA101', 'A'), (4, 'MA101', 'C'),
        (5, 'CS201', 'A'), (5, 'CS202', 'A'), (5, 'MA101', 'B'), (5, 'HS101', 'A');
    expected:
      columns: [name]
      rows:
        - [Esha]
  - name: the students who took all of Chitra's courses
    setup: |
      CREATE TABLE student (sid INTEGER PRIMARY KEY, name TEXT NOT NULL, branch TEXT NOT NULL);
      CREATE TABLE course (cid TEXT PRIMARY KEY, title TEXT NOT NULL, dept TEXT NOT NULL, credits INTEGER NOT NULL);
      CREATE TABLE enrolled (sid INTEGER NOT NULL, cid TEXT NOT NULL, grade TEXT, PRIMARY KEY (sid, cid));
      CREATE TABLE target (sid INTEGER NOT NULL);
      INSERT INTO target VALUES (3);
      INSERT INTO student VALUES
        (1, 'Asha', 'CSE'), (2, 'Bilal', 'ECE'), (3, 'Chitra', 'CSE'),
        (4, 'Dev', 'ME'), (5, 'Esha', 'CSE'), (6, 'Farhan', 'ECE');
      INSERT INTO course VALUES
        ('CS201', 'Databases', 'CSE', 4), ('CS202', 'Operating Systems', 'CSE', 4),
        ('CS301', 'Compilers', 'CSE', 4), ('MA101', 'Discrete Mathematics', 'MATH', 3),
        ('EC210', 'Signals and Systems', 'ECE', 3), ('HS101', 'Technical Writing', 'HSS', 2);
      INSERT INTO enrolled VALUES
        (1, 'CS201', 'A'), (1, 'CS202', 'B'), (1, 'MA101', 'A'), (2, 'EC210', 'A'),
        (2, 'MA101', 'B'), (3, 'CS201', 'B'), (3, 'MA101', 'A'), (4, 'MA101', 'C'),
        (5, 'CS201', 'A'), (5, 'CS202', 'A'), (5, 'MA101', 'B'), (5, 'HS101', 'A');
    expected:
      columns: [name]
      rows:
        - [Asha]
        - [Esha]
  - name: "a student with no courses: every other student qualifies"
    setup: |
      CREATE TABLE student (sid INTEGER PRIMARY KEY, name TEXT NOT NULL, branch TEXT NOT NULL);
      CREATE TABLE course (cid TEXT PRIMARY KEY, title TEXT NOT NULL, dept TEXT NOT NULL, credits INTEGER NOT NULL);
      CREATE TABLE enrolled (sid INTEGER NOT NULL, cid TEXT NOT NULL, grade TEXT, PRIMARY KEY (sid, cid));
      CREATE TABLE target (sid INTEGER NOT NULL);
      INSERT INTO target VALUES (6);
      INSERT INTO student VALUES
        (1, 'Asha', 'CSE'), (2, 'Bilal', 'ECE'), (3, 'Chitra', 'CSE'),
        (4, 'Dev', 'ME'), (5, 'Esha', 'CSE'), (6, 'Farhan', 'ECE');
      INSERT INTO course VALUES
        ('CS201', 'Databases', 'CSE', 4), ('CS202', 'Operating Systems', 'CSE', 4),
        ('CS301', 'Compilers', 'CSE', 4), ('MA101', 'Discrete Mathematics', 'MATH', 3),
        ('EC210', 'Signals and Systems', 'ECE', 3), ('HS101', 'Technical Writing', 'HSS', 2);
      INSERT INTO enrolled VALUES
        (1, 'CS201', 'A'), (1, 'CS202', 'B'), (1, 'MA101', 'A'), (2, 'EC210', 'A'),
        (2, 'MA101', 'B'), (3, 'CS201', 'B'), (3, 'MA101', 'A'), (4, 'MA101', 'C'),
        (5, 'CS201', 'A'), (5, 'CS202', 'A'), (5, 'MA101', 'B'), (5, 'HS101', 'A');
    expected:
      columns: [name]
      rows:
        - [Asha]
        - [Bilal]
        - [Chitra]
        - [Dev]
        - [Esha]
  - name: other data, where one student took a superset and one a different set
    setup: |
      CREATE TABLE student (sid INTEGER PRIMARY KEY, name TEXT NOT NULL, branch TEXT NOT NULL);
      CREATE TABLE course (cid TEXT PRIMARY KEY, title TEXT NOT NULL, dept TEXT NOT NULL, credits INTEGER NOT NULL);
      CREATE TABLE enrolled (sid INTEGER NOT NULL, cid TEXT NOT NULL, grade TEXT, PRIMARY KEY (sid, cid));
      CREATE TABLE target (sid INTEGER NOT NULL);
      INSERT INTO target VALUES (11);
      INSERT INTO student VALUES (10, 'Ravi', 'ECE'), (11, 'Meena', 'CSE'), (12, 'Kiran', 'ME'), (13, 'Lata', 'ME');
      INSERT INTO course VALUES
        ('CS201', 'Databases', 'CSE', 4), ('CS250', 'Computer Networks', 'CSE', 4),
        ('ME110', 'Mechanics', 'ME', 3), ('HS102', 'Ethics', 'HSS', 2);
      INSERT INTO enrolled VALUES
        (10, 'CS250', 'B'), (11, 'CS201', 'A'), (11, 'CS250', 'A'),
        (12, 'CS201', 'B'), (12, 'CS250', 'C'), (12, 'ME110', 'A');
    expected:
      columns: [name]
      rows:
        - [Kiran]
A hint

Write the divisor first, the courses of the target student, as a subquery. Then keep each other student for whom no course of the target student exists that this student has not taken.

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

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 R(a, b) has 12 tuples and S(b) has 3. What is the largest number of tuples R ÷ S can have?

    Type a number.

    Show the answer to question 1

    Answer: 4 tuples

    Every a in the answer must appear in R with all 3 values of S, so it uses up at least 3 tuples of R. 12 tuples are enough for at most 12 ÷ 3 = 4 such values of a.

  2. Question 2 of 6 R(a, b) holds (1, x), (1, y) and (2, x), and S(b) is empty. What is R ÷ S?

    Choose one answer.

    Show the answer to question 2

    Answer: The a values 1 and 2

    R ÷ S keeps each a of R that is paired with every b of S. With no b in S, that holds for every a of R, so the answer is π a (R). division.py checks this edge case, and the formula agrees with the definition on all 4,096 pairs it tries.

  3. Question 3 of 6 Core is empty. Which way of asking for "students who took every core course" returns no rows?

    Choose one answer.

    Show the answer to question 3

    Answer: GROUP BY sid with HAVING count(DISTINCT cid) = (SELECT count(*) FROM core), over enrolments in core courses

    The counting form only looks at enrolments in core courses. With no core course there are none, so no group exists to compare with the count 0, and the answer is empty. Both NOT EXISTS forms and the algebra treat "every course of an empty set" as true, as every_core.sql shows after DELETE FROM core.

  4. Question 4 of 6 How many tuples does γ branch; count(sid) → students (Student) return for the sample Student relation, whose six students study CSE, ECE or ME?

    Type a number.

    Show the answer to question 4

    Answer: 3 tuples

    Grouping returns one tuple per group, here one per branch: CSE with 3, ECE with 2 and ME with 1.

  5. Question 5 of 6 What does SELECT sum(credits) FROM course WHERE dept = 'LAW' return when no course belongs to that department?

    Choose one answer.

    Show the answer to question 5

    Answer: One row whose value is NULL

    Without GROUP BY, an aggregate query returns exactly one row, and sum over no rows is NULL (count would be 0). With GROUP BY dept, there would be no group and therefore no row.

  6. Question 6 of 6 Which of these laws still hold when union and difference work on bags (SQL's UNION ALL and EXCEPT ALL)?

    Choose every answer that is right.

    Show the answer to question 6

    Answer:

    • σ c (R ⊎ S) = σ c (R) ⊎ σ c (S)
    • (R ⊎ S) −bag S = R

    A bag union adds the copies, m + n, and a bag difference takes away n copies, so adding S and then taking S away leaves R exactly, and a selection can be applied to each operand first. R ⊎ R doubles every copy, so the set law R ∪ R = R fails, as bags.py prints.

Interview questions

What does R ÷ S return when S is empty? It returns π A (R), every value of A that appears in R. A value qualifies when it is paired in R with every tuple of S, and with no tuple in S that condition holds for every value, so nothing is removed. Two consequences follow: values of A that do not appear in R are still not returned, and an SQL version that counts the divisor’s rows returns nothing at all, so it does not agree with division in this case.

Key takeaways

  • R(A, B) ÷ S(B) returns the values of A that R pairs with every tuple of S; it answers “every”, “all” and “each” questions.
  • R ÷ S = π A (R) − π A ((π A (R) × S) − R): the candidates, minus those that miss at least one pair.
  • With an empty divisor, division returns π A (R); a double NOT EXISTS agrees with “every”, while the counting form returns nothing.
  • Extended algebra adds γ (grouping and aggregation), generalised projection, δ, τ and outer joins; count over no rows is 0, but sum is NULL.
  • SQL works with bags: a tuple appears m + n times in UNION ALL, min(m, n) times in INTERSECT ALL and max(m − n, 0) times in EXCEPT ALL, and R ⊎ R ≠ R.
  • Relational algebra cannot compute a transitive closure; recursive SQL queries can.

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.