从 MySQL 到 SQLite:方言差异踩坑实录

insertId、ON DUPLICATE KEY、时区、反斜杠转义……一次数据库迁移中遇到的所有方言差异,一篇说完。

最近把一个项目的数据库从 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 + 抽样对比原库,字节级确认。