Menu

SQLite DELETE:用 WHERE 和 RETURNING 安全删除行

SQLite 中 DELETE 的工作方式:写出安全的 WHERE 子句、删除所有行、级联删除关联表,以及用 RETURNING 取回被删除的行。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

DELETE 只删除行,别的什么都不做

DELETE 把行从表中移除。它不会删掉表,不会改变表结构,也不会影响其他表(除非你设置了级联)。写法很简短:

DELETE FROM users WHERE id = 2; 找到符合条件的行并删除它们。另外两行原封不动。表本身仍然存在,你可以继续往里插入数据。

心智模型:DELETE 就是一个把匹配的行扔掉、而不是返回它们的 SELECT。

一切都取决于 WHERE 子句

每一条正式的 DELETE 成败都在于它的 WHERE 子句。写对了,删掉的就是你想删的;写错了,你会删掉更多的数据,有时甚至是整张表。

两个未发布、浏览量为零的草稿都没了。已发布的行留了下来,因为它们不符合条件。WHERE 接受的任何表达式都可以用:IN、LIKE、BETWEEN、子查询,以及 AND/OR 组合。

一个值得养成的习惯:运行 DELETE 之前,先用同样的 WHERE 子句运行一次 SELECT。

-- Preview what will go:
SELECT * FROM posts WHERE published = 0 AND views = 0;

-- Happy with the rows? Now delete them:
DELETE FROM posts WHERE published = 0 AND views = 0;

这个两步走的习惯拯救过的数据库,比所有备份工具加起来还多。

不带 WHERE 的 DELETE 会清空表

去掉 WHERE,DELETE 就会删除每一行:

表被清空了,但仍然存在。SQLite 没有 TRUNCATE 语句,DELETE FROM table; 就是它的等价写法。SQLite 会应用内部的“truncate 优化”,一次性释放所有页,而不是逐行删除。速度很快,但它仍然是一个可以回滚的事务性操作。

如果主键用了 AUTOINCREMENT,计数器不会自己重置。要让 id 重新从 1 开始,还要清除对应的序列行:

DELETE FROM log;
DELETE FROM sqlite_sequence WHERE name = 'log';

对于普通的 INTEGER PRIMARY KEY(没有 AUTOINCREMENT),SQLite 本来就会自由地复用 id,所以不需要这一步。

删除多个指定的行

要删除一组已知的行,IN 是最简洁的写法:

你也可以用子查询来驱动删除操作,当要删除的行是由连接或另一张表决定时,这很方便:

SQLite 不像 MySQL 那样支持 DELETE ... JOIN 语法,但在 WHERE 中使用子查询能达到同样的效果。

RETURNING:看看你删了什么

加上 RETURNING,就能像 SELECT 一样把被删除的行作为结果集取回:

你会得到每一个被删除行的 id 和 email。这在以下场景中非常有用:

  • 准确记录删除了哪些数据。
  • 实现撤销功能(把返回的行暂存到别处)。
  • 只用一次往返,就确认删除操作影响的正是你预期的那些行。

RETURNING 适用于 INSERT、UPDATE 和 DELETE。它有专门的一页详细介绍。

用 ON DELETE CASCADE 处理关联的行

当父表和子表通过外键关联时,删除父行会留下孤立的子行,除非你让 SQLite 进行级联:

删除作者的同时,也会删除他的书。如果没有 ON DELETE CASCADE,同样的 DELETE 要么执行成功并留下孤立的书(外键约束关闭时),要么因约束错误而失败(外键约束开启时)。

最大的坑:SQLite 默认关闭外键约束。每个连接都必须运行 PRAGMA foreign_keys = ON;。如果没有设置这条 pragma,ON DELETE CASCADE 会被悄悄忽略,书会留下来。大多数应用驱动要么会替你设置,要么提供一个选项;检查一下你用的驱动。

其他值得了解的级联选项:ON DELETE SET NULL(清空外键)、ON DELETE RESTRICT(存在子行时拒绝删除)、ON DELETE NO ACTION(默认值,大多数情况下与 RESTRICT 相同)。

带 LIMIT 的 DELETE(编译时选项)

有些 SQLite 版本支持 DELETE ... LIMIT,适合分批清理超大的表:

DELETE FROM logs
WHERE created_at < '2024-01-01'
ORDER BY created_at
LIMIT 1000;

这要求 SQLite 在编译时开启 SQLITE_ENABLE_UPDATE_DELETE_LIMIT。官方二进制文件和大多数语言绑定(Python 的 sqlite3、Node 的 better-sqlite3)都开启了它。如果你的版本没有开启,会得到语法错误,这时就退而使用子查询:

DELETE FROM logs
WHERE id IN (
    SELECT id FROM logs
    WHERE created_at < '2024-01-01'
    ORDER BY created_at
    LIMIT 1000
);

分批删除能让事务保持小规模,当有其他连接正在读取数据库时,这一点很重要。

把大批量删除放进事务

DELETE 本身隐含着事务:要么所有匹配的行都被删除,要么一行都不删。但当你要删除大量数据时,把它放进一个显式事务里,就能在发现不对劲时 ROLLBACK:

ROLLBACK 会完全撤销这次删除。在实际会话中,确认数量没问题后你会执行 COMMIT。当你用一条条语句逐行删除大量数据时,事务还能大幅提速:把循环放在 BEGIN/COMMIT 之间,就不用每次删除都做一次 fsync。

这些操作不会删除什么

几个值得指出的常见误解:

  • DELETE FROM table; 会清空表,但不会删除表。要删除表本身,用 DROP TABLE table;。
  • DELETE 不会让数据库文件变小。被释放的页只是被标记为可重用。要回收磁盘空间,运行 VACUUM;(在性能一章中讲到)。
  • 删除一行不会删除其他表中的子行,除非设置了 ON DELETE CASCADE,“并且”开启了外键约束。
  • 一条没有匹配任何行的 DELETE 不是错误,它是一条成功执行、changes() = 0 的语句。如果你需要知道结果,就检查受影响的行数。

下一篇:UPSERT

很多时候你其实并不想删除,而是想在行不存在时插入,在行已存在时更新。SQLite 把这叫作 UPSERT,而 ON CONFLICT 子句让它只需一条语句,而不是三条。这就是下一页的内容。

常见问题

如何在 SQLite 中删除一行?

使用 DELETE FROM table_name WHERE condition;。WHERE 子句决定删除哪些行。例如,DELETE FROM users WHERE id = 7; 会删除 id 为 7 的那一个用户。如果不加 WHERE,表中的每一行都会被删除。

如何删除 SQLite 表中的所有行?

运行不带 WHERE 子句的 DELETE FROM table_name;。SQLite 没有 TRUNCATE 语句,不带过滤条件的 DELETE 就是它的等价写法,而且 SQLite 会在内部对它做优化(即“truncate 优化”)。如果还想重置 AUTOINCREMENT 计数器,之后再从 sqlite_sequence 中删除对应的行。

SQLite 能把删除操作级联到关联表吗?

能,前提是你在外键上声明了 ON DELETE CASCADE,并且用 PRAGMA foreign_keys = ON; 开启了外键约束。SQLite 默认关闭外键约束,所以这条 pragma 很关键:没有它,级联操作会被悄悄忽略。

如何查看哪些行被删除了?

在语句末尾加上 RETURNING 子句:DELETE FROM users WHERE active = 0 RETURNING id, email; 会像 SELECT 一样返回被删除的行。这对记录日志、实现撤销功能,或者确认你删掉的正是你想删的行都很有用。

Coddy 编程语言插图

和 Coddy 一起学编程

开始