INSERT, UPDATE and DELETE
By the end of this lesson
Change data safely, always checking the affected rows first.
Reading data is forgiving. A wrong SELECT gives you a wrong answer, you notice, and you fix the query. Writing data is not forgiving in the same way: a wrong UPDATE changes rows, and the previous values are gone.
The syntax here is short. The habits around it are the actual content of this lesson.
-- Step 1: see exactly which rows you are about to change
SELECT order_id, status, order_date
FROM orders
WHERE status = 'pending'
AND order_date < '2026-01-01';
-- Step 2: the same WHERE, now as an UPDATE
UPDATE orders
SET status = 'expired'
WHERE status = 'pending'
AND order_date < '2026-01-01';- The WHERE clause is character-for-character identical in both statements. That is the point — you verified this exact condition.
- The UPDATE reports how many rows it changed. Compare that number to the count you saw in step 1. If they differ, something changed underneath you and it is worth understanding why before continuing.
INSERT: always name your columns
-- One row, columns named
INSERT INTO customers (company_name, email, country, credit_limit)
VALUES ('Harbour Electronics', 'accounts@harbour.example', 'Ireland', 5000.00);
-- Several rows in one statement
INSERT INTO products (product_name, category, unit_price, units_in_stock, discontinued)
VALUES
('USB-C Cable 2m', 'Cables', 9.99, 400, 0),
('HDMI Cable 3m', 'Cables', 12.50, 250, 0),
('Desk Microphone', 'Audio', 74.00, 30, 0);
-- Insert the result of a query
INSERT INTO archived_orders (order_id, customer_id, order_date, status)
SELECT o.order_id, o.customer_id, o.order_date, o.status
FROM orders AS o
WHERE o.order_date < '2024-01-01';- customer_id is not in the first column list because it is an IDENTITY column — the database generates it. Supplying it would be rejected.
- Naming the columns means the statement keeps working when someone adds a column to the table. Omit the list and the values are matched by position, so a new column silently shifts everything.
- Multi-row INSERT is one statement and one round trip, which is meaningfully faster than three separate statements when you are loading many rows.
- INSERT ... SELECT copies rows between tables without pulling them into your application first.
UPDATE without WHERE changes every row
There is no confirmation prompt and no warning. UPDATE products SET unit_price = 9.99 sets every product in the table to 9.99, in one statement, and reports success.
The WHERE clause is not optional in practice. It is the difference between changing one price and repricing your entire catalogue.
-- One row, identified by its primary key
UPDATE products
SET unit_price = 11.49
WHERE product_id = 318;
-- Several columns at once, with an expression
UPDATE products
SET unit_price = unit_price * 1.05,
discontinued = 0
WHERE category = 'Cables';
-- Using a related table to decide which rows to change (SQL Server form)
UPDATE o
SET o.status = 'priority'
FROM orders AS o
JOIN customers AS c ON c.customer_id = o.customer_id
WHERE c.credit_limit >= 50000
AND o.status = 'pending';- SET unit_price = unit_price * 1.05 reads the current value and writes the result back, per row.
- The third statement is SQL Server's UPDATE ... FROM syntax. PostgreSQL writes UPDATE orders SET ... FROM customers WHERE ...; the standard form uses a correlated subquery. Check your database's syntax rather than assuming.
- A join in an UPDATE is where the SELECT-first habit earns its keep. If the join matches more rows than you expect, the UPDATE quietly changes all of them.
Transactions are the net under the wire
A transaction groups statements so they either all take effect or none do. While the transaction is open, you can inspect the result of your change and then decide: COMMIT to keep it, or ROLLBACK to undo it as though it never happened.
For a risky change this converts an irreversible mistake into a reversible one. Transactions get a full lesson later; this is the part you need now.
BEGIN TRANSACTION;
UPDATE orders
SET status = 'expired'
WHERE status = 'pending'
AND order_date < '2026-01-01';
-- Check the damage before making it permanent
SELECT COUNT(*) AS expired_now
FROM orders
WHERE status = 'expired';
-- If that number is right:
COMMIT;
-- If it is not:
-- ROLLBACK;- Between BEGIN and COMMIT, your change is visible to your session and not to anyone else.
- ROLLBACK discards it entirely. The rows return to their previous values with no repair work.
- One caution: an open transaction holds locks on the rows it changed, so other users trying to read or write them wait. Do not leave a transaction open while you go for lunch.
DELETE and TRUNCATE both empty rows out of a table, and they are not interchangeable:
| DELETE | TRUNCATE TABLE | |
|---|---|---|
| Can target specific rows | Yes, with WHERE | No — it removes every row |
| Speed on a large table | Slower: each row is logged individually | Much faster: it deallocates pages rather than processing rows |
| Respects foreign keys | Yes, and is refused if child rows exist | Refused outright if the table is referenced by a foreign key |
| Fires triggers | Yes | No |
| Identity counter | Continues from where it was | Resets to the seed value in SQL Server |
| Can be rolled back | Yes, inside a transaction | Yes in SQL Server and PostgreSQL; no in MySQL and Oracle, where it commits |
Summary
- Run the WHERE as a SELECT first and check the row count — this prevents most data accidents
- An UPDATE or DELETE with no WHERE affects every row, with no warning
- Name the columns in every INSERT so a new column cannot shift your values
- Wrap risky changes in a transaction so you can inspect the result and ROLLBACK if it is wrong
- TRUNCATE is faster than DELETE and removes everything, skips triggers, and is refused on a referenced table
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Try it yourself
Try it yourself
A supplier has raised prices on the Audio category by 8%. Apply it, but verify first. Show every statement you would run, in order.
Show solution
The SELECT comes first, and it shows both the current and proposed price so you can sanity-check a couple of rows by hand before anything changes.
Wrapping the UPDATE in a transaction lets you confirm the new values and roll back if the row count surprises you. On a real system this is the difference between a five-second fix and restoring from a backup.
Rounding is worth a thought: unit_price * 1.08 on a DECIMAL(10,2) column rounds to two decimal places on assignment. Writing ROUND(..., 2) makes that explicit rather than relying on the column definition.
-- 1. Verify which rows, and what the new price would be
SELECT product_id, product_name, unit_price,
ROUND(unit_price * 1.08, 2) AS proposed_price
FROM products
WHERE category = 'Audio';
-- 2. Apply inside a transaction
BEGIN TRANSACTION;
UPDATE products
SET unit_price = ROUND(unit_price * 1.08, 2)
WHERE category = 'Audio';
-- 3. Confirm, then keep or discard
SELECT product_id, product_name, unit_price
FROM products
WHERE category = 'Audio';
COMMIT;
-- ROLLBACK; if the numbers are wrongThink about it
Think about it
You run an UPDATE and it reports "0 rows affected". Nothing errored. What are the possible explanations, and how would you tell them apart?
Show solution
The most likely cause is that the WHERE clause matched nothing. Run it as a SELECT to confirm, and check for a typo in a text value — 'pending' and 'Pending' differ under a case-sensitive collation.
A second possibility is that the rows already hold the values you are setting. Some databases still report them as affected, so this varies; either way the data is now correct.
A third is that you are connected to a different database or schema than you think. Checking which server and database the session is using is worth doing before assuming the data is wrong.
Zero rows affected is information, not failure. Treating it as a signal to investigate rather than to add more statements is the habit worth having.
Saved in this browser only.