最近工作上碰到需要對 Sql Server 做 snapshot 的場合,所以就順便來研究一下怎麼對 Sql Server 做備份
按照過去的經驗,我第一個想法就是找 Sql Server 有沒有類似 mysqldump 或 pg_dump 的工具可以直接將 schema 及 data 導出成 SQL 檔,這樣方便閱讀也方便各種場景下重建資料庫,尤其是在資料庫的 table 數量及資料量並不算大的前提下
結果,我太天真了,Microsoft 怎麼可能這麼好用 (誤),人家就是要走自己的路!
備份檔案格式
Sql Server 備份出來的檔案格式有以下幾種
- bak: schema + data + log + file,適合做定時備份,因為他會保持包含 index 在內的資料以維持 single point of time 的 transactionally consistent
- DACPAC: schema,適合建立測試環境或比對各環境版本;但也支持下參數加上 data 的備份。實際上是 xml 的壓縮檔
- BACPAC: schema + data,適合轉移或 archive db。schema 部分與 DACPAC 相同,只是加上用 BCP 匯出資料
- sql
備份工具
主要有以下幾種備份工具
SqlCmd (官方)
需要安裝 Sql Server 或 Microsoft Command Line Utility。若 powershell 要使用,還需要安裝 SqlServer 模組
只能產生出 bak 檔,可包含 schema 及 data,但本身並不包含建立 database
BCP (官方)
針對同步大量資料的工具,不支援同步 schema
SMO (官方)
這已經算是要寫程式來控制備份機制了,除了可以用 powershell 來寫以外,其實也可以直接寫 c# 專案來使用這個 library 做備份工作
只要有安裝過 Sql Server 後就會內附該有的 dll
會產生 schema 及 data 的 sql script,但我沒找到產生建立 database 部分的選項可以做設定
可以參考這篇 Automated Script-generation with Powershell and SMO 來撰寫 powershell 檔
SqlPackage.exe (官方)
可以不需要安裝 sql server,是獨立的程式,而且跨平台
可以選擇 extract (產出 DACPAC) 或 export (產出 BACPAC)
dacpac 只是壓縮檔,解開後,schema 的部分都是 xml,data 就是用 bcp 輸出的檔案。bacpac 也是一樣
不過 SqlPackage 有幾個問題
- Extract dacpac 也可以包含 data (
ExtractAllTableData='True'
),而且還可以設定是否要 verify (VerifyExtraction='False'
),這樣 Export bacpac 不知道要拿來幹嘛 - 還有 Export 不能關閉 verify (
VerifyExtraction='False'
),會出現以下警告,所以甚至像是在 store procedure 內有用到別的 database 的狀況也會被拒絕
*** ‘VerifyExtraction’ is not a valid argument for the ‘Export’ action.
dbatools
這是一個第三方套件,功能多到看不完,但官方文件雖然列的一堆功能,但使用者要怎麼使用坦白說我覺得不是很友善
mssql-scripter
需要安裝並設定好 Python 的環境變數,然後用 pip 安裝
可以產生 schema 及 data 的 sql script
對本地安裝的 Sql Server 操作沒問題,但我拿它來對 docker container 中的 mssql 操作會報權限問題
結論
因為考慮到資料庫 table 以及非 log 資料並不多,所以最後選擇用 powershell 操作 smo 的方式產出 sql 檔的方式做 snapshot
以下是我參考這篇 Automated Script-generation with Powershell and SMO 撰寫的 powershell 檔
param($datasource, $username, $password, $backupPath)
# set "Option Explicit" to catch subtle errors
set-psdebug -strict
# you can opt to stagger on, bleeding, if an error occurs
$ErrorActionPreference = "stop"
# create backup directory if not exist
New-Item -ItemType Directory -Force -Path $backupPath | Out-Null
cd $backupPath
# get current date in yyyyMMdd format
$currentDate = Get-Date -format yyyyMMdd
Write-Output "Current Date = $currentDate"
# get the latest backup directory
Get-ChildItem -Directory -Path . | Sort-Object CreationTime -Descending | Select-Object -First 1 -ExpandProperty Name -OutVariable latestDir | Out-Null
Write-Output "Latest Backup Directory = $latestDir"
# get the latest version of today's backups
$currentVersion = 0
if ($latestDir -Match $currentDate)
{
$currentVersion = $latestDir.Substring($currentDate.Length + 1)
}
Write-Output "Current Version = $currentVersion"
# create directory for the next version of backup
$nextVersion = [int]$currentVersion + 1
Write-Output "Next Version = $nextVersion"
$nextVersionDir = "$($backupPath)\$($currentDate)_$($nextVersion)"
New-Item -ItemType Directory -Force -Path $nextVersionDir | Out-Null
# load SMO assembly
$msPrefix = 'Microsoft.SqlServer'
[System.Reflection.Assembly]::LoadWithPartialName("$msPrefix.SMO") | out-null
[System.Reflection.Assembly]::LoadWithPartialName("$msPrefix.SMOExtended") | out-null
$smoPrefix = "$msPrefix.Management.Smo"
# set connection
$server = New-Object ("$smoPrefix.Server") $datasource
$server.ConnectionContext.LoginSecure = $false
$server.ConnectionContext.set_Login($username)
$server.ConnectionContext.set_Password($password)
# check connection
if ($server.Version -eq $null ) {Throw "Can't find the instance $datasource"}
# loop through databases names to do back up
foreach ($database in $server.databases)
{
# skip system databases
if ($database.Name -in 'Master','Model','MSDB','TempDB','SSISDB','distribution','ReportServer','ReportServerTempDB') {continue}
# skip offline databases
if ($database.Status -ne 'Normal') {
Write-Output ("Skipping Offline: {0}" -f $database.Name)
continue
}
$backupFile = "$($nextVersionDir)\$($database.Name).sql"
Write-Output "Start to back up $($database.Name)"
# manually add create database sql statement at the beginning of the sql file
New-Item $backupFile -ItemType File | Out-Null
Add-Content $backupFile "USE [master]"
Add-Content $backupFile "GO"
Add-Content $backupFile "Drop Database if exists [$($database.Name)];"
Add-Content $backupFile "GO"
Add-Content $backupFile "USE [master]"
Add-Content $backupFile "GO"
Add-Content $backupFile "CREATE DATABASE [$($database.Name)]"
Add-Content $backupFile "GO"
Add-Content $backupFile "USE [$($database.Name)]"
Add-Content $backupFile "GO"
Add-Content $backupFile ""
# set export configuration
$transfer = New-Object ("$smoPrefix.Transfer") $database
$transfer.Options.ScriptSchema = $true
$transfer.Options.ScriptData = $true
$transfer.Options.ScriptBatchTerminator = $true
$transfer.Options.ToFileOnly = $true
$transfer.Options.AppendToFile = $true
$transfer.Options.Encoding = New-Object ("System.Text.UTF8Encoding")
$transfer.Options.AllowSystemObjects = $false
$transfer.Options.Permissions = $true
$transfer.Options.SchemaQualify = $true
$transfer.Options.ExtendedProperties = $true
$transfer.Options.DRIAll = $true
$transfer.Options.Indexes = $true
$transfer.Options.Triggers = $true
$transfer.Options.IncludeHeaders = $true
$transfer.Options.IncludeIfNotExists = $true
$transfer.Options.Filename = "$backupFile"
$transfer.Options.EnforceScriptingOptions = $true
# start scripting
$transfer.EnumScriptTransfer()
Write-Output "Finished backing up $databaseName to $backupFile"
}
Last modified on 2021-11-16