用途
本技能面向本地缓存数据库的清理场景,说明如何正确安排级联删除的顺序。缓存清理表面上像是“删除所有没有被引用的行”,实际上是一次图遍历,访问图的顺序决定了清理是否真正生效。
主要能力
技能给出了一条可用的删除链顺序:
- 固定活跃记录(pin the live record)
- 事件日志
- 其附属行以状态标志为键的容器(以及这些附属行)
- 级联到子项的容器
- 剩余容器
- 叶子行,分批删除
- 本次运行前已经孤立的附属行
- 检查点 + 压缩
- 解除固定(unpin)
文中给出了一个真实 57 MB 数据库的测量结果:删除了 10 607 条叶子行,以及分布在四张容器表中的 470 + 238 + 50 + 8 条容器行和 8 818 条附属行,文件最终回到约 12 MB。
使用方式与关键陷阱
叶子优先删除不会删除任何行,并以零报告。 叶子只有在所有容器都不再引用它之后才会孤立,因此从叶子开始的清理会发现它们仍被引用,一条也删不掉。文中测量:12 221 条 track 中有 11 322 条被缓存播放列表保活,其余被其他引用表和 kept-by-state 列保活,因此单独的 track 清理删除零行却报告成功。修复方法:先删容器,最后删叶子,并让每条语句返回受影响行数,使日志能显示清理实际生效的位置。
“没有被任何东西引用”对状态列是错误的判断。 liked 标志和 download 标记不是外键,它们是 Favourites 和 Downloads 库的全部存储支撑。直接从搜索结果点赞的 track 不属于任何播放列表和专辑,按字面理解孤立性会删除用户的收藏及其下载文件的唯一数据库链接。每个此类列都必须作为谓词出现,例如:
-- adapted
SELECT trackId FROM song
WHERE liked = 0 AND downloadState = 0
AND trackId NOT IN (SELECT trackId FROM playlist_track)
...
downloadState = 0 有双重作用:它还能保护仍在传输中的下载,否则会在传输过程中被删除。这里的每个 NOT IN 子查询之所以安全,仅因为所选列声明为 NOT NULL——在可空列上该谓词会静默地匹配不到任何行(该失败有独立的兄弟技能:sql-not-in-nullable-trap)。
同一规则向上适用一层,原因不同。 支撑被保留叶子的容器也应存活——不是因为删除会丢数据,而是因为一旦专辑封面和曲目列表消失,叶子的详情页就无法离线渲染。应通过与保留叶子的关联来保留容器,而不是笼统地“保留一切”:
-- adapted: an album stays if any liked or downloaded track points at it
DELETE FROM album
WHERE liked = 0 AND downloadState = 0
AND NOT EXISTS (SELECT 1 FROM song s
WHERE s.albumId = album.id AND (s.liked = 1 OR s.downloadState != 0))
预计算的 id 列表在计算完成的那一刻就已过期。 先选出孤立 id 再按 id 删除是两条语句,期间用户仍在点赞和浏览。应在 DELETE 内重复孤立条件,而不是信任列表:
DELETE FROM song WHERE trackId IN (:ids)
AND liked = 0 AND downloadState = 0
AND trackId NOT IN (SELECT trackId FROM playlist_track) -- re-checked, not assumed
这在级联指回自身时最为重要:一个在叶子删除时级联的联结表意味着清理中途加入播放列表的 track 会被直接从该播放列表中拉出。
正在使用的记录看起来像孤立的。 正在播放、打开或编辑的内容通常是延迟持久化的——在暂停、变更、退出时写入,且仅在用户选择启用持久化时才写入。因此活跃工作集经常不在清理读取的表中,从而被删除。应在清理开始前将其写入磁盘,故意忽略用户的持久化设置:这不是“记住我的会话”,而是“不要删除屏幕上正在显示的内容”。之后如果设置是关闭的,再把固定移除——保留它会既存储用户拒绝的状态,又保护这些行免受未来所有清理,无论它们多久之前就不再活跃。
附属行需要两遍,而不是一遍。 挂在叶子下的行(缓存歌词、格式记录、额外元数据)必须在叶子之后删除,且仅针对实际被删除的 id——上面的叶子 DELETE 可能保留某行,被保留的行必须保留其附属行:
DELETE FROM lyrics WHERE trackId IN (:ids) AND trackId NOT IN (SELECT trackId FROM song)
该语句只能触及在途的 id,因此叶子在更早版本中已消失的附属行对它不可达,会永远累积。应添加一个单独的无条件遍历(WHERE trackId NOT IN (SELECT trackId FROM song)),在最后运行一次。
id 列表会超出语句的参数上限。 引擎限制单条语句可绑定的值数量,而孤立列表可达数千。应分块删除。不要硬编码从博客文章读到的限制——查阅你实际发布的引擎版本的参数上限,并选择远低于它的块大小(此代码库使用 400)。
验证方式
让每个删除返回 Int 并记录计数。所有数字都为零的清理是一个信号,而不是干净的数据库——两个最常见的原因是顺序问题,以及 NOT IN 作用于可空列而永远无法求值为真。
发布前,统计每个阶段应找到的行数:将容器语句的 WHERE 子句作为 SELECT COUNT(*) 运行,并与删除报告的数量比较。不匹配意味着某个谓词匹配不到任何行,而不是匹配正确。
检查文件大小前后变化,而不仅仅是行数——删除会在文件内部释放页,直到压缩前不会归还给文件系统。
适用场景
适用于本地缓存数据库的清理、级联删除顺序设计、状态列参与孤立判断、延迟持久化活跃记录保护、附属行清理以及分块删除等场景。
限制
文中未提供该技能的具体安装方式、支持平台、收费信息或版本要求,这些事实暂未提供。