最近把一个项目的数据库从 MySQL(MariaDB)迁到了 SQLite 系(Cloudflare D1)。SQL 看着都差不多,实际迁起来方言差异一个接一个。全记录如下,希望能帮后来人省点时间。
1. 自增主键与返回值
-- MySQL
id INT AUTO_INCREMENT PRIMARY KEY
-- SQLite
id INTEGER PRIMARY KEY AUTOINCREMENT
代码层差异更隐蔽:
// mysql2
result.insertId
result.affectedRows
// D1 (SQLite)
result.meta.last_row_id
result.meta.changes
这种差异编译器帮不了你,只能全局搜索逐个改。
2. Upsert 语法
-- MySQL
INSERT INTO settings (...) VALUES (...)
ON DUPLICATE KEY UPDATE site_title = VALUES(site_title);
-- SQLite
INSERT INTO settings (...) VALUES (...)
ON CONFLICT(id) DO UPDATE SET site_title = excluded.site_title;
注意 SQLite 的 ON CONFLICT 必须指明冲突列,excluded. 前缀对应 MySQL 的 VALUES()。
3. 时间与时区
MySQL 连接串里可以配 timezone: '+08:00',驱动帮你转好。SQLite 没有时区概念,CURRENT_TIMESTAMP 一律是 UTC。
如果历史数据存的是北京时间字符串,两个选择:全量转 UTC(动数据),或者让新数据也存北京时间(动 schema):
created_at TEXT DEFAULT (datetime('now', '+8 hours'))
我选了后者——保持与历史数据一致,前端零改动。不优雅,但迁移的第一原则是别把能跑的东西改坏。
4. 反斜杠转义(最阴的一个)
MySQL 的字符串里 \n 是转义序列,SQLite 标准语法里反斜杠没有特殊含义。直接把 mysqldump 的 INSERT 拿去 SQLite 执行,所有 \' 全部炸掉。
我的做法:不走 dump 文件,用脚本从 MySQL 读出 JSON,再程序化生成 SQLite INSERT(单引号翻倍转义)。多一步,但字符串边界绝对干净。
5. 布尔值
MySQL 的 TINYINT(1) 到了 SQLite 就是 INTEGER,查询结果是 0/1 不是 true/false。前端如果有 === true 的严格比较会静默失败——又是一个不报错的坑。
总结
方言迁移没有魔法,就是把每一条 SQL、每一个结果字段的用法过一遍。数据量小的话,强烈建议迁完后逐表 COUNT + 抽样对比原库,字节级确认。