给已有表新增字段是日常开发中频率很高的操作,但在不同的数据库里,这件事的成本可能天差地别。在MySQL或者PostgreSQL这种传统数据库中,对一个千万行的表执行ALTER TABLE ADD COLUMN,某些情况下会触发表重建,耗时可能以分钟甚至小时计。而SQLite长期以来执行同样的语句几乎是瞬间完成的,3.35版本又针对这个语句做了一轮重要改进,让它在更多场景下保持高效。这篇文章就来聊聊这些改进背后的机制。

先理解SQLite为什么能做到秒加字段
要理解3.35版本的改进,得先明白SQLite的存储模型。SQLite把整个数据库放在一个文件里,表数据存放在B树结构中,而表结构定义则记录在sqlite_master这个系统表里。当你执行ALTER TABLE ADD COLUMN时,SQLite并不需要去改动任何一行实际数据,它做的事情仅仅是修改sqlite_master中存储的CREATE TABLE语句文本,在末尾追加一个新的字段定义。
这种设计的代价极低。假设一张表有一千万行数据,新加字段的操作复杂度与行数完全无关,只和schema文本长度有关。已有的数据行在磁盘上保持原样,查询时如果发现某行记录的字段数量少于表定义的字段数量,缺失的字段就会自动用默认值补齐。这也是为什么SQLite的加字段操作历来被人称道——它本质上是一次schema层面的元数据修改,而不是数据层面的重写。
不过老实现里有一个隐藏的坑:如果新增字段带DEFAULT非空值,旧版本会在执行语句时把默认值实际写回每一行数据。表越大,这个操作越慢,原本的元数据修改优势就打了折扣。3.35版本的改进正是围绕这一点展开的。
3.35版本的核心改进:默认值不再回填数据行
3.35之前的行为是这样的:执行ALTER TABLE t ADD COLUMN c TEXT DEFAULT 'x',SQLite会遍历表中所有已存在的行,把默认值'x'物理地写入每一行的记录中。对于百万行级别的表,这个过程可能耗费可观的时间,并且会产生大量脏页,导致数据库文件体积膨胀,WAL模式下还会生成大的日志文件。
3.35版本改变了策略:带非空默认值的新增字段,默认值只记录在schema定义中,不再回填到已有数据行。读取旧数据时,引擎在解码记录的阶段动态补上默认值,写入和更新时才真正落盘。来看一个对比验证的例子:
-- 3.35及以上版本执行,秒级完成 CREATE TABLE big_table(id INTEGER PRIMARY KEY, val TEXT); -- 假设已插入一千万行数据 ALTER TABLE big_table ADD COLUMN status TEXT DEFAULT 'active'; -- 查询旧行,status自动返回'active' SELECT id, status FROM big_table LIMIT 3; -- 默认值不占用已有行的存储空间 -- 只有UPDATE该行后,值才被物理写入
这个改动的直接收益有三点。第一,加字段操作的耗时与表大小彻底解耦,再大的表也是常数时间。第二,不再产生回填带来的写放大,WAL日志体积保持平稳。第三,避免了回填过程中可能出现的锁竞争,对移动端应用的可用性特别友好。
需要留意的使用限制
虽然机制上优化了,但ALTER TABLE ADD COLUMN的约束条件依然存在,这些限制与版本无关,只是在使用新特性时更容易被忽略。首先,新增字段不能带PRIMARY KEY或UNIQUE约束,因为已有数据无法满足唯一性要求。其次,如果带NOT NULL,则必须同时给出非NULL的默认值,否则已有行无法通过约束检查。
另外有一个细节值得注意:如果默认值是CURRENT_TIMESTAMP、CURRENT_DATE或CURRENT_TIME这类动态时间函数,ADD COLUMN操作依然不被允许。原因在于这些值在插入时求值,如果允许使用,已有行补齐出来的值语义上是不确定的。遇到这种需求,常见做法是先加一个允许NULL的字段,再用UPDATE分批回填,最后重建表加上约束。
还需要提醒的是,虽然新版本读取旧行时会动态补默认值,但这个补值发生在记录解码阶段,理论上有一点点CPU开销。如果业务确定要对全表做时间上集中的一次性回填,主动执行一次UPDATE批量更新,把值物化到数据行中,反而可能对后续高频查询更友好。这是个权衡问题,不是绝对结论。
与其他表结构变更操作的对比
理解ADD COLUMN的改进后,不妨把它放回ALTER TABLE的整个能力版图中看。SQLite的ALTER TABLE长期只支持改表名和加字段两个操作,从3.25版本开始增加了RENAME COLUMN,3.35又带来了DROP COLUMN。其中DROP COLUMN的实现思路与ADD COLUMN相反,它需要实际处理数据,删除字段的操作成本会随表大小增长,这一点要提前有预期。
像修改字段类型、调整字段顺序这类操作,SQLite至今没有直接支持,官方推荐的通用套路是:创建新结构的表,把数据复制过去,删除旧表,重命名新表。整个过程最好包在一个事务里,保证原子性:
BEGIN; CREATE TABLE new_table( id INTEGER PRIMARY KEY, val TEXT, status TEXT DEFAULT 'active' ); INSERT INTO new_table(id, val) SELECT id, val FROM old_table; DROP TABLE old_table; ALTER TABLE new_table RENAME TO old_table; COMMIT;
所以做表结构演进规划时,一个实用建议是:能用ADD COLUMN解决的,尽量用ADD COLUMN,享受元数据级别的性能优势;确实需要改类型或删字段时,评估好数据迁移的耗时窗口,在应用侧做好版本兼容处理,比如通过PRAGMA table_info检查实际表结构来决定代码分支。
总结一下,SQLite 3.35把带默认值的加字段操作从数据回填改成了schema记录加动态补值,让这个高频操作真正做到了与表大小无关的常数耗时。对于在移动端、嵌入式设备或者桌面应用中使用SQLite做本地存储的项目,这个改进配合合理的表结构演进策略,能显著降低升级版本时的迁移成本。
SQLiteALTER TABLE数据库修改时间:2026-09-03 17:05:02