D1 Time Travel 與備份:時間點還原與災難復原 | Cloudflare 完整教學
上一篇《D1 Sessions API 與讀取複本》,我們讓全球讀取又快又不失一致。但資料庫的日常從來不缺意外——手滑一個
DROP TABLE、跑錯一條沒加WHERE的UPDATE、一個壞掉的 migration 把 schema 搞砸。這時你需要的是「時光倒流」。Cloudflare D1 內建的 Time Travel(時間點還原) 永遠啟用、免費,能把資料庫還原到過去 30 天內任一時刻;搭配wrangler d1 export/import的匯出匯入與 R2 長期封存,就能建立起完整的備份、還原與災難復原防線。這篇我們把 Time Travel 的info/restore、書籤撤銷、匯出匯入,到災難復原流程一次講透,為 D1 系列收尾。
前言
Time Travel 是 D1 內建的時間點還原(Point-in-Time Recovery,PITR) 機制:它會持續記錄資料庫的變更歷史,讓你能把整個資料庫還原(restore) 到過去某個時刻的狀態——免費方案保留 7 天、付費方案保留 30 天,而且永遠啟用、不額外收費,你什麼設定都不用做。而 匯出/匯入(Export/Import) 則是另一條路:用 wrangler d1 export 把資料庫倒成一份可攜的 .sql 檔,做冷備份與長期封存。
打個比方:想像你在寫一份重要文件。Time Travel 就像編輯器的版本歷史——它在背景自動幫你存每個版本,你隨時能「還原到 30 分鐘前」,不必手動存檔,但這份歷史只保留最近一段時間。而 wrangler d1 export 就像另存新檔到隨身碟:你主動把當下的完整內容存成一份獨立檔案,想放多久就放多久、想搬到哪台電腦都行,但它只是那個「按下存檔那一刻」的快照,不會自動更新。真正穩健的備份策略,是兩者並用:平時靠版本歷史應付手滑,再定期另存一份到隨身碟做長期保險。
這篇聚焦 D1 的 Time Travel 與備份,承接前面幾篇的 D1 知識,為整個 D1 段落收尾。讀完你會掌握:
- Time Travel 的運作與限制——PITR 如何用書籤(Bookmark) 標記時間點、7/30 天保留期、還原為何是「破壞性原地覆蓋」
time-travel info/restore實作——用時間戳或書籤查詢與還原、如何用「還原前的書籤」撤銷誤還原- 匯出/匯入與長期備份——
wrangler d1 export/import、結合 R2 與 Cron Trigger 做超過 30 天的冷備份 - 災難復原與回滾思維——誤刪、壞掉的遷移該怎麼一步步救回,以及 Time Travel 與 migrations 搭配的回滾心法
核心概念
什麼是 Time Travel?時間點還原(PITR)的運作原理
Time Travel 是 D1 對每個資料庫永遠啟用的備份能力。它的核心不是「定時拍一張完整快照」,而是持續記錄資料庫狀態的變更歷史,並用一個可排序的字串識別符——書籤(Bookmark)——來標記「資料庫在某個瞬間的狀態」。因為歷史是連續記錄的,你能還原的不只是幾個離散的備份點,而是保留期內的任一時刻(可精確到秒),這正是「時間點還原(Point-in-Time Recovery)」的精髓。
寫入歷史(連續記錄,非離散快照)
│
├── T-30天 ────────────────────────────── 保留邊界(付費方案)
│ (更早的歷史已被丟棄,無法還原)
│
├── T-3天 誤跑了一條壞掉的 UPDATE
│ ↑ 你想還原到「這之前」的狀態
│
├── T-1天 一切還正常 ← 理想還原目標
│
└── T-0(現在) ← 當前書籤(current bookmark)
兩個關鍵設計要記牢:
- 保留期有限:免費方案(Free)保留 7 天、付費方案(Workers Paid)保留 30 天。超過保留期的歷史會被丟棄,想還原也還原不了。這條邊界會每天往前滾動——今天能還原到 30 天前,明天最舊就只能到 29 天前的那個點。
- 書籤即狀態:每個時間點對應一個書籤,還原時你可以用「時間戳(timestamp)」讓 D1 幫你找對應書籤,也可以直接指定「書籤字串」精準還原。
還原是「破壞性原地覆蓋」——最重要的一件事
這是整篇最需要放在心上的觀念:還原(restore)是破壞性(destructive)的原地覆蓋操作。當你 restore 到某個時間點,D1 會把整個資料庫的當前狀態,覆蓋成那個時間點的狀態:
- 整庫還原,不支援部分還原:你無法只還原某一張表、某幾列。要嘛整個資料庫一起回到過去,要嘛都不動。
- 會抹掉之後的寫入:從還原目標時間點到「現在」之間的所有寫入都會消失。
- 進行中的查詢會被取消:還原期間飛行中(in-flight)的查詢會被中斷。
聽起來很可怕,但有一個關鍵的安全網:還原這個動作本身,也會被 Time Travel 記錄。也就是說,「還原前」的那個狀態同樣有一個書籤。只要你在還原前先記下當前書籤,萬一還原錯了,就能再 restore 回那個書籤、把整件事撤銷(undo)。這讓「破壞性」變得可控——前提是你養成還原前先記書籤的習慣。
關鍵術語速覽
| 術語 | 一句話定義 |
|---|---|
| Time Travel | D1 內建、永遠啟用的時間點還原機制,免費 7 天 / 付費 30 天 |
| PITR(Point-in-Time Recovery) | 時間點還原,能還原到保留期內任一時刻(精確到秒) |
| Bookmark(書籤) | 代表資料庫某瞬間狀態的可排序字串,還原的精準座標 |
| Restore(還原) | 把整個資料庫原地覆蓋成過去某狀態的破壞性操作 |
| Export / Import | 用 wrangler d1 export/import 匯出匯入 .sql,做冷備份與搬遷 |
| 保留期(Retention) | 可還原的時間視窗,免費 7 天、付費 30 天,每天往前滾動 |
實作範例
以下 CLI 皆可直接執行(把 production-db 換成你的資料庫名稱)。建議先確認 Wrangler 版本夠新(Time Travel 需 v3.4.0 以上),且資料庫在 D1 生產後端。
1. 前置確認:版本與資料庫狀態
# 確認 Wrangler 版本(Time Travel 需 v3.4.0 以上)
wrangler --version
# 查看資料庫基本資訊(大小、版本等)
wrangler d1 info production-db
2. time-travel info:查詢時間點對應的書籤
info 的用途是「查」——查出某個時間點對應的書籤,或查當前狀態的書籤。它只是查詢,不會改動任何資料,可以安心多跑幾次:
# 查詢「當前」資料庫狀態的書籤(還原前務必先記下這個!)
wrangler d1 time-travel info production-db
# 輸出範例:
# 🚧 Time Traveling...
# ⚠️ Timestamp '2026-08-22T09:15:00Z' corresponds to bookmark '00000085-00000000-00004c1e-...'
# 查詢「特定時間點」對應的書籤(ISO 8601 格式)
wrangler d1 time-travel info production-db \
--timestamp="2026-08-21T10:00:00Z"
# 也可以用 Unix timestamp(秒)
wrangler d1 time-travel info production-db \
--timestamp=1755770400
心法:任何 restore 之前,先跑一次不帶 --timestamp 的 info,把「當前書籤」抄下來——這就是你的後悔藥。
3. time-travel restore:還原到過去某一刻
確認好目標時間點後,用 restore 執行還原。可用時間戳、也可用書籤:
# ⚠️ 破壞性操作!會把整個資料庫覆蓋成指定時間點的狀態
# 方式一:用時間戳還原(讓 D1 自動對應到最接近的書籤)
wrangler d1 time-travel restore production-db \
--timestamp="2026-08-21T10:00:00Z"
# 方式二:用書籤精準還原(座標最明確,推薦)
wrangler d1 time-travel restore production-db \
--bookmark="00000082-00000000-00004c1e-xxxxxxxxxxxxxxxx"
# 輸出範例:
# 🚧 Restoring database production-db to bookmark 00000082-...
# ⚠️ This will overwrite all data in your database. Are you sure? (y/N)
# ✅ Database restored back to bookmark 00000082-...
4. 用「還原前的書籤」撤銷誤還原(undo)
這是把「破壞性」變可控的關鍵。假設你剛才還原到了錯的時間點,只要之前有記下還原前的當前書籤,就能一鍵撤銷:
# 情境:你在 restore 前先跑過 info,記下了當前書籤 00000085-...
# 還原後發現選錯時間點 → 用那個書籤把資料庫「撤銷」回還原前
wrangler d1 time-travel restore production-db \
--bookmark="00000085-00000000-00004c1e-xxxxxxxxxxxxxxxx"
# 資料庫回到還原前的狀態,誤還原被撤銷
注意頻率限制:每個資料庫每 10 分鐘最多 10 次還原操作。慌亂中別連續狂按,先想清楚要還原到哪個書籤再動手。
5. wrangler d1 export:匯出成 .sql 做冷備份
export 把資料庫倒成一份可攜的 SQL 檔——這是 Time Travel 之外的第二道防線,尤其用於超過 30 天的長期封存與異地備份:
# 完整匯出(schema + 資料),遠端生產資料庫
wrangler d1 export production-db --remote --output=./backup.sql
# 只匯出資料(不含 schema),方便灌進已有 schema 的資料庫
wrangler d1 export production-db --remote --no-schema --output=./data-only.sql
# 只匯出 schema(不含資料),用於版本控管結構
wrangler d1 export production-db --remote --no-data --output=./schema-only.sql
# 匯出本地開發資料庫
wrangler d1 export production-db --local --output=./local-backup.sql
匯出注意事項:超大型資料庫(>1GB)可能逾時,建議分表匯出;大型 BLOB 二進位資料可能無法正確匯出,建議改存 R2;匯出是非原子操作,期間可能有新寫入,若要精確一致,可在低流量時段執行。
6. wrangler d1 import:把備份灌回資料庫
匯出的 .sql 可以用 import 灌回——用於環境搬遷、災難復原,或把生產資料拉到本地測試:
# 把備份 SQL 匯入資料庫(整份 schema + 資料)
wrangler d1 import production-db --remote --file=./backup.sql
# 匯入到本地開發環境(常見:用生產快照在本地重現問題)
wrangler d1 import staging-db --local --file=./backup.sql
超大檔案匯入若擔心逾時,可以先切檔再依序匯入:
# 先把大 SQL 切成每 10000 行一塊
split -l 10000 backup.sql chunk_
# 依序匯入每一塊
for f in chunk_*; do
wrangler d1 import production-db --remote --file="$f"
done
7. 結合 R2 + Cron:超過 30 天的長期備份
Time Travel 最多保留 30 天,若合規或稽核需要更久,就用 Cron Trigger 定期把匯出的備份存進 R2。以下是 Worker 排程備份的骨架:
import type {
D1Database,
R2Bucket,
ScheduledController,
ExecutionContext,
} from "@cloudflare/workers-types";
interface Env {
DB: D1Database;
BACKUP_BUCKET: R2Bucket; // 綁定一個 R2 bucket 存備份
}
export default {
// wrangler.jsonc 的 triggers.crons 設定排程(例如每天 03:00 UTC)
async scheduled(_controller: ScheduledController, env: Env, ctx: ExecutionContext) {
ctx.waitUntil(backupToR2(env));
},
};
async function backupToR2(env: Env): Promise<void> {
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
const key = `d1-backups/backup-${stamp}.sql`;
// 小型資料庫可在 Worker 內直接組出 SQL 快照存進 R2;
// 大型資料庫建議改在 CI/CD 用 `wrangler d1 export` 後再上傳 R2。
const tables = await env.DB
.prepare(
"SELECT name FROM sqlite_master WHERE type='table' " +
"AND name NOT LIKE 'sqlite_%' AND name != 'd1_migrations'",
)
.all<{ name: string }>();
const lines: string[] = [`-- D1 backup: ${new Date().toISOString()}`, "PRAGMA foreign_keys=OFF;", ""];
for (const { name } of tables.results) {
const rows = await env.DB.prepare(`SELECT * FROM ${name}`).all();
for (const row of rows.results) {
const cols = Object.keys(row);
const vals = cols
.map((c) => {
const v = (row as Record<string, unknown>)[c];
if (v === null) return "NULL";
if (typeof v === "number") return String(v);
return `'${String(v).replace(/'/g, "''")}'`; // 轉義單引號
})
.join(", ");
lines.push(`INSERT INTO ${name} (${cols.join(", ")}) VALUES (${vals});`);
}
}
lines.push("PRAGMA foreign_keys=ON;");
// 存入 R2,想保留多久就保留多久(可再搭配 R2 lifecycle 規則自動清理)
await env.BACKUP_BUCKET.put(key, lines.join("\n"), {
httpMetadata: { contentType: "text/plain" },
});
console.log(`備份完成:${key}`);
}
搭配 wrangler.jsonc 的排程設定:
{
"triggers": {
"crons": ["0 3 * * *"] // 每天 UTC 03:00 執行一次備份
}
}
如此一來,Time Travel 顧近 30 天的細粒度還原,R2 冷備份 顧超過 30 天的長期封存,兩層防護俱全。
常見錯誤與最佳實踐
坑一:以為 Time Travel 能無限回溯,結果超過保留期救不回來。
最傷的誤解:把 Time Travel 當成永久備份。事實是——免費 7 天、付費 30 天,而且邊界每天往前滾動。三個月前誤刪的資料,Time Travel 一點忙都幫不上。更糟的是,很多人是「事發很久後」才發現資料不對,這時可還原的最舊時間點可能已經滾過事發點了。
# ❌ 錯誤心態:反正 Time Travel 會一直留著,不用另外備份
# ✅ 正確:需要 30 天以上的保存,務必用 export 定期冷備份到 R2
wrangler d1 export production-db --remote --output=./backup-$(date +%F).sql
# 再上傳到 R2 或版控;搭配 Cron 自動化(見前面範例)
坑二:還原前沒記下當前書籤,還原錯了無法撤銷。
還原是破壞性的,而「後悔藥」正是還原前的那個書籤。跳過 info 直接 restore,一旦選錯時間點,你就失去了乾淨撤銷的座標:
# ❌ 錯誤:沒查當前書籤就直接還原,還原錯了無從撤銷
wrangler d1 time-travel restore production-db --timestamp="2026-08-10T00:00:00Z"
# ✅ 正確:先 info 記下當前書籤,再 restore;錯了就用該書籤撤銷
wrangler d1 time-travel info production-db # 抄下 current bookmark
wrangler d1 time-travel restore production-db --timestamp="2026-08-10T00:00:00Z"
# 若還原錯 → wrangler d1 time-travel restore production-db --bookmark="<剛才那個 current bookmark>"
坑三:忘了還原是「整庫覆蓋」,以為能只救回一張表。
Time Travel 不支援部分還原。若你只想救回誤刪的一張表、卻直接 restore 整庫,會把那個時間點之後所有正常的寫入也一併抹掉,可能造成更大的資料損失。更安全的做法是先匯出、再局部撈:
# ✅ 只想救一張表:先把「事發前時間點」的整庫匯出到本地,再只挑那張表的資料撈回來
# 1) 這裡用 export 拿一份當前備份(或在測試庫先 restore 後 export)
wrangler d1 export production-db --remote --output=./snapshot.sql
# 2) 從 snapshot.sql 中挑出目標表的 INSERT 語句,整理成 patch.sql
# 3) 只把需要的資料灌回生產(避免整庫覆蓋)
wrangler d1 import production-db --remote --file=./patch.sql
坑四:把 Time Travel 和 export 當成二選一。
兩者是互補不是替代。只靠 Time Travel,你沒有可帶走的獨立備份檔、也撐不過 30 天;只靠 export,你少了細到「秒」的即時還原能力、且備份間隔內的資料無法救。正解是搭配:日常安全網交給 Time Travel,長期封存與異地備份交給 export + R2。
坑五:壞掉的 migration 只想著改 code,忘了資料也要回滾。
當一個 migration 不只改壞 schema、還污染了資料(例如一條錯誤的 UPDATE/資料轉換),光把 migration 檔改回來、或寫一個「反向 migration」往往救不回被覆蓋的舊值。這時 Time Travel 才是真正的回滾工具:
- 先止血:確認事發時間點,用
time-travel info查出壞掉的 migration 執行前的書籤。 - 回到過去:記下當前書籤後,
restore到 migration 前的狀態,schema 與資料一起回滾。 - 修正再上:在本地把 migration 改對、
--local測過,確認無誤再--remote套用。
這就是 Time Travel 與 migrations 搭配的回滾思維:migration 管「往前演進」,Time Travel 管「一鍵倒帶」,兩者一起才是安全的 schema 演進。
最佳實踐小結:記牢——Time Travel 免費 7 天 / 付費 30 天,且邊界每天往前滾;還原前一定先 info 記下當前書籤(這是你的後悔藥);還原是破壞性整庫覆蓋,只救單表請走 export + 局部匯入;超過 30 天的保存靠 export + R2 + Cron 定期冷備份;壞掉的 migration 用 Time Travel 連 schema 帶資料一起倒帶,再修正重上。守住這幾條,誤刪與壞遷移都不再是災難。
小結
上一篇《D1 Sessions API 與讀取複本》,我們讓全球讀取又快又不失一致;這一篇,我們補上資料庫最後、也最關鍵的一道防線——D1 的 Time Travel 與備份:
- Time Travel(時間點還原)——D1 內建、永遠啟用、免費的 PITR,免費 7 天 / 付費 30 天,用書籤標記狀態,能精確還原到保留期內任一秒;但保留邊界每天往前滾動。
info/restore與撤銷——info只查不改、還原前務必記下當前書籤;restore是破壞性整庫覆蓋,選錯時能用還原前的書籤撤銷;每 10 分鐘最多 10 次還原。- 匯出 / 匯入與長期備份——
wrangler d1 export/import做可攜的冷備份與搬遷,結合 R2 + Cron Trigger 顧超過 30 天的長期封存。 - 災難復原與回滾思維——誤刪先查事發前書籤、記當前書籤再還原;只救單表走 export + 局部匯入;壞掉的 migration 用 Time Travel 連 schema 帶資料一起倒帶。
到這裡,D1 段落正式收尾——從建立資料庫、查詢 API、Migrations、Sessions API 與讀取複本,到這篇的 Time Travel 與備份,你已經有能力在 Cloudflare 上安心地建置、擴展與守護一個 SQLite 資料庫了。接下來,我們要離開「資料表」的世界,進入 Cloudflare 上更強大的有狀態運算基元。下一篇《Durable Objects 入門》,我們就來看如何用單一、全域唯一的物件,協調 WebSocket、即時聊天室、多人協作與計數器這類需要強一致狀態的場景。
想先查閱官方對 D1 Time Travel 的完整說明,可以隨時參考 Cloudflare D1 Time Travel 官方文件。備份與還原這一課已就緒,我們下一篇《Durable Objects 入門》見。