專案名稱:LogCarver
專案簡介 :
SQL Server 交易記錄檔解析工具。多數資料救援工具都要求你「事先」開啟 Audit、CDC 或 Change Tracking,才能追蹤資料異動;但真實世界裡,大部分中小企業自架的 SQL Server 從來沒開過這些功能——資料被誤刪之後才想到要查,通常已經來不及。LogCarver 直接解析 SQL Server 自己的 fn_dblog,不需要事先設定任何東西,就能重建一張表完整的 INSERT / UPDATE(前後值)/ DELETE 歷程,並產生可審閱的 Undo / Replay SQL。
開發狀態 :已上線持續更新。 從v0.1.0 發到 v0.1.5,包含 3 個真實環境測出來的 bug 修補跟 3 個 SQL Server 版本(2019、2016、2022)的相容性驗證。
GitHub :https://github.com/caiderek/LogCarver (MIT 授權)
##技術實作內容##
開發動機
起點是一個很常見的場景:資料庫每天備份,但自己家裡的程式備份反而很隨便,想到才會備份一下。曾經遇過一個情況 只需要還原一小段資料,手上卻只有一天前的完整備份,結果變成得把當天所有的紀錄一筆一筆比對,才能抓出真正需要復原的那幾列。
問題是,SQL Server 的交易記錄檔本身就記著這些變更歷程,只是 fn_dblog 回傳的欄位、row image 的位元組配置幾乎沒有公開文件 想要知道格式,只能自己拿真實資料庫做實驗、直接讀 log 的原始位元組去反推。這段逆向工程過程本身記在另一份研究紀錄裡,是純粹靠實驗跑出來的,不是查文件或問 AI 就能得到的答案。LogCarver 則是把這個研究成果,加上 AI 輔助把邏輯寫成程式碼,落地成一個真正能用的工具。
技術架構
核心邏輯與技術挑戰
工具上線後,我自己拿一張不重要的表做真實測試,沒有主鍵、或主鍵是 NONCLUSTERED (這種寫法建出來的底層儲存結構其實是 Heap,不是叢集索引)。結果查出來是 0 筆事件 ,而且工具也會很明確的說:這通常代表 VLF 已經被標記可重用、資料可能已經從 fn_dblog 的可視範圍滾出去了。
但是,這個解釋是錯的。
實際去查 fn_dblog 的原始輸出才發現:Heap 表的 row 操作(INSERT/UPDATE/DELETE)在 log 裡的 Context 欄位標記是 LCX_HEAP,但程式碼裡的查詢條件只認 LCX_CLUSTERED 跟 LCX_MARK_AS_GHOST 這兩種——Heap 表的紀錄從一開始就被整批濾掉了,跟「VLF 輪替」一點關係都沒有。更麻煩的是,這個錯誤訊息看起來很合理,如果沒有動手去查 fn_dblog 的原始輸出,很容易就會相信「喔,原來資料真的救不回來了」然後放棄。
修完 Context 過濾之後,又冒出第二個問題:表上如果有任何額外的索引(不管是 Heap 表上的 NONCLUSTERED PRIMARY KEY,還是叢集索引表上隨手加的一個查詢用索引),原本用來比對「這是哪張表」的邏輯是拿表名去猜 AllocUnitName 的字串樣式,但索引自己的內部維護紀錄,用的也是「表名.索引名稱」這種格式,會被同一組樣式誤抓進來,混進錯誤的欄位配置。同一張表上,兩種完全不同用途的資料被當成同一種東西解讀。
解決方案
與其繼續用字串樣式去猜,不如換一個方向:先查 SQL Server 自己的 sys.indexes,找出這張表真正對應的儲存結構(index_id 是 0 代表 Heap、1 代表叢集索引),組出唯一、精確的 AllocUnitName 字串,再用完全比對取代模糊比對。這樣一來,不管表上有幾個額外索引,都不會再誤抓進不相關的內部紀錄。
sys.system_internals_partition_columns 這邊也有一樣的根因:原本的查詢沒有過濾 index_id,對有額外索引的表,會把索引自己的欄位配置也一併撈進來,混進正確的表結構資料。加上同一個 index_id IN (0, 1) 過濾就解決了。
這整個除錯過程有一個明確的原則貫穿始終: 每一個假設都要拿真實的 SQL Server 去驗證,不能只憑文件或直覺 。fn_dblog 本身幾乎沒有公開文件,任何的猜測都有可能是錯的,這次三個 bug,沒有一個是靠讀文件發現的,全部是實際連上真實資料庫、直接看 fn_dblog 的原始輸出才抓出來。當天在三個不同版本(SQL Server 2022、2019、2016 SP2)上重跑同一套測試矩陣,確認修法在不同版本上行為一致,才正式把這三個版本列進「已驗證版本」清單。