SQLite WITHOUT ROWID 主键:同样的建表字段,为什么空值规则变了
主键写在纸上,约束还要看表的类型
把普通表的建表语句末尾加上 WITHOUT ROWID,字段名称没有变化,旧数据却可能导入失败。最容易忽略的原因是主键中的空值:SQLite 为兼容历史行为,普通、非 STRICT 的行号表里,未另外声明 NOT NULL 的文本主键以及一般复合主键,可以存入 NULL。这个结论有明确范围,不能简化成“SQLite 的所有主键都允许空值”。
WITHOUT ROWID 表对主键的每一列执行非空约束。复合主键中只要一个分量为空,也会被拒绝。因此迁移时需要检查整个主键元组,不能只统计“全部字段同时为空”的记录。是否唯一与是否非空是两个问题,不能因为重复值测试通过就认为空值也已经排除。
配图为 AI 生成的概念插画:空槽与完整排列对照主键约束,不表示数据库实际存储结构。
在同一个内存数据库中对照
下面的 Python 程序只使用标准库,不连接业务数据库。两张复合主键表字段完全相同,只改变表的类型;再增加一对 INTEGER PRIMARY KEY 表,观察省略编号时的差异。错误分支只捕获预期的完整性异常,让每项实验都继续执行。
import sqlite3
con = sqlite3.connect(":memory:")
print("SQLite", sqlite3.sqlite_version)
for table, suffix in (("ordinary", ""), ("compact", " WITHOUT ROWID")):
con.execute(f"CREATE TABLE {table}(site TEXT, code TEXT, PRIMARY KEY(site, code)){suffix}")
for values in ((None, "A"), ("east", None)):
try:
con.execute(f"INSERT INTO {table} VALUES (?, ?)", values)
print(table, values, "accepted")
except sqlite3.IntegrityError as exc:
print(table, values, type(exc).__name__)
con.execute("CREATE TABLE ids(id INTEGER PRIMARY KEY, note TEXT)")
con.execute("CREATE TABLE compact_ids(id INTEGER PRIMARY KEY, note TEXT) WITHOUT ROWID")
con.execute("INSERT INTO ids(note) VALUES ('auto')")
print("ordinary id:", con.execute("SELECT id FROM ids").fetchone()[0])
try:
con.execute("INSERT INTO compact_ids(note) VALUES ('missing')")
except sqlite3.IntegrityError as exc:
print("compact missing id:", type(exc).__name__)
con.execute("INSERT INTO compact_ids VALUES (7, 'explicit')")
print("compact id:", con.execute("SELECT id FROM compact_ids").fetchone()[0])
con.close()普通表成功保存两条主键中含空值的记录;另一张表的两个插入都报告 IntegrityError。这里分别让第一列和第二列为空,证明限制作用于主键的每个列。若把相同的非空键值重复插入,两类表都会受到主键唯一性约束,不能把本例理解成普通表没有约束。
整数主键还有一层特殊行为
普通行号表中,示例的 INTEGER PRIMARY KEY 是 rowid 的别名。省略该字段时,SQLite 会自动选择整数,所以第一条记录显示编号一。没有 rowid 的表不再拥有这层别名机制:同样省略编号,会因为主键不能为空而失败;显式提供七则可以插入。这是行为变化,不是插入语法发生了变化。
需要保留类型拼写的精确性,不能把示例里的 INTEGER 改成 INT 后仍期待行号别名效果。WITHOUT ROWID 表也不支持 AUTOINCREMENT;添加该关键字不能恢复自动编号。如果应用依赖数据库生成编号,应先确认表设计是否应该继续使用普通整数主键。
迁移前把检查落到实际数据上
对已有复合键表,可以先用各主键列的 IS NULL 条件以 OR 连接,找出任意分量为空的记录,再决定补齐、隔离还是拒绝迁移。普通表也可以显式添加非空约束来表达业务要求,并不需要为了禁止空值就改变表的类型。
测试迁移时还要覆盖省略主键、显式传入空值、完整合法主键和重复主键四类输入。不要把空字符串当成 NULL:它仍是一个实际文本值,是否允许应由业务规则另行决定。确认约束符合预期之后,再用真实数据量衡量空间与查询效果,不能从本例推出性能必然变快。
另外,迁移脚本最好在独立副本中完成新表建立、数据导入与核对,再决定切换。若导入因为空主键失败,不应直接用同一个固定值替换全部空值,否则可能把原来不同记录变成重复键。应先确定哪些字段能够稳定标识同一条业务记录,再制定补值规则。
若已有表另外声明了非空约束,空值插入本来就会失败,应先核对实际建表语句。仅凭应用界面上标出的“主键”两个字,无法判断这次迁移是否真的改变了允许写入的数据集合。


