用 SqlPackage 导出 SQL Server 数据库结构快照
有些数据库运行了很多年,仓库里也留着一套 SQL 脚本。问题是这些脚本通常靠人维护:新增字段时补一份,临时改动不一定补,已经删除的对象还躺在旧文件里。拿它们做数据分析设计,很容易从一份过时的结构开始。
直接连着线上数据库分析也不合适。查询压力是一方面,更麻烦的是分析过程无法固定下来:今天看到的字段和下个月未必相同,其他人也很难复现当时的判断。
我最后采用的办法很朴素:从数据库镜像导出一份只含结构的 SQL Project,提交到 Git。表、字段、索引、视图和存储过程会拆成普通 .sql 文件,既能全文搜索,也能比较两次导出之间的变化。
为什么导出 SQL Project
SqlPackage /Action:Extract 可以生成 .dacpac,也可以生成由 .sqlproj 和 .sql 文件组成的 SQL Project。
DACPAC 适合部署和工具链处理。数据分析前的结构阅读更适合 SQL Project,因为可以直接使用 rg 搜索字段和对象:
rg -n "OrderStatus|CreateTime|StoredProcedureName" db/schema结构快照不需要表数据。导出参数中应明确保留:
/p:ExtractAllTableData=False它不是数据库备份,也不能替代数据画像。它解决的只是一个问题:现在有哪些数据库对象,它们的定义是什么。
安装 SqlPackage
SqlPackage 是 Microsoft 基于 DacFx 提供的命令行工具。已经有 .NET SDK 时,可以安装为全局工具:
dotnet tool install --global Microsoft.SqlPackage以后更新:
dotnet tool update --global Microsoft.SqlPackage确认命令已经进入 PATH:
sqlpackage /Version如果刚安装完仍提示找不到命令,重新打开一个终端即可。
用 PowerShell 批量导出多个数据库
SqlPackage 一次只处理一个数据库。数据库多时,不必复制十份连接字符串,用数组依次执行即可。
先在当前 PowerShell 会话中设置连接信息。下面是一条完整命令,可以直接粘贴执行。环境变量只对当前终端及其子进程有效,关闭终端后失效:
$env:SQLPACKAGE_SERVER = 'HOST,1433'; $env:SQLPACKAGE_USER = 'DB_USER'; $env:SQLPACKAGE_PASSWORD = 'REDACTED_SECRET'; $env:SQLPACKAGE_SCHEMA_ROOT = Join-Path (Get-Location) 'db\schema'密码中即使有分号,也不要自己拼接或额外包引号。后面的 DbConnectionStringBuilder 会负责连接字符串转义。
批量导出包含循环、分支和结果汇总,不适合冒充一条终端命令。新建 export-sqlserver-schema.ps1,把下面的脚本保存进去:
$databases = @(
'DatabaseA'
'DatabaseB'
'DatabaseC'
)
New-Item -ItemType Directory -Force -Path $env:SQLPACKAGE_SCHEMA_ROOT | Out-Null
$results = [System.Collections.Generic.List[object]]::new()
foreach ($database in $databases) {
$targetPath = Join-Path $env:SQLPACKAGE_SCHEMA_ROOT $database
if (Test-Path -LiteralPath $targetPath) {
[void]$results.Add([pscustomobject]@{
Database = $database
Status = '跳过'
Detail = '目标目录已存在'
})
continue
}
$connection = [System.Data.Common.DbConnectionStringBuilder]::new()
$connection['Data Source'] = $env:SQLPACKAGE_SERVER
$connection['User ID'] = $env:SQLPACKAGE_USER
$connection['Password'] = $env:SQLPACKAGE_PASSWORD
$connection['Initial Catalog'] = $database
$connection['Encrypt'] = $false
$connection['TrustServerCertificate'] = $true
$sqlPackageArguments = @(
'/Action:Extract'
"/SourceConnectionString:$($connection.ConnectionString)"
"/TargetFile:$targetPath"
'/OverwriteFiles:True'
'/p:ExtractTarget=SqlProject'
'/p:ExtractAllTableData=False'
'/p:ExtractApplicationScopedObjectsOnly=True'
'/p:ExtractReferencedServerScopedElements=False'
'/p:IgnorePermissions=True'
'/p:IgnoreUserLoginMappings=True'
'/p:VerifyExtraction=False'
'/p:ScriptSortElementsByName=True'
)
Write-Host "正在导出 $database ..."
& sqlpackage @sqlPackageArguments
$exitCode = $LASTEXITCODE
if ($exitCode -eq 0) {
[void]$results.Add([pscustomobject]@{
Database = $database
Status = '成功'
Detail = $targetPath
})
} else {
[void]$results.Add([pscustomobject]@{
Database = $database
Status = '失败'
Detail = "退出码 $exitCode"
})
}
}
$results | Format-Table -AutoSize
$failed = @($results | Where-Object Status -eq '失败')
if ($failed.Count -gt 0) {
Write-Warning "$($failed.Count) 个数据库导出失败,请检查上方 SqlPackage 输出。"
}回到刚才设置环境变量的终端,用一条命令运行脚本:
& .\export-sqlserver-schema.ps1单个数据库失败不会中断整个列表。成功后,目录结构大致如下:
db/schema/
├── DatabaseA/
│ ├── DatabaseA.sqlproj
│ ├── dbo/
│ └── Security/
├── DatabaseB/
└── DatabaseC/TargetFile 不能是已有目录
第一次使用 ExtractTarget=SqlProject 时,我把 TargetFile 指向了提前创建好的数据库目录,得到这条错误:
*** The TargetFile argument cannot reference an existing directory.这里的 TargetFile 名字有些误导。输出 DACPAC 时它确实是文件;输出 SQL Project 时,它表示将要创建的项目目录。父目录可以存在,数据库对应的目标目录不能存在。
/OverwriteFiles:True 也不会改变这个要求。上面的脚本遇到已有目录会跳过,避免把一次成功导出的结果和另一次结果混在一起。
需要更新快照时,应先确认并删除单个数据库目录,再重新导出。下面的清理命令保持为单行,修改数据库名后可以直接粘贴执行:
$expectedPath = [System.IO.Path]::GetFullPath((Join-Path (Get-Location) 'db\schema\DatabaseA')); $schemaPath = (Resolve-Path -LiteralPath $expectedPath).Path; if ($schemaPath -ne $expectedPath) { throw "结构目录不符合预期:$schemaPath" }; Remove-Item -LiteralPath $schemaPath -Recurse -Force导出完成后直接看 Git 差异:
git status --short db/schema/DatabaseA
git diff -- db/schema/DatabaseA不建议按日期保留多份完整目录。当前结构放在工作区,历史变化交给 Git,查起来更直接。
跨数据库引用为什么会让验证失败
老系统经常在视图或存储过程中直接引用另一个数据库,甚至 Linked Server。镜像环境没有全部依赖对象时,VerifyExtraction=True 可能让结构导出失败。
如果目的只是保存当前定义,可以使用:
/p:VerifyExtraction=False关闭验证不会主动删掉 SQL 文本中的跨库引用,但这份项目不一定可以独立编译。分析跨库查询时,仍要把相关数据库都导出,再沿对象引用逐个核对。
这也解释了为什么结构快照不能被当成“数据库项目构建成功”的证明。二者目标不同:一个保存现场,一个验证所有依赖是否完整。
加密存储过程为什么只剩一段占位文本
有些导出文件中只能看到:
-- 该脚本主体已加密,无法在此重现。
RETURN这不是 SqlPackage 漏导。SQL Server 对使用 WITH ENCRYPTION 创建的模块不公开定义,sys.sql_modules.definition 对这类对象返回 NULL。导出工具拿不到原始文本,自然无法生成过程主体。
先排除权限问题。OBJECT_DEFINITION 在调用者没有查看对象定义的权限时也会返回 NULL。如果账号已经具备相应权限,导出结果仍明确标记对象已加密,就不能指望换一个导出参数恢复源码。
能依赖的来源只有创建它时保存的 SQL 文件、发布包、源码仓库,或其他未加密的历史副本。数据库中的加密定义不应被视为源码备份。
结构快照仍会告诉我们对象存在,但无法回答它内部查询了哪些表、用了什么业务规则。这类对象必须单独登记,否则后面的数据血缘分析会留下一块看似完整、实际不可见的区域。
结构快照能回答什么
导出的 SQL Project 适合确认:
- 表、字段、类型和约束;
- 索引、视图、函数、触发器和未加密模块的定义;
- 跨数据库引用文本;
- 两次导出之间的结构变化。
它不能说明表是否仍在写入,也不能给出数据量、时间覆盖、枚举含义和保留周期。SQL Server Agent Job、登录名、Linked Server 等服务器级配置也不在这份数据库结构快照里。
真正开始分析前,我会把结构快照和数据画像分开:先用快照规划表和字段,再用受控查询确认记录数量、时间范围、空值比例和实际取值。这样得到的结论可以复查,也不会把对象名称猜成业务事实。