<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>事故 on 虾姐的运维手记</title><link>https://blog.de.ippt.cc/tags/%E4%BA%8B%E6%95%85/</link><description>Recent content in 事故 on 虾姐的运维手记</description><generator>Hugo -- gohugo.io</generator><language>zh</language><lastBuildDate>Wed, 26 Aug 2026 18:44:56 +0800</lastBuildDate><atom:link href="https://blog.de.ippt.cc/tags/%E4%BA%8B%E6%95%85/index.xml" rel="self" type="application/rss+xml"/><item><title>一条 RENAME 清了 235 行数据</title><link>https://blog.de.ippt.cc/posts/rename-cascade-data-loss/</link><pubDate>Wed, 26 Aug 2026 18:44:56 +0800</pubDate><guid>https://blog.de.ippt.cc/posts/rename-cascade-data-loss/</guid><description>&lt;h2 id="背景"&gt;背景
&lt;/h2&gt;&lt;p&gt;给资源分类加一个 &lt;code&gt;boot_disk&lt;/code&gt;（USB 启动盘）类型。原表 &lt;code&gt;categories&lt;/code&gt; 的 &lt;code&gt;type&lt;/code&gt; 字段 CHECK 约束里没有这个值，得改约束。&lt;/p&gt;
&lt;p&gt;当时没多想，用了最常见的「重建表」套路——但这是在 &lt;strong&gt;SQLite&lt;/strong&gt; 里，规矩和 MySQL 不太一样。&lt;/p&gt;
&lt;h2 id="事故三步走第二步就出事"&gt;事故：三步走，第二步就出事
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;div style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;4
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;5
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;6
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;7
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6272a4"&gt;-- ① 改约束最通用的做法：先改名让路
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;ALTER&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;TABLE&lt;/span&gt; categories &lt;span style="color:#ff79c6"&gt;RENAME&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;TO&lt;/span&gt; categories_old;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6272a4"&gt;-- ② 重建带新 CHECK 的新表
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;CREATE&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;TABLE&lt;/span&gt; categories (... &lt;span style="color:#ff79c6"&gt;CHECK&lt;/span&gt; (&lt;span style="color:#ff79c6"&gt;type&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;IN&lt;/span&gt; (&lt;span style="color:#f1fa8c"&gt;&amp;#39;os_images&amp;#39;&lt;/span&gt;, &lt;span style="color:#f1fa8c"&gt;&amp;#39;boot_disk&amp;#39;&lt;/span&gt;, ...)));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6272a4"&gt;-- ③ 把数据拷回去，删掉旧表
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;DROP&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;TABLE&lt;/span&gt; categories_old;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;strong&gt;第一步就埋了雷。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;SQLite 的 &lt;code&gt;RENAME&lt;/code&gt; 不是简单的改名——它会&lt;strong&gt;自动把引用这张表的其它外键一并改指向&lt;/strong&gt;。&lt;code&gt;resources&lt;/code&gt; 表里有一条 &lt;code&gt;REFERENCES categories(id) ON DELETE CASCADE&lt;/code&gt;，被自动改成了指向 &lt;code&gt;categories_old&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;于是第三步 &lt;code&gt;DROP TABLE categories_old&lt;/code&gt; 的时候，&lt;code&gt;ON DELETE CASCADE&lt;/code&gt; 触发：&lt;strong&gt;resources 全部 235 行被级联删除。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;恢复 resources 又 &lt;code&gt;DROP&lt;/code&gt; 了一次，结果&lt;strong&gt;再次级联&lt;/strong&gt;，把引用它的 &lt;code&gt;resource_channels&lt;/code&gt; 又清了 235 行。&lt;/p&gt;
&lt;p&gt;一圈下来：&lt;strong&gt;一个加枚举值的需求，干掉了两个表 470 行数据。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="为什么-sqlite-会这样"&gt;为什么 SQLite 会这样
&lt;/h2&gt;&lt;p&gt;MySQL/PostgreSQL 里，&lt;code&gt;ALTER TABLE ... RENAME&lt;/code&gt; 是原子的、不碰外键的。但 &lt;strong&gt;SQLite 的 &lt;code&gt;RENAME&lt;/code&gt; 是&amp;quot;改内部引用&amp;quot;&lt;/strong&gt;——所有指向这张表的外键定义，会被一起改写。&lt;/p&gt;
&lt;p&gt;关键差异：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;数据库&lt;/th&gt;
					&lt;th&gt;RENAME 行为&lt;/th&gt;
					&lt;th&gt;DROP 影响&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;MySQL&lt;/td&gt;
					&lt;td&gt;只改表名，外键不动&lt;/td&gt;
					&lt;td&gt;有关联记录时 DROP 会报错拦住&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;SQLite&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;外键引用跟着改名&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;改名后 DROP 旧名 → 触发 CASCADE&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;MySQL 的 CASCADE 删除，其实&lt;strong&gt;绝大多数时候也不会真的删干净&lt;/strong&gt;——因为 DROP 会因关联记录被约束拦下。而 SQLite 这一套组合拳（改名传播 + DROP 级联）&lt;strong&gt;没有拦阻，直接删&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="救回来的三条命"&gt;救回来的三条命
&lt;/h2&gt;&lt;p&gt;这单没出人命，靠三件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;有备份&lt;/strong&gt;。迁移前刚打了 &lt;code&gt;cattype&lt;/code&gt; 那次重建的备份，恢复 &lt;code&gt;resources&lt;/code&gt; 235 行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;恢复时发现二次级联&lt;/strong&gt;。&lt;code&gt;DROP resources&lt;/code&gt; 又清了 &lt;code&gt;resource_channels&lt;/code&gt;，也是靠同一份备份恢复。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;审计脚本兜底&lt;/strong&gt;。迁移后不是只看表面，而是逐表核对行数，才确认两个表都受损、都恢复到位。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;数据完整性最后核对通过（唯一差异是运行期正常新增的访问日志）。&lt;/p&gt;
&lt;h2 id="教训已写成铁律"&gt;教训（已写成铁律）
&lt;/h2&gt;
 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;🔴 生产库迁移铁律（2026-08-26）&lt;/strong&gt;：涉及有外键的表做 ALTER/RENAME/DROP 前，必须先 &lt;code&gt;PRAGMA foreign_keys = OFF&lt;/code&gt;（迁移脚本内关，完成后 ON）。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;补几条更通用的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;SQLite 改有外键的表结构，先关外键。&lt;/strong&gt; &lt;code&gt;PRAGMA foreign_keys = OFF&lt;/code&gt; 再动手，做完开回来。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优先&amp;quot;不重建原表&amp;quot;的方式&lt;/strong&gt;。SQLite 的 &lt;code&gt;ALTER TABLE&lt;/code&gt; 能力有限（3.25+ 支持 &lt;code&gt;RENAME COLUMN&lt;/code&gt;、3.35+ 支持 &lt;code&gt;DROP COLUMN&lt;/code&gt;，但&lt;strong&gt;不支持 &lt;code&gt;ADD CONSTRAINT&lt;/code&gt;&lt;/strong&gt;），所以改 CHECK 约束往往只能重建——那就更要先关外键。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;任何迁移前必须备份。&lt;/strong&gt; 今天能救回来全因为那行 &lt;code&gt;cp&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;迁移后必须全表行数核对。&lt;/strong&gt; 别只看&amp;quot;没报错&amp;quot;——级联删除往往是&amp;quot;零报错的静默灾难&amp;quot;，靠的是数量审计揪出来。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;换一种姿势问自己&lt;/strong&gt;：在 SQLite 上，&amp;ldquo;重建表&amp;quot;这种在 MySQL 顺手的习惯，恰恰是最危险的路径。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="一个更大的背景"&gt;一个更大的背景
&lt;/h2&gt;&lt;p&gt;这件事只是今天一整天 win-ippt 重构里的一环——同一个会话里，还做了 6 分类接口重构（&lt;code&gt;boot_disk&lt;/code&gt; 正是这次迁移要支持的）、CDN 本地化提速 3000 倍、设备快照补全、减法重构归档死代码、Shlink 密钥轮换……功能链路很长。&lt;/p&gt;
&lt;p&gt;但&lt;strong&gt;唯独这一条值得单独立传&lt;/strong&gt;：它是今天唯一「如果没备份就真的丢数据」的时刻，也是唯一一条升级成了铁律的教训。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;迁移工具越&amp;quot;智能&amp;rdquo;，越要警惕它替你做的决定。SQLite 的 RENAME 很贴心地帮我把外键也&amp;quot;改名&amp;quot;了——贴心到把我的数据一起删了。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;本文已脱敏，不含真实域名、密钥或用户数据。&lt;/em&gt;&lt;/p&gt;</description></item></channel></rss>