SQL Server 的備份檔,這個月又比上個月大了好幾 GB。打開資料庫一看,資料表筆數沒什麼變,那這些空間是哪來的?

很多人第一反應是「資料庫膨脹了」,然後跑去 DBCC SHRINKDATABASE 想把它壓回來。先停手——SHRINK 是 SQL Server 裡最容易幫倒忙的指令之一,後面會講為什麼。先搞懂備份檔為什麼會長大。

完整備份複製的是資料庫裡「已配置的資料頁」加上一小段交易記錄,不是 mdf 檔的實體大小。一個 100 GB 的 mdf,裡面只有 40 GB 的頁真有資料,備份檔大約就是 40 GB 出頭。所以備份檔變大,意思是已配置的頁變多了——或者,根本不是資料庫的問題,是備份檔本身在累積。順這條線,由最常見往下查。

真因一:你每天往同一個檔案疊備份(最常見)

頭號元兇,也最容易被忽略。BACKUP DATABASE 不指定 WITH INIT 時,預設是 NOINIT——把這次備份附加到既有檔案,不是覆蓋。

如果排程固定備份到 D:\Backup\MyDB.bak、每天跑又沒寫 INIT,這個檔案裡其實疊了一份又一份完整備份,當然每天長大。

要確認是不是這個問題,用 RESTORE HEADERONLY 把檔案裡的備份集列出來:

1
RESTORE HEADERONLY FROM DISK = 'D:\Backup\MyDB.bak';

回傳結果每一列是一份備份,看 Position 欄位。如果跑出來是一長串、Position 從 1 排到 30,那答案就在這了——不用再往下查。修法是改用 INIT 覆蓋,或更好的做法:每次用帶時間戳的新檔名。

1
2
3
4
5
6
7
-- ❌ 不指定就是 NOINIT,一直往同一檔疊
BACKUP DATABASE MyDB TO DISK = 'D:\Backup\MyDB.bak';

-- ✅ 用 INIT 覆蓋,更好的是每次用帶時間戳的新檔名
BACKUP DATABASE MyDB
TO DISK = 'D:\Backup\MyDB_20241022.bak'
WITH INIT, COMPRESSION;

真因二:頁密度太低(內部碎片)

如果備份檔不是疊出來的,那才往資料庫裡找。這裡要先澄清一個常見誤解——索引碎片分兩種:

  • 外部碎片:頁的邏輯順序和實體順序對不上。它影響查詢效能,但不影響備份大小,因為備份是一頁一頁原樣複製。
  • 內部碎片:頁面沒填滿,同樣資料用掉更多頁。這個會讓備份變大,因為已配置的頁變多了。

所以拿 avg_fragmentation_in_percent(外部碎片指標)去解釋備份變大,會找錯方向。要看的是頁密度:

1
2
3
4
5
6
7
8
9
10
-- 看頁密度:avg_page_space_used_in_percent 偏低 = 內部碎片重
-- 第五個參數要傳 'DETAILED',這個欄位才會有值
SELECT OBJECT_NAME(ips.object_id) AS TableName,
i.name AS IndexName,
ips.avg_page_space_used_in_percent,
ips.page_count
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'DETAILED') ips
JOIN sys.indexes i ON i.object_id = ips.object_id AND i.index_id = ips.index_id
WHERE ips.page_count > 1000
ORDER BY ips.avg_page_space_used_in_percent;

頁密度低通常是 fill factor 設太低、或大量隨機寫入造成 page split。用 ALTER INDEX ... REBUILD 配合合理的 fill factor 重建,能把頁填回去。

順帶澄清兩個常被當兇手、其實不是的:RCSI/快照隔離的資料列版本存在 tempdb,而 tempdb 不進使用者資料庫的備份,所以它通常不是備份變大的原因;LOB 欄位異動也是頁層級備份,不會因為改一個欄位就把整個物件重複塞進去。

真因三:資料真的長大了

最樸素的可能:資料就是變多了。查實際各表佔用:

1
2
EXEC sp_spaceused;   -- 整庫概況
-- 或用 sys.dm_db_partition_stats 看各表已用頁數

如果是這個,那不是「異常」,是該規劃容量和歸檔策略了。

為什麼別 SHRINK

回到開頭那個衝動。DBCC SHRINKDATABASE 會把檔案末端的頁搬到前面的空位,問題是——每搬一頁,就在它所屬的索引製造碎片。跑完一次 shrink,索引碎片可以從接近 0% 直接飆到接近 100%,你做的索引維護全廢。

更糟的是「先重建索引、再 shrink」:REBUILD 剛把索引整理乾淨,shrink 立刻打散,檔案下次又長回來,陷入 shrink→碎片→rebuild→長大→再 shrink 的死循環。

備份檔變大,幾乎沒有一種情況的正解是 shrink 資料庫。對症處理上面三個真因就好。

正確的維護方式

  • 備份用 WITH INIT(或每次帶時間戳的新檔名),並加 WITH COMPRESSION——壓縮通常能明顯縮小備份檔,是該預設打開的選項。再設保留策略定期清舊檔。
  • 索引維護用 ALTER INDEX ... REBUILD(碎片不嚴重時 REORGANIZE),別再用 DBCC DBREINDEX——那指令 SQL Server 2005 起就標記棄用了。
  • 例行 DBCC CHECKDB 確認沒有實體損壞、監控成長趨勢設警示,這些是日常該帶上的維護。

收尾

備份檔暴增,照這個順序查最快:先用 RESTORE HEADERONLY 看是不是同一個檔疊了多份備份(最常見),再用 sp_spaceused 看資料是不是真的長大,最後才查頁密度。

SHRINK 不在這個清單裡。它解決的是「檔案實體很大但裡面很空」這種特定情況,代價是把索引打爛——動之前要非常清楚自己在做什麼。下次看到 .bak 又肥一圈,先 RESTORE HEADERONLY 打開看看,那通常就是答案。