Core CS · SQL
One missing WHERE changes every row
INSERT, UPDATE and DELETE each pick a set of rows and write it. This lesson is about how that set gets decided, which constraints look at it on the way through, and the one word that decides whether you can take any of it back.
Run eight statements, then take them all back →01 The idea
Every DML statement is a set of rows
A SELECT asks a question. INSERT, UPDATE and DELETE change what is stored, and those three together are the Data Manipulation Language, or DML. They are the statements you will type most often once you are past learning to query, and they are the only ones in this course that can destroy somebody else’s work, so they are worth slowing down for.
The thing to hold on to is that none of the three operates on a row. Each one operates on a set of rows, and that set is decided in full before a single byte is written. An INSERT builds its set from the values you supply, or from the rows a SELECT returns. An UPDATE and a DELETE build theirs by testing every row in the table against the WHERE clause and keeping the ones where the test came out true. Leave the WHERE clause out and there is no test, so nothing is filtered, and the set is the whole table.
That is why the most expensive mistake in SQL is a typing mistake rather than a logic one. Nothing about UPDATE student SET marks = marks + 5; is invalid. The server does not ask whether you meant it. It rewrites every row, hands back a count, and moves on. Whether you can undo that afterwards depends on something the statement itself never mentions: whether you were inside a transaction when you ran it.
Most clients run with autocommit on, which means the server wraps each statement in its own transaction and commits it the instant the statement returns. Type BEGIN first and it stops doing that. Every change you make from then on is held in the transaction’s undo, invisible to every other session, until you type COMMIT to keep it or ROLLBACK to throw it away. Two identical scripts, one word apart, and only one of them is survivable.
02 Worked example
Two tables, eight statements, and one that should never have run
One student table with four rows carries the rest of this lesson, and a second table, lab, holds a practical score for three of them. The lab rows earn their place twice over: a subquery reads them to decide who gets moderation marks, and a foreign key from lab back to student decides what a DELETE is allowed to do.
student · four rows
| sid | name | marks |
|---|---|---|
S1 | Asha | 78 |
S2 | Ravi | 65 |
S3 | Meera | 35 |
S4 | Vikram | 52 |
lab · foreign key to student
| sid | score |
|---|---|
S1 | 9 |
S3 | 8 |
S4 | 6 |
Eight statements run against those two tables, in this order. Section 04 steps through all eight and shows both tables after every one of them. Here is the shape of the run, and the node that ruins the afternoon.
Node 3 is the node. It differs from node 2 by one clause, it is accepted without a murmur, and the reported count jumps from 1 to 6. Nothing about it is invalid. marks + 5 is arithmetic the server is happy to perform on every row it can see, and the one row it cannot perform it on, the NULL row, it rewrites anyway with the same NULL that was already there.
The refusal at the end of node 4 is the part people expect to be dangerous, and it is the safe one. A DELETE that a foreign key refuses does not remove some rows and stop; it is rejected as a single unit and removes nothing at all. The constraint is not a warning you can ignore, it is a wall the statement bounces off.
Node 5 is what decides whether node 3 was a mistake or a disaster. Everything from node 1 onwards sat in the transaction’s undo, which is why one word puts the table back. Run the same eight statements with autocommit on and node 5 has nothing to work with: each statement committed itself as it returned, and the only route back is a restore from backup.
03 Mechanics
Which rows, which constraints, and what a rollback can take back
Six statement shapes, and the same three questions asked of each: which rows end up in the set, what gets checked on the way through, and what number comes back. The middle column is the one candidates skip. A constraint is not paperwork you satisfied once at design time; it runs on every row of every write, and it is the reason a statement you typed correctly can still be refused.
| Statement | The set of rows it writes | What gets checked on it | What it reports |
|---|---|---|---|
| INSERT ... VALUES | exactly the rows in the VALUES list |
Data type and width, NOT NULL and CHECK on every column of the new row, PRIMARY KEY and UNIQUE against the whole table, FOREIGN KEY on the child side. A column you did not name gets its declared default, and NULL if none was declared. | one per row in the list |
| INSERT ... SELECT | whatever the SELECT returns, lined up with the target by position |
The same set, on every row the SELECT produced. The SELECT itself checks nothing; it only decides how many rows arrive at the target. | the SELECT’s row count. 0 is a normal answer, not an error |
| UPDATE ... WHERE | the rows where the WHERE came out TRUE |
NOT NULL, CHECK, UNIQUE and PRIMARY KEY on the columns you actually changed, FOREIGN KEY on the child side, plus any ON UPDATE action on the parent side. | rows matched, which is not always rows changed |
| UPDATE with no WHERE | every row in the table |
The same checks, on every row. If one row fails a CHECK the whole statement is rejected and nothing is written. | the table’s entire row count |
| DELETE ... WHERE | the rows where the WHERE came out TRUE |
FOREIGN KEY on the parent side only. RESTRICT and NO ACTION reject the statement, CASCADE deletes the children too, SET NULL blanks their reference. | rows removed |
| DELETE with no WHERE | every row in the table |
The same, row by row, which is why it is slow on a large table and why it is still undoable before COMMIT. | the table’s entire row count |
Here are the two tables as they were declared. Two things in this schema decide most of what happens later: marks is nullable on purpose, and lab.sid carries two referential actions rather than none.
CREATE TABLE student ( sid CHAR(2) PRIMARY KEY, name VARCHAR(20) NOT NULL, marks INT CHECK (marks BETWEEN 0 AND 100) -- nullable on purpose: no mark yet is not a zero);CREATE TABLE lab ( sid CHAR(2) PRIMARY KEY REFERENCES student(sid) ON UPDATE CASCADE ON DELETE RESTRICT, score INT NOT NULL CHECK (score BETWEEN 0 AND 10));
Line 4 is the quiet one. A CHECK rejects a row only when the condition comes out false. For a NULL marks, marks BETWEEN 0 AND 100 comes out unknown, and unknown is not false, so the row is accepted. If you want to forbid a missing mark, the keyword is NOT NULL, not a tighter CHECK.
And here is the run itself, with the number of rows each statement reports. Assume you typed BEGIN before line 1, which is what gives the last line something to undo. Every count below is worked out row by row against the rows the table holds at that moment, six rows for lines 3 to 6 and five once line 7 has removed one; none of them is a guess.
INSERT INTO student VALUES ('S5', 'Neha', 41); -- 1 row · positional, by column orderINSERT INTO student (sid, name) VALUES ('S6', 'Dev'); -- 1 row · marks not named, so marks is NULLUPDATE student SET marks = marks + 5 WHERE marks < 40; -- 1 row · S6 not matched: NULL < 40 is unknownUPDATE student SET marks = marks + 3 WHERE sid IN (SELECT sid FROM lab WHERE score >= 8); -- 2 rows · the subquery returns S1 and S3UPDATE student SET marks = marks + 5; -- 6 rows · no WHERE, so every rowDELETE FROM student WHERE sid = 'S5'; -- 1 rowUPDATE student SET sid = 'S9' WHERE sid = 'S3'; -- 1 row here, 1 row cascaded into labDELETE FROM student WHERE marks < 50; -- 0 rows · rejected, lab still points at S9ROLLBACK; -- 14 row changes discarded
Line 3 and line 6 are the same assignment. The only difference is seventeen characters of WHERE, and the difference in effect is one row against six. Line 5 is the shape to memorise for a subquery: the inner SELECT runs first, produces a list of key values, and the outer WHERE ... IN matches against that list. The subquery reads the table as it stood when the statement began, so an UPDATE can never chase its own changes around the table.
The other form of INSERT takes its rows from a query instead of a literal list. Nothing new is checked; the target’s constraints see every row the SELECT hands over, exactly as if you had typed them.
-- topper(sid, name, marks) is empty. student holds six rows at this point:-- S1 Asha 78 · S2 Ravi 65 · S3 Meera 35 · S4 Vikram 52 · S5 Neha 41 · S6 Dev NULLINSERT INTO topper (sid, name, marks)SELECT sid, name, marks FROM student WHERE marks >= 60;-- the SELECT returns S1 Asha 78 and S2 Ravi 65.-- S6 is not returned: NULL >= 60 is unknown, and WHERE keeps only TRUE.-- 2 rows inserted. Whatever the SELECT returns is what you get, zero included.
The columns line up by position, not by name. SELECT name, sid, marks into a target declared (sid, name, marks) is not a mistake the database can catch for you unless the types happen to disagree, and here they would: a name will not fit a CHAR(2). Rely on that and one day two same-typed columns will swap silently.
The last piece is what line 8 did to lab. Renaming a key in the parent table is not a change to one table; it is a change to every row anywhere that points at it. The ON UPDATE clause is where you decide, once, at design time, what that means.
| Clause on lab.sid | UPDATE student SET sid = 'S9' WHERE sid = 'S3' | lab afterwards |
|---|---|---|
| ON UPDATE NO ACTION the default when you write nothing | rejected | unchanged: S1 9, S3 8, S4 6. The child row would have been orphaned, so the parent is not allowed to move. |
| ON UPDATE RESTRICT | rejected | unchanged. Same outcome, different timing: RESTRICT is checked immediately and cannot be deferred, NO ACTION is checked at the end of the statement and can be deferred to COMMIT where the database supports DEFERRABLE. |
| ON UPDATE CASCADE what this schema declares | allowed | S1 9, S9 8, S4 6. The database rewrites the child key to follow the parent, in the same statement, and counts it as part of the same transaction. |
| ON UPDATE SET NULL | cannot apply here | The child key would become NULL, which needs a nullable column. Here lab.sid is lab’s own PRIMARY KEY, so it can never hold NULL. MySQL refuses the table definition outright; PostgreSQL accepts the declaration and then fails the UPDATE when the action fires. |
| ON UPDATE SET DEFAULT | fails on this schema | The child key becomes the column’s declared default, and lab.sid declares none, so the default is NULL and this ends exactly where SET NULL ended. Declare a default and it must itself exist as a row in student, or the action violates the very constraint it is servicing. MySQL InnoDB parses this clause and then refuses the table definition. |
05 Cheat sheet
The whole of DML on one card
Six shapes, the count each one gives back, and the trap that gets people. The last column is the part worth rehearsing, because every one of those traps is a question somebody has been asked.
| Statement | Rows affected | The trap |
|---|---|---|
| INSERT ... VALUES | one per row in the VALUES list | No column list means positional. An ALTER TABLE that adds or reorders a column silently changes what your statement means. |
| INSERT ... SELECT | whatever the SELECT returns, 0 included | Columns line up by position, not by name. One statement, so one constraint failure rejects the whole batch. |
| UPDATE ... WHERE | rows where the test came out TRUE | Unknown is not true. A row whose column is NULL is skipped by marks < 40 and by marks >= 40 alike. |
| UPDATE with no WHERE | every row in the table | No error, no warning, no prompt. The row count is the only signal you get that anything went wrong. |
| DELETE ... WHERE | rows where the test came out TRUE | A child row under RESTRICT or NO ACTION rejects the entire statement, not only the row it protects. |
| DELETE with no WHERE | every row in the table | Undoable before COMMIT, and logged row by row, so it is slow. TRUNCATE is faster and is DDL in Oracle and MySQL, which means an implicit COMMIT and nothing to roll back. |
06 Where & why
Where the undo actually lives
The statements are ANSI and they look identical everywhere. What differs between products is the part that decides whether a bad afternoon is recoverable: whether autocommit is on, whether a failed statement kills the transaction, and whether the foreign key you declared is even switched on.
DDL is transactional here, so CREATE TABLE and ALTER TABLE come back with a ROLLBACK too, which is why migrations are usually written as one transaction. UPDATE ... RETURNING * hands back the rows it changed so you can read them before you commit. The trap is that one failed statement puts the whole block into an aborted state: every statement after it errors until you ROLLBACK or return to a SAVEPOINT.
autocommit = 1 is the default, so a bare UPDATE is committed the moment it returns and there is nothing to roll back. Start the client with --safe-updates and it refuses any UPDATE or DELETE that has no WHERE and no key limit, which is one setting that has saved a lot of tables. MySQL also reports Rows matched and Changed separately, and ALTER TABLE commits your pending DML implicitly.
SQL*Plus does not autocommit, so DML stays provisional across as many statements as you like and other sessions see none of it. DDL still fires an implicit COMMIT before and after itself. MERGE does insert-or-update in a single statement, and flashback query lets you read a table as of a point in time before your mistake, which is the closest thing to an undo that survives a commit.
Every statement is its own transaction unless you type BEGIN, and the whole database is one file, so a rollback is a file operation. The trap is PRAGMA foreign_keys = ON: it is per connection and off by default, so an ON UPDATE CASCADE you carefully declared is stored in the schema and never fires. INSERT OR REPLACE is also a delete followed by an insert, so it triggers ON DELETE actions people did not expect.
07 Interview questions
What they actually ask
DML is where interviewers stop asking definitions and start asking what a statement returns. Most of these have a number for an answer, and the number is checkable, so a confident guess is worse than saying you would work it out row by row.
What counts as DML, and when does what it writes become permanent?
You wrote INSERT INTO student VALUES ('S5', 'Neha', 41) with no column list. What does that depend on?
What is INSERT ... SELECT, and how many rows does it insert?
What happens if you leave the WHERE clause off an UPDATE?
Can an UPDATE use a subquery? Where can it go?
DELETE FROM student WHERE sid NOT IN (SELECT sid FROM enrolment), where enrolment.sid is an ordinary nullable column and one of its rows holds NULL. How many rows go?
You delete a parent row that a child row references. What are your options?
What is the difference between ON DELETE RESTRICT and ON DELETE NO ACTION?
DELETE versus TRUNCATE versus DROP.
What is the difference between rows matched and rows changed?
Your UPDATE reported 0 rows. Is that an error?
You ran the wrong UPDATE against production with autocommit on. What now?
08 Practice problems
Six to work through
Write the table out, apply the WHERE clause to one row at a time, and mark each row true, false or unknown before you count anything. Every answer below is a number or a statement you can check against the rows you were given.