SQL (PostgreSQL, MySQL, SQLite) Module 22 – SQL interview preparation
SQL theory questions and output prediction
Short, exact answers to classic SQL theory questions, and ten predict-the-output puzzles on NULLs, joins and aggregates, each run in three engines.
What you will learn
- Answer classic SQL theory questions in two or three exact sentences
- Predict the output of queries with NULLs, duplicates, joins and empty inputs
- Explain why an answer differs between SQLite, PostgreSQL and MySQL
Before you start
What you’ll learn · Before you start
What you will learn
- Answer classic SQL theory questions in two or three exact sentences
- Predict the output of queries with NULLs, duplicates, joins and empty inputs
- Explain why an answer differs between SQLite, PostgreSQL and MySQL
Before you start
On this page
SQL theory questions ask you to say exactly what a feature does, and predict-the-output questions ask you to run a
query in your head. Both reward the same habit: knowing the precise rule, including what happens with NULLs,
duplicates and empty inputs, and saying which engine you mean when the engines disagree. A strong theory answer has
three parts: a sentence that defines the thing, a sentence on the difference or consequence that matters, and a
small example. A reliable way to predict an output is to walk the query in its logical order, from FROM and the
joins to ORDER BY and LIMIT, and count the rows after each step. This lesson answers ten classic theory questions
in that shape, then sets ten original predict-the-output puzzles. Each puzzle was run on SQLite 3.49.1 in the page,
and all ten again on PostgreSQL 18.6 and MySQL 8.4.11, where two of them print something else.
Ten theory questions, answered briefly
Each answer below is the length an interviewer wants: two to four sentences, with the engine named where the answer depends on it, and a link to the lesson that goes deeper.
Keys: superkey, candidate key, primary key and foreign key
A superkey is any set of columns whose values differ in every row, and a candidate key is a minimal superkey: take
away any one of its columns and uniqueness is no longer guaranteed. The primary key is the candidate key a table chooses as
its identifier, which PostgreSQL’s manual defines as unique and not null together, at most one per table; the other
candidate keys are alternate keys, declared UNIQUE and NOT NULL. A foreign key is a column, or a set of columns,
whose values must match a row of the referenced table or be NULL. In orders(id, customer_id), id is the primary
key and customer_id a foreign key to customers.
Normal forms: 1NF, 2NF, 3NF and BCNF
First normal form asks for one value from the column’s domain in every cell, with no lists or repeating groups, the
property Codd’s relational model starts from. Second normal form adds that no non-key column depends on only part of a
composite candidate key, and third that no non-key column depends on another non-key column. Boyce-Codd normal form is
stricter: in every non-trivial functional dependency X → Y, X must be a superkey. A table
(student, course, teacher) where each teacher teaches one course can be in 3NF but not in BCNF, because teacher
determines course without being a key; the lesson on normalisation
works through the decompositions.
ACID
A transaction is atomic: all of its changes happen or none do. It keeps the database consistent: every constraint holds when it commits. It is isolated: work that another transaction has not committed yet stays invisible to it, as far as the chosen isolation level guarantees. And it is durable: once committed, it survives a crash, because the commit is written to a log before it is acknowledged. PostgreSQL’s tutorial calls a transaction an all-or-nothing operation, and MySQL’s manual lists, for each letter, the InnoDB features that provide it.
Isolation levels and their defaults
The SQL standard defines four levels by the anomalies they allow: read uncommitted allows dirty reads, read committed prevents those but allows non-repeatable reads, repeatable read prevents those too but allows phantoms, and serializable allows none of them. The defaults differ: PostgreSQL uses read committed (and runs read uncommitted as read committed), InnoDB in MySQL uses repeatable read, and SQLite runs every transaction as serializable. Name the engine whenever you answer, because PostgreSQL’s repeatable read also prevents phantoms, which the standard does not require.
Index types
A B-tree keeps keys sorted, so it serves equality, ranges, ORDER BY and prefix matches, and it is the default in all
three engines: PostgreSQL’s CREATE INDEX builds one unless told otherwise, InnoDB’s ordinary indexes are B-trees,
and SQLite stores every table and index as a B-tree. PostgreSQL adds hash indexes for equality only, GIN for
values with many parts such as arrays, JSON and full-text documents, GiST and SP-GiST for geometry and ranges, and BRIN
for huge tables whose rows arrive in key order. A composite index helps queries that use its leading columns, which is
why (city, placed_at) serves WHERE city = ? but not WHERE placed_at > ? on its own.
Clustered tables and heap tables
In a clustered table the rows live inside the primary-key index, in key order; in a heap the rows sit wherever there is
room, and every index points to them. InnoDB clusters every table on its primary key, or else on its first unique
index whose columns are all NOT NULL, or else on a hidden row id, so its secondary indexes store the clustering
key. PostgreSQL tables are heaps, and its CLUSTER command sorts a
table once without keeping it sorted. SQLite’s ordinary tables are B-trees keyed by the rowid, and a WITHOUT ROWID
table is clustered on its declared primary key.
WHERE and HAVING
WHERE decides which input rows enter the grouping, so no aggregate exists yet when it runs; HAVING decides which
finished groups are returned, so it may test count(*), sum() and the other aggregates. WHERE amount > 100 removes small orders before anything is counted, while
HAVING count(*) > 100 keeps the customers with more than 100 orders. A condition on a grouped column can sit in
either clause, but in WHERE it removes rows before the grouping work is done.
UNION and UNION ALL
UNION appends the second result to the first and removes duplicate rows, the way DISTINCT does; UNION ALL keeps
every row. Removing duplicates needs a sort or a hash of the whole result, so UNION ALL is the right default when the
two parts cannot overlap or when repeats must be counted. Both need the same number of columns in each part, matched by
position, not by name.
Views and materialised views
A view is a stored query: reading it runs the query, so it always shows current data and stores no rows. A materialised
view stores the result when it is created and when it is refreshed, so reads are fast but can be stale; in PostgreSQL,
REFRESH MATERIALIZED VIEW updates it, and its CONCURRENTLY option needs a unique index. MySQL and SQLite have no
materialised views; a summary table refreshed by a job plays that role there.
Correlated and uncorrelated subqueries
An uncorrelated subquery does not refer to the outer query, so the engine can run it once, as in
WHERE id IN (SELECT customer_id FROM orders). A correlated subquery refers to a column of the outer query, as in
WHERE amount > (SELECT avg(amount) FROM orders AS o2 WHERE o2.customer_id = o.customer_id), so logically it runs once
per outer row. Optimisers can rewrite both kinds into joins, as MySQL’s manual describes for its semijoin
transformations, so the plan, not the spelling, tells you what actually runs.
Predicting an output: walk the query in its logical order
Predicting an output, one logical step at a time
Text description of the diagram
The diagram lists the logical steps of a query from top to bottom, each with the rule to check when you predict what it returns.
- FROM and JOIN: count the rows. A key that appears twice on the left and three times on the right gives six joined rows.
- WHERE keeps only the rows for which the condition is true. A comparison with NULL is unknown, so those rows are dropped.
- GROUP BY makes one row per group, and all the NULLs of a grouped column form one group together.
- Aggregates and HAVING: count(*) counts rows, while count of a column and the other aggregates skip NULLs. With no input rows, an aggregate query without GROUP BY still returns one row, and with GROUP BY it returns none.
- SELECT, DISTINCT and UNION: duplicate rows are removed, and here two NULLs count as equal.
- ORDER BY and LIMIT: whether NULL sorts first or last depends on the engine and the direction of the sort.
Read the query in the order it is evaluated, not the order it is written, and
write down the rows after each step.
Most surprises come from four rules. A join multiplies matching rows, so duplicate keys on both sides multiply too. A
comparison with NULL is neither true nor false, and WHERE keeps only the rows for which it is true. Aggregates skip
NULLs, except count(*), which counts rows. And an aggregate over no rows still returns one row when there is no
GROUP BY, but no rows when there is one. For ORDER BY, also ask the engine: NULL sorts first in ascending order in
SQLite and MySQL, and last in PostgreSQL.
Ten predict-the-output puzzles
Each puzzle shows a short script; choose what it prints. The puzzles are original, and each answer and its reason appear once you have chosen. The runs follow the puzzles, with a short explanation of each.
Predict the output: ten puzzles
10 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.
The ten runs, explained
-- Customers who never ordered, with one order whose customer is not recorded.
CREATE TABLE customers (id INTEGER, name TEXT);
INSERT INTO customers VALUES (1, 'Asha'), (2, 'Bilal'), (3, 'Chitra');
CREATE TABLE orders (id INTEGER, customer_id INTEGER);
INSERT INTO orders VALUES (10, 1), (11, NULL);
SELECT count(*) AS never_ordered
FROM customers
WHERE id NOT IN (SELECT customer_id FROM orders); Output
┌───────────────┐ │ never_ordered │ ├───────────────┤ │ 0 │ └───────────────┘
Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < p01_not_in_null.sql
Runs on this device, in your browser. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs.
Your run, in this browser
No customer is returned, although Bilal and Chitra never ordered. id NOT IN (1, NULL) means id <> 1 AND id <> NULL,
and the second comparison is unknown for every id, so the condition is never true. NOT EXISTS, or adding
WHERE customer_id IS NOT NULL to the subquery, returns the two customers.
-- Five rows: two in Pune, one in Kochi and two without a city.
CREATE TABLE visits (city TEXT);
INSERT INTO visits VALUES ('Pune'), ('Pune'), (NULL), ('Kochi'), (NULL);
SELECT count(*) AS all_rows, count(city) AS with_city, count(DISTINCT city) AS cities
FROM visits; Output
┌──────────┬───────────┬────────┐ │ all_rows │ with_city │ cities │ ├──────────┼───────────┼────────┤ │ 5 │ 3 │ 2 │ └──────────┴───────────┴────────┘
Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < p02_count_three_ways.sql
Runs on this device, in your browser. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs.
Your run, in this browser
count(*) counts the 5 rows, count(city) the 3 that have a city, and count(DISTINCT city) the 2 different cities:
NULL is not a value, so it is neither counted nor counted as distinct.
-- The key 1 appears twice on the left and three times on the right.
CREATE TABLE lefty (k INTEGER);
INSERT INTO lefty VALUES (1), (1), (2);
CREATE TABLE righty (k INTEGER);
INSERT INTO righty VALUES (1), (1), (1), (3);
SELECT count(*) AS joined_rows
FROM lefty JOIN righty ON lefty.k = righty.k; Output
┌─────────────┐ │ joined_rows │ ├─────────────┤ │ 6 │ └─────────────┘
Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < p03_join_duplicates.sql
Runs on this device, in your browser. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs.
Your run, in this browser
Each of the two left rows with key 1 matches each of the three right rows with key 1, so the join returns 2 × 3 = 6
rows; keys 2 and 3 match nothing. This is the fan-out that makes a SUM over a join too large.
-- Three customers; Asha has a paid and a refunded order, Bilal a refunded one, Chitra none.
CREATE TABLE customers (id INTEGER, name TEXT);
INSERT INTO customers VALUES (1, 'Asha'), (2, 'Bilal'), (3, 'Chitra');
CREATE TABLE orders (id INTEGER, customer_id INTEGER, status TEXT);
INSERT INTO orders VALUES (10, 1, 'paid'), (11, 1, 'refunded'), (12, 2, 'refunded');
SELECT
(SELECT count(*) FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id AND o.status = 'paid') AS filter_in_on,
(SELECT count(*) FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id
WHERE o.status = 'paid') AS filter_in_where; Output
┌──────────────┬─────────────────┐ │ filter_in_on │ filter_in_where │ ├──────────────┼─────────────────┤ │ 3 │ 1 │ └──────────────┴─────────────────┘
Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < p04_left_join_filter.sql
Runs on this device, in your browser. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs.
Your run, in this browser
With the status test in ON, the outer join keeps every customer: Asha with her paid order, and Bilal and Chitra with
NULLs on the order side, 3 rows. In WHERE, the test runs after the join, and the NULL rows fail it, so only Asha’s paid
order remains and the outer join has quietly become an inner one.
-- Two orders without a coupon and one with the coupon NEW10.
CREATE TABLE carts (id INTEGER, coupon TEXT);
INSERT INTO carts VALUES (1, NULL), (2, 'NEW10'), (3, NULL);
SELECT
(SELECT count(*) FROM carts WHERE coupon = NULL) AS equals_null,
(SELECT count(*) FROM carts WHERE coupon IS NULL) AS is_null,
(SELECT count(*) FROM carts WHERE coupon <> 'NEW10') AS not_new10; Output
┌─────────────┬─────────┬───────────┐ │ equals_null │ is_null │ not_new10 │ ├─────────────┼─────────┼───────────┤ │ 0 │ 2 │ 0 │ └─────────────┴─────────┴───────────┘
Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < p05_equals_null.sql
Runs on this device, in your browser. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs.
Your run, in this browser
coupon = NULL is never true, so it counts 0 rows; IS NULL counts the 2 carts without a coupon. coupon <> 'NEW10'
also counts 0: the two NULL coupons make the comparison unknown, and the only coupon present is NEW10 itself.
-- Rows kept by UNION and by UNION ALL, and by UNION of two NULLs.
SELECT
(SELECT count(*) FROM (SELECT 1 AS x UNION SELECT 1 UNION SELECT 2) AS s) AS with_union,
(SELECT count(*) FROM (SELECT 1 AS x UNION ALL SELECT 1 UNION ALL SELECT 2) AS s) AS with_union_all,
(SELECT count(*) FROM (SELECT NULL AS x UNION SELECT NULL) AS s) AS null_rows; Output
┌────────────┬────────────────┬───────────┐ │ with_union │ with_union_all │ null_rows │ ├────────────┼────────────────┼───────────┤ │ 2 │ 3 │ 1 │ └────────────┴────────────────┴───────────┘
Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < p06_union_counts.sql
Runs on this device, in your browser. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs.
Your run, in this browser
UNION keeps the 2 distinct values, UNION ALL all 3 rows, and the UNION of two NULLs keeps 1 row. For removing
duplicates, two NULLs count as the same, even though NULL = NULL is not true; DISTINCT and GROUP BY treat them the
same way.
-- Two ratings, 4 and 5 stars, stored as integers.
CREATE TABLE ratings (stars INTEGER);
INSERT INTO ratings VALUES (4), (5);
SELECT 9 / 2 AS nine_halves, avg(stars) AS avg_stars, sum(stars) / count(*) AS sum_over_count
FROM ratings; Output
┌─────────────┬───────────┬────────────────┐ │ nine_halves │ avg_stars │ sum_over_count │ ├─────────────┼───────────┼────────────────┤ │ 4 │ 4.5 │ 4 │ └─────────────┴───────────┴────────────────┘
Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < p07_integer_average.sql
Runs on this device, in your browser. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs.
Your run, in this browser
SQLite divides two integers as integers, so 9 / 2 is 4, and so is sum(stars) / count(*), the “average” written by
hand. avg() returns a floating-point value, 4.5. In MySQL all three are 4.5000, because its / divides two
integers exactly and shows four decimal places by default.
-- No order is above 1000, so both queries read no rows.
CREATE TABLE orders (city TEXT, amount INTEGER);
INSERT INTO orders VALUES ('Pune', 400), ('Kochi', 650);
SELECT count(*) AS orders, sum(amount) AS total, coalesce(sum(amount), 0) AS total_or_zero
FROM orders
WHERE amount > 1000;
SELECT city, count(*) AS orders
FROM orders
WHERE amount > 1000
GROUP BY city; Output
┌────────┬───────┬───────────────┐ │ orders │ total │ total_or_zero │ ├────────┼───────┼───────────────┤ │ 0 │ │ 0 │ └────────┴───────┴───────────────┘
Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < p08_aggregates_no_rows.sql
Runs on this device, in your browser. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs.
Your run, in this browser
The first query has no GROUP BY, so it returns one row even though no order matches: count(*) is 0, sum() is NULL,
shown as an empty cell, and coalesce() turns it into 0. The second query groups by city, and there is no group to
show, so it returns no rows at all and prints nothing.
-- Four players; Bilal has no score yet.
CREATE TABLE players (name TEXT, score INTEGER);
INSERT INTO players VALUES ('Asha', 70), ('Bilal', NULL), ('Chitra', 90), ('Dev', 70);
SELECT name, score
FROM players
ORDER BY score, name
LIMIT 2; Output
┌───────┬───────┐ │ name │ score │ ├───────┼───────┤ │ Bilal │ │ │ Asha │ 70 │ └───────┴───────┘
Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < p09_null_sort.sql
Runs on this device, in your browser. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs.
Your run, in this browser
SQLite treats NULL as smaller than any value, so in ascending order Bilal comes first and Asha second, and name
breaks the tie between Asha and Dev. PostgreSQL sorts NULL as larger than any value, so the same query returns Asha and
Dev there.
-- A UNIQUE column, with one address and two rows without one.
CREATE TABLE subscribers (email VARCHAR(100) UNIQUE);
INSERT INTO subscribers VALUES ('asha@example.in'), (NULL), (NULL);
SELECT count(*) AS stored_rows,
count(DISTINCT email) AS distinct_emails,
(SELECT count(*) FROM (SELECT DISTINCT email FROM subscribers) AS d) AS distinct_rows
FROM subscribers; Output
┌─────────────┬─────────────────┬───────────────┐ │ stored_rows │ distinct_emails │ distinct_rows │ ├─────────────┼─────────────────┼───────────────┤ │ 3 │ 1 │ 2 │ └─────────────┴─────────────────┴───────────────┘
Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < p10_unique_and_null.sql
Runs on this device, in your browser. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs.
Your run, in this browser
Both NULL rows were accepted: a UNIQUE constraint compares values, and NULL is not equal to anything, so SQLite,
PostgreSQL by default and MySQL all allow several NULLs. count(DISTINCT email) ignores them and returns 1, while
SELECT DISTINCT keeps one NULL row beside the address, so it returns 2 rows.
The same puzzles in PostgreSQL and MySQL
The scripts below run the ten puzzles one after another, dropping each puzzle’s tables before the next, and add an
eleventh that SQLite cannot run: comparisons with ALL and ANY against a subquery that returns no rows.
pg/eleven_items.py
"""The ten items of this lesson, and an eleventh with ALL and ANY, run in PostgreSQL 18.6."""
from pg_server import psql
psql(r"""
\echo '-- 1.'
-- Customers who never ordered, with one order whose customer is not recorded.
CREATE TABLE customers (id INTEGER, name TEXT);
INSERT INTO customers VALUES (1, 'Asha'), (2, 'Bilal'), (3, 'Chitra');
CREATE TABLE orders (id INTEGER, customer_id INTEGER);
INSERT INTO orders VALUES (10, 1), (11, NULL);
SELECT count(*) AS never_ordered
FROM customers
WHERE id NOT IN (SELECT customer_id FROM orders);
DROP TABLE customers, orders;
\echo '-- 2.'
-- Five rows: two in Pune, one in Kochi and two without a city.
CREATE TABLE visits (city TEXT);
INSERT INTO visits VALUES ('Pune'), ('Pune'), (NULL), ('Kochi'), (NULL);
SELECT count(*) AS all_rows, count(city) AS with_city, count(DISTINCT city) AS cities
FROM visits;
DROP TABLE visits;
\echo '-- 3.'
-- The key 1 appears twice on the left and three times on the right.
CREATE TABLE lefty (k INTEGER);
INSERT INTO lefty VALUES (1), (1), (2);
CREATE TABLE righty (k INTEGER);
INSERT INTO righty VALUES (1), (1), (1), (3);
SELECT count(*) AS joined_rows
FROM lefty JOIN righty ON lefty.k = righty.k;
DROP TABLE lefty, righty;
\echo '-- 4.'
-- Three customers; Asha has a paid and a refunded order, Bilal a refunded one, Chitra none.
CREATE TABLE customers (id INTEGER, name TEXT);
INSERT INTO customers VALUES (1, 'Asha'), (2, 'Bilal'), (3, 'Chitra');
CREATE TABLE orders (id INTEGER, customer_id INTEGER, status TEXT);
INSERT INTO orders VALUES (10, 1, 'paid'), (11, 1, 'refunded'), (12, 2, 'refunded');
SELECT
(SELECT count(*) FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id AND o.status = 'paid') AS filter_in_on,
(SELECT count(*) FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id
WHERE o.status = 'paid') AS filter_in_where;
DROP TABLE customers, orders;
\echo '-- 5.'
-- Two orders without a coupon and one with the coupon NEW10.
CREATE TABLE carts (id INTEGER, coupon TEXT);
INSERT INTO carts VALUES (1, NULL), (2, 'NEW10'), (3, NULL);
SELECT
(SELECT count(*) FROM carts WHERE coupon = NULL) AS equals_null,
(SELECT count(*) FROM carts WHERE coupon IS NULL) AS is_null,
(SELECT count(*) FROM carts WHERE coupon <> 'NEW10') AS not_new10;
DROP TABLE carts;
\echo '-- 6.'
-- Rows kept by UNION and by UNION ALL, and by UNION of two NULLs.
SELECT
(SELECT count(*) FROM (SELECT 1 AS x UNION SELECT 1 UNION SELECT 2) AS s) AS with_union,
(SELECT count(*) FROM (SELECT 1 AS x UNION ALL SELECT 1 UNION ALL SELECT 2) AS s) AS with_union_all,
(SELECT count(*) FROM (SELECT NULL AS x UNION SELECT NULL) AS s) AS null_rows;
\echo '-- 7.'
-- Two ratings, 4 and 5 stars, stored as integers.
CREATE TABLE ratings (stars INTEGER);
INSERT INTO ratings VALUES (4), (5);
SELECT 9 / 2 AS nine_halves, avg(stars) AS avg_stars, sum(stars) / count(*) AS sum_over_count
FROM ratings;
DROP TABLE ratings;
\echo '-- 8.'
-- No order is above 1000, so both queries read no rows.
CREATE TABLE orders (city TEXT, amount INTEGER);
INSERT INTO orders VALUES ('Pune', 400), ('Kochi', 650);
SELECT count(*) AS orders, sum(amount) AS total, coalesce(sum(amount), 0) AS total_or_zero
FROM orders
WHERE amount > 1000;
SELECT city, count(*) AS orders
FROM orders
WHERE amount > 1000
GROUP BY city;
DROP TABLE orders;
\echo '-- 9.'
-- Four players; Bilal has no score yet.
CREATE TABLE players (name TEXT, score INTEGER);
INSERT INTO players VALUES ('Asha', 70), ('Bilal', NULL), ('Chitra', 90), ('Dev', 70);
SELECT name, score
FROM players
ORDER BY score, name
LIMIT 2;
DROP TABLE players;
\echo '-- 10.'
-- A UNIQUE column, with one address and two rows without one.
CREATE TABLE subscribers (email VARCHAR(100) UNIQUE);
INSERT INTO subscribers VALUES ('asha@example.in'), (NULL), (NULL);
SELECT count(*) AS stored_rows,
count(DISTINCT email) AS distinct_emails,
(SELECT count(*) FROM (SELECT DISTINCT email FROM subscribers) AS d) AS distinct_rows
FROM subscribers;
DROP TABLE subscribers;
\echo '-- 11.'
-- Comparisons with ALL and ANY against a subquery that returns no rows (SQLite has neither).
CREATE TABLE players (name VARCHAR(20), score INTEGER);
INSERT INTO players VALUES ('Asha', 70), ('Bilal', NULL), ('Chitra', 90), ('Dev', 70);
SELECT
(SELECT count(*) FROM players WHERE score > ALL (SELECT score FROM players WHERE name = 'Nobody')) AS above_all_of_none,
(SELECT count(*) FROM players WHERE score > ANY (SELECT score FROM players WHERE name = 'Nobody')) AS above_any_of_none;
DROP TABLE players;
""") Output
-- 1.
never_ordered
---------------
0
(1 row)
-- 2.
all_rows | with_city | cities
----------+-----------+--------
5 | 3 | 2
(1 row)
-- 3.
joined_rows
-------------
6
(1 row)
-- 4.
filter_in_on | filter_in_where
--------------+-----------------
3 | 1
(1 row)
-- 5.
equals_null | is_null | not_new10
-------------+---------+-----------
0 | 2 | 0
(1 row)
-- 6.
with_union | with_union_all | null_rows
------------+----------------+-----------
2 | 3 | 1
(1 row)
-- 7.
nine_halves | avg_stars | sum_over_count
-------------+--------------------+----------------
4 | 4.5000000000000000 | 4
(1 row)
-- 8.
orders | total | total_or_zero
--------+-------+---------------
0 | | 0
(1 row)
city | orders
------+--------
(0 rows)
-- 9.
name | score
------+-------
Asha | 70
Dev | 70
(2 rows)
-- 10.
stored_rows | distinct_emails | distinct_rows
-------------+-----------------+---------------
3 | 1 | 2
(1 row)
-- 11.
above_all_of_none | above_any_of_none
-------------------+-------------------
4 | 0
(1 row)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 eleven_items.py
pg/pg_server.py
"""Runs SQL on a private, throwaway PostgreSQL 18.6 server and prints what psql printed.
Each run makes a new database cluster in its own temporary folder (initdb), starts a server that listens only on a
Unix socket in that folder (no TCP port), sends the SQL to psql, then stops the server and deletes the folder. Every
run therefore starts from an empty database and prints the same text.
The settings keep the output the same on every run: no autovacuum (statistics change only when the SQL runs ANALYZE
or VACUUM), statistics taken from every row instead of a random sample (default_statistics_target = 10000 samples up
to 3,000,000 rows), no parallel workers, no JIT compilation, and WAL buffers large enough never to fill up during a
run. The cluster uses the C locale, UTF-8 and UTC.
PostgreSQL 18.6 comes from the folder where mise installs it (mise install postgres@18.6). The output is psql's own,
except that the "psql:<stdin>:<line>:" in front of each message is left out.
"""
import atexit
import os
import pwd
import re
import shutil
import subprocess
import sys
BIN = os.path.join(pwd.getpwuid(os.getuid()).pw_dir, ".local", "share", "mise", "installs", "postgres", "18.6", "bin")
WHERE = re.compile(r"^psql:<stdin>:\d+: ", re.MULTILINE)
# Short names: a Unix socket path may not be longer than about 100 characters.
_base = os.path.join(os.environ.get("TMPDIR", os.getcwd()), "pg")
_data = os.path.join(_base, "d")
_sock = os.path.join(_base, "s")
_env = dict(os.environ, PGHOST=_sock, PGUSER="postgres", PGDATABASE="postgres", LC_ALL="C", LANG="C", TZ="UTC")
_running = False
def _quiet(*argv: str) -> None:
subprocess.run(argv, env=_env, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def _start() -> None:
global _running
shutil.rmtree(_base, ignore_errors=True)
os.makedirs(_sock)
_quiet(os.path.join(BIN, "initdb"), "-D", _data, "-U", "postgres", "-A", "trust", "-E", "UTF8", "--locale=C", "--no-sync", "--no-instructions")
options = (
f"-k {_sock} -c listen_addresses='' -c fsync=off -c timezone=UTC -c autovacuum=off"
" -c default_statistics_target=10000 -c max_parallel_workers_per_gather=0 -c jit=off -c wal_buffers=64MB"
)
_quiet(os.path.join(BIN, "pg_ctl"), "start", "-w", "-s", "-D", _data, "-l", os.path.join(_base, "server.log"), "-o", options)
_running = True
atexit.register(_stop)
def _stop() -> None:
global _running
if _running:
subprocess.run([os.path.join(BIN, "pg_ctl"), "stop", "-s", "-m", "fast", "-D", _data], env=_env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
_running = False
shutil.rmtree(_base, ignore_errors=True)
def psql(sql: str) -> None:
"""Sends a block of SQL to psql and prints what it printed, errors included, in order."""
if not _running:
_start()
run = subprocess.run(
[os.path.join(BIN, "psql"), "-X", "-q", "-v", "ON_ERROR_STOP=0", "-P", "pager=off"],
input=sql.strip() + "\n", env=_env, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
sys.stdout.write(WHERE.sub("", run.stdout))
sys.stdout.flush() mysql/eleven_items.py
"""The ten items of this lesson, and an eleventh with ALL and ANY, run in MySQL 8.4.11."""
from mysql_server import mysql
mysql(r"""
SELECT '1.' AS item;
-- Customers who never ordered, with one order whose customer is not recorded.
CREATE TABLE customers (id INTEGER, name TEXT);
INSERT INTO customers VALUES (1, 'Asha'), (2, 'Bilal'), (3, 'Chitra');
CREATE TABLE orders (id INTEGER, customer_id INTEGER);
INSERT INTO orders VALUES (10, 1), (11, NULL);
SELECT count(*) AS never_ordered
FROM customers
WHERE id NOT IN (SELECT customer_id FROM orders);
DROP TABLE customers, orders;
SELECT '2.' AS item;
-- Five rows: two in Pune, one in Kochi and two without a city.
CREATE TABLE visits (city TEXT);
INSERT INTO visits VALUES ('Pune'), ('Pune'), (NULL), ('Kochi'), (NULL);
SELECT count(*) AS all_rows, count(city) AS with_city, count(DISTINCT city) AS cities
FROM visits;
DROP TABLE visits;
SELECT '3.' AS item;
-- The key 1 appears twice on the left and three times on the right.
CREATE TABLE lefty (k INTEGER);
INSERT INTO lefty VALUES (1), (1), (2);
CREATE TABLE righty (k INTEGER);
INSERT INTO righty VALUES (1), (1), (1), (3);
SELECT count(*) AS joined_rows
FROM lefty JOIN righty ON lefty.k = righty.k;
DROP TABLE lefty, righty;
SELECT '4.' AS item;
-- Three customers; Asha has a paid and a refunded order, Bilal a refunded one, Chitra none.
CREATE TABLE customers (id INTEGER, name TEXT);
INSERT INTO customers VALUES (1, 'Asha'), (2, 'Bilal'), (3, 'Chitra');
CREATE TABLE orders (id INTEGER, customer_id INTEGER, status TEXT);
INSERT INTO orders VALUES (10, 1, 'paid'), (11, 1, 'refunded'), (12, 2, 'refunded');
SELECT
(SELECT count(*) FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id AND o.status = 'paid') AS filter_in_on,
(SELECT count(*) FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.id
WHERE o.status = 'paid') AS filter_in_where;
DROP TABLE customers, orders;
SELECT '5.' AS item;
-- Two orders without a coupon and one with the coupon NEW10.
CREATE TABLE carts (id INTEGER, coupon TEXT);
INSERT INTO carts VALUES (1, NULL), (2, 'NEW10'), (3, NULL);
SELECT
(SELECT count(*) FROM carts WHERE coupon = NULL) AS equals_null,
(SELECT count(*) FROM carts WHERE coupon IS NULL) AS is_null,
(SELECT count(*) FROM carts WHERE coupon <> 'NEW10') AS not_new10;
DROP TABLE carts;
SELECT '6.' AS item;
-- Rows kept by UNION and by UNION ALL, and by UNION of two NULLs.
SELECT
(SELECT count(*) FROM (SELECT 1 AS x UNION SELECT 1 UNION SELECT 2) AS s) AS with_union,
(SELECT count(*) FROM (SELECT 1 AS x UNION ALL SELECT 1 UNION ALL SELECT 2) AS s) AS with_union_all,
(SELECT count(*) FROM (SELECT NULL AS x UNION SELECT NULL) AS s) AS null_rows;
SELECT '7.' AS item;
-- Two ratings, 4 and 5 stars, stored as integers.
CREATE TABLE ratings (stars INTEGER);
INSERT INTO ratings VALUES (4), (5);
SELECT 9 / 2 AS nine_halves, avg(stars) AS avg_stars, sum(stars) / count(*) AS sum_over_count
FROM ratings;
DROP TABLE ratings;
SELECT '8.' AS item;
-- No order is above 1000, so both queries read no rows.
CREATE TABLE orders (city TEXT, amount INTEGER);
INSERT INTO orders VALUES ('Pune', 400), ('Kochi', 650);
SELECT count(*) AS orders, sum(amount) AS total, coalesce(sum(amount), 0) AS total_or_zero
FROM orders
WHERE amount > 1000;
SELECT city, count(*) AS orders
FROM orders
WHERE amount > 1000
GROUP BY city;
DROP TABLE orders;
SELECT '9.' AS item;
-- Four players; Bilal has no score yet.
CREATE TABLE players (name TEXT, score INTEGER);
INSERT INTO players VALUES ('Asha', 70), ('Bilal', NULL), ('Chitra', 90), ('Dev', 70);
SELECT name, score
FROM players
ORDER BY score, name
LIMIT 2;
DROP TABLE players;
SELECT '10.' AS item;
-- A UNIQUE column, with one address and two rows without one.
CREATE TABLE subscribers (email VARCHAR(100) UNIQUE);
INSERT INTO subscribers VALUES ('asha@example.in'), (NULL), (NULL);
SELECT count(*) AS stored_rows,
count(DISTINCT email) AS distinct_emails,
(SELECT count(*) FROM (SELECT DISTINCT email FROM subscribers) AS d) AS distinct_rows
FROM subscribers;
DROP TABLE subscribers;
SELECT '11.' AS item;
-- Comparisons with ALL and ANY against a subquery that returns no rows (SQLite has neither).
CREATE TABLE players (name VARCHAR(20), score INTEGER);
INSERT INTO players VALUES ('Asha', 70), ('Bilal', NULL), ('Chitra', 90), ('Dev', 70);
SELECT
(SELECT count(*) FROM players WHERE score > ALL (SELECT score FROM players WHERE name = 'Nobody')) AS above_all_of_none,
(SELECT count(*) FROM players WHERE score > ANY (SELECT score FROM players WHERE name = 'Nobody')) AS above_any_of_none;
DROP TABLE players;
""") Output
+------+ | item | +------+ | 1. | +------+ +---------------+ | never_ordered | +---------------+ | 0 | +---------------+ +------+ | item | +------+ | 2. | +------+ +----------+-----------+--------+ | all_rows | with_city | cities | +----------+-----------+--------+ | 5 | 3 | 2 | +----------+-----------+--------+ +------+ | item | +------+ | 3. | +------+ +-------------+ | joined_rows | +-------------+ | 6 | +-------------+ +------+ | item | +------+ | 4. | +------+ +--------------+-----------------+ | filter_in_on | filter_in_where | +--------------+-----------------+ | 3 | 1 | +--------------+-----------------+ +------+ | item | +------+ | 5. | +------+ +-------------+---------+-----------+ | equals_null | is_null | not_new10 | +-------------+---------+-----------+ | 0 | 2 | 0 | +-------------+---------+-----------+ +------+ | item | +------+ | 6. | +------+ +------------+----------------+-----------+ | with_union | with_union_all | null_rows | +------------+----------------+-----------+ | 2 | 3 | 1 | +------------+----------------+-----------+ +------+ | item | +------+ | 7. | +------+ +-------------+-----------+----------------+ | nine_halves | avg_stars | sum_over_count | +-------------+-----------+----------------+ | 4.5000 | 4.5000 | 4.5000 | +-------------+-----------+----------------+ +------+ | item | +------+ | 8. | +------+ +--------+-------+---------------+ | orders | total | total_or_zero | +--------+-------+---------------+ | 0 | NULL | 0 | +--------+-------+---------------+ +------+ | item | +------+ | 9. | +------+ +-------+-------+ | name | score | +-------+-------+ | Bilal | NULL | | Asha | 70 | +-------+-------+ +------+ | item | +------+ | 10. | +------+ +-------------+-----------------+---------------+ | stored_rows | distinct_emails | distinct_rows | +-------------+-----------------+---------------+ | 3 | 1 | 2 | +-------------+-----------------+---------------+ +------+ | item | +------+ | 11. | +------+ +-------------------+-------------------+ | above_all_of_none | above_any_of_none | +-------------------+-------------------+ | 4 | 0 | +-------------------+-------------------+
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 eleven_items.py
mysql/mysql_server.py
"""Runs SQL on a private, throwaway MySQL 8.4.11 server and prints what the mysql client printed.
Each run makes a new data folder in its own temporary folder (mysqld --initialize-insecure), starts a server that
listens only on a Unix socket in that folder (--skip-networking: no TCP port), creates the database "shop", sends
the SQL to the mysql client, then shuts the server down and deletes the folder. Every run starts from an empty
database.
Two settings keep the plans the same on every run: InnoDB recomputes statistics only when the SQL runs ANALYZE TABLE
(innodb_stats_auto_recalc=OFF), and ANALYZE TABLE reads every page instead of a random sample of 20
(innodb_stats_persistent_sample_pages). EXPLAIN ANALYZE prints how long each step took, which differs from run to
run, so this file prints "actual time=…" in its place; everything else is the client's own output.
MySQL 8.4.11 comes from the folder where mise installs it (mise install mysql@8.4.11).
"""
import atexit
import os
import pwd
import re
import shutil
import subprocess
import sys
import time
HOME = os.path.join(pwd.getpwuid(os.getuid()).pw_dir, ".local", "share", "mise", "installs", "mysql", "8.4.11")
BIN = os.path.join(HOME, "bin")
TIMES = re.compile(r"actual time=[0-9.e+-]+\.\.[0-9.e+-]+")
_base = os.path.join(os.environ.get("TMPDIR", os.getcwd()), "my")
_data = os.path.join(_base, "d")
_sock = os.path.join(_base, "m.sock")
_server = None
def _client(*args: str, sql: str = "") -> subprocess.CompletedProcess:
return subprocess.run(
[os.path.join(BIN, "mysql"), "--no-defaults", f"--socket={_sock}", "-uroot", *args],
input=sql, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
def _start() -> None:
global _server
shutil.rmtree(_base, ignore_errors=True)
os.makedirs(_base)
server = [
os.path.join(BIN, "mysqld"), "--no-defaults", f"--basedir={HOME}", f"--datadir={_data}",
f"--socket={_sock}", "--skip-networking", "--mysqlx=OFF", f"--pid-file={os.path.join(_base, 'pid')}",
f"--log-error={os.path.join(_base, 'error.log')}", "--skip-log-bin", "--performance-schema=OFF",
"--innodb-stats-auto-recalc=OFF", "--innodb-stats-persistent-sample-pages=100000",
"--default-time-zone=+00:00",
]
subprocess.run(server[:4] + ["--initialize-insecure", f"--log-error={os.path.join(_base, 'init.log')}"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
_server = subprocess.Popen(server, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
atexit.register(_stop)
for _ in range(600):
if _client("-e", "CREATE DATABASE shop").returncode == 0:
return
time.sleep(0.1)
raise RuntimeError("the MySQL server did not start")
def _stop() -> None:
global _server
if _server is not None:
subprocess.run([os.path.join(BIN, "mysqladmin"), "--no-defaults", f"--socket={_sock}", "-uroot", "shutdown"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
try:
_server.wait(timeout=60)
except subprocess.TimeoutExpired:
_server.kill()
_server = None
shutil.rmtree(_base, ignore_errors=True)
def mysql(sql: str) -> None:
"""Sends a block of SQL to the mysql client (tables as boxes, \\G for one row per block) and prints its output."""
if _server is None:
_start()
run = _client("--table", "--force", "--comments", "shop", sql=sql.strip() + "\n")
sys.stdout.write(TIMES.sub("actual time=…", run.stdout))
sys.stdout.flush() | Criterion | SQLite 3.49.1 | PostgreSQL 18.6 | MySQL 8.4.11 |
|---|---|---|---|
| Puzzle 7: 9 / 2 and sum / count | 4 and 4 | 4 and 4 | 4.5000 and 4.5000 |
| Puzzle 7: avg(stars) | 4.5 | 4.5000000000000000 | 4.5000 |
| Puzzle 9: the first two rows | Bilal (NULL), Asha | Asha, Dev | Bilal (NULL), Asha |
| Puzzle 11: > ALL and > ANY of no rows | Not supported | 4 and 0 | 4 and 0 |
| Puzzles 1 to 6, 8 and 10 | The same numbers | The same numbers | The same numbers |
Puzzle 11 surprises people in both directions. A comparison with ALL is true when it holds for every row of the
subquery, which is trivially the case when there are no rows, so all four players pass, even Bilal with his NULL score.
A comparison with ANY needs at least one row that makes it true, so with no rows nobody passes. PostgreSQL’s manual
states both cases, and SQLite has no ALL or ANY at all: write NOT EXISTS for the first and EXISTS for the
second there.
SQL questions in GATE CS
The Databases section of the GATE 2027 CS syllabus names ER modelling, then relational algebra, tuple calculus and SQL as parts of the relational model, then integrity constraints, normal forms, file organisation, B and B+ tree indexing, transactions and concurrency control. The puzzles here practise the SQL part: counting the rows a query returns when NULLs, duplicates and aggregates meet. Relational algebra and tuple calculus are taught in the DBMS track.
Your turn
Exercise · Easy · SQL
Paid orders for every product, including products with none
A shop has two tables:
products(id, name): one row per product; two products may share a name;orders(id, product_id, status): one row per order, withstatusone of'paid','refunded'or'pending'.
The report below should list every product with its number of paid orders, and 0 for a product that has none, but a colleague's first draft leaves some products out. Predict what the draft returns, then fix it.
Return two columns, name and paid_orders, one row per product (not per name), sorted by paid_orders, highest first, then by name. The order of the rows is checked.
Starter code · paid_orders.sql
-- Every product with its number of paid orders. This draft leaves out some products.
SELECT p.name, count(*) AS paid_orders
FROM products AS p
LEFT JOIN orders AS o ON o.product_id = p.id
WHERE o.status = 'paid'
GROUP BY p.name
ORDER BY paid_orders DESC, p.name; The sample tests · tests.yaml
# Sample tests: each one builds small products and orders tables, runs your query on them and compares the rows it
# returns, in order.
tests:
- name: keeps products without a paid order, with 0
ordered: true
setup: |
CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT NOT NULL);
CREATE TABLE orders (id INTEGER PRIMARY KEY, product_id INTEGER, status TEXT NOT NULL);
INSERT INTO products VALUES (1, 'Kettle'), (2, 'Lamp'), (3, 'Mixer');
INSERT INTO orders VALUES (10, 1, 'paid'), (11, 1, 'paid'), (12, 1, 'refunded'), (13, 2, 'refunded');
expected:
columns: [name, paid_orders]
rows:
- ['Kettle', 2]
- ['Lamp', 0]
- ['Mixer', 0]
- name: counts only paid orders
ordered: true
setup: |
CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT NOT NULL);
CREATE TABLE orders (id INTEGER PRIMARY KEY, product_id INTEGER, status TEXT NOT NULL);
INSERT INTO products VALUES (1, 'Fan'), (2, 'Iron');
INSERT INTO orders VALUES (20, 1, 'paid'), (21, 1, 'paid'), (22, 1, 'paid'), (23, 2, 'paid'), (24, 2, 'pending');
expected:
columns: [name, paid_orders]
rows:
- ['Fan', 3]
- ['Iron', 1]
- name: keeps two products that share a name apart
ordered: true
setup: |
CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT NOT NULL);
CREATE TABLE orders (id INTEGER PRIMARY KEY, product_id INTEGER, status TEXT NOT NULL);
INSERT INTO products VALUES (1, 'Bulb'), (2, 'Bulb'), (3, 'Torch');
INSERT INTO orders VALUES (30, 1, 'paid'), (31, 1, 'paid'), (32, 2, 'paid'), (33, 3, 'refunded');
expected:
columns: [name, paid_orders]
rows:
- ['Bulb', 2]
- ['Bulb', 1]
- ['Torch', 0] A hint
Puzzle 4 of the lesson shows where the draft loses rows: a condition on the right-hand table of a LEFT JOIN belongs in its ON clause. Then puzzle 2 shows which count() gives 0 for a product without a match.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
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.
Interview questions
Fresher · What is the difference between DELETE, TRUNCATE and DROP?
DELETE removes rows, all of them or those its WHERE clause selects, one by one: it fires delete triggers, checks
foreign keys and can be rolled back. TRUNCATE empties a whole table without scanning it, which is much faster on a
large table, but it fires no ON DELETE triggers and refuses a table that other tables reference. DROP TABLE
removes the table itself, with its rows, indexes, constraints and triggers. The engines differ on TRUNCATE:
PostgreSQL’s can be rolled back inside a transaction and keeps identity counters unless you add RESTART IDENTITY,
while MySQL’s commits implicitly, cannot be rolled back and resets AUTO_INCREMENT. SQLite has no TRUNCATE; a
DELETE without WHERE gets the same fast path there.
Fresher · Why is NULL = NULL not true, and how do you test for NULL?
NULL stands for a value that is missing or unknown, so comparing it with anything, another NULL included, gives
unknown rather than true or false. WHERE keeps only the rows for which its condition is true, so WHERE x = NULL
matches nothing, and so does WHERE x <> NULL. Test with x IS NULL and x IS NOT NULL instead. To compare two
columns that may both be NULL as if NULLs were equal, use a IS NOT DISTINCT FROM b in PostgreSQL, a IS b in
SQLite, or a <=> b in MySQL. Operations that remove duplicates, such as DISTINCT, GROUP BY and UNION, are the
exception: there, two NULLs are treated as the same.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
On a staging copy you run BEGIN, then TRUNCATE orders, then ROLLBACK, once on PostgreSQL 18 and once on MySQL 8.4. What is left in orders in each engine, what id does the next new order get, and when would you choose DELETE instead?
Key takeaways
- Answer a theory question in three sentences: the definition, the difference that matters, a small example, with the engine named when it changes the answer.
- Predict an output by walking the logical order,
FROMand joins first, and counting the rows after every step. - A join multiplies duplicate keys;
WHEREkeeps only true conditions, so NULL comparisons drop rows; aggregates skip NULLs exceptcount(*). - An aggregate over no rows returns one row without
GROUP BYand none with it;sum()of nothing is NULL. - Integer division and NULL sorting differ between engines, and SQLite has no
ALLorANY: say which engine you assume.
References
- PostgreSQL documentation: Constraints (primary keys and foreign keys) (The PostgreSQL Global Development Group)
- A Relational Model of Data for Large Shared Data Banks (E. F. Codd, Communications of the ACM 13(6), 1970) (Association for Computing Machinery)
- PostgreSQL documentation: Transactions (tutorial) (The PostgreSQL Global Development Group)
- MySQL 8.4 Reference Manual: InnoDB and the ACID Model (Oracle Corporation)
- PostgreSQL documentation: Transaction Isolation (The PostgreSQL Global Development Group)
- MySQL 8.4 Reference Manual: Transaction Isolation Levels (Oracle Corporation)
- SQLite: Isolation In SQLite (SQLite)
- PostgreSQL documentation: Index Types (The PostgreSQL Global Development Group)
- MySQL 8.4 Reference Manual: Clustered and Secondary Indexes (Oracle Corporation)
- PostgreSQL documentation: CLUSTER (The PostgreSQL Global Development Group)
- SQLite: Clustered Indexes and the WITHOUT ROWID Optimization (SQLite)
- PostgreSQL documentation: Aggregate Functions (tutorial: WHERE and HAVING) (The PostgreSQL Global Development Group)
- PostgreSQL documentation: Combining Queries (UNION, INTERSECT, EXCEPT) (The PostgreSQL Global Development Group)
- PostgreSQL documentation: CREATE MATERIALIZED VIEW (The PostgreSQL Global Development Group)
- PostgreSQL documentation: REFRESH MATERIALIZED VIEW (The PostgreSQL Global Development Group)
- MySQL 8.4 Reference Manual: Optimizing Subqueries with Semijoin Transformations (Oracle Corporation)
- MySQL 8.4 Reference Manual: Correlated Subqueries (Oracle Corporation)
- PostgreSQL documentation: Subquery Expressions (IN, NOT IN, ANY, ALL) (The PostgreSQL Global Development Group)
- PostgreSQL documentation: Comparison Functions and Operators (IS NULL, IS DISTINCT FROM) (The PostgreSQL Global Development Group)
- PostgreSQL documentation: Aggregate Functions (The PostgreSQL Global Development Group)
- PostgreSQL documentation: Sorting Rows (ORDER BY) (The PostgreSQL Global Development Group)
- PostgreSQL documentation: Mathematical Functions and Operators (The PostgreSQL Global Development Group)
- PostgreSQL documentation: TRUNCATE (The PostgreSQL Global Development Group)
- MySQL 8.4 Reference Manual: TRUNCATE TABLE Statement (Oracle Corporation)
- MySQL 8.4 Reference Manual: Working with NULL Values (Oracle Corporation)
- MySQL 8.4 Reference Manual: Arithmetic Operators (Oracle Corporation)
- SQLite: NULL Handling in SQLite Versus Other Database Engines (SQLite)
- SQLite: DELETE (the truncate optimization) (SQLite)
- SQLite: Datatypes In SQLite (sort order of NULL) (SQLite)
- GATE 2027 Computer Science and Information Technology (CS) syllabus (Indian Institute of Technology Madras (organising institute, GATE 2027))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress