删除数据
从表中移除已有行使用 DELETE。它删除的是数据,不会删除表结构;列、约束和索引仍然存在。
删除通常难以恢复,因此重点不只是写出语法,还要准确限定范围、理解关联数据的处理方式,并在执行前准备验证和恢复方案。本文以通用 SQL 为主,方言差异会单独说明。
DELETE 的基本结构
DELETE FROM table_name
WHERE condition;
删除编号为 BK-001 的商品:
DELETE FROM products
WHERE sku = 'BK-001';
数据库会删除所有满足 WHERE 条件的行。应用程序应检查受影响行数:按唯一键删除时通常预期为 0 或 1,超过预期说明条件或数据唯一性可能有问题。
先确认删除范围
执行删除前,先把相同条件用于 SELECT:
SELECT product_id, sku, product_name, status
FROM products
WHERE status = 'off_sale'
AND stock = 0
ORDER BY product_id;
确认结果后再删除:
DELETE FROM products
WHERE status = 'off_sale'
AND stock = 0;
预览时最好明确列出主键和关键业务字段,并查看行数。只执行 COUNT(*) 能看到规模,却不能发现条件选错了哪批记录。
预览和删除是两条独立语句,中间可能发生并发变化。若必须删除预览到的同一批行,应在事务中锁定目标行,或先把稳定主键保存到受控的工作表中,再按这些主键删除。具体锁语法取决于数据库。
省略 WHERE 的后果
-- 删除 products 中的所有行
DELETE FROM products;
表结构会保留,但所有当前可删除的行都会成为目标。全表删除确有用途,不过应该把它当作高风险操作审查,而不是把 WHERE 当成可有可无的部分。
编写可靠的删除条件
优先使用主键或具有唯一约束的业务键:
DELETE FROM products
WHERE product_id = 101;
组合条件可以进一步表达业务前置状态:
DELETE FROM products
WHERE product_id = 101
AND status = 'draft';
如果受影响行数为 0,可能是记录不存在,也可能是它已经不再处于 draft 状态。应用应根据业务语义处理,而不是自动放宽条件。
判断空值必须使用 IS NULL:
DELETE FROM temporary_products
WHERE imported_at IS NULL;
混合使用 AND 和 OR 时添加括号,避免扩大范围:
DELETE FROM products
WHERE status = 'draft'
AND (created_by = 1001 OR created_by IS NULL);
根据其他表删除
EXISTS 可以清晰表达“有关联记录就删除”,并且具有较好的可移植性:
DELETE FROM products AS p
WHERE EXISTS (
SELECT 1
FROM discontinued_skus AS d
WHERE d.sku = p.sku
);
也可以使用 IN:
DELETE FROM products
WHERE sku IN (
SELECT sku
FROM discontinued_skus
);
如果需要表达“不存在关联记录”,通常优先使用 NOT EXISTS。当子查询可能返回 NULL 时,NOT IN 会受到 SQL 三值逻辑影响,结果可能一行也不匹配。
PostgreSQL 支持 DELETE ... USING:
DELETE FROM products AS p
USING discontinued_skus AS d
WHERE d.sku = p.sku;
MySQL 和 SQL Server 有各自的连接删除语法。它们的目标表写法和可删除表数量并不相同,因此跨数据库代码宜使用 EXISTS,或为目标数据库单独实现并测试。
外键如何影响删除
删除父表行时,引用它的子表可能阻止操作,也可能自动执行关联动作。行为由外键的 ON DELETE 规则决定:
| 外键动作 | 典型效果 | 使用时的考虑 |
|---|---|---|
RESTRICT / NO ACTION | 存在子行时拒绝删除 | 适合必须显式处理的业务数据 |
CASCADE | 自动删除引用该行的子行 | 方便,但删除范围可能远大于一张表 |
SET NULL | 把子表外键设为 NULL | 外键列必须允许 NULL,业务上也要能表示“无归属” |
SET DEFAULT | 把子表外键设为默认值 | 默认值必须仍满足外键,实际使用较少 |
例如,订单明细通常可以随订单一起删除,但历史订单通常不应因客户账户注销而消失。级联策略必须对应真实的数据生命周期,不能仅为了让删除语句成功而选择 CASCADE。
执行父表删除前,应检查直接和间接引用关系。多级级联可能删除大量数据、持有许多锁,并触发其他表上的触发器。
硬删除与软删除
DELETE 属于硬删除:行不再作为普通表数据存在。若业务要求保留历史、支持恢复或满足审计,可以采用软删除,即更新删除标记:
UPDATE products
SET deleted_at = CURRENT_TIMESTAMP,
status = 'deleted'
WHERE product_id = 101
AND deleted_at IS NULL;
软删除不是免费的“撤销按钮”。采用它后还需要系统性处理:
- 所有正常查询都要排除已删除行;
- 唯一键是否允许删除后复用,需要专门设计;
- 外键引用仍然存在,不会触发
ON DELETE动作; - 表和索引会持续增长,需要保留期限和最终清理任务;
- 恢复时要重新检查名称冲突、父记录状态和其他业务约束;
- 敏感数据的法定删除要求可能不允许只做软删除。
可以用视图、行级安全策略或统一的数据访问层减少漏加过滤条件的风险,但必须覆盖报表、后台任务和直接 SQL 等所有入口。
删除前归档
需要长期保留但不应继续留在业务表中的数据,可以先复制到归档表,再删除。两步必须处于同一事务中:
BEGIN;
INSERT INTO archived_products (
product_id,
sku,
product_name,
price,
archived_at
)
SELECT
product_id,
sku,
product_name,
price,
CURRENT_TIMESTAMP
FROM products
WHERE status = 'off_sale';
DELETE FROM products
WHERE status = 'off_sale';
COMMIT;
这段示例仍可能在两条语句之间受到并发写入影响。生产方案应锁定目标行、按已归档的主键删除,或使用数据库提供的原子能力。
PostgreSQL 可以用数据修改 CTE 将“删除并归档”合并为一条语句:
WITH deleted_products AS (
DELETE FROM products
WHERE status = 'off_sale'
RETURNING product_id, sku, product_name, price
)
INSERT INTO archived_products (
product_id,
sku,
product_name,
price,
archived_at
)
SELECT
product_id,
sku,
product_name,
price,
CURRENT_TIMESTAMP
FROM deleted_products;
这是 PostgreSQL 方言。归档表插入失败时整条语句失败,不会留下“已删除但未归档”的中间状态。
DELETE、TRUNCATE 与 DROP
三者用途不同:
| 语句 | 删除对象 | 可使用 WHERE | 表结构是否保留 | 常见用途 |
|---|---|---|---|---|
DELETE | 选中的行 | 是 | 是 | 按业务条件删除数据 |
TRUNCATE TABLE | 表中全部行 | 否 | 是 | 快速清空整张表 |
DROP TABLE | 表对象及其数据 | 否 | 否 | 移除不再需要的表结构 |
TRUNCATE 往往比全表 DELETE 更快,但它不是简单的性能替换:锁类型、触发器、外键限制、标识计数器处理、事务回滚和权限要求都可能不同。选择前必须核对目标数据库。
DROP TABLE 会连同表定义一起移除,属于数据定义操作,不应用来清空业务数据。
事务、锁与并发
重要删除应放在明确的事务边界中:
BEGIN;
DELETE FROM products
WHERE product_id = 101
AND status = 'draft';
-- 应用检查受影响行数和相关业务状态
COMMIT;
发生异常或检查不通过时执行 ROLLBACK。不过,事务不是备份:提交后能否恢复取决于备份、时间点恢复、审计日志或归档设计。
删除会锁定目标行,并可能与更新、外键检查和级联操作相互等待。多个流程应尽量按一致顺序访问表和行,事务内不要进行长时间的人工交互或远程调用。
并发事务可能在你预览之后新增符合条件的行。若要求“只删除审批时看到的记录”,应保存主键清单或版本值;若要求“删除提交时所有符合条件的记录”,则需要接受并明确这种集合语义。
大批量删除
一次删除数百万行可能造成长时间锁定、事务日志增长、复制延迟,以及 PostgreSQL 等 MVCC 数据库中的待回收空间。常见策略包括:
- 按主键或时间范围分批删除;
- 每批使用稳定排序和明确边界;
- 控制事务大小,并记录最后完成的位置;
- 监控锁等待、日志空间、复制延迟和磁盘占用;
- 在删除后按数据库机制安排统计信息更新或空间维护。
不同数据库对 DELETE 限制行数的语法不同。批处理还要保证重试安全,不能只靠无顺序的“前 N 行”推断进度。
对于按日期整体淘汰的超大数据集,可以在设计阶段考虑表分区。移除完整旧分区通常比逐行删除高效,但仍需处理外键、归档和恢复要求。
返回被删除的行
PostgreSQL 可以在删除时直接返回被删数据:
DELETE FROM products
WHERE product_id = 101
RETURNING product_id, sku, product_name;
这比删除后再次查询可靠,因为被删除的行已经无法通过普通查询找到。RETURNING 不是通用 SQL;SQL Server 常用 OUTPUT deleted...,其他数据库和版本的支持方式不同。下一篇将系统讲解修改语句的返回值。
参数化、权限与审计
应用程序应使用参数化查询:
DELETE FROM products
WHERE product_id = :product_id
AND status = :expected_status;
不要把用户输入直接拼进 SQL。删除接口还应在服务端执行身份认证、对象级授权和业务状态检查,不能只依靠前端隐藏按钮。
数据库账户应遵循最小权限。除 DELETE 权限外,读取条件、子查询或 USING 表还可能需要 SELECT 权限。行级安全策略、触发器和审计规则也可能改变可删除范围或记录操作信息。
删除数据检查清单
- 是否用相同条件查看了具体记录和预计行数;
WHERE是否使用稳定标识,并正确处理NULL、AND与OR;- 是否检查受影响行数,且超出预期时停止后续操作;
- 是否了解所有外键限制与级联删除范围;
- 业务真正需要硬删除、软删除,还是归档;
- 是否需要事务、行锁、主键清单或版本条件应对并发;
- 提交后的恢复依赖备份、审计还是归档,是否实际验证过;
- 大批量删除是否需要分批或使用分区生命周期管理;
- 应用是否使用参数绑定,并在服务端完成授权;
- 是否确认目标数据库关于
TRUNCATE、多表删除和返回值的行为。
可靠的删除流程应同时回答三个问题:删哪些数据、会连带影响什么、提交后如何证明或恢复这次操作。