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 一样返回被删除的行。这对记录日志、实现撤销功能,或者确认你删掉的正是你想删的行都很有用。