一套內部系統的 SQL Server 資料庫有 505 張表、6868 個欄位,而手上的資料字典是一份人工維護的 Excel,涵蓋不到三分之一。
我用 Claude Code 接 MSSQL MCP 直接讀 schema,把整份字典重生了一次。結果很值得記錄:結構的部分(表名、欄位、型態、長度、PK、FK、index)補到 100%,但 6868 個欄位裡只有 27.9% 有中文名稱、3.4% 有備註說明——而後者才是決定這份文件有沒有人要看的東西。
這篇寫怎麼做的、落差的實際數字、以及這套做法要付的三個代價。文中的資料庫名、表名、檔名都已代換過,數字全部是實際跑出來的。
一、資料字典會死,是因為兩種落差
一份資料字典有兩種東西會過期,而且性質完全不同。
結構落差——DBA 上週加了三個欄位,文件裡沒有。這種落差是機械性的:答案就在 INFORMATION_SCHEMA 裡,任何時候都查得到,只是沒人有空去同步。
語意落差——一個叫 PROC_FLAG 的 char(1) 欄位到底代表什麼?Y 跟 1 有沒有差別?這個答案不在資料庫裡,它在當年寫這支程式的人腦袋裡。
傳統的手寫資料字典兩種落差一起吃:一次性寫完,結構立刻開始漂移,語意則從一開始就只補得到自己知道的那部分。然後文件失去可信度,沒人維護,正式死亡。
把這兩種落差分開看,才會發現它們該用完全不同的方式處理——這也是整篇文章的結論。
二、三步流水線
實際的做法是一條三步的 Python 流水線,openpyxl 是唯一的第三方依賴:
flowchart TB
subgraph S["Schema 來源"]
mcp["Claude Code
+ MSSQL MCP"]
mcp --> raw["schema / PK 查詢結果
(JSON)"]
end
raw --> gen["gen_db_docs.py"]
gen --> files["output/*.xlsx
505 個單表檔案"]
files --> merge["merge_xlsx.py"]
merge --> book["APPDB.xlsx
目錄頁 + 505 張工作表"]
manual["既有人工文件
TableList.xlsx"] --> write["write_chinese.py"]
book --> write
write --> final["完成品:
機器結構 + 人工語意"]
Schema 不是自己寫 SQL 撈的,是讓 Claude 透過 MSSQL MCP server 的 list_tables / describe_table / read_data 查出來。MCP 這層有稽核日誌,logs/audit.jsonl 記下了每次呼叫:
1 | {"timestamp":"2026-03-13T07:07:50.105Z","toolName":"read_data", |
recordCount 是 6868——跟最後 Excel 裡數出來的欄位總數一模一樣。這種能對得起來的數字是後續一切統計的基礎,也是我願意相信這份產出的原因。
同一份日誌裡還有三次 QUERY_EXECUTION_FAILED。失敗一樣被記下來這件事本身有價值——查詢打歪了會留下痕跡,不會像一次性腳本那樣跑完就沒了。
為什麼先產 505 個檔案,再合併
gen_db_docs.py 不直接產出一份大 Excel,而是一張表一個 .xlsx,第二步才用 merge_xlsx.py 合併並加上目錄頁與超連結。
多這一步的理由很實際:單表可以獨立重產。某張表的欄位查漏了、格式不對,重跑那一張就好,不必整份 505 張表重來。合併腳本本身也明確不修改 output/ 底下的原檔,所以合併可以無限次重跑。
代價是合併這步不便宜:字型、填色、框線、對齊、數值格式都要逐格複製,openpyxl 沒有整表複製這種東西。505 張表乘以每張表的儲存格數,全部要跑過一遍。
三、真正的關鍵設計:只填空,絕不覆蓋
第三步 write_chinese.py 是整套東西唯一有巧思的地方,也是決定這份文件會不會活過三個月的關鍵。
它從既有的人工文件裡把中文表名、中文欄位名、備註抓出來,回填進機器產生的字典。核心規則只有一條:
1 | if cn_name and row[3].value is None: # D 欄空的才寫中文名 |
row[3].value is None 這個條件就是全部的重點:已經有內容的格子永遠不動。
為什麼這條規則這麼重要?因為它讓「重新產生」變成一個安全的操作。schema 變了就重跑一次流水線,人工補過的中文說明不會被機器蓋掉。少了這條,任何人在字典上補過的心血都會在下次重生時消失,而只要發生過一次,就不會再有人願意花時間補了——這正是大多數自動產生的文件最後沒人維護的死法。
腳本另外還會在動手前自動備份一份,這是把「不覆蓋」這個承諾再加一道保險。
對齊方式則相對脆弱:它靠 B 欄的項次序號把來源檔和目標檔的欄位對起來,不是靠欄位名稱。所以如果哪天欄位順序變了,中文名就會對到錯的欄位上——這是這套設計裡我最不放心的一處。
四、落差的實際數字
流水線跑完之後,把成品拉出來統計(openpyxl 開 read-only 掃 506 張工作表,約 3 秒):
| 項目 | 數量 | 覆蓋率 |
|---|---|---|
| 資料表 | 505 | — |
| 欄位總數 | 6868 | — |
| 結構資訊(型態、長度、NULL、PK、FK、index) | 6868 | 100% |
| 有中文表名的表 | 192 | 38.0% |
| 有中文名稱的欄位 | 1916 | 27.9% |
| 有備註說明的欄位 | 231 | 3.4% |
| 欄位中文名全數補齊的表 | 86 | 17.0% |
| 完全沒有任何中文說明的表 | 373 | 73.9% |
單表最多 162 個欄位,平均 13.6 個。
這張表就是整件事的結論:機器把結構落差壓到零,語意落差幾乎原封不動。
回填率之所以只有這樣,是因為既有的人工文件本身就只涵蓋 137 張工作表、180 筆表名描述——來源就這麼多,回填再準確也生不出沒有的東西。
換句話說,這套工具沒有「產生」任何語意知識,它只是把散落在一份舊 Excel 裡的既有知識,準確地搬到一份結構完整的新文件上。這個定位要講清楚,不然很容易對 AI 產生錯誤的期待。
五、三個要老實承認的代價
1. 這條流水線現在跑不起來
gen_db_docs.py 的輸入寫死在兩個暫存檔路徑上:
1 | SCHEMA_FILE = r'C:\Users\<user>\AppData\Local\Temp\1773385670199-copilot-tool-output-f1jwyc.txt' |
那是 AI 工具查詢完之後 dump 出來的暫存輸出。寫這篇的時候我去確認了一下:
1 | ls: cannot access '...1773385670199-copilot-tool-output-f1jwyc.txt': No such file or directory |
檔案已經不在了,這支腳本現在跑不起來。
這是整件事最值得記下來的一個坑:把 AI 工具的暫存輸出當成 pipeline 的輸入,等於做出一個註定不可重現的東西。當下很順——反正檔案就在那裡,路徑複製貼上就能跑——但它的壽命只有幾天。
正確的做法是查詢結果一拿到就存進專案目錄,當成版控過的中繼產物。多一個步驟,換來這條流水線隨時可以重跑。
2. 有三類資訊是內聯在程式碼裡的常量
schema 和 PK 是從查詢結果讀的,但 FK、identity、index 這三類資訊是直接以 Python list 寫死在腳本裡的:
1 | FK_DATA = [ |
當下這樣做最快,但意思是換一個資料庫快照就要重查一次、重貼一次。這不是一條可以排程的 pipeline,是一次性的產出工具——認清這件事,比假裝它是自動化來得有用。
3. 三個腳本靠約定的儲存格座標協作
B4 是中文表名、第 5 列是表頭、資料從第 6 列開始、C 欄是欄位名、D 欄是中文名、O 欄是備註——這組座標同時被三個腳本硬編碼。改任何一處版面,三支程式都要跟著改,而且改漏了不會報錯,只會安靜地把中文名寫到錯的欄位去。
這是典型的隱性契約:能動,但沒有任何機制保護它。如果這套東西要長期維護,第一件該做的事是把版面定義抽成一個共用的常數模組。
六、所以什麼該交給 AI
跑完這一輪,我對「AI 幫忙寫文件」的看法變得比較具體:
| 結構 | 語意 | |
|---|---|---|
| 事實在哪裡 | 資料庫裡,隨時查得到 | 人的腦袋、舊文件、程式碼慣例 |
| AI 能做的 | 全量、精確、可重複產生 | 只能搬運既有的,生不出新的 |
| 落差怎麼消 | 重跑流水線 | 只能靠人補,沒有捷徑 |
| 這次的結果 | 100% | 27.9% |
所以真正的價值不在「AI 幫我寫了文件」,而在它把 6868 個欄位裡「還沒有人說得出它是什麼」的那 72% 清楚地標了出來。在此之前,沒有人知道這個缺口有多大;現在它是一個可以排進工作項目的具體數字,而且每補一格,下次重生都還會在。
這跟我之前比對兩版 DLL 的經驗是同一件事:機器負責把全量差異攤開來,人的價值在判斷哪些有意義。工具做的是把問題的規模量化,不是把問題解決掉。
同分類的其他筆記:
- LLM Wiki 介紹:把 session 裡蒸發掉的經驗變成持續累積的知識庫
- 兩版 DLL 差了 76 個檔案,真正的功能變更只有 1 處:同樣是機器產出全量、人做判斷
- 批次程式效能優化:同樣是先把數字量出來,才知道力氣該花在哪
留言