想像一個資源回收桶,你明明在上面貼了「寶特瓶專用」的標籤,結果別人丟了一張紙片進去,它居然默默收下,連一聲抗議都沒有?
這就是開發者第一次遇見 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 鋪好後路。