Your country

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

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

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

Subqueries: IN, EXISTS, ANY and ALL

Scalar, row and correlated subqueries, and how IN, EXISTS, ANY and ALL behave with empty sets and NULLs, so you know when a rewrite changes the result.

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

What you will learn

  • Evaluate scalar, row and table subqueries, including correlated ones
  • Predict the results of ANY and ALL with empty sets and with NULLs
  • Rewrite between IN, EXISTS and joins, and identify the rewrites that change the result
  • Replace NOT IN with NOT EXISTS where a NULL would empty the result

Before you start

What you’ll learn · Before you start

What you will learn

  • Evaluate scalar, row and table subqueries, including correlated ones
  • Predict the results of ANY and ALL with empty sets and with NULLs
  • Rewrite between IN, EXISTS and joins, and identify the rewrites that change the result
  • Replace NOT IN with NOT EXISTS where a NULL would empty the result

Before you start

On this page

A subquery is a SELECT inside another statement, and it comes in three shapes: a scalar subquery returns one value, a row subquery returns one row, and a table subquery returns a set of rows that IN, EXISTS, ANY and ALL test. A correlated subquery refers to a column of the outer query, so logically it runs again for every outer row. EXISTS only asks whether at least one row comes back, so it is always TRUE or FALSE. IN, ANY and ALL compare a value with every row and follow three-valued logic: x IN S means x = ANY S, x NOT IN S means x <> ALL S, ALL over an empty set is TRUE, ANY over an empty set is FALSE, and a single NULL among the rows can turn a FALSE or a TRUE into UNKNOWN. These few rules decide when NOT IN, NOT EXISTS and a join return the same rows.

This lesson uses the rules of the three-valued logic lesson throughout.

Three shapes of subquery

The shape decides where a subquery may stand. A scalar subquery can go wherever a value can, a row subquery is compared with a row of values, and a table subquery is the right-hand side of IN, EXISTS, ANY or ALL. Five students and five results:

A scalar subquery, a two-row surprise and a row subquery SQL · subquery_shapes.sql
-- Five students and their results; Esha (105) has not taken any test yet.
CREATE TABLE students (roll INTEGER PRIMARY KEY, name TEXT NOT NULL, dept TEXT NOT NULL);
CREATE TABLE results (roll INTEGER NOT NULL, course TEXT NOT NULL, marks INTEGER);
INSERT INTO students VALUES (101, 'Aarav', 'CSE'), (102, 'Diya', 'ECE'), (103, 'Kabir', 'CSE'),
                            (104, 'Meera', 'ME'), (105, 'Esha', 'ECE');
INSERT INTO results VALUES (101, 'CS201', 91), (101, 'MA101', 78), (102, 'CS201', 64),
                           (103, 'CS201', 55), (104, 'ME105', 88);

-- A scalar subquery gives one value for each outer row; with no rows, the value is NULL.
SELECT s.name, quote((SELECT max(r.marks) FROM results AS r WHERE r.roll = s.roll)) AS best
FROM students AS s
ORDER BY s.roll;

-- A scalar subquery that returns two rows: SQLite takes the first one; PostgreSQL stops with an error.
SELECT (SELECT r.marks FROM results AS r WHERE r.roll = 101) AS one_of_two;

-- A row subquery compares two columns at once: the first ECE student by roll number.
SELECT name FROM students
WHERE (dept, roll) = (SELECT dept, min(roll) FROM students WHERE dept = 'ECE' GROUP BY dept);

Output

┌───────┬──────┐
│ name  │ best │
├───────┼──────┤
│ Aarav │ 91   │
│ Diya  │ 64   │
│ Kabir │ 55   │
│ Meera │ 88   │
│ Esha  │ NULL │
└───────┴──────┘
┌────────────┐
│ one_of_two │
├────────────┤
│ 91         │
└────────────┘
┌──────┐
│ name │
├──────┤
│ Diya │
└──────┘

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

  • A scalar subquery per outer row. Each student’s best mark is computed by a subquery that refers to s.roll, a column of the outer query. Esha has no results, so her subquery returns no rows and its value is NULL, which both engines specify (SQLite: subquery expressions, PostgreSQL 18: scalar subqueries).
  • Two rows where one was expected. Student 101 has two results. SQLite takes the first row the subquery returns and prints 91 without complaint. PostgreSQL treats a scalar subquery with more than one row as an error, and stops with “more than one row returned by a subquery used as an expression”. The SQLite answer depends on which row happens to come first, so a query that relies on it is a bug waiting for new data.
  • A row subquery compares the pair (dept, roll) with one row: the first ECE student by roll number, Diya.

EXISTS, correlation and semi-joins

EXISTS (subquery) is TRUE if the subquery returns at least one row and FALSE otherwise. What the rows contain does not matter, which is why EXISTS (SELECT 1 …) is the usual spelling, and NULLs in them do not matter either: EXISTS is never UNKNOWN (PostgreSQL 18: subquery expressions). When the subquery is correlated, the outer row’s values act as constants during each evaluation, so the question is asked once per outer row.

EXISTS, IN and a join for the same question SQL · exists_in_join.sql
CREATE TABLE students (roll INTEGER PRIMARY KEY, name TEXT NOT NULL, dept TEXT NOT NULL);
CREATE TABLE results (roll INTEGER NOT NULL, course TEXT NOT NULL, marks INTEGER);
INSERT INTO students VALUES (101, 'Aarav', 'CSE'), (102, 'Diya', 'ECE'), (103, 'Kabir', 'CSE'),
                            (104, 'Meera', 'ME'), (105, 'Esha', 'ECE');
INSERT INTO results VALUES (101, 'CS201', 91), (101, 'MA101', 78), (102, 'CS201', 64),
                           (103, 'CS201', 55), (104, 'ME105', 88);

-- Students with a result of 70 or more, three ways. EXISTS and IN return each student once.
SELECT 'EXISTS' AS form, group_concat(name, ', ') AS students FROM (
  SELECT s.name FROM students AS s
  WHERE EXISTS (SELECT 1 FROM results AS r WHERE r.roll = s.roll AND r.marks >= 70)
  ORDER BY s.roll)
UNION ALL
SELECT 'IN', group_concat(name, ', ') FROM (
  SELECT s.name FROM students AS s
  WHERE s.roll IN (SELECT r.roll FROM results AS r WHERE r.marks >= 70)
  ORDER BY s.roll)
UNION ALL
-- A join returns one row per matching result, so Aarav appears twice.
SELECT 'JOIN', group_concat(name, ', ') FROM (
  SELECT s.name FROM students AS s JOIN results AS r ON r.roll = s.roll
  WHERE r.marks >= 70
  ORDER BY s.roll);

-- NOT EXISTS: the students with no result at all.
SELECT s.name FROM students AS s
WHERE NOT EXISTS (SELECT 1 FROM results AS r WHERE r.roll = s.roll);

Output

┌────────┬─────────────────────┐
│  form  │      students       │
├────────┼─────────────────────┤
│ EXISTS │ Aarav, Meera        │
│ IN     │ Aarav, Meera        │
│ JOIN   │ Aarav, Aarav, Meera │
└────────┴─────────────────────┘
┌──────┐
│ name │
├──────┤
│ Esha │
└──────┘

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

EXISTS and IN both return Aarav and Meera, once each: a student either has a result of 70 or more or does not. The join returns Aarav twice, because it produces one row per matching pair and Aarav has two such results. In algebra, EXISTS is a semi-join: it keeps the rows of the left table that have a partner on the right, without copying anything from the right. NOT EXISTS is an anti-join, which keeps the rows without a partner: Esha, the only student with no results. Questions with “every”, such as the students who took every course, nest two NOT EXISTS; the division lesson builds them step by step.

“Once per outer row” is the meaning, not the plan. EXPLAIN QUERY PLAN shows how SQLite runs the two forms of the question (SQLite: EXPLAIN QUERY PLAN):

How SQLite plans EXISTS and IN SQL · subquery_plans.sql
CREATE TABLE students (roll INTEGER PRIMARY KEY, name TEXT NOT NULL);
CREATE TABLE results (roll INTEGER NOT NULL, course TEXT NOT NULL, marks INTEGER);

-- A correlated EXISTS and the IN form of the same question, as SQLite 3.49.1 plans them.
EXPLAIN QUERY PLAN
SELECT s.name FROM students AS s
WHERE EXISTS (SELECT 1 FROM results AS r WHERE r.roll = s.roll AND r.marks >= 70);

EXPLAIN QUERY PLAN
SELECT s.name FROM students AS s
WHERE s.roll IN (SELECT r.roll FROM results AS r WHERE r.marks >= 70);

Output

┌────┬────────┬─────────┬──────────────────────────────┐
│ id │ parent │ notused │            detail            │
├────┼────────┼─────────┼──────────────────────────────┤
│ 2  │ 0      │ 216     │ SCAN s                       │
│ 5  │ 0      │ 0       │ CORRELATED SCALAR SUBQUERY 1 │
│ 9  │ 5      │ 216     │ SCAN r                       │
└────┴────────┴─────────┴──────────────────────────────┘
┌────┬────────┬─────────┬──────────────────────────────────────────────┐
│ id │ parent │ notused │                    detail                    │
├────┼────────┼─────────┼──────────────────────────────────────────────┤
│ 2  │ 0      │ 91      │ SEARCH s USING INTEGER PRIMARY KEY (rowid=?) │
│ 6  │ 0      │ 0       │ LIST SUBQUERY 1                              │
│ 9  │ 6      │ 216     │ SCAN r                                       │
│ 17 │ 6      │ 0       │ CREATE BLOOM FILTER                          │
└────┴────────┴─────────┴──────────────────────────────────────────────┘

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

For EXISTS it scans the students and runs the correlated subquery for each one (CORRELATED SCALAR SUBQUERY). For IN it builds the list of qualifying roll numbers once (LIST SUBQUERY) and looks each roll number up through the primary key; the Bloom filter in the plan is a quick test that rules out most numbers that cannot match. Same rows, different work: optimisers rewrite subqueries into joins and lists whenever they can prove the result is unchanged.

Version note

Rewriting subqueries is delicate even for optimisers. SQLite 3.51.0 added an optimisation that turns some EXISTS subqueries into joins; release 3.51.1 fixed incorrect results from nested EXISTS queries that it caused (SQLite 3.51.1), and 3.51.2 fixed further problems in the same EXISTS-to-JOIN optimisation (SQLite: release history). The SQLite in your browser, 3.49.1, predates it; on your own systems, keep the engine patched.

IN, NOT IN and the NULL that empties a result

x IN (subquery) is TRUE if some row equals x, FALSE if none does (an empty subquery included), and UNKNOWN if none is equal but some comparison is UNKNOWN. NOT IN is its negation, so it is TRUE only when every row is known to be different from x. PostgreSQL’s manual states both rules, and that IN is equivalent to = ANY and NOT IN to <> ALL (PostgreSQL 18: subquery expressions). The consequence, with one payment that arrived without a roll number:

Who has not paid: NOT IN against NOT EXISTS SQL · not_in_trap.sql
-- Fee payments. One payment came in without a roll number and is still unmatched.
CREATE TABLE students (roll INTEGER PRIMARY KEY, name TEXT NOT NULL);
CREATE TABLE payments (receipt INTEGER PRIMARY KEY, roll INTEGER, amount INTEGER NOT NULL);
INSERT INTO students VALUES (101, 'Aarav'), (102, 'Diya'), (103, 'Kabir'), (104, 'Meera');
INSERT INTO payments VALUES (1, 101, 45000), (2, 103, 45000), (3, NULL, 45000);

-- Who has not paid? NOT IN returns nobody: for 102 and 104 the condition is UNKNOWN.
SELECT 'NOT IN' AS form, count(*) AS unpaid FROM students
WHERE roll NOT IN (SELECT roll FROM payments)
UNION ALL
-- NOT EXISTS ignores the payment without a roll number.
SELECT 'NOT EXISTS', count(*) FROM students AS s
WHERE NOT EXISTS (SELECT 1 FROM payments AS p WHERE p.roll = s.roll)
UNION ALL
-- NOT IN with the NULLs filtered out of the list gives the same answer as NOT EXISTS here.
SELECT 'NOT IN, NULLs removed', count(*) FROM students
WHERE roll NOT IN (SELECT roll FROM payments WHERE roll IS NOT NULL);

SELECT s.roll, s.name FROM students AS s
WHERE NOT EXISTS (SELECT 1 FROM payments AS p WHERE p.roll = s.roll);

Output

┌───────────────────────┬────────┐
│         form          │ unpaid │
├───────────────────────┼────────┤
│ NOT IN                │ 0      │
│ NOT EXISTS            │ 2      │
│ NOT IN, NULLs removed │ 2      │
└───────────────────────┴────────┘
┌──────┬───────┐
│ roll │ name  │
├──────┼───────┤
│ 102  │ Diya  │
│ 104  │ Meera │
└──────┴───────┘

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

NOT IN finds nobody. For Diya, 102 NOT IN (101, 103, NULL) is 102 <> 101 AND 102 <> 103 AND 102 <> NULL, which is TRUE AND TRUE AND UNKNOWN: UNKNOWN, and WHERE drops her. One NULL in the subquery’s column makes NOT IN FALSE or UNKNOWN for every row. NOT EXISTS asks a different question, “is there a payment row for this roll number?”, and the NULL row is never a match, so it returns Diya and Meera. Filtering the NULLs out of the subquery makes NOT IN agree.

NOT IN and NOT EXISTS return the same rows only when neither side can be NULL: no NULL in the subquery’s column, and none in the outer value. A NULL on the outer side breaks them apart too: if roll itself were NULL, NOT IN would be UNKNOWN for any non-empty subquery, while NOT EXISTS would be TRUE, because p.roll = s.roll is never TRUE. For “rows with no match”, NOT EXISTS says what is meant; declaring the key columns NOT NULL makes both forms safe.

ANY and ALL, with empty sets and NULLs

x op ANY (subquery) is TRUE if the comparison is TRUE for at least one row, FALSE if it is TRUE for none (an empty subquery included), and UNKNOWN if none is TRUE but at least one is UNKNOWN. x op ALL (subquery) is TRUE if the comparison is TRUE for every row (an empty subquery included), FALSE if it is FALSE for at least one, and UNKNOWN if none is FALSE but at least one is UNKNOWN (PostgreSQL 18: subquery expressions). These are OR and AND over all the rows, with FALSE and TRUE as the results for zero rows, so they follow the lowest-and-highest rule of three-valued logic.

Two decision chains: ALL is FALSE on any FALSE, else UNKNOWN on any UNKNOWN, else TRUE; ANY is the mirror image with TRUE first.x op ALL (S)x op ANY (S)Is the comparison FALSEfor some row of S?yes: FALSEIf not, is it UNKNOWNfor some row?yes: UNKNOWNIf not: TRUE(also when S is empty)Is the comparison TRUEfor some row of S?yes: TRUEIf not, is it UNKNOWNfor some row?yes: UNKNOWNIf not: FALSE(also when S is empty)

How ALL and ANY decide, row by row of the subquery

Text description of the diagram

The figure has two chains of three questions, one above the other, read from top to bottom.

For x op ALL (S):

  1. Is the comparison FALSE for some row of S? If yes, the result is FALSE.
  2. If not, is it UNKNOWN for some row? If yes, the result is UNKNOWN.
  3. If not, the result is TRUE. This is also the result when S has no rows at all.

For x op ANY (S):

  1. Is the comparison TRUE for some row of S? If yes, the result is TRUE.
  2. If not, is it UNKNOWN for some row? If yes, the result is UNKNOWN.
  3. If not, the result is FALSE. This is also the result when S has no rows at all.

SQLite has no ANY or ALL with a subquery, so the example below rebuilds both from EXISTS, using IS TRUE, IS FALSE and IS NULL to look at each comparison’s truth value. It prints what PostgreSQL’s ANY and ALL return for the same values, next to the common rewrite with max:

x > ALL and x > ANY, rebuilt from EXISTS SQL · any_all.sql
-- SQLite has no ANY or ALL comparison with a subquery.
SELECT 5 > ALL (SELECT 1);

-- Sets of values to compare with; the set named '{}' has no members at all.
CREATE TABLE members (set_name TEXT, v INTEGER);
INSERT INTO members VALUES ('{1, 2}', 1), ('{1, 2}', 2), ('{1, NULL}', 1), ('{1, NULL}', NULL);
CREATE TABLE cases (k INTEGER, x INTEGER, set_name TEXT);
INSERT INTO cases VALUES (1, 5, '{}'), (2, 5, '{1, 2}'), (3, 5, '{1, NULL}'), (4, 0, '{1, NULL}');
CREATE TABLE tv (t INTEGER, name TEXT);
INSERT INTO tv VALUES (1, 'TRUE'), (0, 'FALSE'), (NULL, 'UNKNOWN');

-- x > ALL (S): FALSE if some comparison is FALSE, else UNKNOWN if some is UNKNOWN, else TRUE (also for an empty S).
-- x > ANY (S): TRUE if some comparison is TRUE, else UNKNOWN if some is UNKNOWN, else FALSE (also for an empty S).
-- Both built from EXISTS, next to the common rewrite x > (SELECT max(v) ...).
SELECT c.x, c.set_name AS s,
  CASE WHEN EXISTS (SELECT 1 FROM members AS m WHERE m.set_name = c.set_name AND (c.x > m.v) IS FALSE) THEN 'FALSE'
       WHEN EXISTS (SELECT 1 FROM members AS m WHERE m.set_name = c.set_name AND (c.x > m.v) IS NULL) THEN 'UNKNOWN'
       ELSE 'TRUE' END AS "x > ALL (s)",
  CASE WHEN EXISTS (SELECT 1 FROM members AS m WHERE m.set_name = c.set_name AND (c.x > m.v) IS TRUE) THEN 'TRUE'
       WHEN EXISTS (SELECT 1 FROM members AS m WHERE m.set_name = c.set_name AND (c.x > m.v) IS NULL) THEN 'UNKNOWN'
       ELSE 'FALSE' END AS "x > ANY (s)",
  (SELECT tv.name FROM tv WHERE tv.t IS NOT DISTINCT FROM
     (c.x > (SELECT max(m.v) FROM members AS m WHERE m.set_name = c.set_name))) AS "x > max(s)"
FROM cases AS c
ORDER BY c.k;

Output (exit status 1)

┌───┬───────────┬─────────────┬─────────────┬────────────┐
│ x │     s     │ x > ALL (s) │ x > ANY (s) │ x > max(s) │
├───┼───────────┼─────────────┼─────────────┼────────────┤
│ 5 │ {}        │ TRUE        │ FALSE       │ UNKNOWN    │
│ 5 │ {1, 2}    │ TRUE        │ TRUE        │ TRUE       │
│ 5 │ {1, NULL} │ UNKNOWN     │ TRUE        │ TRUE       │
│ 0 │ {1, NULL} │ FALSE       │ UNKNOWN     │ FALSE      │
└───┴───────────┴─────────────┴─────────────┴────────────┘

Printed as an error (standard error)

Parse error near line 2: near "ALL": syntax error

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

  • 5 > ALL ({}) is TRUE and 5 > ANY ({}) is FALSE. With no rows, no comparison is FALSE and none is TRUE.
  • 5 > ALL ({1, NULL}) is UNKNOWN: 5 > 1 is TRUE, 5 > NULL is UNKNOWN, and no comparison is FALSE.
  • 0 > ALL ({1, NULL}) is FALSE: 0 > 1 is FALSE, which settles it whatever the NULL says.
  • 5 > ANY ({1, NULL}) is TRUE, settled by 5 > 1, and 0 > ANY ({1, NULL}) is UNKNOWN.

The last column is the trap. x > (SELECT max(v) …) looks like x > ALL, but max of no rows is NULL, so over the empty set it gives UNKNOWN where ALL gives TRUE, and max ignores NULLs, so over {1, NULL} it gives TRUE where ALL gives UNKNOWN. The two agree only on sets that are not empty and hold no NULL, like {1, 2}.

Rewrites that keep the answer, and rewrites that change it

Most subquery bugs are rewrites that look equivalent and are not. In a WHERE clause, where only TRUE keeps a row:

  • x IN (SELECT y …) to EXISTS (… WHERE y = x) keeps the same rows in a WHERE clause. Used as a value, they differ: IN can be UNKNOWN, EXISTS never is.
  • x IN (SELECT y …) to a join on y = x keeps the same rows only when y is unique, such as a key. When y repeats, the join returns one row per match.
  • x NOT IN (SELECT y …) to NOT EXISTS (… WHERE y = x) keeps the same rows only when neither x nor y can be NULL. A NULL y empties NOT IN, and with a NULL x only NOT EXISTS keeps the row.
  • x > ALL (SELECT y …) to x > (SELECT max(y) …) keeps the same rows only when the subquery returns rows and no NULLs among them.
  • x = ANY (SELECT y …) to x IN (SELECT y …) is always safe: they are the same predicate.

When a join must replace IN, for example to read columns of the other table, put DISTINCT on the result or join to a subquery that returns each key once. When NOT IN must stay, add WHERE y IS NOT NULL inside the subquery. The exact WHERE form of x > ALL (S) with EXISTS is NOT EXISTS (SELECT 1 FROM S WHERE (x > y) IS NOT TRUE): it keeps a row only when every comparison is TRUE, which is what WHERE x > ALL (S) keeps.

SQLite Online (SQL Playground) Load two tables with a NULL in the key column and compare NOT IN with NOT EXISTS yourself. SQL Formatter Indent nested and correlated subqueries so you can see which columns come from the outer query.

Interview questions

Fresher · What is the difference between a correlated and an uncorrelated subquery, and how does a database run each?

An uncorrelated subquery does not refer to the outer query, so its result is the same for every outer row and can be computed once, such as WHERE roll IN (SELECT roll FROM payments). A correlated subquery refers to a column of the outer query, such as WHERE EXISTS (SELECT 1 FROM results AS r WHERE r.roll = s.roll), so its result can differ from row to row, and logically it is evaluated again for each outer row. That is the meaning; the plan may differ: optimisers often turn a correlated EXISTS into a semi-join, or build a list or hash table once, as long as the rows returned are the same. A good answer adds that EXISTS is never UNKNOWN, while IN can be.

Key takeaways

  • A scalar subquery returns one value, NULL when it finds no row. With several rows PostgreSQL raises an error and SQLite silently takes the first.
  • A correlated subquery is evaluated once per outer row in meaning; optimisers may decorrelate it into a join.
  • EXISTS is TRUE or FALSE, never UNKNOWN; it is a semi-join, and NOT EXISTS an anti-join.
  • IN is = ANY and NOT IN is <> ALL. One NULL in the subquery makes NOT IN return no rows.
  • ALL over an empty set is TRUE, ANY over an empty set is FALSE, and NULLs make both UNKNOWN unless a TRUE (for ANY) or a FALSE (for ALL) settles them.
  • x > (SELECT max …) differs from x > ALL for empty sets and NULLs; a join differs from IN when the matched column repeats.

Exercise

Exercise · Easy · SQL

Find the students who have not paid, NULLs and all

The table students(roll, name) lists the students of a batch, and payments(receipt, roll, amount) lists the fee payments received. A payment that arrived without a roll number has roll set to NULL until someone matches it.

Return the roll and name of every student who has no payment with their roll number. The order of the rows does not matter.

The starter query works until the first unmatched payment arrives; then it returns nobody at all.

Starter code · unpaid_students.sql

-- Meant to find the students without a payment, but one payment without a roll number empties the result.
SELECT roll, name
FROM students
WHERE roll NOT IN (SELECT roll FROM payments);
The sample tests · tests.yaml
# Sample tests: each one builds the two tables, runs your query and compares the rows it returns (in any order).
tests:
  - name: ignores a payment that has no roll number yet
    setup: |
      CREATE TABLE students (roll INTEGER PRIMARY KEY, name TEXT NOT NULL);
      CREATE TABLE payments (receipt INTEGER PRIMARY KEY, roll INTEGER, amount INTEGER NOT NULL);
      INSERT INTO students VALUES (101, 'Aarav'), (102, 'Diya'), (103, 'Kabir'), (104, 'Meera');
      INSERT INTO payments VALUES (1, 101, 45000), (2, 103, 45000), (3, NULL, 45000);
    expected:
      columns: [roll, name]
      rows:
        - [102, Diya]
        - [104, Meera]
  - name: works when every payment is matched
    setup: |
      CREATE TABLE students (roll INTEGER PRIMARY KEY, name TEXT NOT NULL);
      CREATE TABLE payments (receipt INTEGER PRIMARY KEY, roll INTEGER, amount INTEGER NOT NULL);
      INSERT INTO students VALUES (201, 'Farah'), (202, 'Gopal');
      INSERT INTO payments VALUES (10, 202, 30000), (11, 202, 15000);
    expected:
      columns: [roll, name]
      rows:
        - [201, Farah]
  - name: returns every student before any payment arrives
    setup: |
      CREATE TABLE students (roll INTEGER PRIMARY KEY, name TEXT NOT NULL);
      CREATE TABLE payments (receipt INTEGER PRIMARY KEY, roll INTEGER, amount INTEGER NOT NULL);
      INSERT INTO students VALUES (301, 'Hema'), (302, 'Indu');
    expected:
      columns: [roll, name]
      rows:
        - [301, Hema]
        - [302, Indu]
A hint

roll NOT IN (SELECT roll FROM payments) compares the student's roll with every payment, and roll <> NULL is UNKNOWN, so with one unmatched payment the condition is never TRUE. Ask whether a matching payment row exists instead: NOT EXISTS (SELECT 1 FROM payments AS p WHERE p.roll = s.roll).

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

Check yourself

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

  1. Question 1 of 6 The subquery returns no rows. What is 5 > ALL (subquery)?

    Choose one answer.

    Show the answer to question 1

    Answer: TRUE

    ALL is TRUE unless some comparison is FALSE or UNKNOWN, and with no rows there are no comparisons at all. That is also why it differs from 5 > (SELECT max(…)), which compares 5 with NULL and is UNKNOWN.

  2. Question 2 of 6 The subquery returns 1 and NULL. What is 5 > ALL (subquery)?

    Choose one answer.

    Show the answer to question 2

    Answer: UNKNOWN

    5 > 1 is TRUE and 5 > NULL is UNKNOWN. No comparison is FALSE and one is UNKNOWN, so ALL is UNKNOWN, and a WHERE clause drops the row. SQLite has no ALL operator at all.

  3. Question 3 of 6 The subquery returns 1 and NULL. What is 0 > ANY (subquery)?

    Choose one answer.

    Show the answer to question 3

    Answer: UNKNOWN

    0 > 1 is FALSE and 0 > NULL is UNKNOWN. No comparison is TRUE and one is UNKNOWN, so ANY is UNKNOWN. A NULL is ignored by aggregates such as max, not by ANY.

  4. Question 4 of 6 students has 4 rows; payments.roll holds 101, 103 and NULL. How many rows does SELECT * FROM students WHERE roll NOT IN (SELECT roll FROM payments) return?

    Type a number.

    Show the answer to question 4

    Answer: 0 rows

    For the paying students the condition is FALSE; for the others it ends in AND roll <> NULL, which is UNKNOWN. No row makes it TRUE, so none is returned. NOT EXISTS would return the two students who have not paid.

  5. Question 5 of 6 Aarav has two results of 70 or more and Meera one. How many rows does SELECT s.name FROM students AS s JOIN results AS r ON r.roll = s.roll WHERE r.marks >= 70 return for them?

    Type a number.

    Show the answer to question 5

    Answer: 3 rows

    A join returns one row per matching pair: two for Aarav and one for Meera. WHERE s.roll IN (SELECT …) or EXISTS would return each student once, 2 rows.

  6. Question 6 of 6 A scalar subquery in the select list returns two rows for one outer row. What happens?

    Choose one answer.

    Show the answer to question 6

    Answer: PostgreSQL raises an error; SQLite uses the first row

    PostgreSQL treats more than one row in a scalar subquery as an error. SQLite defines the value of a subquery expression as its first row, so it returns a value that depends on which row happens to come first.

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.