SQLite 的列类型并不像 MySQL 或 PostgreSQL 那样具有强制约束力。一个声明为 INTEGER 的列可以接受整数,也可以接受无法转换的文本,结果就是同一列里出现多种存储类。这个特性在早期被当作灵活优势,但到了需要严格数据质量的应用中,问题开始增多。STRICT 表模式给 SQLite 增加了一条更严格的路径,建表时声明 STRICT,后续写入就会按列类型做校验。

一、动态类型与类型亲和性到底如何工作
SQLite 内部使用五种存储类:NULL、INTEGER、REAL、TEXT 和 BLOB。建表时写的 VARCHAR、INT、DATETIME 等类型名,最终会被映射为 TEXT、INTEGER、NUMERIC、REAL 或 BLOB 亲和性。列亲和性决定写入数据时是否尝试转换,而不是决定数据必须以某种形式存储。比如声明为 TEXT 的列,写入整数 100 时通常会转成文本存储;声明为 INTEGER 的列,写入能被解析的数字字符串时会转成整数,但写入无法解析的字符串时则保留为文本。
这种机制意味着同一列可以出现不同存储类。下面这段 SQL 可以直观看到动态类型的行为。
CREATE TABLE loose_log (
id INTEGER PRIMARY KEY,
payload TEXT,
count INTEGER
);
INSERT INTO loose_log (payload, count) VALUES ('ok', 10);
INSERT INTO loose_log (payload, count) VALUES (123, 'abc');
SELECT id, typeof(payload), typeof(count) FROM loose_log;
第一条记录中 count 的存储类是 integer,第二条记录中 count 的存储类则是 text。查询时需要额外判断 typeof 的结果,否则在比较、排序、聚合时可能得到不一致的结果。这类问题在原型阶段不容易暴露,因为写入路径非常宽松,但数据进入生产后,清理成本通常远高于写入时多做一次校验。
动态类型的价值在于处理半结构化数据和快速迭代场景,例如日志收集、动态表单、外部导入数据等。对于字段内容不确定的情况,它允许业务层先落库再逐步规范化。不过一旦应用逻辑开始依赖某些类型假设,例如 count 必须做数值运算,那么动态类型就从便利变成隐患。
二、STRICT 表模式有哪些硬性限制
启用 STRICT 模式只需要在 CREATE TABLE 语句末尾追加 STRICT 关键字,位置在右括号之后。下面是一个完整的建表示例。
CREATE TABLE strict_log (
id INTEGER PRIMARY KEY,
payload TEXT NOT NULL,
count INTEGER NOT NULL DEFAULT 0,
price REAL NOT NULL DEFAULT 0.0,
raw BLOB,
extra ANY
) STRICT;
STRICT 表的类型名只允许使用 INT、INTEGER、REAL、TEXT、BLOB 和 ANY。如果写成 VARCHAR(255) 或 DATETIME,建表时就会直接报错,提示未知数据类型。写入时也会按存储类做强制校验:TEXT 列只接受文本和 NULL,INTEGER 列只接受整数,BLOB 列只接受二进制,ANY 列不做限制。REAL 列稍微特殊一点,可以接受整数和浮点数,因为整数可以无损转换为浮点。
用插入语句测试可以更清楚地看到差异。下面两条 INSERT 的第一条会成功,第二条会因为 count 收到文本而失败。
INSERT INTO strict_log (payload, count, price) VALUES ('ok', 10, 9.99);
INSERT INTO strict_log (payload, count, price) VALUES ('bad', 'ten', 9.99);
第二条会产生类似 cannot store TEXT value in INTEGER column strict_log.count 的错误。这个失败在动态类型表中不会出现,因为动态类型会尝试转换,转换不了就把文本原样存进去。STRICT 模式从数据库层拦截了错误数据,减少了应用层重复校验的代码。
STRICT 还有几个需要特别注意的限制。旧版 SQLite 3.37 以下无法打开包含 STRICT 表的数据库,部署环境存在旧版本时不能直接使用。ALTER TABLE 也不能给现有表动态添加 STRICT,必须重建表。STRICT 只约束写入路径,对查询和已有数据不做任何修正,如果旧数据里已经混合了多种类型,启用新表后仍需要单独清洗。
三、旧库迁移与 ORM 兼容怎么做
已经使用动态类型的表要切换到 STRICT,通常需要走创建新表、清洗数据、导入、重命名的流程。先把异常数据找出来,再决定是修正、丢弃还是转成默认值。下面是一个迁移示例。
-- 先找出不符合类型的数据
SELECT rowid, * FROM loose_log
WHERE typeof(count) != 'integer'
OR typeof(payload) != 'text';
-- 处理异常数据后创建 STRICT 表
CREATE TABLE loose_log_new (
id INTEGER PRIMARY KEY,
payload TEXT NOT NULL,
count INTEGER NOT NULL DEFAULT 0
) STRICT;
INSERT INTO loose_log_new (id, payload, count)
SELECT id,
CASE WHEN typeof(payload) = 'text' THEN payload ELSE CAST(payload AS TEXT) END,
CASE WHEN typeof(count) = 'integer' THEN count ELSE CAST(count AS INTEGER) END
FROM loose_log;
DROP TABLE loose_log;
ALTER TABLE loose_log_new RENAME TO loose_log;
迁移前需要检查原表上是否有触发器、视图、外键或索引。这些对象不会自动跟随新表,DROP 旧表后需要重新创建。对于数据量较大的表,建议在事务中执行迁移,并在测试库先完整跑一遍,确认约束冲突和数据丢失范围可控。
ORM 框架的兼容性也值得关注。很多框架依赖 SQLite 的宽松类型处理,比如把字符串写入整数字段、用 TEXT 列存任意 JSON 字符串。启用 STRICT 后,如果模型字段类型与列声明类型不完全一致,写入就可能直接报错。使用 SQLAlchemy、Django 等项目时,应先用迁移脚本统一列类型,再调整模型字段。JSON 数据建议明确存为 TEXT,布尔值可以继续使用 INTEGER 列存 0 和 1,但不要声明成 BOOLEAN 类型,因为 STRICT 表不识别这个类型名。
四、性能影响与选择建议
STRICT 模式的类型检查发生在写入阶段,数据库需要判断每个值的存储类并处理不符合规则的情况,这会增加少量 CPU 开销。对于普通事务和单条插入,开销基本可以忽略。真正能观察到差异的是大批量导入,例如一次导入百万行数据时,多出的类型判断可能带来几个百分点的耗时增长。但存储格式没有变化,查询性能、索引效率和动态类型表保持一致,不会因为启用 STRICT 而变慢。
新项目如果数据由应用层控制,且希望数据库层提供兜底约束,建议在初始建表时直接使用 STRICT。这样成本最低,也不需要后续迁移。原型开发、日志存储、外部数据临时落地等场景,可以继续使用动态类型,或者只对关键列添加 CHECK 约束,例如 CHECK (typeof(count) = 'integer'),在保留灵活性的同时限制关键字段的类型。
老项目不要一次性全面迁移,风险较大。可以先对新表启用 STRICT,观察一段时间后再逐步处理旧表。迁移顺序可以按表的重要程度和数据质量来排,优先处理财务、订单、库存等对类型敏感的表。至于部署环境,上线前必须确认 SQLite 的最低版本大于等于 3.37,可以通过 SELECT sqlite_version(); 快速检查,否则驱动连接数据库时可能会直接失败。
SQLite STRICT模式动态类型数据类型约束修改时间:2026-09-28 08:14:22