想象一个资源回收桶,你明明在上面贴了“宝特瓶专用”的标签,结果别人丢了一张纸片进去,它居然默默收下,连一声抗议都没有?
这就是开发者第一次遇见 SQLite 类型系统时的惊悚体验。
如果你习惯了 PostgreSQL 那种严格的海关作风(类型不对直接拒绝入境),SQLite 的随性程度可能会让你怀疑人生。
更恐怖的是,当你想 修改数据表结构 时,它会告诉你:
“数据表改不了,请盖一栋新房子,把家具搬过去,然后把旧房子炸掉。”
SQLite 底层只有 5 种存储类别
不管你在 CREATE TABLE 时宣告了什么华丽的类型名称(VARCHAR(255)、BIGINT、DECIMAL),SQLite 底层只认得这 5 种存储类别:
| 存储类别 | 说明 |
|---|---|
| NULL | 空值 |
| INTEGER | 整数(依数字大小自动占用 1 到 8 bytes) |
| REAL | 浮点数(固定 8 bytes) |
| TEXT | 字符串(默认 UTF-8 编码) |
| BLOB | 二进制数据(原封不动存储) |
你在字段上设置的类型,对 SQLite 来说 只是一个“建议”,而不是“强制规定”。
这叫做 “类型亲和性(Type Affinity)”,
SQLite会尝试把你的数据转成建议的类型,但如果转不了,它照样把原始数据塞进去,完全不报错。
你可以在 SQLite 宣告一个 age INTEGER 字段,然后塞入字符串 '永远的十八岁',它会欣然接受。
三个最容易踩坑的类型陷阱
陷阱 1:没有原生 Boolean
SQLite 没有布尔类型。True 和 False 只能用整数 1 和 0 代替。
当你用 Node.js 从 SQLite 捞出数据时,拿到的会是数字 1 或 0,不是 true 或 false。
如果你直接拿去做 if (user.is_admin === true) 的判断,永远不会成立。
陷阱 2:没有 Date/Time 类型
SQLite 没有日期时间类型。你只能把时间存成:
| 存储方式 | 范例 | 优缺点 |
|---|---|---|
| TEXT(ISO-8601 字符串) | '2026-05-19T18:00:00Z' |
最推荐,可读性高,未来搬到 PostgreSQL 可无缝转换 |
| INTEGER(Unix Timestamp) | 1747656000 |
占用空间小,但人类不可读 |
千万不要存像 2026/5/19 下午六点 这种自创格式,否则未来做数据迁移时会是一场灾难。
陷阱 3:字符串塞进整数字段不会报错
在 PostgreSQL,你把字符串塞进 INTEGER 字段会直接报错。但 SQLite 只会默默尝试转换,转不了就原样收下。
这意味着你的数据库里 可能悄悄混进了脏数据,直到某天你的程序因为拿到非预期的类型而崩溃,才会发现问题。
防御性编程:用严格的态度对待随性的数据库
面对 SQLite 的随性,在 Node.js 开发中必须建立起 严格的防御机制:
| 防御层级 | 工具 | 作用 |
|---|---|---|
| 编译期挡错 | TypeScript | 在代码阶段就挡下错误的类型 |
| API 入口验证 | Zod | 接收数据时严格验证(确认 age 一定是数字) |
| 底层类型转换 | Prisma / Drizzle ORM | 自动处理 SQLite 和 PostgreSQL 之间的类型差异 |
把 “数据验证的守门员” 从数据库层移到
Node.js应用层,这是同时驾驭 SQLite 开发速度与未来扩展性的关键策略。
使用 ORM 时,你只要在程序里宣告 type: 'boolean',ORM 存入 SQLite 时自动转成 1/0,拿出来时自动转回 true/false,完美遮蔽底层的类型差异。
ALTER TABLE 是半残的:什么能改、什么不能改
SQLite 对数据表结构的修改支持非常有限:
| 操作 | 是否支持 |
|---|---|
新增字段(ADD COLUMN) |
支持 |
重新命名字段(RENAME COLUMN) |
支持 |
删除字段(DROP COLUMN) |
支持 (较新版本) |
重新命名数据表(RENAME TO) |
支持 |
| 更改字段类型 | 不支持 |
增加/移除 UNIQUE、NOT NULL 限制 |
不支持 |
| 修改 Primary Key | 不支持 |
| 修改 Foreign Key | 不支持 |
一旦你需要做“不支持”的那些修改,
SQLite会要求你执行 “重建并搬移” 的策略。
重建搬移四步曲:SQLite 的数据表升级之道
既然无法直接改,官方建议的标准做法就是 盖新房子、搬家具、炸旧房子、贴新门牌:
| 步骤 | 动作 | 说明 |
|---|---|---|
| 1 | 建立新表 | CREATE TABLE users_new (...) 使用正确的结构 |
| 2 | 复制数据 | INSERT INTO users_new SELECT ... FROM users |
| 3 | 删除旧表 | DROP TABLE users |
| 4 | 改名 | ALTER TABLE users_new RENAME TO users |
这四个步骤必须 一气呵成,中途断电或程序出错就会造成数据丢失。
确保升级不丢失数据:两道安全防线
防线 1:物理防御,直接复制文件
SQLite 的本质就是一个文件。在执行任何结构变更前,先把 .db 文件复制一份当备份。
const fs = require('fs');
fs.copyFileSync('my_project.db', 'my_project_backup.db');
万一搞砸了,文件盖回去就瞬间复原。这是其他大型数据库做不到的优势。
防线 2:Transaction 包装,数据库的时光机
把搬家的步骤包在一个 Transaction 里面,只要其中一步失败,整个过程就会 自动还原(Rollback),当作没发生过。
const Database = require('better-sqlite3');
const db = new Database('my_project.db');
const migrateData = db.transaction(() => {
db.prepare(`
CREATE TABLE users_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
age INTEGER NOT NULL DEFAULT 18
)
`).run();
db.prepare(`
INSERT INTO users_new (id, name, age)
SELECT id, name, COALESCE(age, 18) FROM users
`).run();
db.prepare('DROP TABLE users').run();
db.prepare('ALTER TABLE users_new RENAME TO users').run();
});
try {
migrateData();
console.log('数据表升级成功');
} catch (error) {
console.error('升级失败,数据已安全还原:', error.message);
}
“备份文件 + Transaction 绑定” 就是你的数据库迁移的安全气囊。
用严格的态度驾驭随性的 SQLite
用 严格的应用层架构 来驾驭随性的
SQLite,享受它极速开发的优势,同时避开未来的技术债。
SQLite 的类型系统很随性,ALTER TABLE 也有诸多限制。
但只要在 Node.js 端做好 TypeScript 类型检查 + Zod 验证 + ORM 抽象层,再搭配 物理备份 + Transaction 的安全策略,你就能安心享受 SQLite 带来的开发效率,同时为未来可能搬移至 PostgreSQL 铺好后路。