What actually goes wrong when an UPDATE or DELETE runs without a WHERE clause, and how do you guard against it?
Every row in the table is affected. There is no error, no warning and no partial protection: the statement is valid SQL and the engine executes it exactly as written. For an UPDATE that means every row gets the same new value; for a DELETE the table is emptied but still exists.
CREATE TABLE tickets (
id INTEGER PRIMARY KEY,
title VARCHAR(160) NOT NULL,
status VARCHAR(20) NOT NULL,
priority INTEGER NOT NULL
);
INSERT INTO tickets (id, title, status, priority) VALUES
(1, 'Login page times out', 'open', 3),
(2, 'Invoice PDF is blank', 'open', 1),
(3, 'Export times out', 'open', 2);
-- Step 1: look at what you are about to change.
SELECT id, title, status FROM tickets WHERE status = 'open' AND priority <= 2;
-- Step 2: change it inside a transaction, then check the count.
BEGIN;
UPDATE tickets SET status = 'triaged' WHERE status = 'open' AND priority <= 2;
SELECT changes() AS rows_updated;
COMMIT;
SELECT id, status FROM tickets ORDER BY id;The first SELECT shows two rows that would change — tickets 2 and 3. The UPDATE reports 2 rows affected, and the final SELECT confirms that only those two moved to 'triaged' while ticket 1 stayed open.
Two steps, and the order matters. Running the SELECT first costs a millisecond and shows you the blast radius before you commit to it. Wrapping the UPDATE in a transaction and checking the row count before the COMMIT means that if the count is not what you expected — 20,000 instead of 2 — you can ROLLBACK and nobody has noticed yet.
There is a subtler failure than a missing WHERE clause: a WHERE clause that is present but broader than you meant. A soft-delete flag you forgot to exclude, a tenant id you left out in a multi-tenant table, a status that included archived rows. The same defences catch it, which is why the row count is the single most valuable thing to check on any destructive statement.
And remember that the WHERE clause should be as narrow as the requirement allows. Filtering on the primary key is the strongest option, and adding the column's expected current value on top of that gives you an optimistic-locking check for free: WHERE id = 1 AND status = 'open' will match nothing if somebody else changed the row while you were writing the query.