00:00

MySQL DELETE Statement

Last updated:

The DELETE statement removes rows from a table: expired sessions, cancelled orders, spam accounts. It is the "D" in CRUD. Deleting is easy, and so is deleting the wrong rows, so this tutorial pairs every technique with a way to stay safe.

1. Syntax

syntax.sql
DELETE FROM table_name
[WHERE condition]
[ORDER BY ...] [LIMIT n];
DELETE removes only the rows that match WHEREA sessions table with five rows. DELETE FROM sessions WHERE expires_at < '2026-03-01' removes the two expired rows and keeps the other three.sessionsiduserexpires_at1u72026-02-102u32026-03-153u92026-02-274u32026-03-205u12026-03-31DELETE FROM sessionsWHERE expires_at < '2026-03-01';afteriduserexpires_at2u32026-03-154u32026-03-205u12026-03-31red rows are deleted; the table and other rows stay
Figure 1: DELETE removes matching rows; the table and its structure remain.

2. Deleting Matching Rows

Always preview with SELECT, then delete with the same WHERE:

query.sql
SELECT * FROM sessions WHERE expires_at < '2026-03-01';

Preview: rows that will be deleted

iduser_idexpires_at
172026-02-10
392026-02-27
query.sql
DELETE FROM sessions WHERE expires_at < '2026-03-01';
Output
Query OK, 2 rows affected
query.sql
SELECT * FROM sessions;

Remaining rows

iduser_idexpires_at
232026-03-15
432026-03-20
512026-03-31
672026-04-02

3. Deleting One Row by Key

query.sql
DELETE FROM sessions WHERE id = 6;
Output
Query OK, 1 row affected

Deleting by primary key is the safest form: it can never touch more than one row. If nothing matches, MySQL simply reports 0 rows affected:

query.sql
DELETE FROM sessions WHERE id = 999;
Output
Query OK, 0 rows affected

4. ORDER BY and LIMIT

Delete only the oldest N rows, or delete a huge table in small batches to keep locks short:

limit.sql
DELETE FROM sessions ORDER BY expires_at LIMIT 1;
SELECT * FROM sessions;

Result: 2 rows

iduser_idexpires_at
432026-03-20
512026-03-31
batch-delete.sql
-- batch pattern: repeat until 0 rows affected
DELETE FROM audit_log WHERE created_at < '2025-01-01' ORDER BY id LIMIT 10000;

5. DELETE with JOIN

Delete rows based on another table. Remove sessions of banned users:

delete-join.sql
INSERT INTO sessions VALUES (10, 7, '2026-05-01'), (11, 7, '2026-05-02');

DELETE s
FROM sessions s
JOIN users u ON u.user_id = s.user_id
WHERE u.status = 'banned';

SELECT * FROM sessions ORDER BY id;

Result: 2 rows

iduser_idexpires_at
432026-03-20
512026-03-31

Name the table(s) to delete from right after DELETE: DELETE s, u FROM … would delete from both.

6. DELETE with a Sub-query

orphans.sql
INSERT INTO sessions VALUES (12, 42, '2026-06-01');   -- user 42 doesn't exist

DELETE FROM sessions
WHERE user_id NOT IN (SELECT user_id FROM users);

SELECT * FROM sessions ORDER BY id;

Result: 2 rows

iduser_idexpires_at
432026-03-20
512026-03-31

7. Deleting Duplicate Rows

The contacts table has repeated emails. Keep the earliest row (lowest id) of each email:

query.sql
SELECT email, COUNT(*) AS copies FROM contacts GROUP BY email HAVING COUNT(*) > 1;

Result: 2 rows

emailcopies
a@x.example3
b@x.example2
dedupe.sql
DELETE c1
FROM contacts c1
JOIN contacts c2 ON c2.email = c1.email AND c2.id < c1.id;

SELECT * FROM contacts ORDER BY id;

Result: 3 rows

idemailadded
1a@x.example2026-01-01
2b@x.example2026-01-02
4c@x.example2026-01-06
Add a UNIQUE constraint on email afterwards so duplicates can't come back.

8. Foreign Keys and DELETE

If other tables reference the rows you delete, the foreign key rule decides what happens: RESTRICT blocks the delete, CASCADE deletes the child rows too, SET NULL unlinks them. See MySQL Foreign Key.

cascade.sql
CREATE TABLE posts (post_id INT PRIMARY KEY, user_id INT,
  FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE);
INSERT INTO posts VALUES (100, 9), (101, 9), (102, 3);
DELETE FROM users WHERE user_id = 9;
SELECT * FROM posts;

Result: 1 row

post_iduser_id
1023

Divya's two posts were deleted with her account.

9. Undoing a DELETE

Inside a transaction, DELETE can be rolled back:

rollback.sql
START TRANSACTION;
DELETE FROM users;                          -- oops, no WHERE
SELECT COUNT(*) AS users_left FROM users;
ROLLBACK;
SELECT COUNT(*) AS users_left FROM users;

Inside the transaction

users_left
0

After ROLLBACK

users_left
3
With autocommit on (the default), each DELETE outside a transaction is committed immediately and can only be undone from a backup. Safe-update mode (SET sql_safe_updates = 1) rejects DELETE without a key in WHERE or a LIMIT.

10. Soft Deletes

Many applications never physically delete important rows. They mark them as deleted instead, keeping history and allowing "undo":

soft-delete.sql
ALTER TABLE users ADD COLUMN deleted_at DATETIME NULL;
UPDATE users SET deleted_at = '2026-03-25 10:00:00' WHERE user_id = 7;
SELECT user_id, name FROM users WHERE deleted_at IS NULL;

Result: 2 rows

user_idname
1Ankit
3Bela

Queries must then filter on deleted_at IS NULL; a view of active rows makes that easy.

11. DELETE vs TRUNCATE vs DROP

DELETETRUNCATEDROP
RemovesMatching rowsAll rowsTable + rows
WHERE / LIMITYesNoNo
RollbackYesNoNo
Triggers fireYesNoNo
AUTO_INCREMENTUnchangedResetGone
Speed on huge tablesSlowFastFast

12. Practice Exercises

Exercise 1: Delete all sessions that expire in March 2026.
solution.sql
DELETE FROM sessions WHERE expires_at >= '2026-03-01' AND expires_at < '2026-04-01';
SELECT * FROM sessions;
Output
Query OK, 2 rows affected

Result: Empty set (0 rows)

iduser_idexpires_at
No rows
Exercise 2: Delete users with no remaining sessions, but only those not soft-deleted.
solution.sql
DELETE FROM users
WHERE deleted_at IS NULL
  AND user_id NOT IN (SELECT user_id FROM sessions);
SELECT user_id, name, deleted_at FROM users;
Output
Query OK, 2 rows affected

Result: 1 row

user_idnamedeleted_at
7Charan2026-03-25 10:00:00

13. Summary

  • DELETE FROM t WHERE …; removes matching rows; without WHERE it removes all rows.
  • Preview with SELECT, delete by primary key where possible, and use transactions for risky deletes.
  • ORDER BY + LIMIT deletes a few rows or works in batches.
  • Multi-table DELETE … JOIN and sub-queries delete based on other tables.
  • Foreign key rules may block or cascade deletes; consider soft deletes for important data.