目錄
- 什麼是交易記錄檔
- 記錄檔的實體結構:VLF
- 三種復原模式
- 關鍵區別:截斷 vs 收縮
- log_reuse_wait_desc:為什麼空間不能回收
- 記錄檔與複寫 / CDC / 下游消費者
- CHECKPOINT 在做什麼
- 記錄檔備份與記錄檔鏈
- 收縮記錄檔 DBCC SHRINKFILE
- 診斷指令
- 實戰:記錄檔爆掉的排查流程
- 最佳實踐
- 常見問題
- 總結與速查表
什麼是交易記錄檔
每個 SQL Server 資料庫都至少有兩種實體檔案:資料檔(.mdf / .ndf,存放實際的資料頁)與交易記錄檔(.ldf,記錄「每一筆對資料的修改」)。任何 INSERT / UPDATE / DELETE、甚至 DDL,都會先在記錄檔留下一筆 log record。
核心機制是 WAL(Write-Ahead Logging,預寫式記錄):log 一定比資料頁先落地。(WAL 的通用原理、兩條規則與 ARIES 崩潰復原見 預寫日誌 WAL 完全指南,本篇聚焦 SQL Server 的具體實作與維運。)
交易要改一筆資料
→ ① 先把「這筆改動」寫成 log record,循序寫進記錄檔 .ldf(循序寫入,很快)
→ ② COMMIT 時,log record 確實 flush 到磁碟:此刻交易才算持久 (Durable)
→ ③ 被改動的資料頁(dirty page)還在記憶體,稍後才由 CHECKPOINT 批次寫回資料檔 .mdf
這樣設計換來幾件事:
| 用途 | 說明 |
|---|---|
| 當機復原 | 重啟時 redo 已 commit、undo 未 commit 的交易,保證資料庫回到一致狀態 |
| 交易回復 | ROLLBACK 靠 log 把已改的東西還原回去 |
| 持久性 (Durability) | commit 只需保證 log 落地,資料頁可稍後再寫,兼顧安全與效能 |
| 時間點還原 (PITR) | 搭配記錄檔備份,可還原到任意時間點(需 FULL 模式) |
| 下游同步 | 複寫(Replication)、CDC、鏡像、Always On 都靠讀 log 把變更送出去 |
正因為「commit 只要 log 落地」,記錄檔是循序寫入而非隨機寫入,這是 OLTP 高吞吐的關鍵之一。相對地,記錄檔一旦有問題(滿了、壞了),整個資料庫就寫不進去。
補充:dirty page(髒頁)與 buffer pool
上面流程裡的 dirty page 是理解 log 與 checkpoint 的前提,補充三個相關名詞:
- page(頁):SQL Server 儲存與 I/O 的最小單位,固定 8 KB;資料表與索引都以 page 為單位存放(8 個連續 page = 1 個 extent,64 KB)。
- buffer pool(緩衝池):SQL Server 不會每次都直接讀寫磁碟,而是把 page 讀進記憶體的 buffer pool,之後的讀寫都在記憶體上進行。
- dirty page(髒頁):buffer pool 裡已被修改、但還沒寫回磁碟資料檔(
.mdf) 的 page(記憶體版本比磁碟新)。相對地,記憶體與磁碟一致的叫 clean page。
一次修改的完整路徑:
改一筆資料
→ 目標 page 不在記憶體,就先從 .mdf 讀進 buffer pool
→ 在記憶體裡改它:變成 dirty page(同時 log record 依 WAL 先寫進 .ldf)
→ dirty page 不立刻落地,稍後由 CHECKPOINT / lazy writer 批次寫回 .mdf
→ 寫回後變回 clean page
這樣設計:改資料只要「記憶體改 + 循序寫 log」,不必每次隨機寫資料檔,且同一 page 被改多次可合併成一次寫回;安全性則靠 WAL 兜底——就算 dirty page 還沒寫回就當機,重啟時能用 log redo 把已 commit 的變更重做回來。負責把 dirty page 寫回磁碟的角色:
| 角色 | 時機 |
|---|---|
| CHECKPOINT | 定期批次刷 dirty page,並在 log 記檢查點以縮短復原時間 |
| Lazy writer | 記憶體有壓力時,為騰出 buffer 空間而把 dirty page 寫回並淘汰 |
關鍵區分:dirty page 講的是「記憶體 vs 資料檔
.mdf」的落差,走隨機寫、延後批次刷;交易記錄檔.ldf走循序寫、commit 即落地。兩條路分工,正是 WAL 又快又安全的原因。可用sys.dm_os_buffer_descriptors(is_modified = 1即為 dirty)查目前的髒頁量。
為什麼「循序寫 log」是高吞吐的關鍵
重點在 循序 I/O 遠快於隨機 I/O,再加上 commit 只需等 log、不需等資料頁:
- 資料頁散落各處:一筆交易可能改到磁碟不同位置的多個 page,把它們寫回是隨機寫(每次都要 seek/旋轉定位),併發時磁頭在散落位置間來回抖動。
- log 只往檔尾 append:所有 log record 循序寫在記錄檔結尾,連續寫入實體相鄰、幾乎免 seek。HDD 上差距是數量級;SSD 上也較快且寫入放大低。
如果沒有 WAL,要讓交易在 commit 當下就持久,得先把所有被改的資料頁(隨機位置)全部刷回磁碟——每次 commit 一堆隨機寫。有了 WAL,commit 只要把該交易的 log(檔尾、循序)flush 一次就算數,資料頁留在記憶體當 dirty page 稍後由 checkpoint 批次寫回。等於把「每筆交易多次隨機寫」換成「每筆交易一次循序 append」——這就是吞吐暴增的來源。
再疊上兩個放大效果:
| 機制 | 效果 |
|---|---|
| 群組提交(group commit) | 高併發時多筆交易的 log 合併成一次 flush,每筆交易的 I/O 成本再被攤薄 |
| 寫入合併 | 同一資料頁被多筆交易改,checkpoint 只寫回一次,而非每筆交易寫一次 |
由於每個 committing 交易都得等自己的 log 落地(等待類型
WRITELOG),log 的寫入速度直接決定 commit 吞吐——它是提交路徑上的關鍵瓶頸,這也是實務上會把.ldf放在低延遲儲存的原因。同一招是通用原理:PostgreSQL 的 WAL、MySQL InnoDB 的 redo log 都靠它衝吞吐。
記錄檔的實體結構:VLF
先建立直覺:環形跑道
交易記錄檔邏輯上是一本**「一直往後寫、不回頭」的流水帳**(每筆修改 append 一筆 log record)。但實體檔案不可能無限長,所以 SQL Server 把 .ldf 當成環形跑道重複使用:寫到檔尾就繞回開頭,把那些「已經不需要的」舊記錄所佔的空間覆蓋掉。
為了判斷「哪段空間可以覆蓋了」,SQL Server 不會一筆一筆去追(太細),而是把檔案切成若干個大格子——VLF(Virtual Log File,虛擬記錄檔),以 VLF 為單位判斷可否回收。VLF 就是這條跑道上的「格子」。
寫入方向 →(填滿就繞回開頭)
┌───────┬───────┬───────┬───────┬───────┐
│ VLF 1 │ VLF 2 │ VLF 3 │ VLF 4 │ VLF 5 │
└───────┴───────┴───────┴───────┴───────┘
哪些空間「還需要」:active 與 MinLSN
一筆 log record 只要符合以下任一條件,就還「需要」、不能被覆蓋:
- 它所屬的交易還沒結束(還可能要 rollback);
- 它還沒被 log 備份(FULL / BULK_LOGGED 模式);
- 下游還沒讀走(複寫 / CDC 等)。
從「最舊那筆還被需要的記錄」(它的位置叫 MinLSN)到「最新一筆」之間的所有記錄,合稱 active(作用中) 區段,絕對不能覆蓋;比 MinLSN 更舊的都算 inactive(非作用中),可以回收。以上圖為例(假設 MinLSN 落在 VLF 3):
| VLF | 狀態 | 能不能覆蓋 |
|---|---|---|
| VLF 1、VLF 2 | inactive(都比 MinLSN 舊) | ✅ 可重用 |
| VLF 3、VLF 4 | active(含 MinLSN 到最新的記錄) | ❌ 不可覆蓋 |
| VLF 5 | 尚未寫到 | 空的 |
兩個關鍵後果
- 以整個 VLF 為單位回收:一個 VLF 裡只要還有任何一筆 active 記錄,整格就不能重用。所以一個忘了關的長交易,會把它最舊記錄所在的那格「釘住」——因為環狀是循序的、不能跳過它去用後面的格子,它後面累積的空間全都繞不回來,這就是長交易撐爆 log 的原理。
- 繞不回去就得變大:跑道繞一圈,下一個要用的 VLF 還是 active(沒有可重用的格子),SQL Server 只好 autogrowth 在檔尾追加新的 VLF,
.ldf實體檔案因此變大。
對回前面的名詞:「截斷」的真面目就是把不再需要的 VLF 標記成 inactive/可重用(不是刪資料,是打上「可覆寫」標記);「收縮」 則是把檔尾那些空的 VLF 實體移除、還空間給 OS。
VLF 碎片化
若初始檔案太小、又用很小的 autogrowth 增量(例如每次只長 1MB / 10%),檔案會被切出成千上萬個小 VLF。VLF 過多會拖慢資料庫啟動、備份、還原與崩潰復原。
-- 查目前 VLF 數量(SQL Server 2016 SP2+ / 2017+)
SELECT COUNT(*) AS vlf_count
FROM sys.dm_db_log_info(DB_ID());
-- 舊版可用未公開指令:DBCC LOGINFO;
三種復原模式
復原模式(recovery model) 決定「記錄檔何時可以被釋放」以及「能不能做時間點還原」。這是理解記錄檔行為的總開關。
| 復原模式 | 記錄空間何時釋放 | 能否 log 備份 / 時間點還原 | 適用情境 |
|---|---|---|---|
| SIMPLE | CHECKPOINT 後自動截斷 | ❌ 不能做 log 備份,無 PITR | 開發/測試、資料可從來源重建、只需還原到上次完整/差異備份 |
| FULL | 只有在 BACKUP LOG 之後 |
✅ 可還原到任意時間點 | 正式環境、不允許遺失資料、需要 PITR |
| BULK_LOGGED | 同 FULL(BACKUP LOG 後) |
⚠️ 大量操作期間那段無法 PITR | 短暫切換,給大量匯入/索引重建降低 log 量用 |
查目前設定:
SELECT name, recovery_model_desc
FROM sys.databases
WHERE name = 'xxx';
切換模式(注意:切走再切回 FULL 會斷掉記錄檔鏈,見後文):
ALTER DATABASE xxx SET RECOVERY SIMPLE; -- 或 FULL / BULK_LOGGED
最常見的災難組合是:資料庫是 FULL 模式,但從來沒做過
BACKUP LOG。此時記錄檔永遠等不到釋放時機,只會一路長大直到塞爆磁碟。要嘛排定期 log 備份,要嘛(若不需要 PITR)改成 SIMPLE。
預設值:新資料庫繼承自 model
新建立的資料庫沒有寫死的固定預設值,而是繼承自 model 系統資料庫的復原模式(連初始大小、autogrowth 也一併繼承)。因此「預設是什麼」取決於該執行個體的 model 設定,而 model 的出廠值又跟 edition(版本) 有關——與作業系統(Linux / Windows)無關:
| Edition | model 預設復原模式 → 新資料庫預設 |
|---|---|
| Express / Express LocalDB | SIMPLE |
| Standard / Enterprise / Developer / Web / Evaluation | FULL |
這就是為什麼同一句建庫語法在不同機器上預設不同:
- Docker 映像(
mcr.microsoft.com/mssql/server)未指定MSSQL_PID時預設跑 Developer / Evaluation →model為 FULL → 新資料庫預設 FULL。 - Windows 安裝看安裝時選的版本;若裝的是免費的 Express →
model為 SIMPLE → 新資料庫預設 SIMPLE。
查證方式:
-- 版本與 edition
SELECT SERVERPROPERTY('Edition') AS edition,
SERVERPROPERTY('ProductVersion') AS version;
-- model 的復原模式 = 新資料庫的預設
SELECT name, recovery_model_desc
FROM sys.databases
WHERE name = 'model';
把
model改成某個模式後,之後所有新建資料庫都會跟著變(可當成統一政策的手段)。另外tempdb永遠是 SIMPLE 且不可更改,master/msdb預設 SIMPLE——這些系統庫的行為與「使用者新 DB 繼承model」是兩回事。
復原模式會影響記錄檔大小嗎?
會,但要把兩件常被混為一談的事分開看:「單筆操作寫多少 log」 和 「log 累積成多大檔案」。
一般 DML(INSERT / UPDATE / DELETE):每筆操作產生的 log 記錄量,三種模式幾乎一樣——復原模式不會讓單筆 DML 少寫 log。真正的差別在保留策略:SIMPLE 在 checkpoint 後就截斷,.ldf 通常維持得小;FULL / BULK_LOGGED 要等 BACKUP LOG 才截斷,沒做或少做備份就會一路累積、愈長愈大。所以日常負載下「log 檔多大」主要取決於模式 + 記錄檔備份頻率,不是單筆寫多少。
大量操作(BULK INSERT / bcp / SELECT INTO / 索引重建):這裡復原模式真的會改變產生的 log 量:
- SIMPLE / BULK_LOGGED:這些操作可觸發最小記錄(minimal logging)——只記錄空間配置(extent),不逐筆記,log 量大幅下降。這正是 BULK_LOGGED 存在的理由:短暫切過去跑大量匯入/建索引,壓低 log。
- FULL:同樣操作完整記錄,逐筆寫進 log → log 量暴增。
| 情境 | SIMPLE | FULL | BULK_LOGGED |
|---|---|---|---|
| 單筆 DML 的 log 量 | 一樣 | 一樣 | 一樣 |
.ldf 累積大小 |
小(checkpoint 自動截斷) | 易變大(需 log 備份才截斷) | 易變大(同 FULL) |
| 大量操作的 log 量 | 少(可最小記錄) | 多(完整記錄) | 少(可最小記錄) |
一個容易忽略的陷阱:BULK_LOGGED 下雖然記錄檔本身沒暴增,但接下來的
BACKUP LOG備份檔會很大——因為 log 備份必須把那段期間被改動的資料 extent 一併備進去,且那段期間無法時間點還原。最小記錄還需要TABLOCK、目標表未被複寫等額外條件,詳見 SQL Server 大量寫入與 TDS。
關鍵區別:截斷 vs 收縮
這是整個主題最容易搞混、也最重要的一組概念。兩者常被混為一談,但做的事完全不同:
| 截斷 Truncation | 收縮 Shrink | |
|---|---|---|
| 做什麼 | 把 inactive 的 VLF 標記為「可重用」 | 把記錄檔實體檔案縮小,還空間給作業系統 |
| 影響 | 釋放檔案內部空間(可被後續 log 覆寫) | 減少 .ldf 在磁碟上的實體大小 |
| 檔案大小 | 不變 | 變小 |
| 何時發生 | SIMPLE:checkpoint 後自動;FULL/BULK:BACKUP LOG 後 |
只有你手動 DBCC SHRINKFILE(或開啟自動收縮,不建議) |
| 日常角色 | 正常、自動、頻繁發生 | 例外操作,只在異常膨脹後一次性收尾 |
一句話記憶:
- 截斷 = 把檔案「內部」清空出可用空間(檔案還是那麼大)。
- 收縮 = 把「已經清空的部分」實體還給磁碟(檔案變小)。
常見誤解:「截斷記錄檔可以讓
.ldf變小」。不會。截斷只是讓內部空間可重用;要讓實體檔案變小得靠收縮,而收縮的前提是「內部已經先被截斷空出來」。所以正確順序永遠是:先讓它可截斷(備份 / checkpoint)→ 再收縮。
log_reuse_wait_desc:為什麼空間不能回收
當記錄檔一直長大、截斷不掉時,sys.databases 的 log_reuse_wait_desc 欄位會直接告訴你「是誰卡住了記錄空間的回收」。這是排查記錄檔問題的第一線索。
SELECT name, recovery_model_desc, log_reuse_wait_desc
FROM sys.databases
WHERE name = 'xxx';
常見值與對策:
| log_reuse_wait_desc | 意思 | 對策 |
|---|---|---|
NOTHING |
沒有東西擋著,空間可正常回收 | 正常,無需處理 |
CHECKPOINT |
等待下一次 checkpoint(通常短暫) | 多為暫時性;必要時手動 CHECKPOINT |
LOG_BACKUP |
FULL/BULK_LOGGED 下,等一次記錄檔備份 | 最常見:執行 BACKUP LOG,並排定期 log 備份 |
ACTIVE_TRANSACTION |
有長時間未 commit 的交易卡住 MinLSN | 找出並處理長交易(見診斷段) |
ACTIVE_BACKUP_OR_RESTORE |
正在進行備份/還原 | 等它完成 |
REPLICATION |
交易尚未送到散發者(複寫/CDC) | 檢查複寫/CDC 是否落後或停擺 |
AVAILABILITY_REPLICA |
Always On 次要複本同步落後 | 檢查 AG 健康度與次要複本 |
DATABASE_MIRRORING |
鏡像同步落後 | 檢查鏡像狀態 |
OLDEST_PAGE |
間接檢查點的最舊頁面所致 | 與 indirect checkpoint 設定有關 |
排查心法:先看
log_reuse_wait_desc找根因,再決定手段。看到LOG_BACKUP卻只顧著SHRINKFILE,是治標不治本——備份沒做,收縮完馬上又長回來。
記錄檔與複寫 / CDC / 下游消費者
交易記錄檔不只給資料庫自己復原用,它同時是許多下游功能的資料來源——它們都靠「讀 log」把變更送出去,因此都會反過來影響記錄檔何時能截斷。可以把記錄檔想成「單一事實來源」,而復原、回復、複寫、CDC、AG、鏡像都是它的下游消費者;任何一個消費者落後,都會卡住截斷。
交易式複寫是「log 讀取者」
交易式複寫(transactional replication)的 Log Reader Agent(記錄讀取器) 會掃描發行資料庫的交易記錄檔,把標記為要複寫的交易複製到散發資料庫(distribution DB),再送給訂閱端。所以複寫本質上是在「消費 log」。
關鍵後果:被標記待複寫的 log 記錄,在 Log Reader Agent 讀走之前都算 active,不能截斷。 於是一旦複寫停擺、Log Reader Agent 沒在跑、或散發端塞住,記錄檔就會一直長大,log_reuse_wait_desc 顯示 REPLICATION。
陷阱:SIMPLE 模式也會因此爆 log
交易式複寫支援任意復原模式(不強制 FULL),而待複寫的 log 是跨復原模式受保護的。因此即使是 SIMPLE 模式,checkpoint 也清不掉那些還沒被 log reader 讀走的記錄——這會讓「SIMPLE 就不會爆 log」的直覺失效,是很常見的踩雷點。
哪些複寫類型會 hold 住 log
| 類型 | 是否靠讀 log | 會不會 hold 住記錄檔 |
|---|---|---|
| 交易式複寫 / 對等式(P2P) | ✅ Log Reader Agent | 會 |
| CDC(變更資料擷取) | ✅ 同一套 log reader | 會 |
| 快照式複寫(snapshot) | ❌ 大量快照複製,不持續讀 log | 不會 |
| 合併式複寫(merge) | ❌ 靠觸發程序 + 系統表 | 不會 |
CDC 用的是與複寫相同的 log reader 機制,所以就算沒設定任何複寫,只要開了 CDC,
log_reuse_wait_desc一樣可能是REPLICATION,行為與交易式複寫相同。
其他也吃 log 的下游功能
複寫不是唯一讀 log 的功能。以下也一樣靠 log 傳遞變更,但用不同的 log_reuse_wait_desc 值,且對復原模式要求不同:
| 功能 | log_reuse_wait_desc | 復原模式要求 |
|---|---|---|
| 交易式複寫 / CDC | REPLICATION |
任意(含 SIMPLE) |
| Always On 可用性群組 | AVAILABILITY_REPLICA |
必須 FULL |
| 資料庫鏡像 | DATABASE_MIRRORING |
必須 FULL |
| 記錄傳送(log shipping) | 靠 BACKUP LOG |
FULL / BULK_LOGGED |
排查:懷疑是複寫卡住 log
-- 1) 確認就是複寫在卡(值為 REPLICATION 就往下查)
SELECT name, log_reuse_wait_desc
FROM sys.databases
WHERE name = 'xxx';
-- 2) 看複寫延遲與待散發的量、Log Reader Agent 進度
EXEC sp_replcounters;
-- 或到 distribution DB 查 undistributed commands、用 Replication Monitor 看 Log Reader Agent 狀態
-- 3) 已移除複寫卻仍卡 REPLICATION(殘留的孤兒中繼資料)
EXEC sp_removedbreplication 'xxx'; -- 清掉殘留複寫設定(確認真的不用了再執行)
-- 若是 CDC:EXEC sys.sp_cdc_disable_db;
治本是「讓 log reader 正常消化」——把 Log Reader Agent 修好、讓散發端跟上;而不是去收縮記錄檔。log reader 沒動,收縮完馬上又長回來。
CHECKPOINT 在做什麼
CHECKPOINT 把記憶體裡的 dirty page(改過但還沒寫回磁碟的資料頁)批次寫回資料檔,並在記錄檔寫下一個檢查點標記。
它的兩個作用:
- 縮短當機復原時間:復原時的 redo 只需從「最後一個檢查點」開始掃,而不是從頭掃整個 log。
- 在 SIMPLE 模式下觸發截斷:checkpoint 完成後,不再需要的 log 會被標記成可重用(active 記錄之外的部分)。
CHECKPOINT; -- 手動強制一次檢查點
觸發來源:
| 類型 | 觸發時機 |
|---|---|
| 自動 (automatic) | 依 recovery interval 伺服器設定,SQL Server 自行判斷 |
| 間接 (indirect) | 依資料庫的 TARGET_RECOVERY_TIME,把 dirty page 持續刷出以壓低復原時間(2016+ 預設) |
| 手動 (manual) | 執行 CHECKPOINT 陳述式 |
| 內部 (internal) | 備份、ALTER DATABASE 等操作前系統自動下 |
注意:在 FULL / BULK_LOGGED 模式,
CHECKPOINT不會截斷記錄檔——那裡的截斷條件是「做過BACKUP LOG」。所以在 FULL 模式對著爆掉的 log 猛下CHECKPOINT是沒用的。
記錄檔備份與記錄檔鏈
在 FULL / BULK_LOGGED 模式,只有 BACKUP LOG 能釋放記錄空間。它做兩件事:把自上次以來的 active log 記錄備份出去,然後截斷已不需要的部分。
BACKUP LOG xxx
TO DISK = N'D:\backup\xxx_log_20260702_1200.trn';
記錄檔鏈(log chain) 是一連串「完整備份 → 一連串 log 備份」的連續序列,時間點還原就是靠它把資料庫還原到某個精確時刻:
完整備份 (Full)
→ log 備份#1 → log 備份#2 → log 備份#3 → ...
(沿著這條鏈套用,就能還原到任一時間點)
會斷鏈的動作要特別小心(斷鏈後必須立刻重做一次完整或差異備份,PITR 才能接續):
- 把復原模式切成 SIMPLE(即使又切回 FULL,中間那段鏈已斷)。
- 用
BACKUP LOG ... WITH NO_LOG/TRUNCATE_ONLY(舊版才有,且已棄用)強制截斷。
正確做法:想控制 FULL 模式下的記錄檔大小,答案幾乎永遠是「排定期
BACKUP LOG」(例如每 15 分鐘一次),而不是去收縮它。定期 log 備份同時達成兩件事:控制 log 大小 + 保住 PITR 能力。
收縮記錄檔 DBCC SHRINKFILE
DBCC SHRINKFILE 把指定檔案的實體大小縮小,把已截斷的空閒空間還給作業系統。它只能收回已經是「可重用」的那部分,所以收縮前必須先讓記錄檔可截斷。
-- 語法:DBCC SHRINKFILE ( { 邏輯檔名 | file_id }, 目標大小_MB )
DBCC SHRINKFILE (N'xxx_log', 512); -- 縮到約 512 MB
參數細節:
- 第一個參數是邏輯檔名(logical name),不是實體
.ldf路徑。N前綴代表 Unicode 字串。 - 第二個參數單位是 MB,代表「想縮到的目標大小」。
- 查邏輯檔名對照:
SELECT name AS logical_name, type_desc, physical_name,
size * 8 / 1024 AS size_mb
FROM sys.database_files; -- LOG 那列的 name 就是要填的邏輯檔名
常見卡點:收縮不動 / 只縮一點點。 原因是 active 記錄剛好落在檔案實體尾端的 VLF——尾端被佔著就無法把檔案縮短。解法是先讓 active 位置往前移,再收縮第二次:
-- FULL 模式
BACKUP LOG xxx TO DISK = N'...'; -- 釋放並讓 active VLF 往前挪
DBCC SHRINKFILE (N'xxx_log', 512); -- 再收一次
-- SIMPLE 模式
CHECKPOINT;
DBCC SHRINKFILE (N'xxx_log', 512);
⚠️ 收縮是反模式,別排成定期維護作業。 記錄檔縮小後,遇到大交易又會 autogrow 長回來,一縮一長會造成 VLF 碎片化與實體檔案碎片,反而拖慢啟動、備份與還原。收縮只該當作「異常膨脹後的一次性收尾」,縮完就設一個合理大小與 autogrowth,別再動它。同理,資料庫層級的自動收縮(AUTO_SHRINK)也應關閉。
診斷指令
一組平時/出事時常用的查詢:
-- 1) 復原模式 + 為什麼截斷不掉(第一線索)
SELECT name, recovery_model_desc, log_reuse_wait_desc
FROM sys.databases
WHERE name = 'xxx';
-- 2) 目前資料庫的記錄檔用量(總大小、已用位元組、已用百分比)
SELECT * FROM sys.dm_db_log_space_usage; -- 2012+,只回目前 DB
-- 3) 一次看所有資料庫的 log 大小與使用率
DBCC SQLPERF(LOGSPACE);
-- 4) VLF 數量(碎片化指標)
SELECT COUNT(*) AS vlf_count FROM sys.dm_db_log_info(DB_ID());
-- 5) 找出卡住記錄回收的長交易(log_reuse_wait_desc = ACTIVE_TRANSACTION 時)
DBCC OPENTRAN('xxx');
-- 或用 DMV 看誰交易開最久
SELECT s.session_id, s.login_name, s.host_name,
t.transaction_id, t.transaction_begin_time
FROM sys.dm_tran_active_transactions t
JOIN sys.dm_tran_session_transactions st ON st.transaction_id = t.transaction_id
JOIN sys.dm_exec_sessions s ON s.session_id = st.session_id
ORDER BY t.transaction_begin_time;
-- 6) 記錄檔的實體檔案資訊(邏輯名、路徑、大小、autogrowth)
SELECT name AS logical_name, physical_name,
size * 8 / 1024 AS size_mb,
CASE is_percent_growth WHEN 1 THEN CONCAT(growth, ' %')
ELSE CONCAT(growth * 8 / 1024, ' MB') END AS autogrowth
FROM sys.database_files
WHERE type_desc = 'LOG';
實戰:記錄檔爆掉的排查流程
以最常見情境「某資料庫的 .ldf 異常變大、快塞爆磁碟」為例,把前面的觀念串成一套 SOP:
① 診斷:查復原模式 + log_reuse_wait_desc(先搞清楚根因)
→ ② 依根因處理(讓記錄檔變成「可截斷」狀態)
→ ③ 讓內部空間釋放(BACKUP LOG 或 CHECKPOINT)
→ ④ 一次性收縮實體檔案,還空間給 OS
→ ⑤ 設定合理大小與 autogrowth,避免再犯
對照本筆記開頭那組指令,它其實就是「SIMPLE 模式下的收尾三步」:
-- ① 診斷:這個 DB 是什麼模式?為什麼截斷不掉?
SELECT recovery_model_desc, log_reuse_wait_desc
FROM sys.databases
WHERE name = 'xxx';
-- 假設看到 recovery_model_desc = SIMPLE、log_reuse_wait_desc = NOTHING/CHECKPOINT
-- ② + ③ 強制一次檢查點:SIMPLE 模式下觸發截斷,把內部空間釋放出來
CHECKPOINT;
-- ④ 一次性收縮:把已釋放的空間實體還給磁碟
DBCC SHRINKFILE (N'xxx_log', 512);
不同根因對應不同的第 ②③ 步:
| 診斷結果 | 處理 |
|---|---|
SIMPLE + NOTHING/CHECKPOINT |
CHECKPOINT; 後即可收縮(上例) |
FULL + LOG_BACKUP |
先 BACKUP LOG(治本應排定期備份),再視需要收縮一次 |
任何模式 + ACTIVE_TRANSACTION |
先用 DBCC OPENTRAN 找出長交易並結束它,log 才回收得掉 |
FULL + REPLICATION |
檢查複寫/CDC 是否落後或停擺,讓它把 log 消化掉 |
若已經跳出 error 9002(交易記錄檔已滿):緊急處理是「先讓 log 可截斷」——FULL 模式立刻
BACKUP LOG(若磁碟真的滿了,可備份到另一顆磁碟或臨時加一個 log 檔);SIMPLE 模式則CHECKPOINT。切忌直接刪.ldf,那會讓資料庫無法啟動。
最佳實踐
- 選對復原模式:需要 PITR 才用 FULL;不需要就用 SIMPLE。別預設 FULL 卻從不做 log 備份。
- FULL 模式一定排定期
BACKUP LOG:這是控制記錄檔大小的正解,同時保住 PITR。 - 別把收縮當維護作業:只在異常膨脹後一次性收尾,並關閉
AUTO_SHRINK。 - 一開始就設足夠的初始大小:給
.ldf合理初始大小,避免上線後狂 autogrow 長出一堆小 VLF。 - autogrowth 用固定 MB 增量:例如 512MB / 1GB,不要用百分比、也別用太小的 1MB。
- 監控 log 使用率:
sys.dm_db_log_space_usage/DBCC SQLPERF(LOGSPACE)納入日常監控,別等塞爆才處理。 - 切模式要小心斷鏈:SIMPLE ⇄ FULL 之間切換後,回到 FULL 要立刻做一次完整或差異備份,重建記錄檔鏈。
常見問題
問題 1:截斷記錄檔會讓 .ldf 檔案變小嗎?
不會。截斷只釋放檔案內部的可重用空間;要讓實體檔案變小得用 DBCC SHRINKFILE(收縮),且收縮前必須先截斷空出空間。兩者是不同動作。
問題 2:為什麼我的記錄檔一直長大、縮不掉?
查 log_reuse_wait_desc。絕大多數是 FULL 模式沒做 BACKUP LOG(值為 LOG_BACKUP),其次是長交易(ACTIVE_TRANSACTION)或複寫落後(REPLICATION)。對症處理,別只顧收縮。
問題 3:可以直接刪掉 .ldf 檔案嗎?
不行。記錄檔是資料庫一致性的一部分,直接刪會導致資料庫無法啟動(進入 suspect / recovery pending)。要縮小請用收縮,不是刪檔。
問題 4:收縮後記錄檔又長回原本大小?
代表你把它縮得比實際工作負載需要的還小,或 autogrowth 設定不當。找出正常運作所需的大小,設成初始大小,並用合理的固定 MB autogrowth,就別再收縮它。
問題 5:CHECKPOINT 為什麼在 FULL 模式沒把 log 清掉?
FULL / BULK_LOGGED 模式的截斷條件是「做過 BACKUP LOG」,不是 checkpoint。CHECKPOINT 只在 SIMPLE 模式才會順帶觸發截斷。
問題 6:SIMPLE 復原模式可以做複寫嗎?
可以。交易式、快照式、合併式複寫都支援任意復原模式,包含 SIMPLE——複寫不強制 FULL(強制 FULL 的是 Always On 可用性群組與資料庫鏡像)。SIMPLE 下待複寫的 log 一樣會被 hold 到 Log Reader Agent 讀走(log_reuse_wait_desc = REPLICATION),讀走後才在 checkpoint 截斷。差別在代價:SIMPLE 沒有記錄檔備份鏈、無法時間點還原,publisher 若需從舊備份還原,較可能得重新初始化訂閱。因此正式環境的 publisher 仍常選 FULL,但那是為了復原能力,不是複寫的硬性要求。
問題 7:這些指令別的資料庫也能用嗎?
不能。sys.databases、recovery_model_desc、log_reuse_wait_desc、獨立的 CHECKPOINT 陳述式、DBCC SHRINKFILE 都是 SQL Server(T-SQL)專屬語法。概念(交易記錄、檢查點、記錄檔管理)各家資料庫相通,但 PostgreSQL 的 WAL、MySQL InnoDB 的 redo log、Oracle 的 redo/archive log 各有自己的機制與指令。
總結與速查表
核心要點
- 交易記錄檔靠 WAL(log 先於資料頁落地)保證持久性與可復原性。
.ldf內部由 VLF 組成,環狀重複使用;active 記錄不能被覆寫。- 復原模式是總開關:SIMPLE(checkpoint 自動截斷)、FULL(
BACKUP LOG才截斷、可 PITR)、BULK_LOGGED(給大量操作降 log 量)。 - 截斷 ≠ 收縮:截斷釋放內部空間(檔案不變小、且自動頻繁發生),收縮縮小實體檔案(例外操作,別排定期)。
- 出事先看
log_reuse_wait_desc找根因,再決定BACKUP LOG/CHECKPOINT/ 處理長交易,最後才視需要收縮一次。
指令速查
| 目的 | 指令 |
|---|---|
| 查模式 + 截斷卡在哪 | SELECT recovery_model_desc, log_reuse_wait_desc FROM sys.databases WHERE name='xxx'; |
| 查記錄檔用量(目前 DB) | SELECT * FROM sys.dm_db_log_space_usage; |
| 查所有 DB 的 log 使用率 | DBCC SQLPERF(LOGSPACE); |
| 查 VLF 數量 | SELECT COUNT(*) FROM sys.dm_db_log_info(DB_ID()); |
| 找長交易 | DBCC OPENTRAN('xxx'); |
| 記錄檔備份(FULL/BULK) | BACKUP LOG xxx TO DISK = N'...'; |
| 強制檢查點(SIMPLE 截斷) | CHECKPOINT; |
| 收縮記錄檔 | DBCC SHRINKFILE (N'xxx_log', 512); |
| 切復原模式 | ALTER DATABASE xxx SET RECOVERY SIMPLE|FULL|BULK_LOGGED; |
快速記憶
| 方面 | 說明 |
|---|---|
| 是什麼 | 記錄每筆修改的 .ldf,靠 WAL 保證持久與可復原 |
| 為什麼會爆 | FULL 模式沒做 log 備份 / 長交易 / 下游同步落後 |
| 怎麼查 | log_reuse_wait_desc 一眼看出誰卡住回收 |
| 怎麼救 | 依根因 BACKUP LOG 或 CHECKPOINT → 需要時收縮一次 → 設好大小別再動 |
建立日期:2026-07-02