Chapter 5

Chapter 5 — Modifying Data

The first four chapters focused on reading data with SELECT. Now it's time to write. This chapter covers the three DML statements that change the content of your tables: INSERT to add rows, UPDATE to change existing rows, and DELETE to remove them. We also look at TRUNCATE — a faster way to empty a table entirely — and when to use each.

DML recap. Data Manipulation Language statements change row contents without altering the table structure. They are transactional — you can wrap them in BEGIN / COMMIT / ROLLBACK to undo mistakes (covered in Chapter 10). For now we'll run them directly and see the effect immediately.

1. INSERT — Adding Rows

INSERT adds one or more new rows to a table. There are two forms: explicit column names (recommended) and positional (fragile — avoid it).

Single-row INSERT with column names

INSERT INTO authors (first_name, last_name, nationality) VALUES ('Matt', 'Haig', 'British');
-- Insert one author INSERT INTO authors (first_name, last_name, nationality) VALUES ('Matt', 'Haig', 'British'); Query OK, 1 row affected (0.01 sec) -- MySQL auto-assigned author_id. Check it: SELECT LAST_INSERT_ID(); -- returns the AUTO_INCREMENT value just generated
Always name your columns. Omitting the column list and relying on position (INSERT INTO authors VALUES (1, 'Matt', 'Haig', 'British')) breaks silently when someone adds, removes, or reorders a column on the table. Named columns make the INSERT self-documenting and resilient to schema changes.

Omitting optional columns

You don't have to include every column. Columns with DEFAULT values or that allow NULL can be left out entirely:

-- nationality is nullable — we can omit it INSERT INTO authors (first_name, last_name) VALUES ('Ursula', 'Le Guin'); -- nationality will be NULL for this row -- stock has DEFAULT 0, so we can omit it when inserting a book INSERT INTO books (author_id, title, price, published_year) VALUES (3, 'The Left Hand of Darkness', 8.49, 1969); -- stock defaults to 0 automatically

Multi-row INSERT

You can insert many rows in a single statement by listing multiple value groups. This is much faster than running one INSERT per row — MySQL processes the batch in a single operation:

INSERT INTO authors (first_name, last_name, nationality) VALUES ('Frank', 'Herbert', 'American'), ('Yuval', 'Harari', 'Israeli'), ('Agatha', 'Christie', 'British'), ('George', 'Orwell', 'British'); Query OK, 4 rows affected (0.01 sec) Records: 4 Duplicates: 0 Warnings: 0

INSERT … SELECT

You can insert the results of a SELECT directly into a table. Useful for copying rows between tables, archiving data, or seeding test tables:

-- Copy all British authors into an audit table INSERT INTO british_authors (first_name, last_name) SELECT first_name, last_name FROM authors WHERE nationality = 'British';

Seeding the bookshop — inserting our full dataset

Let's populate all four tables with enough data to make the rest of the course meaningful. Run these in order — foreign key constraints require parent rows to exist first:

-- 1. Authors (no dependencies) INSERT INTO authors (first_name, last_name, nationality) VALUES ('Matt', 'Haig', 'British'), ('Frank', 'Herbert', 'American'), ('Yuval', 'Harari', 'Israeli'), ('Agatha', 'Christie', 'British'), ('Ursula', 'Le Guin', 'American'); -- 2. Books (depend on authors) INSERT INTO books (author_id, title, genre, price, stock, published_year) VALUES (1, 'The Midnight Library', 'Fiction', 8.99, 42, 2020), (1, 'Reasons to Stay Alive', 'Non-fiction',7.99, 18, 2015), (2, 'Dune', 'Sci-fi', 9.99, 27, 1965), (3, 'Sapiens', 'Non-fiction',10.99, 31, 2011), (4, 'And Then There Were None', 'Mystery', 6.99, 55, 1939), (5, 'The Left Hand of Darkness', 'Sci-fi', 8.49, 9, 1969); -- 3. Customers (no dependencies) INSERT INTO customers (first_name, last_name, email, joined_date) VALUES ('Alice', 'Nguyen', 'alice@example.com', '2022-03-10'), ('Ben', 'Okafor', 'ben@example.com', '2023-07-22'), ('Chloe', 'Martinez', 'chloe@example.com', '2021-11-05'), ('David', 'Singh', 'david@example.com', '2024-01-18'); -- 4. Orders (depend on customers and books) INSERT INTO orders (customer_id, book_id, quantity, total_price, order_date) VALUES (1, 1, 2, 17.98, '2024-02-14 10:30:00'), (1, 4, 1, 10.99, '2024-02-14 10:30:00'), (2, 3, 1, 9.99, '2024-03-05 14:15:00'), (3, 5, 3, 20.97, '2024-03-20 09:00:00'), (4, 2, 1, 7.99, '2024-04-10 16:45:00');

With data in all four tables, every SELECT from here on will produce real results to verify against.

2. UPDATE — Changing Existing Rows

UPDATE changes the values of one or more columns in rows that match a condition. It does not add or remove rows — only modifies them.

UPDATE books SET price = 11.99, stock = 35 WHERE book_id = 4;
-- Correct a price error on Sapiens UPDATE books SET price = 11.99 WHERE book_id = 4; Query OK, 1 row affected (0.01 sec) Rows matched: 1 Changed: 1 Warnings: 0
Before
book_idtitleprice
4Sapiens10.99
After
book_idtitleprice
4Sapiens11.99

Updating multiple columns at once

-- Update both price and stock in one statement UPDATE books SET price = 9.49, stock = 50 WHERE book_id = 3; -- Dune

Updating with expressions

SET can use the column's current value in the expression — useful for increments, percentage changes, or clearing a field relative to what's there now:

-- Apply a 10% price increase to all Fiction books UPDATE books SET price = ROUND(price * 1.10, 2) WHERE genre = 'Fiction'; -- Restock all books that have run low UPDATE books SET stock = stock + 20 WHERE stock < 10; -- Clear a nullable field (set it to NULL) UPDATE authors SET nationality = NULL WHERE author_id = 5;

UPDATE with a subquery

-- Give a 5% discount to books by British authors UPDATE books SET price = ROUND(price * 0.95, 2) WHERE author_id IN ( SELECT author_id FROM authors WHERE nationality = 'British' );
UPDATE without WHERE updates every row in the table. UPDATE books SET price = 0 will zero out all prices — there is no undo unless you are inside a transaction. Always write the WHERE clause first, run it as a SELECT to confirm it matches the right rows, then wrap it in UPDATE. The safe pattern is below.

The safe UPDATE pattern

Step 1
Write SELECT with the same WHERE clause — confirm the right rows come back
Step 2
Count matches — is it the number of rows you expected?
Step 3
Replace SELECT … with UPDATE … SET … keeping the same WHERE
Step 4
Check "Rows matched" in the output matches your expected count
-- Step 1 & 2: verify which rows will be affected SELECT book_id, title, price FROM books WHERE genre = 'Sci-fi'; -- shows 2 rows: Dune and The Left Hand of Darkness -- Step 3: now run the update UPDATE books SET price = ROUND(price * 0.90, 2) WHERE genre = 'Sci-fi'; -- Step 4: check the output Rows matched: 2 Changed: 2 Warnings: 0 -- ✓ matches expected

3. DELETE — Removing Rows

DELETE removes rows from a table. Like UPDATE, it only affects rows that match the WHERE clause — and without a WHERE clause, it removes every row.

DELETE FROM books WHERE stock = 0 AND published_year < 1950;
-- Remove a single order by its primary key DELETE FROM orders WHERE order_id = 5; Query OK, 1 row affected (0.01 sec) -- Remove all out-of-print books with zero stock DELETE FROM books WHERE stock = 0 AND published_year < 2000;

DELETE and foreign keys

If a row is referenced by another table via a foreign key, MySQL will block the delete by default — this is referential integrity in action:

-- Attempt to delete an author who has books DELETE FROM authors WHERE author_id = 1; -- Matt Haig has 2 books ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fails (`bookshop`.`books`, CONSTRAINT `fk_books_author` FOREIGN KEY (`author_id`) REFERENCES `authors` (`author_id`)) -- Solution: delete the child rows first, then the parent DELETE FROM books WHERE author_id = 1; -- remove books first DELETE FROM authors WHERE author_id = 1; -- now the author can go
Apply the same safe pattern as UPDATE. Run the WHERE clause as a SELECT first to confirm which rows will be deleted. Delete is irreversible outside of a transaction — getting it wrong means restoring from a backup.

LIMIT on DELETE

You can add LIMIT n to a DELETE to cap how many rows are removed at once. Useful when cleaning up a large number of rows in batches to avoid locking the table for too long:

-- Delete old orders in batches of 1000 to avoid long locks DELETE FROM orders WHERE order_date < '2020-01-01' LIMIT 1000; -- Run this repeatedly until 0 rows affected

4. TRUNCATE vs DELETE — Emptying a Table

Both DELETE FROM table (with no WHERE) and TRUNCATE TABLE table remove all rows. They behave very differently, though:

FeatureDELETE (no WHERE)TRUNCATE TABLE
Speed Slow on large tables — deletes row by row, logs each deletion Very fast — drops and recreates the data pages
Transaction log Fully logged — each row delete is recorded Minimal logging — only the deallocation is logged
Can be rolled back? Yes — if inside a transaction No — TRUNCATE implicitly commits; can't be rolled back
WHERE clause Supported — you can filter which rows to delete Not supported — always removes all rows
AUTO_INCREMENT reset Counter keeps its current value Counter resets to 1
Foreign key checks Enforced — will fail if rows are referenced elsewhere Will fail if the table is a parent in an FK relationship
Triggers Fires DELETE triggers Does NOT fire DELETE triggers
Use when… You need rollback safety, need to fire triggers, or are deleting a subset You want to completely wipe a table fast — typically for dev/test resets
-- Wipe all rows — slow, logged, can be rolled back, keeps AUTO_INCREMENT DELETE FROM orders; -- Wipe all rows — fast, cannot be rolled back, resets AUTO_INCREMENT TRUNCATE TABLE orders;
TRUNCATE resets AUTO_INCREMENT to 1. If any other table holds foreign keys pointing at the truncated table's primary key, and you then INSERT new rows that get IDs 1, 2, 3 again, those IDs will now clash with the references in the child table. In development this is usually fine — you truncate everything and reinsert. In production it is almost always the wrong choice. Use DELETE there.

5. Upsert — REPLACE and INSERT … ON DUPLICATE KEY UPDATE

Sometimes you want to insert a row if it doesn't exist, or update it if it does. This is called an upsert. MySQL offers two ways to do it.

INSERT … ON DUPLICATE KEY UPDATE

The most precise upsert: if the INSERT would violate a unique constraint, run the UPDATE clause instead. Existing rows that don't conflict are inserted normally:

-- Add stock if the book already exists, insert if it doesn't INSERT INTO books (book_id, title, price, stock) VALUES (3, 'Dune', 9.99, 10) ON DUPLICATE KEY UPDATE stock = stock + VALUES(stock); -- add the 10 to existing stock -- VALUES(col) refers to the value that was in the attempted INSERT

REPLACE INTO

REPLACE INTO is simpler but more destructive: it DELETEs the conflicting row and INSERTs a new one. This resets AUTO_INCREMENT IDs and loses any columns you didn't include in the REPLACE. Use INSERT … ON DUPLICATE KEY UPDATE instead for precision:

-- REPLACE deletes the old row and inserts a brand new one -- Any columns not listed get their DEFAULT values (or NULL) REPLACE INTO books (book_id, title, price, stock) VALUES (3, 'Dune', 9.99, 30); -- The old Dune row is gone; a new one is inserted -- genre and published_year would reset to NULL/DEFAULT!

Chapter Summary

StatementWhat it doesKey rules
INSERT INTO … VALUES Add one or more new rows Always name the columns. Insert parents before children (foreign keys). Use multi-row VALUES for bulk inserts.
INSERT … SELECT Insert the result of a SELECT Column count and types in the SELECT must match the INSERT column list.
UPDATE … SET … WHERE Change values in matching rows No WHERE = every row updated. Always SELECT-first to verify. SET can reference the current column value.
DELETE FROM … WHERE Remove matching rows No WHERE = every row deleted. Blocked by foreign key constraints. SELECT-first pattern applies.
TRUNCATE TABLE Remove all rows instantly Cannot be rolled back. Resets AUTO_INCREMENT. Does not fire triggers. Use only in dev/test resets.
INSERT … ON DUPLICATE KEY UPDATE Insert if new, update if exists (upsert) Preferred over REPLACE. Only updates the conflicting row; doesn't replace other columns.
LAST_INSERT_ID() Return the AUTO_INCREMENT value from the last INSERT Session-scoped — returns your own last insert, not someone else's concurrent insert.
Next: Chapter 6 — CREATE TABLE and Schema Design. Chapter 5 assumed the tables already existed. Chapter 6 goes back to the beginning: how to create tables from scratch, choose constraints, define primary and foreign keys, and alter or drop tables when requirements change.