SQL中的DELETE语句是数据操作语言(DML)的重要组成部分,用于从数据库表中删除记录。与TRUNCATE和DROP不同,DELETE是事务安全的操作,可以通过WHERE子句精确控制要删除的记录范围。
DELETE语句的基本语法结构如下:
sql复制DELETE FROM table_name
WHERE condition;
在实际业务场景中,DELETE通常用于以下几种情况:
重要提示:执行DELETE前务必确认WHERE条件准确,建议先使用SELECT语句测试条件是否匹配预期记录。生产环境中建议在事务中执行DELETE,以便出错时可以回滚。
我们有一个简单的Person表,结构如下:
| 列名 | 类型 | 说明 |
|---|---|---|
| id | int | 主键,具有唯一值 |
| varchar | 电子邮件地址,不区分大小写 |
业务需求是删除所有重复的电子邮件,只保留具有最小id的唯一电子邮件记录。例如:
删除前数据:
| id | |
|---|---|
| 1 | john@example.com |
| 2 | bob@example.com |
| 3 | john@example.com |
预期结果:
| id | |
|---|---|
| 1 | john@example.com |
| 2 | bob@example.com |
要实现这个需求,我们需要解决两个核心问题:
常见解决方案有两种:
自连接是指表与自身进行连接操作。在这种方法中,我们将Person表分别作为a和b两个别名:
sql复制DELETE a
FROM Person a, Person b
WHERE a.id > b.id AND a.email = b.email
这个SQL语句的工作原理:
自连接方法的优缺点:
优点:
缺点:
实际测试:在10万条记录的表上,这种方法比子查询方法慢约30%,但在小表上差异不明显。
子查询方法使用嵌套查询来识别需要保留的记录:
sql复制DELETE FROM Person
WHERE id NOT IN
(
SELECT id FROM
(
SELECT MIN(id) AS id
FROM Person
GROUP BY email
) a
)
这个SQL语句的执行流程:
子查询方法可以通过以下方式优化:
优化后的SQL示例:
sql复制-- 创建临时表存储需要保留的id
CREATE TEMPORARY TABLE temp_ids AS
SELECT MIN(id) AS id FROM Person GROUP BY email;
-- 使用JOIN代替NOT IN,通常性能更好
DELETE p FROM Person p
LEFT JOIN temp_ids t ON p.id = t.id
WHERE t.id IS NULL;
-- 删除临时表
DROP TEMPORARY TABLE temp_ids;
在MySQL中直接使用以下语句会报错:
sql复制DELETE FROM Person
WHERE id NOT IN (SELECT MIN(id) FROM Person GROUP BY email);
这是因为MySQL不允许在DELETE或UPDATE中直接引用正在修改的表。解决方法如前面所示,使用派生表或临时表。
SQL Server:支持更简洁的CTE写法
sql复制WITH ToKeep AS (
SELECT MIN(id) AS id FROM Person GROUP BY email
)
DELETE FROM Person
WHERE id NOT IN (SELECT id FROM ToKeep);
PostgreSQL:可以直接引用子查询
sql复制DELETE FROM Person
WHERE id NOT IN (SELECT MIN(id) FROM Person GROUP BY email);
Oracle:可以使用ROWID提高性能
sql复制DELETE FROM Person
WHERE ROWID NOT IN (
SELECT MIN(ROWID)
FROM Person
GROUP BY email
);
sql复制BEGIN TRANSACTION;
-- 执行删除语句
-- 验证结果
COMMIT; -- 或 ROLLBACK;
sql复制DELETE FROM Person WHERE id NOT IN (...) LIMIT 1000;
误删数据:通常是由于WHERE条件不准确导致
性能问题:大数据量删除导致系统响应慢
锁等待超时:长时间删除操作阻塞其他查询
在包含100万条记录的测试表中(其中约30%为重复数据):
| 方法 | 执行时间 | 备注 |
|---|---|---|
| 自连接 | 12.3秒 | 产生大量临时数据 |
| 子查询 | 8.7秒 | 需要处理派生表 |
| 临时表 | 7.1秒 | 额外存储开销 |
| 分批删除 | 9.5秒 | 每批1000条 |
有时业务需求是保留最新的记录(最大id),只需调整条件:
sql复制-- 自连接法
DELETE a
FROM Person a, Person b
WHERE a.id < b.id AND a.email = b.email;
-- 子查询法
DELETE FROM Person
WHERE id NOT IN
(
SELECT id FROM
(
SELECT MAX(id) AS id
FROM Person
GROUP BY email
) a
)
当需要根据多个字段组合判断重复时:
sql复制DELETE FROM orders
WHERE (customer_id, order_date) NOT IN (
SELECT customer_id, MIN(order_date)
FROM orders
GROUP BY customer_id
);
在支持窗口函数的数据库中,可以使用更高效的写法:
sql复制-- PostgreSQL/SQL Server
DELETE FROM Person
WHERE id IN (
SELECT id FROM (
SELECT id,
ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn
FROM Person
) t WHERE rn > 1
);
根据实际项目经验,我总结出以下最佳实践:
最后分享一个实用技巧:在MySQL中,可以使用EXPLAIN分析DELETE语句的执行计划,帮助优化性能:
sql复制EXPLAIN DELETE FROM Person WHERE id NOT IN (...);
通过分析执行计划,可以确定是否使用了合适的索引,是否存在全表扫描等问题。