在SQLite里,你可以把文本塞进一个声明为INTEGER的列,数据库不会报错,照单全收。这不是bug,是SQLite从诞生起的设计:类型属于值,不属于列。开发者Evan Hahn写了篇博客,建议大家给表加一个词——STRICT——把这个口子堵上。
问题是,这个关键字早就存在了。
STRICT到底改了什么
写法很简单,建表语句末尾加STRICT。加了之后,SQLite从"来者不拒"变成"较真到底":
- 插入
'garbage'到INTEGER列,直接报错,不再默默存成怪东西 - 建表时列类型只能写INT、INTEGER、REAL、TEXT、BLOB、ANY六种,写
DATETIME、JSON、UUID这些SQLite本来就不支持的类型名,以前能建表成功(纯属放水),现在直接报错
这两点确实解决了SQLite最被诟病的毛病:类型声明形同虚设。Evan Hahn的示范代码干净利落,读完你会觉得,这功能早该是默认的。
它不是新功能。STRICT在SQLite 3.37.0版本(2021年11月)就已经加入,快四年了,依然不是默认行为,依然要手动在每张表后面加这个词才生效。一个能挡住低级错误的开关,推出四年还得靠开发者自己记得开,这本身就说明了问题——要么普及成本比想象中高,要么大多数人压根不知道有这回事。
严格里的三个不严格
Evan Hahn提到了STRICT保留的一个逃生舱:ANY类型,声明成ANY的列什么都能塞,和不加STRICT没区别。他自己说"没找到用途",但这恰恰是需要警惕的地方——如果一张STRICT表里大半列都用了ANY,那这张表和普通表在实质上没什么两样,STRICT只是心理安慰。
- 提醒.STRICT不会自动打开外键约束,
PRAGMA foreign_keys = ON还是得单独执行。父子表类型不一致时,STRICT反而更容易在写入阶段就报错暴露,这算是个隐性收益,但前提是你记得开外键检查。 - 生成列(generated columns)在STRICT表里同样受类型名单约束,声明类型必须是那六种之一,而且生成表达式算出来的结果类型也得和声明匹配,否则插入或迁移时会炸。这一层坑,原文完全没提。
迁移这道坎:改不了,只能重建
STRICT最大的实操摩擦,是没法给已有表直接开。SQLite不支持ALTER TABLE ... STRICT,只能新建一张STRICT表、把数据搬过去、删掉旧表、把新表改名。如果旧表里已经混进了脏数据——比如某个INTEGER列历史上被塞过文本——搬家那一步会直接报错,你得先清洗或者做类型转换。
这意味着"给现有生产库全面上STRICT"基本不现实,更现实的做法是"新表一律STRICT,老表维持原样",但这又会造成同一个数据库里两套校验标准并存,对后来的开发者反而是新的困惑源。
官方自己都不完全站这边
SQLite官方文档专门有一页叫"The Advantages Of Flexible Typing",论证宽松类型的合理性:纯键值存储、存杂项属性、原样导入一份脏CSV又不想丢数据——这些场景里,强类型反而添乱。Evan Hahn引用了这页,承认官方立场和他不一致。
把这事放进整个数据库生态里看,会更清楚STRICT的分量。MySQL现代版本默认SQL模式里已经包含严格事务表校验;PostgreSQL的类型转换体系一直被当作关系数据库类型正确性的参照标准;DuckDB干脆把自己定位成"强类型数据库系统",语义更接近Postgres。SQLite加了STRICT,只是让自己不那么例外,离Postgres那种domain、enum、原生JSONB级别的类型完整性,还差着一截。
严格是选项,不是天性,这才是SQLite这道题的真正难点。
新建表就该默认STRICT,这几乎没有争议成本。真正需要判断力的,是那些已经跑在生产环境里的老表——搬家的风险和留着不管的风险,得自己权衡,没有一劳永逸的开关。
