Tools mentioned in this article
Open the browser-based tool while you read and try the workflow immediately.
「這張表到底連到哪裡去了?」
「這個 user_id 參照的是哪張表?」「咦,這張表該不會沒有被任何地方參照吧?」——光靠閱讀原始 SQL 來追蹤關聯,就像在腦中拼拼圖一樣。
SQL to ER 圖轉換工具 接收 CREATE TABLE 語句後,能立即把資料表、欄位、主鍵、外鍵渲染成 Mermaid ER 圖。所有處理都在瀏覽器內完成,SQL 不會外流。

這篇文章要說明 DDL 解析器實際上是怎麼運作的,以及因為這種運作方式,您應該知道哪些涵蓋範圍上的限制。
使用方法
CREATE TABLE users (
id BIGINT PRIMARY KEY,
name VARCHAR(255) NOT NULL
);
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
total_amount INTEGER NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id)
);
貼上這段之後,會得到以下 Mermaid erDiagram 程式碼:
erDiagram
users {
BIGINT id PK
VARCHAR name
}
orders {
BIGINT id PK
BIGINT user_id
INTEGER total_amount
}
orders }|--|| users : "user_id -> id"
把產生的程式碼直接貼到 GitHub README 或 Pull Request,就會自動渲染出來。
解析器的原理:用正規表示式拆解 DDL
這個工具沒有使用完整的 SQL 解析函式庫,而是採用以正規表示式為基礎的輕量解析器(SqlParser.parseDdl),分三個階段處理。
第一步:移除註解並拆分語句
const cleanSql = sql
.replace(/--.*$/gm, '') // 移除單行註解
.replace(/\/\*[\s\S]*?\*\//g, '') // 移除區塊註解
.trim();
const statements = cleanSql.split(';').map(s => s.trim()).filter(Boolean);
由於語句是單純以 ; 拆分,若預設值中含有分號的 DDL 就無法正確解析——不過這種情況在實務上幾乎不會發生。
第二步:只在括號外的逗號處拆分欄位定義
如果單純用 , 拆分 CREATE TABLE 的內容,像 DECIMAL(10, 2) 這種型別定義裡的逗號也會被誤判為分隔符。因此解析器會追蹤括號的深度,只有在深度為 0 時的逗號才視為分隔符。
private static splitByCommaOutsideParens(text: string): string[] {
let depth = 0;
// 遇到 '(' depth++,遇到 ')' depth--,只有 depth === 0 時的逗號才是分隔符
for (let i = 0; i < text.length; i++) {
if (text[i] === '(') depth++;
else if (text[i] === ')') depth--;
if (text[i] === ',' && depth === 0) { /* 在此拆分 */ }
}
}
這和 JSON⇔CSV 轉換工具 的 CSV 解析器用的是同一套思路——逐字元掃描並維護狀態。SQL 的欄位定義和加引號的 CSV 欄位表面上不同,但都有「分隔符在特定情況下會失效」這個共通特性。
第三步:把每一行分類成 PRIMARY KEY、FOREIGN KEY 或欄位定義
拆分後的每一行都會用正規表示式比對,歸類成三種之一。
// 資料表層級的主鍵,也支援像 PRIMARY KEY (id, tenant_id) 這樣的複合主鍵
const pkMatch = trimmed.match(/PRIMARY KEY\s*\(([\s\w,`"]+)\)/i);
// 資料表層級的外鍵
const fkMatch = trimmed.match(
/FOREIGN KEY\s*\(([\s\w`"]+)\)\s*REFERENCES\s*(?:['\`"]?(\w+)['\`"]?\.)?['\`"]?(\w+)['\`"]?\s*\(([\s\w`"]+)\)/i
);
// 欄位定義,也會擷取欄位定義結尾內嵌的 REFERENCES 子句
const colMatch = trimmed.match(/^['`"]?(\w+)['`"]?\s+(\w+(?:\([\w\s,]+\))?)(.*)$/i);
REFERENCES 的正規表示式同時能比對欄位定義結尾的內嵌外鍵,以及資料表層級的 FOREIGN KEY 子句。也支援複合主鍵——像 PRIMARY KEY (id, tenant_id) 這樣的定義,會讓兩個欄位都標上 PK。
輸出結果:烏鴉腳表示法
這個工具產生的關聯行採用烏鴉腳表示法:
orders }|--|| users : "user_id -> id"
箭頭從 }|(多)指向 ||(一),代表「多筆訂單屬於同一位使用者」的一對多關係。若想完整了解這套表示法,包括一對一、多對多,以及識別/非識別關聯的差異,可參考 Mermaid ER 圖語法完全參考。
要留意的是,Mermaid 的 erDiagram 語法不允許欄位型別包含括號,因此長度規格會被自動移除——VARCHAR(255) 會變成 VARCHAR(type.replace(/\([^)]*\)/g, ''))。
解析器不支援的 DDL 寫法
由於採用以正規表示式為基礎的輕量解析器,而非完整的 SQL 語法解析——這是為了控制程式體積、讓瀏覽器能快速執行所做的刻意取捨——因此有以下限制:
| 寫法 | 支援狀況 |
|---|---|
CREATE TABLE 內的 PRIMARY KEY/FOREIGN KEY(資料表層級與欄位層級皆可) | ✅ 支援 |
複合主鍵 PRIMARY KEY (a, b) | ✅ 支援 |
ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY | ❌ 不支援(因為在 CREATE TABLE 語句之外) |
CREATE TABLE IF NOT EXISTS | ✅ 支援 |
加上 schema 前綴的 schema.table_name | ✅ 支援(僅擷取資料表名稱部分) |
-- 單行註解//* */ 區塊註解 | ✅ 解析前會先移除 |
有些遷移工具會用另一條 ALTER TABLE 語句事後加上外鍵(Rails 的 ActiveRecord 就是常見例子)。若您遇到這種情況,可以先把外鍵那一行暫時搬進 CREATE TABLE 的內容中,或是在貼上前補上對應的 FOREIGN KEY (...) REFERENCES ... 子句,這樣關聯就能被正確辨識。
檢視產生的圖表時的檢查重點
- 每張資料表是否都設定了主鍵
- 外鍵的方向是否符合預期
- 有沒有沒被任何地方參照的孤立資料表
- 中介表是否正確表達了多對多關係
在 Pull Request 審查時,附上 ER 圖遠比只看 SQL diff 更能清楚傳達異動的影響範圍。由於輸出是純文字(Mermaid),可以直接貼進審查留言中。
常見問題
可以用在 MySQL 或 PostgreSQL 的 DDL 嗎?
以 CREATE TABLE 為主的一般 DDL 都適用。方言特有的型別或選項(例如 ENGINE=InnoDB)會直接被忽略,不影響資料表、欄位、鍵值的擷取。
沒有外鍵的 SQL 也能畫出圖嗎?
可以,會顯示為資料表與欄位的列表。不過關聯線是根據 FOREIGN KEY/REFERENCES 的資訊繪製的,若想清楚呈現關聯,請在 DDL 中加入這些定義。
用 ALTER TABLE 加上的外鍵會被辨識嗎?
不會。目前的解析器只分析 CREATE TABLE 語句內部,因此透過另一條 ALTER TABLE ... ADD CONSTRAINT 語句加入的外鍵不會被辨識。若需要呈現,請先把對應的 FOREIGN KEY (...) REFERENCES ... 子句暫時加進 CREATE TABLE 那一側再貼上。
產生的圖可以貼到文件裡嗎?
可以。由於是以 Mermaid 格式輸出,貼到 GitHub README 或設計文件中,就能以貼近程式碼的方式管理架構圖。烏鴉腳表示法的讀法可參考 Mermaid ER 圖語法完全參考。
總結
- 解析採用以正規表示式為基礎的輕量實作,關鍵在於只在括號外的逗號處拆分
- 支援 PRIMARY KEY、FOREIGN KEY(資料表與欄位層級皆可),以及複合主鍵
- 透過
ALTER TABLE事後加上的外鍵不受支援,依遷移工具的風格可能需要手動調整 - 產生的程式碼採用 Mermaid 的烏鴉腳表示法,讀法可參考另一篇參考文章
把手邊的 SQL 交給這個工具,找出那些您可能一直沒注意到的資料表關聯吧。