数据删除策略是数据库设计中容易被忽视的一环。很多开发者在建表时想都不想就直接用DELETE语句,等到产品经理找上门说“误删的数据能恢复吗”,才发现问题大了。SQLite作为轻量级嵌入式数据库,虽然不像大型数据库那样有复杂的审计和闪回机制,但通过合理的表结构设计,同样可以优雅地实现软删除与硬删除。本文结合具体的项目实践,聊聊这两种删除方式的实现细节和选择依据。

硬删除:直接从表中物理移除数据
硬删除就是最常见的DELETE操作,数据行从数据文件中被真正移除(或者说标记为可复用空间)。它的优点非常直接:表不会膨胀,索引不会越来越重,查询不需要额外的过滤条件。对于临时数据、缓存数据、日志类数据,硬删除几乎是唯一合理的选择。
来看一个简单的例子,假设有一个会话表,过期会话直接删掉即可:
-- 直接删除过期的会话记录
DELETE FROM sessions WHERE expires_at < strftime('%s', 'now');
-- 删除单条记录
DELETE FROM users WHERE id = 42;
但硬删除有两个绕不开的问题。第一,数据删了就没了,一旦误操作几乎没有挽回余地,SQLite虽然理论上可以通过WAL日志或备份恢复,但过程繁琐且不保证成功。第二,很多业务上需要保留删除痕迹,比如订单被取消后仍要可查、用户注销后历史数据仍需留存以满足合规要求。这些场景下,硬删除就不适用了。
另外值得一提的是,SQLite执行大量DELETE后,文件体积并不会自动缩小,被删除的页只是被标记为空闲页供后续复用。如果想真正回收磁盘空间,需要执行VACUUM操作,这一点在移动端应用里尤其需要注意,因为数据库文件大小直接影响应用包体和存储占用。
软删除:用标志位保留数据可恢复性
软删除的核心思路是不真正删除数据,而是给记录加一个标志位,表示它已经被“删除”。最常见的做法是增加一个deleted字段,用0和1表示正常与删除状态,更推荐的做法是加一个deleted_at时间戳字段,NULL表示未删除,非NULL则记录了删除时间,这样既表达了状态,又保留了删除发生的时间点,排查问题时非常有用。
建表示例如下:
CREATE TABLE orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_no TEXT NOT NULL UNIQUE,
user_id INTEGER NOT NULL,
amount REAL NOT NULL DEFAULT 0,
deleted_at INTEGER DEFAULT NULL, -- NULL表示未删除
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
-- 软删除一条订单
UPDATE orders SET deleted_at = strftime('%s', 'now') WHERE id = 1001;
-- 查询所有有效订单(注意过滤已删除数据)
SELECT id, order_no, amount FROM orders
WHERE deleted_at IS NULL AND user_id = 55;
-- 恢复误删的订单
UPDATE orders SET deleted_at = NULL WHERE id = 1001;
软删除最大的坑在于:所有查询都必须记得加上deleted_at IS NULL条件。只要有一条查询忘了加,已删除的数据就会泄漏给用户,这在订单、消息类系统里是严重事故。为了降低出错概率,可以考虑使用视图封装基础查询:
CREATE VIEW v_orders AS SELECT * FROM orders WHERE deleted_at IS NULL; -- 业务代码统一查视图,避免遗漏过滤条件 SELECT * FROM v_orders WHERE user_id = 55;
另一个坑是唯一约束冲突。比如用户表对username字段加了UNIQUE约束,用户注销(软删除)后想用同一个用户名注册新账号,就会撞上唯一约束。解决办法有几种:可以把唯一索引改为部分索引,只对未删除的数据生效,例如CREATE UNIQUE INDEX idx_username ON users(username) WHERE deleted_at IS NULL;也可以在删除时把username改成带时间戳后缀的值。SQLite支持部分索引,第一种方案明显更优雅。
软删除带来的存储膨胀与清理方案
软删除用久了,表里会积累大量“死数据”。这些数据平时不参与业务查询,却占着存储空间、拖慢全表扫描和统计查询。对于SQLite这种单文件数据库,体积膨胀的影响更为直观。所以软删除方案必须配套清理策略。
常见做法是定期任务把删除超过一定时长的数据真正物理删除,比如每天凌晨清理删除超过90天的记录:
-- 物理清理删除超过90天的软删除数据
DELETE FROM orders
WHERE deleted_at IS NOT NULL
AND deleted_at < strftime('%s', 'now', '-90 days');
-- 回收磁盘空间(建议在低峰期执行)
VACUUM;
需要注意的是,VACUUM会重建整个数据库文件,执行期间需要约两倍于原文件大小的磁盘空间,且会锁库,移动端应用最好在用户闲置时或应用启动阶段执行,并做好耗时提示。
如何选择:从业务场景出发
选择软删除还是硬删除,本质上是判断“这条数据删了之后,业务上还有没有可能要看它”。可以通过几个问题来判断:数据是否需要审计追溯?误删的后果是否严重?是否涉及合规要求(如交易记录保存年限)?用户是否可能要求恢复数据?只要有一个答案是肯定的,就应该考虑软删除。
给出一些典型场景的参考:电商订单、支付流水、发票记录必须软删除甚至永久不删;用户账号建议软删除,保留一段可恢复期后清理;消息、通知类数据可软删除,配合定期清理;验证码、会话、缓存、草稿这类数据直接硬删除即可。
还有一种混合策略在实践中很受欢迎:业务表用软删除保证安全,同时后台提供“彻底删除”的管理功能,让管理员在确认无误后执行硬删除。这样既保证了日常操作的可恢复性,又能主动控制数据体积。无论选择哪种方案,建议在项目设计初期就把删除策略确定下来,并写入团队规范,避免各人各写一套,后期统一成本会高得多。