跳到主要内容

删除数据

从表中移除已有行使用 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 数据库中的待回收空间。常见策略包括:

  1. 按主键或时间范围分批删除;
  2. 每批使用稳定排序和明确边界;
  3. 控制事务大小,并记录最后完成的位置;
  4. 监控锁等待、日志空间、复制延迟和磁盘占用;
  5. 在删除后按数据库机制安排统计信息更新或空间维护。

不同数据库对 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 权限。行级安全策略、触发器和审计规则也可能改变可删除范围或记录操作信息。

删除数据检查清单​

  1. 是否用相同条件查看了具体记录和预计行数;
  2. WHERE 是否使用稳定标识,并正确处理 NULL、AND 与 OR;
  3. 是否检查受影响行数,且超出预期时停止后续操作;
  4. 是否了解所有外键限制与级联删除范围;
  5. 业务真正需要硬删除、软删除,还是归档;
  6. 是否需要事务、行锁、主键清单或版本条件应对并发;
  7. 提交后的恢复依赖备份、审计还是归档,是否实际验证过;
  8. 大批量删除是否需要分批或使用分区生命周期管理;
  9. 应用是否使用参数绑定,并在服务端完成授权;
  10. 是否确认目标数据库关于 TRUNCATE、多表删除和返回值的行为。

可靠的删除流程应同时回答三个问题:删哪些数据、会连带影响什么、提交后如何证明或恢复这次操作。