前言
数据安全与审计合规是企业数据库管理的核心需求之一。传统方案依赖触发器、CDC(变更数据捕获)或第三方审计工具,配置复杂、维护成本高,且难以从根本上防止具有高权限的 DBA 篡改历史记录。
SQL Server 2022 引入了 Ledger(账本) 功能------一种基于区块链哈希链思想的数据库级防篡改机制。它能够:
- 自动记录所有 INSERT / UPDATE / DELETE 操作的完整历史
- 防止篡改:即使 DBA 也无法静默修改历史记录
- 提供加密验证:通过哈希摘要证明数据完整性
- 满足合规要求:金融、医疗、审计等场景开箱即用
本文通过完整示例,带你掌握 Ledger 账本表的核心用法。
准备测试数据
-- 创建测试数据库(启用 Ledger 功能)
CREATE DATABASE LedgerDemo;
GO
USE LedgerDemo;
GO
-- 创建"可更新账本表":记录员工工资信息
CREATE TABLE dbo.EmployeeSalary
(
EmployeeID INT NOT NULL PRIMARY KEY,
EmployeeName NVARCHAR(50) NOT NULL,
Department NVARCHAR(30) NOT NULL,
Salary DECIMAL(10,2) NOT NULL,
UpdatedBy NVARCHAR(50) NOT NULL DEFAULT SYSTEM_USER
)
WITH (SYSTEM_VERSIONING = ON, LEDGER = ON);
GO
-- 插入初始员工薪资数据
INSERT INTO dbo.EmployeeSalary (EmployeeID, EmployeeName, Department, Salary, UpdatedBy)
VALUES
(1001, N'张伟', N'研发部', 18000.00, N'HR_Admin'),
(1002, N'李娜', N'财务部', 15000.00, N'HR_Admin'),
(1003, N'王强', N'销售部', 12000.00, N'HR_Admin'),
(1004, N'赵雷', N'研发部', 20000.00, N'HR_Admin'),
(1005, N'陈晓燕', N'人事部', 9800.00, N'HR_Admin');
GO
说明 :
WITH (SYSTEM_VERSIONING = ON, LEDGER = ON)是创建可更新账本表的关键语法。SQL Server 会自动生成配套的历史表和账本视图。
示例一:查看账本表的自动生成结构
-- 查看 Ledger 相关系统对象
SELECT
t.name AS 表名,
t.ledger_type_desc AS Ledger类型,
t.ledger_view_name AS 账本视图名,
t.history_table_name AS 历史表名
FROM sys.tables t
WHERE t.is_ledger_on = 1;
GO
-- 查看账本视图的列结构
SELECT
COLUMN_NAME AS 列名,
DATA_TYPE AS 数据类型,
IS_NULLABLE AS 可空
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'MSSQL_LedgerHistoryFor_EmployeeSalary'
OR TABLE_NAME LIKE '%EmployeeSalary_Ledger%'
ORDER BY ORDINAL_POSITION;
GO
查询结果(sys.tables 账本信息):
| 表名 | Ledger类型 | 账本视图名 | 历史表名 |
|---|---|---|---|
| EmployeeSalary | UPDATABLE_LEDGER_TABLE | EmployeeSalary_Ledger | MSSQL_LedgerHistoryFor_EmployeeSalary |
说明:SQL Server 自动创建了:
- 历史表 :
MSSQL_LedgerHistoryFor_EmployeeSalary(存储旧版本记录)- 账本视图 :
EmployeeSalary_Ledger(合并当前+历史,含操作类型标记)
示例二:执行数据变更并查看账本视图
-- 模拟业务操作:调整张伟的薪资(涨薪)
UPDATE dbo.EmployeeSalary
SET Salary = 21000.00, UpdatedBy = N'Manager_Liu'
WHERE EmployeeID = 1001;
GO
-- 模拟:李娜离职,删除记录
DELETE FROM dbo.EmployeeSalary
WHERE EmployeeID = 1002;
GO
-- 模拟:新员工入职
INSERT INTO dbo.EmployeeSalary (EmployeeID, EmployeeName, Department, Salary, UpdatedBy)
VALUES (1006, N'刘洋', N'研发部', 19500.00, N'HR_Admin');
GO
-- 查询账本视图,查看完整操作历史
SELECT
EmployeeID,
EmployeeName,
Department,
Salary,
UpdatedBy,
ledger_operation_type_desc AS 操作类型,
ledger_transaction_id AS 事务ID,
ledger_sequence_number AS 序列号
FROM dbo.EmployeeSalary_Ledger
ORDER BY ledger_transaction_id, ledger_sequence_number;
GO
账本视图查询结果:
| EmployeeID | EmployeeName | Department | Salary | UpdatedBy | 操作类型 | 事务ID | 序列号 |
|---|---|---|---|---|---|---|---|
| 1001 | 张伟 | 研发部 | 18000.00 | HR_Admin | INSERT | 101 | 0 |
| 1002 | 李娜 | 财务部 | 15000.00 | HR_Admin | INSERT | 101 | 1 |
| 1003 | 王强 | 销售部 | 12000.00 | HR_Admin | INSERT | 101 | 2 |
| 1004 | 赵雷 | 研发部 | 20000.00 | HR_Admin | INSERT | 101 | 3 |
| 1005 | 陈晓燕 | 人事部 | 9800.00 | HR_Admin | INSERT | 101 | 4 |
| 1001 | 张伟 | 研发部 | 18000.00 | HR_Admin | DELETE | 102 | 0 |
| 1001 | 张伟 | 研发部 | 21000.00 | Manager_Liu | INSERT | 102 | 1 |
| 1002 | 李娜 | 财务部 | 15000.00 | HR_Admin | DELETE | 103 | 0 |
| 1006 | 刘洋 | 研发部 | 19500.00 | HR_Admin | INSERT | 104 | 0 |
说明 :UPDATE 操作在账本视图中被分解为 DELETE(旧值)+ INSERT(新值) 两条记录,确保每一次变更都有迹可查。
示例三:溯源查询------还原员工薪资变更历史
-- 查询员工 1001(张伟)的完整薪资变更轨迹
SELECT
EmployeeID,
EmployeeName,
Salary AS 薪资,
UpdatedBy AS 操作人,
ledger_operation_type_desc AS 操作类型,
ledger_transaction_id AS 事务ID
FROM dbo.EmployeeSalary_Ledger
WHERE EmployeeID = 1001
ORDER BY ledger_transaction_id, ledger_sequence_number;
GO
-- 查询被删除的员工记录(在账本视图中仍保留)
SELECT
EmployeeID,
EmployeeName,
Department,
Salary,
ledger_operation_type_desc AS 操作类型,
ledger_transaction_id AS 事务ID
FROM dbo.EmployeeSalary_Ledger
WHERE ledger_operation_type_desc = 'DELETE'
ORDER BY ledger_transaction_id;
GO
张伟薪资变更轨迹:
| EmployeeID | EmployeeName | 薪资 | 操作人 | 操作类型 | 事务ID |
|---|---|---|---|---|---|
| 1001 | 张伟 | 18000.00 | HR_Admin | INSERT | 101 |
| 1001 | 张伟 | 18000.00 | HR_Admin | DELETE | 102 |
| 1001 | 张伟 | 21000.00 | Manager_Liu | INSERT | 102 |
说明:通过账本视图,可以清楚地看到张伟薪资从 18000 变更为 21000 的完整过程,包括操作人。即使没有额外审计日志,也能完整还原历史。
示例四:防篡改验证------生成并核验数据库摘要
-- 生成数据库账本摘要(用于后续验证数据完整性)
EXECUTE sp_generate_database_ledger_digest;
GO
摘要输出示例(JSON格式):
json
复制
{
"database_name": "LedgerDemo",
"block_id": 4,
"hash": "0x3F8A2B1C...",
"last_transaction_commit_time": "2026-04-21T08:45:12",
"digest_version": 1
}
-- 验证账本完整性(检测是否有人在数据库外部直接修改数据)
DECLARE @digest NVARCHAR(MAX) = N'{
"database_name": "LedgerDemo",
"block_id": 4,
"hash": "0x3F8A2B1C...",
"last_transaction_commit_time": "2026-04-21T08:45:12",
"digest_version": 1
}';
EXECUTE sp_verify_database_ledger @digest;
GO
验证结果(数据未被篡改时):
| 消息 |
|---|
| Ledger verification successful. |
说明:
sp_generate_database_ledger_digest:生成包含哈希摘要的 JSON,应定期保存到外部安全存储(如 Azure Blob、AWS S3 或本地文件)sp_verify_database_ledger:用保存的摘要验证当前数据库状态,如果有人直接绕过 SQL Server 修改底层数据文件,验证将失败
示例五:仅追加账本表(Append-only Ledger)
-- 创建"仅追加账本表":适合日志、审计流水等只写场景
CREATE TABLE dbo.AuditLog
(
LogID INT IDENTITY(1,1) PRIMARY KEY,
EventTime DATETIME2 NOT NULL DEFAULT SYSDATETIME(),
EventType NVARCHAR(50) NOT NULL,
Operator NVARCHAR(50) NOT NULL,
Description NVARCHAR(200) NOT NULL
)
WITH (LEDGER = ON (APPEND_ONLY = ON));
GO
-- 插入审计日志
INSERT INTO dbo.AuditLog (EventType, Operator, Description)
VALUES
(N'LOGIN', N'zhang_wei', N'用户从 192.168.1.101 登录系统'),
(N'EXPORT', N'li_na', N'导出财务报表 2026Q1'),
(N'CONFIG', N'admin', N'修改系统配置:MaxConnections=500');
GO
-- 尝试更新(会被拒绝)
-- UPDATE dbo.AuditLog SET EventType = N'MODIFIED' WHERE LogID = 1;
-- 报错:Cannot update or delete from a ledger table that is append-only.
-- 查询审计日志(含账本元数据)
SELECT
LogID,
EventTime,
EventType,
Operator,
Description,
ledger_transaction_id AS 事务ID,
ledger_sequence_number AS 序列号
FROM dbo.AuditLog
ORDER BY LogID;
GO
审计日志查询结果:
| LogID | EventTime | EventType | Operator | Description | 事务ID | 序列号 |
|---|---|---|---|---|---|---|
| 1 | 2026-04-21 08:45:01 | LOGIN | zhang_wei | 用户从 192.168.1.101 登录系统 | 105 | 0 |
| 2 | 2026-04-21 08:45:01 | EXPORT | li_na | 导出财务报表 2026Q1 | 105 | 1 |
| 3 | 2026-04-21 08:45:02 | CONFIG | admin | 修改系统配置:MaxConnections=500 | 106 | 0 |
说明 :仅追加账本表(
APPEND_ONLY = ON)禁止 UPDATE 和 DELETE,适合审计流水、操作日志等场景,从语法层面彻底杜绝历史记录被删改。
与旧版写法对比
| 对比维度 | 传统方案(触发器/CDC) | SQL Server 2022 Ledger |
|---|---|---|
| 配置复杂度 | 高(需手动创建触发器或配置 CDC) | 低(建表时加 LEDGER = ON 即可) |
| 防 DBA 篡改 | ❌ 不能(DBA 可禁用触发器/修改历史表) | ✅ 能(哈希链保护,篡改可被检测) |
| 历史查询 | 需手动设计历史表结构 | 自动生成历史表和账本视图 |
| 完整性证明 | ❌ 无法提供密码学证明 | ✅ 支持哈希摘要验证(可存外部) |
| UPDATE 展现方式 | 依实现而异 | 统一为 DELETE + INSERT 记录 |
| 仅追加场景 | 需额外逻辑控制 | APPEND_ONLY = ON 原生支持 |
| 性能影响 | 触发器有较大开销 | 系统版本控制,开销较小 |
| 代码维护成本 | 高 | 几乎为零 |
旧写法(触发器模拟审计):
-- 传统:用触发器记录薪资变更(繁琐且可被 DBA 绕过)
CREATE TABLE dbo.SalaryAudit_Old
(
AuditID INT IDENTITY PRIMARY KEY,
EmployeeID INT,
OldSalary DECIMAL(10,2),
NewSalary DECIMAL(10,2),
ChangedAt DATETIME2 DEFAULT SYSDATETIME(),
ChangedBy NVARCHAR(50) DEFAULT SYSTEM_USER
);
GO
CREATE TRIGGER trg_SalaryChange
ON dbo.EmployeeSalary_Old
AFTER UPDATE
AS
BEGIN
INSERT INTO dbo.SalaryAudit_Old (EmployeeID, OldSalary, NewSalary)
SELECT d.EmployeeID, d.Salary, i.Salary
FROM deleted d
JOIN inserted i ON d.EmployeeID = i.EmployeeID
WHERE d.Salary <> i.Salary;
END;
GO
-- 问题:DBA 可以执行 DISABLE TRIGGER 或直接修改 SalaryAudit_Old 表
新写法(Ledger 账本表):
-- SQL Server 2022:建表时直接启用 Ledger,无需任何触发器
CREATE TABLE dbo.EmployeeSalary
(
EmployeeID INT NOT NULL PRIMARY KEY,
EmployeeName NVARCHAR(50) NOT NULL,
Salary DECIMAL(10,2) NOT NULL
)
WITH (SYSTEM_VERSIONING = ON, LEDGER = ON);
-- 历史记录、账本视图、哈希保护:全部自动就绪
兼容性说明
| 版本 | 支持情况 |
|---|---|
| SQL Server 2022(16.x) | ✅ 完整支持(可更新账本表 + 仅追加账本表 + 摘要验证) |
| SQL Server 2019 及以下 | ❌ 不支持 |
| Azure SQL Database | ✅ 完整支持(与 SQL Server 2022 同步) |
| Azure SQL Managed Instance | ✅ 支持 |
注意事项:
- 数据库兼容级别:Ledger 功能不依赖兼容级别,只要版本是 SQL Server 2022 即可使用
- 不可逆性 :账本表一旦创建,不能通过 ALTER TABLE 关闭 Ledger 属性
- 历史表限制 :系统自动生成的历史表(
MSSQL_LedgerHistoryFor_*)不能直接删除,只能通过删除主表来删除 - 仅追加表限制 :
APPEND_ONLY = ON的账本表不支持SYSTEM_VERSIONING,两者互斥 - 摘要存储 :强烈建议将
sp_generate_database_ledger_digest的输出定期保存到 数据库外部(Azure Blob/本地文件系统/不可变存储),否则摘要本身也可能被篡改
总结
SQL Server 2022 Ledger 账本表为数据防篡改审计提供了原生的、低成本的解决方案:
| 特性 | 要点 |
|---|---|
| 两种类型 | 可更新账本表(LEDGER = ON)和仅追加账本表(APPEND_ONLY = ON) |
| 自动历史追踪 | UPDATE 自动拆分为 DELETE + INSERT,完整保留变更前后值 |
| 防篡改机制 | 哈希链 + 摘要验证,即使 DBA 也无法静默修改历史 |
| 账本视图 | 自动生成,合并当前数据与历史,直接查询即可溯源 |
| 合规场景 | 金融流水、薪酬记录、操作日志、医疗数据等强审计场景 |
核心建议:在有审计合规要求的业务表上,用
WITH (SYSTEM_VERSIONING = ON, LEDGER = ON)替代传统触发器方案,配合定期保存摘要,即可构建可信的数据防篡改体系。