Migrations 資料庫遷移:讓 schema 變更可版控、可重播 | Supabase 完整教學

2026/10/29
Migrations 資料庫遷移:讓 schema 變更可版控、可重播 | Supabase 完整教學

上一篇我們用 Supabase CLI 在本機用 Docker 蓋好了一間「隨你砸的練習廚房」——但練出來的成果(那些資料表、函式、RLS 政策)該怎麼變成「可版控、可重現、能安全部署上雲」的程式碼?答案就是 Migrations(資料庫遷移)。本篇帶你把每一次 schema 變更都變成一支帶時間戳、納入 git、能一鍵重播的遷移檔:用 supabase migration new 手寫、用 supabase db diff 從本機變更自動產生、用 supabase db push 套用到遠端、用 supabase db reset 本機重建並重跑 seed,並認識 schema drift(結構漂移) 這個最危險的坑,打造一條「本機開發 → 測試 → 生產」的單向流水線。

前言

先一句話定義本篇主角:Migrations(資料庫遷移)就是「用一支支帶時間戳、納入版本控制的 SQL 檔案,來記錄並重播資料庫每一次 schema 變更」的機制。 每當你要建一張表、加一個欄位、改一條約束、寫一個觸發器,都不再是「跑一次 SQL 就算了」,而是把這段變更「凝固」成一支遷移檔,讓它可以在任何環境(本機、Staging、生產)依相同順序重新播放,得到一模一樣的結果。

用一個類比理解為什麼要這麼做。遷移檔對資料庫,就像 git commit 對程式碼。 你不會直接在正式伺服器上改 code、也不會靠記憶維護程式的每一次變動——你 commit、你版控、你 code review、你部署。資料庫的 schema 同樣是「會演化的資產」,卻常被人當成「上線後就不動的東西」隨手在 Dashboard 亂改。遷移機制就是把「程式碼那一套嚴謹的版控紀律」搬到資料庫上:每一次 schema 變更 = 一支遷移檔 = 一次可追溯、可重播、可回顧的變更紀錄。

而這一切的核心設計哲學,延續上一篇的「本機優先」,可以濃縮成一條單向流動的鐵律:schema 變更永遠是「本機開發 → 測試環境 → 生產環境」單向流下去,絕不反過來直接動生產。 你在本機把變更做成遷移檔、驗證無誤、進 git、經 review,最後用 db push 一路推上雲端。任何繞過這條流水線、直接手動改遠端的行為,都會種下 schema drift(結構漂移) 的禍根。

本篇你會學到:

  • 兩種產生遷移檔的方式:supabase migration new(手寫)與 supabase db diff(從本機變更自動捕捉),以及各自的適用時機
  • 完整遷移工作流:db reset 本機重建驗證、migration list 對照本機與遠端狀態、db push 套用到遠端(含 --dry-run 乾跑)
  • 遷移檔命名與版控紀律:時間戳命名、已部署遷移不可改、冪等寫法、什麼進 git 什麼不進
  • schema drift 與 seed.sql:為什麼永不直接改遠端、drift 發生時怎麼補救、seed 為什麼不該塞進 migration

本篇聚焦「遷移工作流」這一件事,CLI 的安裝與本機環境啟動已在上一篇《Supabase CLI 與本機開發》講透,這裡假設你已經能 supabase start 把本機堆疊跑起來。以下指令、旗標、檔案結構都以本文撰寫時的 CLI 版本為準,正式決策前請以官方文件為準。

核心概念

在敲遷移指令之前,先建立三張心智圖:一是「遷移檔長什麼樣、怎麼命名」,二是「一次變更如何從本機流到遠端」,三是「db diff 背後的 Shadow Database 到底在比對什麼」。

一、遷移檔的命名與不可變性

每一支遷移檔都住在 supabase/migrations/ 目錄裡,檔名格式固定為:

{時間戳}_{描述}.sql

例如 20240101120000_create_employees_table.sql。前面那一長串是 CLI 自動生成的 UTC 時間戳(精確到秒),後面接你給的描述。這個設計有兩個關鍵用意:

  • 時間戳決定套用順序:CLI 永遠「按檔名的時間戳由小到大」依序套用遷移。這確保了「先建表、再加欄位、再建索引」的因果順序不會亂——因為它們的時間戳天生就是遞增的。
  • 不可變性(immutability):一支遷移一旦被部署到任何共享環境(尤其生產),就絕對不能再回頭修改它的內容。要修正錯誤,你新增「另一支」遷移去更正,而不是改舊的。這和 git「已推送的 commit 不該 rebase 掉」是同一個道理——因為別人(和別的環境)可能已經套用過舊版本了。

二、一次變更的生命週期:從本機到遠端

第二張心智圖,是一次 schema 變更完整的旅程。把它記牢,後面所有指令都只是這張圖的某一步:

①  在本機開發變更
    ├─ 方式 A:supabase migration new <name> → 手寫 SQL
    └─ 方式 B:在本機 Studio 改 schema → supabase db diff -f <name> 自動生成
                        │
                        ▼
②  supabase db reset          ← 本機打掉重練,驗證遷移能乾淨重播
    (清空 → 依序套用所有遷移 → 執行 seed.sql)
                        │
                        ▼
③  git add supabase/migrations/ && git commit   ← 遷移進版控、開 PR、review
                        │
                        ▼
④  supabase db push           ← 把新遷移套用到已連結的遠端專案
    (建議先 supabase db push --dry-run 乾跑確認)
                        │
                        ▼
⑤  supabase migration list    ← 對照「本機有哪些、遠端套了哪些」

這條流水線的精髓是單向:變更只會從①往⑤流,永遠不會有「直接在遠端改、再想辦法同步回本機」這種逆流。整條線上,本機是唯一的「創作起點」,遠端只是「被動接收部署」的終點。

三、db diff 與 Shadow Database(影子資料庫)

supabase db diff 是本篇最巧妙的機制,值得單獨拆解。它的問題是:「我在本機資料庫上東改西改,怎麼知道『相對於已記錄的遷移歷史,我到底多做了什麼』?」

答案是 Shadow Database(影子資料庫)——上一篇提過的那個 54320 埠的角色。db diff 執行時會:

  1. 建立一個臨時、乾淨的 Shadow Database
  2. 把 supabase/migrations/ 裡的所有已知遷移依序套用到 Shadow Database,得到一條「基準線(baseline)」——這代表「照歷史紀錄,schema 應該長這樣」
  3. 拿這條基準線去比對「你現在實際的目標資料庫」
  4. 把兩者的差異,輸出成一段 SQL

所以 db diff 產生的,永遠是「相對於已知遷移歷史的『淨變更』」,不會把早已記錄過的東西重複輸出。理解了這層,你就懂為什麼它需要一個「乾淨的影子基準」來當對照組。

不過 db diff 有偵測盲區,以下變更它抓不到,必須改用 migration new 手寫:

db diff 抓不到的變更說明
Publication邏輯複寫的發布設定變更
Storage Bucket 設定Bucket 的建立與屬性調整
含 security_invoker 的 View這類 View 的定義
部分系統層級設定少數 Postgres 系統層設定

四、關鍵術語速查

術語白話解釋
Migration(遷移)一支帶時間戳、記錄一次 schema 變更、可重播的 SQL 檔
時間戳命名{UTC 時間戳}_{描述}.sql,時間戳決定套用順序
Shadow Database(影子資料庫)db diff 用來套遷移建基準線、以比對差異的臨時資料庫
db reset本機打掉重練:清空 → 重套所有遷移 → 執行 seed
db push把本機新遷移套用到已連結的遠端專案(增量部署)
schema drift(結構漂移)遷移歷史與資料庫實際 schema 不一致的狀態,多因直接改遠端造成
seed.sql每次 db reset 會自動填入的初始測試資料,非 schema 變更

實作範例

下面走一遍完整的遷移工作流:建遷移(兩種方式)→ 寫 SQL → db reset 驗證 → migration list 對照 → link 遠端 → db push 部署 → 準備 seed。每一步都是可以直接敲的指令與可執行的 SQL。

一、方式 A:手寫遷移 supabase migration new

當你腦中很清楚要下哪些 DDL 時,直接建一支空白遷移檔來手寫:

# 在 supabase/migrations/ 建立一支帶時間戳的空白遷移檔
supabase migration new create_employees_table

它會回報建好的檔案路徑,時間戳是 CLI 依當下 UTC 時間自動生成的:

Created new migration at supabase/migrations/20240101120000_create_employees_table.sql

接著打開這支檔案,把要做的 schema 變更寫進去。這裡示範一張員工表,並示範幾個遷移的良好習慣——用 if not exists 讓遷移冪等、建表後立刻啟用 RLS、加上必要索引:

-- supabase/migrations/20240101120000_create_employees_table.sql

-- 建立員工資料表(if not exists 讓這支遷移可安全重複套用)
create table if not exists public.employees (
  id         bigint primary key generated always as identity,
  name       text        not null,
  email      text        unique,
  department text        default 'General',
  hired_at   date,
  created_at timestamptz default now()
);

-- 每張新表都應立即啟用 Row Level Security(RLS)
alter table public.employees enable row level security;

-- 建立索引加速常見查詢
create index if not exists employees_email_idx on public.employees (email);
create index if not exists employees_department_idx on public.employees (department);

二、方式 B:從本機變更自動生成 supabase db diff

第二種、也是「在 Studio 拉拉點點改 schema」時最順手的做法——先在本機 Studio(http://127.0.0.1:54323)用 Table Editor 或 SQL Editor 建好新表、加好欄位,然後讓 db diff 幫你把差異捕捉成遷移檔:

# 比對本機資料庫與遷移基準線,把差異存成一支新遷移檔
supabase db diff -f add_reviews_table

-f(--file)旗標是關鍵:有它才會把差異「存成新遷移檔」,沒有它只會把差異印在終端機供你檢視。執行時你會看到它建立 Shadow Database、依序套用已知遷移、再比對的過程:

Connecting to local database...
Creating shadow database...
Applying migration 20240101120000_create_employees_table.sql...
Diffing schemas: public
Finished supabase db diff.

Wrote new migration: supabase/migrations/20240103000001_add_reviews_table.sql

生成的遷移檔內容,就是它比對出來的「淨變更」SQL(措辭是機器產生的,欄位會加雙引號):

-- supabase/migrations/20240103000001_add_reviews_table.sql

create table "public"."reviews" (
  "id"         bigint generated always as identity,
  "product_id" bigint not null,
  "rating"     smallint not null,
  "comment"    text,
  "created_at" timestamp with time zone default now(),
  constraint "reviews_pkey" primary key ("id")
);

alter table "public"."reviews" enable row level security;

幾個常用的 db diff 旗標:

supabase db diff                        # 只印出差異,不存檔(先看看)
supabase db diff -f add_feature         # 存成新遷移檔
supabase db diff --schema public        # 只比對特定 schema
supabase db diff --linked               # 拿本機遷移基準線比對「遠端」,用來偵測 drift

三、db reset:本機打掉重練、驗證遷移能乾淨重播

寫完(或生成完)遷移後,最重要的一步是驗證它能從零乾淨地重播。這就是 supabase db reset 的用途:

# 清空本機資料庫 → 依時間戳順序重套所有遷移 → 執行 seed.sql
supabase db reset

它會做三件事,順序固定:

  1. 清空本機資料庫(刪掉重建)
  2. 依檔名時間戳由小到大,重新套用 supabase/migrations/ 裡的每一支遷移
  3. 執行 supabase/seed.sql(若存在),填入種子資料

為什麼這一步不能省?因為它是你「遷移品質」的唯一保證。如果你的遷移之間有順序依賴錯誤、或某支遷移改壞了、或漏了 if not exists 導致重跑衝突,db reset 會當場報錯——在本機爆掉,遠比推到生產才爆掉好一萬倍。實務上的鐵律是:每次寫完遷移、每次 git pull 拉到同事的新遷移後,都先 db reset 一遍確認能乾淨重播,再往下走。

四、migration list:對照本機與遠端狀態

要部署到遠端前,先用 migration list 看清楚「本機有哪些遷移、遠端已經套了哪些」:

# 對照本機與遠端的遷移套用狀態(需先 link 遠端專案)
supabase migration list

輸出是一張左右對照表:

  LOCAL          │ REMOTE         │ TIME (UTC)
  ───────────────┼────────────────┼─────────────────────
  20240101120000 │ 20240101120000 │ 2024-01-01 12:00:00
  20240103000001 │ ─              │ ─

這張表一眼就能讀懂:時間戳 20240101120000 兩邊都有,代表已同步;而 20240103000001 只在 LOCAL、REMOTE 那格是 ─,代表這支遷移還沒推上遠端——正是接下來 db push 要處理的對象。

五、db push:把新遷移套用到遠端

確認本機遷移驗證無誤、也 review 過了,才動手推遠端。先照上一篇的做法 link 到目標雲端專案,然後先乾跑再實推:

# 先乾跑:只顯示「將執行哪些 SQL」,不實際套用(推生產前必做)
supabase db push --dry-run

# 確認無誤後,正式把本機尚未套用的遷移推上遠端
supabase db push

--dry-run 是推生產前的安全帶——它讓你在真正動遠端之前,先用眼睛看清楚「這次會執行哪些 DDL」,避免手滑把破壞性變更(如 drop column)打到正式環境。推送成功後,再 supabase migration list 一次,會看到剛才那支遷移的 REMOTE 欄位也對齊了。

補充:若遠端已有既存 schema(例如你是接手一個已上線的專案),可以用 supabase db pull 把遠端現況拉成一支遷移檔,補回本機的遷移歷史——這也是稍後修補 drift 的手段。

六、seed.sql:初始資料,不是 schema 變更

最後釐清一個高頻誤區:seed.sql 是「初始測試資料」,不是「schema 變更」,兩者要分家。 seed 放在 supabase/seed.sql,內容是 INSERT,並且每次 db reset 都會被自動重跑一遍:

-- supabase/seed.sql:本機環境的初始測試資料(每次 db reset 自動填入)

insert into public.employees (name, email, department)
values
  ('王大明', 'wang@example.com', 'Engineering'),
  ('陳小美', 'chen@example.com', 'Design'),
  ('林志豪', 'lin@example.com',  'Sales')
on conflict (email) do nothing;   -- on conflict 讓 seed 可安全重複執行

因為 seed 會被 db reset 反覆執行,務必用 on conflict do nothing(或 upsert)讓它冪等,否則第二次 reset 就會撞唯一鍵報錯。關鍵原則是:建表、加欄位這類 schema 結構的事寫進 migration;塞測試資料這類「資料內容」的事寫進 seed。 兩者混在一起,會讓你的遷移歷史被一堆測試假資料汙染,也會讓「純 schema」失去可攜性。

常見錯誤與最佳實踐

坑一:直接在雲端 Dashboard 改正式資料庫,造成 schema drift。 這是遷移工作流最危險、也最常見的坑。有人圖方便,直接在雲端 Dashboard 或用 SQL 對正式資料庫加了一個欄位——遠端 schema 變了,但 supabase/migrations/ 裡「完全沒有這件事的紀錄」,於是遷移歷史與實際 schema 對不起來,這就是 schema drift(結構漂移)。後果是:同事 db reset 重現不出線上的 schema、下次 db push 可能因物件已存在而衝突、團隊也失去稽核軌跡。正確做法:永不直接改遠端——所有變更都先在本機做成遷移檔、進 git、經 review、再 db push。萬一 drift 已經發生,用 supabase db pull 把遠端現況拉成一支遷移檔補回歷史,並用 supabase migration list 對照兩邊狀態、必要時用 supabase migration repair --status applied/reverted <時間戳> 修復記錄。

坑二:只在本機套了遷移,忘了 push 到遠端。 在本機 migration new + db reset 改得很開心,卻忘記「本機改好不等於線上生效」——線上專案的 schema 一動也沒動。正確做法:養成「本機驗證 → git commit → db push」的完整反射動作;部署前用 supabase migration list 確認「本機有、遠端還沒有」的那幾支遷移,就是這次要推的對象;db push 後再 list 一次核對兩邊對齊。

坑三:回頭修改「已經部署過」的遷移檔。 發現某支已推上遠端的遷移寫錯了,直接改它的內容——這會讓「已套用過舊版本的環境」和「拿到新版本的環境」永久不一致,是製造 drift 的另一種方式。正確做法:已部署的遷移視為不可變(immutable)。要修正,就新增一支遷移去更正(例如再寫一支 alter table ... drop column ... 或補上正確定義),而不是竄改歷史。這和 git「已推送的 commit 不 rebase」是完全相同的紀律。

坑四:把破壞性變更(drop column/drop table)直接推上生產。 drop column、drop table、改欄位型別這類破壞性、不可逆的變更,一旦推上生產可能造成資料永久遺失或線上服務中斷。正確做法:破壞性變更前,一律先 supabase db push --dry-run 乾跑看清楚要執行什麼;能拆成「向後相容的多步遷移」就拆(例如先新增新欄位、程式雙寫過渡、確認無虞後才在後續遷移刪舊欄位);在 Staging 環境完整演練過再上生產。

坑五:把 seed 資料寫進 migration。 為了省事,把一堆測試用的 INSERT 假資料塞進 migration 檔——結果這些假資料會被 db push 一路推到生產環境,汙染正式資料庫。正確做法:schema 變更歸 migration、初始資料歸 seed.sql。seed 只在本機 db reset 時執行、不會被 db push 推上遠端(預設情況),這才是測試資料該待的地方。

最佳實踐總結:

  • 永不直接改遠端:所有 schema 變更先成遷移檔、進 git、經 review、再 db push
  • 每支遷移做一件事、盡量冪等:善用 if not exists/if exists,讓遷移可安全重播
  • 改完先 db reset 驗證:在本機確認遷移能從零乾淨重播,再往下走
  • 推遠端前先 --dry-run:尤其破壞性變更,看清楚 SQL 再實推
  • 已部署遷移不可改:要修正就新增遷移,別竄改歷史
  • schema 歸 migration、資料歸 seed:seed 用 on conflict 保持冪等,別汙染遷移
  • 細節以官方為準:指令旗標、輸出格式都可能隨 CLI 版本調整

小結

這是 Supabase 系列教學的第 040 篇,緊接上一篇《Supabase CLI 與本機開發》。上一篇我們用 supabase start 在本機以 Docker 蓋好了一整套「隨你砸的練習廚房」,但留下一個問題:在本機 Studio 敲出來的每一次 schema 變更,怎麼變成「可版控、可重現、能安全部署上雲」的程式碼?這一篇就補上了這條「本機 → 測試 → 生產」的單向流水線。

核心觀念濃縮成一句話:Migrations 就是「用帶時間戳、納入 git 的 SQL 檔記錄每一次 schema 變更」;用 migration new 手寫或 db diff -f 從本機變更自動生成遷移,用 db reset 在本機打掉重練驗證,用 migration list 對照本機與遠端狀態,最後用 db push(先 --dry-run)把新遷移套用到遠端——整條線永遠單向流動,已部署的遷移不可改,永不直接動遠端,否則就會種下 schema drift 的禍根。

  • 產生遷移:supabase migration new(手寫 DDL)或 supabase db diff -f(從本機變更、經 Shadow Database 比對自動生成)
  • 本機驗證:supabase db reset(清空 → 依序重套所有遷移 → 執行 seed),確認能乾淨重播
  • 狀態對照與部署:supabase migration list 看本機/遠端差異;supabase db push --dry-run 乾跑後 supabase db push 套用到遠端
  • 紀律與 seed:時間戳命名、已部署遷移不可變、冪等寫法;schema 歸 migration、初始資料歸冪等的 seed.sql

遷移工作流跑通後,你已經能安全地把單一環境的 schema 變更推上雲端。但當團隊多人協作、每個 Pull Request 都想要一個「獨立、隔離、帶完整 schema 與 seed」的預覽環境時,光靠 db push 還不夠。下一篇《Branching 分支開發》,我們會看到 Supabase 如何讓每個 PR 自動擁有一份專屬的 Supabase 實例——把遷移工作流無縫接進 GitHub 的協作與 CI/CD 流程,讓「本機 → 預覽 → 生產」的每一段都自動化、可審核。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。

本週主打

AI 自動化入門包

你每天手動在做的那些煩事,其實 AI 可以自己跑。這份給你 10 個照著做就會的自動化工作流 + 50 個複製即用的提示詞,不用會寫程式。

看看這個產品 →