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 執行時會:
- 建立一個臨時、乾淨的 Shadow Database
- 把
supabase/migrations/裡的所有已知遷移依序套用到 Shadow Database,得到一條「基準線(baseline)」——這代表「照歷史紀錄,schema 應該長這樣」 - 拿這條基準線去比對「你現在實際的目標資料庫」
- 把兩者的差異,輸出成一段 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
它會做三件事,順序固定:
- 清空本機資料庫(刪掉重建)
- 依檔名時間戳由小到大,重新套用
supabase/migrations/裡的每一支遷移 - 執行
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 流程,讓「本機 → 預覽 → 生產」的每一段都自動化、可審核。