Exercise 8 of 10 · Changing data
The Missing WHERE
What you will make
A corrected price list for a tool library, where exactly one deposit was changed and the other four were left alone, wrapped in a transaction and checked before and after.
The one new idea: UPDATE without WHERE changes every row, and a transaction is what makes a mistake undoable
This is the single most expensive typo in SQL. An UPDATE with no WHERE is a valid statement that means every row, there is no confirmation prompt, and outside a transaction it is committed before you have finished reading the result. The habits in this exercise — check first, wrap in a transaction, count what changed — are what working developers use to make sure they never find out the hard way.
Go straight to the code ↓Reading is forgiving, writing is not
Every exercise so far has only read data. A wrong SELECT gives a wrong answer and nothing else happens; run
it again with a better WHERE and no harm was done.
UPDATE and DELETE are different. They change what is stored, and WHERE is the only thing standing
between "change this row" and "change all of them".
CREATE TABLE hives (id INTEGER PRIMARY KEY, label TEXT, jars INTEGER);
INSERT INTO hives (label, jars) VALUES ('north hedge', 14), ('orchard', 31), ('lane end', 6);
UPDATE hives SET jars = 0;
SELECT id, label, jars FROM hives ORDER BY id;id | label | jars
---+-------------+-----
1 | north hedge | 0
2 | orchard | 0
3 | lane end | 0
(3 rows)One hive was robbed and all three are now empty. There is no error, no warning and no confirmation: an
UPDATE with no WHERE is a legal statement whose meaning is "every row". The same is true of
DELETE FROM hives, which empties the table. SQLite does not even have a TRUNCATE TABLE, because that is
what a DELETE with no condition already does.
The three-step habit
This is worth doing every single time, and it takes seconds:
SELECTwith theWHEREyou are about to use. If it shows more rows than you expected, stop.- Run the write, with that same
WHERE. SELECTagain to confirm the result.
CREATE TABLE hives (id INTEGER PRIMARY KEY, label TEXT, jars INTEGER);
INSERT INTO hives (label, jars) VALUES ('north hedge', 14), ('orchard', 31), ('lane end', 6);
SELECT id, label, jars FROM hives WHERE label = 'lane end';
UPDATE hives SET jars = 9 WHERE label = 'lane end';
SELECT id, label, jars FROM hives ORDER BY id;
SELECT changes() AS rows_changed;id | label | jars
---+----------+-----
3 | lane end | 6
(1 row)
id | label | jars
---+-------------+-----
1 | north hedge | 14
2 | orchard | 31
3 | lane end | 9
(3 rows)
rows_changed
------------
1
(1 row)changes() is SQLite's way of saying how many rows the last statement touched, and it is a free sanity
check: a number you did not expect means a WHERE clause you did not mean. PostgreSQL and MySQL report the
same thing through their client libraries instead.
Transactions make a mistake undoable
BEGIN opens a transaction, COMMIT makes everything since permanent, and ROLLBACK throws it all away.
Inside a transaction you can look before you leap:
CREATE TABLE hives (id INTEGER PRIMARY KEY, label TEXT, jars INTEGER);
INSERT INTO hives (label, jars) VALUES ('north hedge', 14), ('orchard', 31), ('lane end', 6);
BEGIN;
UPDATE hives SET jars = 0;
SELECT label, jars AS inside_the_transaction FROM hives ORDER BY id;
ROLLBACK;
SELECT label, jars AS after_rollback FROM hives ORDER BY id;label | inside_the_transaction
------------+-----------------------
north hedge | 0
orchard | 0
lane end | 0
(3 rows)
label | after_rollback
------------+---------------
north hedge | 14
orchard | 31
lane end | 6
(3 rows)The UPDATE really happened — the query inside the transaction proves it — and ROLLBACK put the table back
exactly as it was. Swap that word for COMMIT and the zeroes stay. A transaction is also how you make two
changes that must both happen or neither: mark a loan returned and mark the tool available, with no chance
of stopping halfway.
Constraints are a different kind of safety net
The table in the editor declares name TEXT NOT NULL and deposit INTEGER NOT NULL CHECK (deposit >= 0).
Those refuse values that cannot be right:
CREATE TABLE hives (id INTEGER PRIMARY KEY, label TEXT NOT NULL, jars INTEGER CHECK (jars >= 0));
INSERT INTO hives (label, jars) VALUES ('north hedge', 14);
SELECT label, jars FROM hives;
UPDATE hives SET jars = -3 WHERE label = 'north hedge';label | jars
------------+-----
north hedge | 14
(1 row)
Error in the statement on line 4: CHECK constraint failed: jars >= 0Useful, and no help at all against the mistake in this exercise: 1250 is a legal deposit, so a constraint
has no opinion about it landing on five rows instead of one. Constraints police values; WHERE polices rows.
One SQLite-specific warning while you are here. A FOREIGN KEY is the one constraint SQLite does not
enforce by default — you have to put PRAGMA foreign_keys = ON; at the top of the script, per connection.
PostgreSQL and MySQL enforce them always, so a schema that looks protected in SQLite may not be.
Your turn
The editor holds a five-row tools table and a three-step script. The job is a single correction: the
cordless drill's deposit goes up to 1250, and nothing else changes.
Press Run first and read the third table. Every deposit is 1250, because the UPDATE has no WHERE.
Make two changes:
- Add a
WHEREclause to theUPDATEso it only touches the drill. - Add the same
WHEREclause to the step-1SELECT, so it shows you the one row you are about to change.
Run it again. The first table should show one row, and the last should show the drill at 1250 with the other four untouched.
Then change COMMIT to ROLLBACK and run it again, to watch the correction disappear.
If something goes wrong
If every deposit is still 1250, the WHERE clause went onto the wrong statement — it needs to be on the
UPDATE, before the semicolon.
If the drill's deposit did change but you used double quotes round its name, the query only worked because
SQLite quietly treats an unmatched double-quoted name as text. PostgreSQL would have failed with
column "cordless drill" does not exist. Use single quotes for values.
If you get CHECK constraint failed: deposit >= 0, the new deposit is negative. The table refuses that, by
design.
Nothing here can break anything real. The database is built fresh on every run and thrown away the moment it finishes, which is exactly why this is a good place to make this mistake once.
Write your code
Runs in your browser. Press Run (or Ctrl/Cmd+Enter) and the output is checked for you.
Press Esc then Tab to move keyboard focus out of the code editor.
Output will appear here after you run your code.The runtime is starting in the background. You can type now — it will be ready before you are.
The answer appears here once you have run your code at least once.
Things that often go wrong here
- Only fixing the UPDATE and leaving the check-first SELECT as it was
- The final answer comes out right, so nothing complains. But the whole point of step 1 is to show you the rows the UPDATE is about to hit: with no WHERE it shows all five, which tells you nothing. On real data that SELECT is your last chance to notice that your condition matches more rows than you expected.
- Putting double quotes round the tool name
- `WHERE name = "cordless drill"` works here, and that is the problem. In standard SQL double quotes name a column, so this should mean "the column called cordless drill" — and PostgreSQL refuses it outright. SQLite falls back to treating an unmatched double-quoted name as text, so the query runs in SQLite and breaks the day it is pointed at another database. Values go in single quotes.
- Identifying the row with `WHERE name LIKE 'cordless%'`
- It works here, and it is the kind of condition that quietly grows teeth. LIKE with a wildcard matches anything with that prefix, so the day a cordless hedge trimmer is added, this UPDATE changes two rows. Match on something exact — the primary key is safest.
- Expecting COMMIT to be optional
- Without a COMMIT the transaction is still open at the end of the script. Here the whole database is thrown away when the run finishes, so you would not notice; against a real server an open transaction can hold locks and is eventually rolled back, losing the change. BEGIN and COMMIT come in pairs.
- Thinking a NOT NULL or CHECK constraint would have caught this
- They check that each value is allowed, not that you meant to write it. 1250 is a perfectly legal deposit, so every one of the five rows satisfied `NOT NULL` and `CHECK (deposit >= 0)` while being wrong. Constraints stop impossible data; only a WHERE clause stops the wrong rows.
Longer explanation: read the full lesson. Want a blank editor instead? Open the SQL playground.