SQL DELETE and TRUNCATE
DELETE removes rows from a table; TRUNCATE empties a table completely. Both are permanent once committed, so the habits from the UPDATE lesson matter even more here: check the WHERE with a SELECT first, and use a transaction when the data is valuable.
- Delete rows that match a condition
- Delete using a subquery or a join
- Understand foreign key errors on delete
- Choose between DELETE, TRUNCATE and DROP
DELETE syntax
DELETE FROM table_nameWHERE condition;DELETE FROM table_name; without WHERE removes every row. The table itself stays, empty.
Sample data
| product_id | product_name | category | price | stock |
|---|---|---|---|---|
| 1 | Wireless Mouse | Electronics | 799 | 120 |
| 2 | Mechanical Keyboard | Electronics | 3499 | 45 |
| 3 | USB-C Hub | Electronics | 1999 | 0 |
| 4 | Office Chair | Furniture | 8999 | 12 |
| 5 | Standing Desk | Furniture | 18999 | 5 |
| 6 | Notebook Pack | Stationery | 249 | 300 |
| 7 | Gel Pens | Stationery | 199 | 250 |
| 8 | Desk Lamp | Furniture | 1499 | 30 |
| 9 | Monitor Arm | Furniture | 2799 | 18 |
Delete one row
Product 9 (Monitor Arm) has never been ordered and is discontinued:
DELETE FROM productsWHERE product_id = 9; SELECT product_id, product_name FROM products;| product_id | product_name |
|---|---|
| 1 | Wireless Mouse |
| 2 | Mechanical Keyboard |
| 3 | USB-C Hub |
| 4 | Office Chair |
| 5 | Standing Desk |
| 6 | Notebook Pack |
| 7 | Gel Pens |
| 8 | Desk Lamp |
WHERE product_id = 9matches exactly one row.- MySQL removes it and reports
1 row affected. - The
SELECTshows the 8 remaining products.
Delete several rows
A WHERE that matches several rows deletes them all. Remove every line of order 1001:
DELETE FROM order_itemsWHERE order_id = 1001; SELECT * FROM order_items WHERE order_id IN (1001, 1002);| order_id | product_id | quantity |
|---|---|---|
| 1002 | 4 | 1 |
- Order 1001 has two lines (products 2 and 6), so
2 rows affected. - Order 1002’s line is untouched.
Check first with SELECT
Before deleting, run the same WHERE as a SELECT:
SELECT * FROM orders WHERE status = 'Cancelled';| order_id | customer_id | order_date | status |
|---|---|---|---|
| 1004 | 3 | 2025-02-14 | Cancelled |
If the result is what you expect, change SELECT * to DELETE and run it.
Foreign keys block orphan rows
Order 1004 still has an order line. The foreign key from order_items to orders stops you from deleting the order and leaving the line pointing at nothing:
DELETE FROM orders WHERE order_id = 1004;ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fails (`company`.`order_items`, CONSTRAINT `order_items_ibfk_1` FOREIGN KEY (`order_id`) REFERENCES `orders` (`order_id`))
Delete the child rows first, then the parent:
DELETE FROM order_items WHERE order_id = 1004;DELETE FROM orders WHERE order_id = 1004;SELECT order_id, status FROM orders WHERE status = 'Cancelled';- The first
DELETEremoves the order’s line (1 row). - Now nothing references order 1004, so the second
DELETEsucceeds. - No cancelled orders are left: Empty set.
A foreign key declared with ON DELETE CASCADE deletes the child rows automatically. It is convenient, but make sure that is really what you want.
Delete with a subquery
Delete customers who have never placed an order:
DELETE FROM customersWHERE customer_id NOT IN (SELECT customer_id FROM orders); SELECT customer_id, customer_name FROM customers;| customer_id | customer_name |
|---|---|
| 1 | Ananya Sharma |
| 2 | Rohit Kumar |
| 3 | Fatima Khan |
| 4 | David Fernandes |
| 5 | Ishita Bose |
- The subquery lists every customer id that appears in
orders. NOT INmatches customers 6 and 7, who have no orders. (orders.customer_idisNOT NULL, so the NOT IN NULL trap cannot happen here.)- Two rows are deleted.
Delete with JOIN (MySQL)
Delete the order lines of every delivered order placed before February 2025:
DELETE oiFROM order_items oiJOIN orders o ON o.order_id = oi.order_idWHERE o.status = 'Delivered' AND o.order_date < '2025-02-01'; SELECT COUNT(*) AS remaining_lines FROM order_items;| remaining_lines |
|---|
| 13 |
DELETE oinames the table to delete from;ordersis only used for the condition.- Orders 1001 and 1002 match, with three lines between them.
- 16 lines minus 3 leaves 13.
Delete with ORDER BY and LIMIT
MySQL can delete a limited number of rows. This is useful for removing huge amounts of old data in small batches:
DELETE FROM order_itemsORDER BY order_idLIMIT 2; SELECT COUNT(*) AS remaining_lines FROM order_items;| remaining_lines |
|---|
| 14 |
ORDER BY order_iddecides which rows go first: the two lines of order 1001.LIMIT 2stops after two rows, leaving 14.- In a real clean-up job you would use something like
LIMIT 1000and repeat until no rows are affected, so each batch holds locks only briefly.
TRUNCATE TABLE
TRUNCATE TABLE removes all rows at once and resets AUTO_INCREMENT counters:
CREATE TABLE audit_log (id INT AUTO_INCREMENT PRIMARY KEY, msg VARCHAR(50));INSERT INTO audit_log (msg) VALUES ('login'), ('logout');TRUNCATE TABLE audit_log;INSERT INTO audit_log (msg) VALUES ('fresh start');SELECT * FROM audit_log;| id | msg |
|---|---|
| 1 | fresh start |
- Two rows are inserted with ids 1 and 2.
TRUNCATEempties the table and resets the counter.- The next insert gets id 1 again. After a
DELETE FROM audit_logit would have got id 3.
DELETE vs TRUNCATE vs DROP
| DELETE | TRUNCATE | DROP TABLE | |
|---|---|---|---|
| Removes | chosen rows (WHERE) | all rows | the whole table |
| Table structure | kept | kept | removed |
WHERE allowed | yes | no | no |
| Speed on big tables | slow (row by row) | fast | fast |
Resets AUTO_INCREMENT | no | yes | n/a |
| Fires triggers | yes | no | no |
| Rollback in a transaction | yes | no (implicit commit) | no (implicit commit) |
| Command type | DML | DDL | DDL |
Exercise: Remove out-of-stock products
Delete every product whose stock is 0. Run the matching SELECT first in your head, then show the remaining products.
| product_id | product_name | stock |
|---|---|---|
| 1 | Wireless Mouse | 120 |
| 2 | Mechanical Keyboard | 45 |
| 4 | Office Chair | 12 |
| 5 | Standing Desk | 5 |
| 6 | Notebook Pack | 300 |
| 7 | Gel Pens | 250 |
| 8 | Desk Lamp | 30 |
| 9 | Monitor Arm | 18 |
Hint 1
DELETE FROM products WHERE stock = 0;. The USB-C Hub has order lines, so delete those first.
DELETE FROM order_items WHERE product_id IN (SELECT product_id FROM products WHERE stock = 0);DELETE FROM products WHERE stock = 0;SELECT product_id, product_name, stock FROM products;| product_id | product_name | stock |
|---|---|---|
| 1 | Wireless Mouse | 120 |
| 2 | Mechanical Keyboard | 45 |
| 4 | Office Chair | 12 |
| 5 | Standing Desk | 5 |
| 6 | Notebook Pack | 300 |
| 7 | Gel Pens | 250 |
| 8 | Desk Lamp | 30 |
| 9 | Monitor Arm | 18 |
Quick quiz
1. What does DELETE FROM customers; do?
Without WHERE, every row is removed; the empty table remains.
2. Which command resets the AUTO_INCREMENT counter?
TRUNCATE recreates the table empty, with the counter reset.
3. Why does deleting a parent row fail with error 1451?
The foreign key protects referential integrity.
4. Which can be rolled back inside a transaction?
TRUNCATE and DROP are DDL and commit implicitly.
DELETE FAQ
What is the difference between DELETE and TRUNCATE?
DELETE removes rows one by one, can use WHERE, fires triggers and can be rolled back. TRUNCATE removes all rows at once, resets AUTO_INCREMENT and cannot be rolled back in MySQL.
How do I delete duplicate rows in SQL?
Keep one row per duplicate group and delete the rest, for example with a self join on the duplicate columns and a.id > b.id, or with ROW_NUMBER() in a CTE. See Top N per Group.
Can I recover deleted rows?
Inside an uncommitted transaction, yes: ROLLBACK. After a commit, only from a backup or the binary log.
How do I delete rows from two tables at once in MySQL?
Name both tables after DELETE: DELETE o, oi FROM orders o JOIN order_items oi ON ... WHERE ...;