数据库基础体系 · 第 136/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。
数据库隐私与数据生命周期:分类、最小化、脱敏、留存和删除
数据库隐私治理不是在字段上加一层“隐藏显示”的功能,而是要回答一条数据在整个生命周期中的问题:
- 它是什么,是否包含个人信息或业务敏感信息?
- 为什么要收集它,哪些处理步骤真正需要它?
- 哪些角色可以看到原值、部分值或统计值?
- 数据经过复制、缓存、日志、备份和导出后,是否仍受同样的控制?
- 到期后如何删除,删除是否会被软删除标记、备份或下游副本抵消?
- 删除与查询、更新、同步、恢复并发发生时,最终状态是什么?
因此,隐私治理至少包含五个相互约束的主题:
- 分类:识别数据的敏感程度、用途、主体和传播范围。
- 最小化:只收集、存储、处理和暴露完成业务目的所必需的数据。
- 脱敏:在不需要原值的场景中降低可识别性,但不能把可逆的假名化误称为匿名化。
- 留存:在明确的业务或合规目的下保存数据,并定义可验证的到期条件。
- 删除:从在线库、派生数据、同步链路和备份恢复流程中实现可证明的删除或不可用化。
这些机制相互关联,但不能互相替代。加密不能替代最小化,脱敏不能替代权限控制,软删除不能自动满足删除要求,设置了留存期限也不代表备份中的数据已经消失。
一、先建立数据生命周期模型
1. 数据生命周期不只是“插入到删除”
一条业务数据通常会经历以下状态:
采集
↓
校验与分类
↓
在线存储
↓
查询、修改、派生
↓
复制、缓存、导出、备份
↓
到期、撤回目的或删除请求
↓
删除、匿名化或不可用化
↓
删除验证与审计
例如,用户手机号可能同时存在于:
- 主业务表;
- 唯一索引;
- 搜索索引;
- 订单快照;
- 消息队列;
- CDC(Change Data Capture,变更数据捕获)下游;
- 应用日志;
- 数据仓库;
- 备份文件;
- 监控 SQL 文本或慢查询日志。
如果只执行:
DELETE FROM users WHERE user_id = 42;
那么最多证明主表中的这一行在当前事务提交后被删除。它不能证明其他副本也已经删除。
2. 建议把“数据对象”与“数据副本”分开登记
数据目录中至少应记录以下实体:
| 项目 | 含义 |
|---|---|
| 数据对象 | 例如 users.phone、订单地址 |
| 数据主体 | 数据属于谁,例如客户、员工、设备 |
| 业务目的 | 注册、履约、风控、客服等 |
| 数据分类 | 公开、内部、机密、敏感个人数据等 |
| 原始来源 | 用户输入、设备采集、第三方接口 |
| 派生对象 | 哈希、统计指标、推荐特征 |
| 传播路径 | 主库、只读副本、仓库、导出文件 |
| 责任人 | 能解释用途和删除规则的业务或数据负责人 |
| 留存规则 | 起算事件、期限、例外和删除动作 |
| 控制方式 | 权限、加密、视图、脱敏、审计 |
| 删除状态 | 待删除、已删除、受法律保留、待备份过期 |
这里的“责任人”不等于拥有数据库管理员权限的人。数据库管理员负责执行技术动作,而数据责任人需要确认“为什么保留”和“什么情况下可以删除”。
3. 删除事件必须有明确的起算点
“保留 90 天”并不完整,必须说明 90 天从何时开始计算。例如:
- 从账号创建时间开始;
- 从最后一次业务活动开始;
- 从合同终止时间开始;
- 从订单完成时间开始;
- 从最后一次访问日志产生时间开始。
设一条记录的起算时间为 ,留存期限为 ,当前时间为 ,则最基本的到期条件是:
但真实系统还需要加入例外状态 ,表示存在法律保留、争议处理或审计冻结:
如果没有明确 和 的来源,定时任务只能“猜测”哪些数据可以删除。
二、数据分类:先区分“值是什么”和“如何使用”
1. 分类不是给表打一个标签就结束
同一个字段在不同场景可能有不同风险。
例如:
user_id在单独系统中可能只是内部标识;user_id + 行为时间 + IP可能可以关联到具体个人;- 经过加密的身份证号仍然是高敏感数据,因为有密钥时可恢复;
- 仅保存年龄段可能比保存出生日期更符合统计用途,但“小群体中的年龄段”仍可能重新识别个体。
因此,分类至少要考虑四个维度:
- 内容:字段本身存储什么。
- 可识别性:能否单独或与其他字段结合识别主体。
- 用途:展示、履约、统计、风控还是调试。
- 传播范围:只在数据库内部,还是会进入日志、报表和第三方系统。
2. 常见分类层次
一种可执行的分类方式如下:
公开数据
泄露后通常不会影响个人或核心业务,例如公开的产品名称、公共帮助文档。
内部数据
不应公开,但泄露影响有限,例如内部状态码、非敏感运营配置。
机密业务数据
泄露可能造成竞争、财务或运营风险,例如成本、合同价格、未发布策略。
个人数据
能够直接或间接关联到自然人的数据,例如:
- 姓名;
- 邮箱;
- 手机号;
- 身份证件号码;
- 精确地址;
- 设备标识;
- IP 地址;
- 账户和订单关联关系。
“个人数据”的具体法律定义取决于适用法域,工程上不应只依赖字段名称判断。
高敏感或特殊风险数据
例如认证凭据、支付信息、健康信息、精确位置、未成年人信息等。它们通常需要更严格的访问控制、加密、审计和更短的留存期限。
3. 直接标识符、准标识符和敏感属性
为了理解重识别风险,可以把字段分成三类:
- 直接标识符:单独即可识别主体,例如身份证件号码、手机号。
- 准标识符:单独通常不能唯一识别,但组合后可能识别主体,例如出生日期、邮编、职业。
- 敏感属性:一旦关联主体,可能造成严重影响,例如健康诊断、收入、风险评级。
假设统计表只删除了姓名和手机号,但保留:
出生日期、精确邮编、性别、就诊日期、诊断结果
攻击者可能通过公开资料或另一张表关联这些准标识符,恢复个体身份。删除直接标识符不等于匿名化。
4. 分类应落实为可查询的元数据
可以建立一个数据目录表:
CREATE TABLE data_catalog (
object_name text PRIMARY KEY,
classification text NOT NULL CHECK (
classification IN ('public', 'internal', 'confidential', 'personal', 'sensitive')
),
purpose text NOT NULL,
retention_days integer CHECK (retention_days IS NULL OR retention_days > 0),
owner_name text NOT NULL,
deletion_basis text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
这里的表只是治理元数据,不会自动改变数据库权限,也不会自动删除业务数据。要让分类产生实际效果,还需要把它连接到:
- DDL 审核;
- 权限申请;
- SQL 审计;
- 脱敏视图;
- 数据导出审批;
- 留存任务;
- 删除验证。
三、数据最小化:不仅是“少存几个字段”
1. 最小化的四个层次
数据最小化至少有四种不同含义:
- 收集最小化:业务不需要的字段不采集。
- 存储最小化:不重复保存同一事实,不保存不再需要的精度。
- 处理最小化:某个处理步骤只获得完成任务所需的字段。
- 暴露最小化:调用者只能看到完成任务所需的结果。
例如,配送系统需要判断地址是否在配送范围内,可能只需要:
经纬度网格、城市、区域
而不一定需要永久保存完整门牌地址。另一方面,实际派送又可能确实需要完整地址。因此“最小化”通常是按处理目的分别定义的,而不是要求整个系统永远只保存最粗粒度的数据。
2. 用函数依赖判断是否存在重复存储
假设订单表为:
orders(order_id, user_id, user_phone, amount)
如果业务规则保证:
即 user_id 能确定 user_phone,那么把手机号复制到每一条订单中会产生冗余。它带来两个隐私问题:
- 用户手机号变更时,会有旧快照继续保留;
- 删除用户信息时,订单表仍然持有手机号副本。
可以改为:
users(user_id, phone)
orders(order_id, user_id, amount)
订单履约若需要联系信息,在事务内通过关联读取:
SELECT o.order_id, o.amount, u.phone
FROM orders AS o
JOIN users AS u ON u.user_id = o.user_id
WHERE o.order_id = $1;
但是,规范化并不总是意味着必须消灭所有快照。发票、合同、物流凭证可能需要保留“交易当时的收货地址”。这时应明确该字段是:
- 当前用户资料;
- 还是交易事实的历史快照。
两者的目的和留存规则不同,不能只因为字段内容相同就混用。
3. 用“必要性约束”形式化最小化
设业务功能需要满足的输出为 ,输入字段集合为 ,系统使用字段子集 。如果存在处理函数:
而去掉任意一个字段 后都无法稳定完成该功能:
则 是该功能的一组必要输入。
工程上不需要真的求解数学上的最小集合,但可以逐字段做反事实检查:
如果删除这个字段,功能会失败、结果会降低准确性,还是只是让实现不方便?
“实现不方便”通常不是保留个人数据的充分理由。
4. 例子:登录功能不应读取完整用户行
假设登录只需要验证邮箱、密码哈希和账户状态。应用不应无条件执行:
SELECT *
FROM users
WHERE email = $1;
因为这可能把手机号、地址、备注等不必要字段带入应用内存、调试日志或追踪系统。
更小的查询是:
SELECT user_id, password_hash, account_status
FROM users
WHERE email = $1;
如果邮箱不应作为应用内部长期标识,还可以在登录成功后只返回内部用户 ID 和会话标识,不继续在各服务间传播邮箱。
5. 精度最小化与聚合
分析系统常见的风险不是字段太多,而是精度太高。例如保留:
精确时间、精确经纬度、精确年龄
可能比保留:
小时、城市网格、年龄段
更容易识别个体。
但聚合存在“小群体”风险。设查询结果包含 个主体,若 ,即使只返回平均值,也可能直接暴露个人值。常见控制包括:
- 设置最小群体阈值 ,仅在 时返回;
- 降低时间和空间精度;
- 限制连续查询,防止通过差分推导单条记录;
- 对统计结果加入噪声。
这些方法会影响准确性,必须根据分析目的选择,而不能把任意 GROUP BY 都称为匿名化。
四、脱敏:降低暴露,不等于删除或匿名化
1. 四个容易混淆的概念
加密
加密通常是可逆的。拥有密钥即可恢复原值。它主要解决存储或传输过程中的机密性,不解决“被授权应用是否不应看到原值”。
哈希
哈希通常是单向函数,但低熵值容易被字典枚举。例如手机号、邮箱、短身份证号都可能被预计算或批量猜测。裸 SHA-256 不能自动把这些数据变成匿名数据。
假名化
把原始标识替换为内部 ID、随机标识或令牌。只要映射表、密钥或其他关联信息还存在,就仍可能恢复主体关系。
匿名化
目标是使数据在合理可行的攻击条件下无法再关联到特定主体。它不是一个 SQL 函数,而是要结合数据集、外部信息、攻击模型和发布方式评估。工程上不应仅凭“删掉姓名”宣布完成匿名化。
2. 静态脱敏与动态脱敏
静态脱敏
把生产数据复制到测试或分析环境时,生成一个新的脱敏数据集:
生产手机号:13812345678
测试手机号:13800001234
优点是测试环境不再依赖生产原值。缺点是脱敏任务本身需要读取原值,生成过程和中间文件也必须受到保护。
动态脱敏
原表保留原值,但不同角色通过视图看到不同结果。例如 PostgreSQL:
CREATE TABLE customer (
customer_id bigint PRIMARY KEY,
full_name text NOT NULL,
phone text NOT NULL,
email text NOT NULL
);
CREATE VIEW customer_support_view AS
SELECT
customer_id,
full_name,
CASE
WHEN length(phone) >= 7
THEN repeat('*', length(phone) - 4) || right(phone, 4)
ELSE '****'
END AS phone,
CASE
WHEN position('@' IN email) > 1
THEN left(email, 1) || '***' ||
substring(email FROM position('@' IN email))
ELSE '***'
END AS email
FROM customer;
创建只读角色并授权:
CREATE ROLE support_readonly LOGIN PASSWORD 'use-a-secret-manager';
GRANT USAGE ON SCHEMA public TO support_readonly;
GRANT SELECT ON customer_support_view TO support_readonly;
REVOKE ALL ON customer FROM support_readonly;
预期结果类似:
customer_id | full_name | phone | email
------------+-----------+--------------+----------------
1 | 张三 | ******5678 | z***@example.com
这里有几个重要边界:
- 应用使用的连接角色必须确实是
support_readonly,否则视图没有保护作用。 - 不应同时授予该角色对原表的
SELECT。 - 视图中的
full_name仍然可能是个人数据,脱敏一个字段不代表整行安全。 - 邮箱和手机号格式不同时,固定字符串截取可能泄露长度或导致错误;规则应按业务风险设计。
- 视图只是数据库层控制,导出的结果、日志和缓存仍需单独治理。
PostgreSQL 的列级权限可以进一步限制可读列,但列级权限不等同于值级脱敏。需要按行或按当前用户动态判断时,可以使用行级安全策略(Row-Level Security,RLS),但策略设计、角色绕过能力和表所有者权限必须单独验证。
3. MySQL 中的视图示例
MySQL 8.4 支持视图和授权机制,也可以构造类似的脱敏层:
CREATE TABLE customer (
customer_id BIGINT PRIMARY KEY,
full_name VARCHAR(100) NOT NULL,
phone VARCHAR(32) NOT NULL,
email VARCHAR(255) NOT NULL
);
CREATE VIEW customer_support_view AS
SELECT
customer_id,
full_name,
CASE
WHEN CHAR_LENGTH(phone) >= 7
THEN CONCAT(
REPEAT('*', CHAR_LENGTH(phone) - 4),
RIGHT(phone, 4)
)
ELSE '****'
END AS phone,
CASE
WHEN LOCATE('@', email) > 1
THEN CONCAT(
LEFT(email, 1),
'***',
SUBSTRING(email FROM LOCATE('@', email))
)
ELSE '***'
END AS email
FROM customer;
授权时必须注意视图的安全上下文、定义者和调用者权限配置,以及应用用户是否还能够直接读取基表。不同于 PostgreSQL,不能把一个数据库的角色、RLS 或权限语义直接套到 MySQL。
4. 脱敏函数不应破坏业务约束
脱敏后数据可能不再满足:
- 唯一性;
- 格式校验;
- 外键关联;
- 时间顺序;
- 数值分布;
- 可重复测试。
例如测试系统需要验证“同一个用户的订单能否关联”,随机生成每一行手机号会破坏一致性。更合理的静态脱敏应使用稳定映射:
其中 使用受保护的密钥或映射表,使同一个输入在同一脱敏批次中得到同一个替代值,但不能从替代值反推出原值。
然而,稳定映射也有风险:攻击者可以观察不同数据集中的相同替代值,从而进行关联分析。因此,是否需要跨表一致性、跨批次一致性,应由测试目的决定,而不是默认开启。
五、最小权限与脱敏必须同时存在
脱敏视图解决的是“拿到查询结果后看到什么”,权限控制解决的是“谁可以发起什么查询”。
一个常见失败配置是:
给客服账号授予原表 SELECT
再额外创建一个脱敏视图
要求客服使用视图
这并没有限制客服账号读取原表。正确关系应当是:
原表:仅数据服务账号可读
脱敏视图:授权客服账号读取
还要检查以下旁路:
- 是否可以读取系统元数据中的默认值、注释或表达式;
- 是否可以执行函数间接返回原值;
- 是否可以访问物化视图、临时导出表;
- 是否能通过错误信息、排序、过滤和计数推断隐藏值;
- 是否能读取应用日志、备份或只读副本。
“不能 SELECT phone”不代表“无法推断 phone”。例如一个接口允许按完整手机号查询并返回“是否存在”,攻击者可以批量枚举手机号。值级保密和查询行为都需要考虑。
六、留存:规则必须能被机器执行和人工解释
1. 留存不是“永远保留”或“定时清理”二选一
留存规则至少包含:
数据对象
业务目的
起算事件
留存期限
允许的例外
到期动作
执行频率
验证方式
责任人
例如:
登录审计事件:
起算事件 = 事件发生时间
期限 = 180 天
例外 = 安全事件调查冻结
到期动作 = 删除原始事件,保留按月聚合统计
这里的聚合统计是否可以长期保留,要重新评估识别风险,不能因为“已经聚合”就自动排除隐私影响。
2. 软删除不是物理删除
常见设计:
ALTER TABLE customer
ADD COLUMN deleted_at timestamptz;
查询时加条件:
SELECT *
FROM customer
WHERE deleted_at IS NULL;
这只是业务层不可见。原值仍然存在于:
- 表数据页;
- 索引;
- 只读副本;
- CDC 消息;
- 备份;
- 导出文件;
- 可能的审计记录。
软删除适合表达“暂时停用”或“保留引用关系”,但如果要求数据不可再使用,还必须定义后续物理删除、匿名化或密钥销毁流程。
3. 到期删除的 SQL 示例
以下 PostgreSQL 示例假定:
created_at是留存起算时间;- 记录没有法律保留;
- 按批次删除以降低长事务和锁影响;
- 运行任务的角色确实有删除权限。
CREATE TABLE login_event (
event_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id bigint NOT NULL,
created_at timestamptz NOT NULL,
event_type text NOT NULL,
legal_hold boolean NOT NULL DEFAULT false
);
一次删除一批:
WITH candidates AS (
SELECT event_id
FROM login_event
WHERE created_at < now() - interval '180 days'
AND legal_hold = false
ORDER BY event_id
LIMIT 1000
)
DELETE FROM login_event AS e
USING candidates AS c
WHERE e.event_id = c.event_id
RETURNING e.event_id, e.created_at;
执行过程是:
candidates选出当前到期且未冻结的最多 1000 条记录。DELETE ... USING只删除本批候选记录。RETURNING返回实际删除的行,便于记录审计结果。- 事务提交后,其他事务才会观察到本批删除结果。
如果还有更多记录,任务应循环执行,并在每批之间提交事务。一次删除数千万行可能造成:
- 长时间事务;
- 表和索引膨胀;
- WAL 或 binlog 迅速增长;
- 复制延迟;
- 锁等待;
- 恢复时间增加。
这不是说分批删除一定没有锁,而是把锁和日志压力限制在可观察的批次内。
MySQL 可以使用相同的候选主键分批思想,例如:
DELETE FROM login_event
WHERE event_id IN (
SELECT event_id
FROM (
SELECT event_id
FROM login_event
WHERE created_at < CURRENT_TIMESTAMP - INTERVAL 180 DAY
AND legal_hold = 0
ORDER BY event_id
LIMIT 1000
) AS candidates
);
这里增加一层派生表是为了避免同一语句中直接读写目标表时遇到 MySQL 的目标表限制。实际执行还需要结合索引、事务隔离级别和执行计划验证。
4. 索引决定删除任务是否可控
如果删除条件是:
WHERE created_at < ...
AND legal_hold = false
ORDER BY event_id
LIMIT 1000
应检查是否存在适合的索引。没有索引时,数据库可能反复扫描大量历史数据。索引不是隐私控制,但它决定了删除控制能否稳定运行。
PostgreSQL 可以检查:
EXPLAIN (ANALYZE, BUFFERS)
SELECT event_id
FROM login_event
WHERE created_at < now() - interval '180 days'
AND legal_hold = false
ORDER BY event_id
LIMIT 1000;
生产环境要关注:
- 是否使用了预期索引;
- 是否扫描了大量不符合条件的行;
- 是否产生排序和临时空间;
- 是否导致复制延迟;
- 是否因并发写入反复处理相同范围。
EXPLAIN ANALYZE 会真正执行语句。对删除语句进行计划分析时,应先使用 EXPLAIN,或在明确的测试事务中运行并回滚;不要在生产上无意执行破坏性操作。
七、分区、归档和备份会改变删除路径
1. 按时间分区适合“整段时间到期”的数据
如果日志按月分区,并且整个月份的数据都已到期,那么删除整个分区通常比逐行删除更适合:
DROP TABLE login_event_2024_01;
或在支持分区表的设计中执行相应的分区移除操作。这样可以避免逐行产生大量删除记录。
但它有严格前提:
- 分区内所有记录都已到期;
- 没有法律保留记录;
- 没有外部对象仍把该分区当作在线数据;
- 下游同步、仓库和备份流程知道该分区已删除;
- 分区边界与留存起算时间一致。
如果同一分区中既有到期记录又有未到期记录,不能为了效率直接删除整个分区。
2. 归档不是删除
把在线表移动到归档库仍然是保留。归档库同样需要:
- 分类;
- 权限;
- 加密;
- 审计;
- 留存;
- 删除;
- 恢复验证。
“从生产库删掉,但在归档库永久保留”只是改变了存储位置。
3. 备份中的删除具有延迟语义
假设今天删除了在线库记录,但昨天的全量备份仍然保留 30 天,那么从备份恢复到隔离环境后,旧记录可能重新出现。
这并不意味着备份机制不可用,而是删除策略必须写清楚:
- 删除是否要求立即清除所有备份;
- 备份是否采用固定过期周期;
- 备份是否不可变;
- 恢复环境是否允许对外提供服务;
- 恢复后是否必须重新执行删除清单;
- 加密密钥销毁是否是允许的不可用化措施。
“删除成功”必须定义边界。例如:
在线主库和只读副本:删除任务完成后生效
CDC 与数据仓库:下一个同步窗口内完成
普通备份:按备份过期策略自然淘汰
恢复环境:恢复后执行删除清单,再允许访问
如果业务或适用规则要求备份也必须快速删除,就不能仅依赖自然过期,应设计备份重写、备份分层或密钥管理策略。密钥销毁只能在明确理解加密体系和恢复需求的前提下使用,因为它可能使整批数据都不可恢复,而不是只删除某个主体的数据。
八、删除不是单条 SQL,而是一条传播链
1. 用删除事件传播到派生系统
主库删除后,下游可能通过 CDC 收到删除事件。一个完整事件至少要能表达:
主体或业务对象标识
操作类型 = DELETE
发生时间
来源事务或事件版本
数据分类
幂等键
下游处理必须满足幂等性。若删除事件重复投递,重复执行不应导致错误;若更新事件和删除事件乱序,还需要通过版本号或源事务位置判断哪个状态更新。
可以把对象状态抽象为:
ACTIVE
├── 到期 → DELETION_PENDING
├── 删除请求 → DELETION_PENDING
└── 法律保留 → LEGAL_HOLD
DELETION_PENDING
├── 所有目标系统完成 → DELETED
└── 任一系统失败 → RETRY / ESCALATED
只有“主库删除成功”而下游状态仍为 ACTIVE 时,整体删除流程不能标记为完成。
2. 并发条件决定删除是否稳定
假设删除任务在 查询到一条已到期记录,但另一个事务在 更新了它:
- 如果更新改变了
last_activity_at,记录可能已经不该删除; - 如果更新与删除竞争,最终结果取决于锁等待和提交顺序;
- 如果删除后更新事务基于旧快照继续写入,可能产生重新出现的数据或失败。
因此,删除条件必须在实际 DELETE 时再次判断,而不是先导出 ID,过很久后无条件删除:
DELETE FROM login_event
WHERE event_id = $1
AND created_at < $2
AND legal_hold = false;
更稳妥的批量删除会把资格条件放在删除语句本身,而不是只放在预选阶段。
如果业务允许记录在到期后继续更新,应定义“最后活动时间”是否会延长留存;如果不允许,则应在到期状态上加约束,禁止新业务继续使用。
3. 事务边界必须明确
下面两种操作语义不同:
事务 A:删除主库记录,提交
事务 B:发送删除消息,提交
如果事务 A 提交后事务 B 失败,就出现“主库已删、下游未删”。
常见解决方案包括:
- 事务内写 outbox:删除业务记录的同一事务中写入删除事件;独立投递器可靠发送。
- CDC:从数据库事务日志读取变更。
- 重试与对账:下游失败时重试,并定期比较主库删除清单和下游状态。
不能简单假设“执行了数据库删除”就自动通知所有系统。是否能可靠传播,取决于部署架构和事件链路。
九、删除验证:验证结果,不只验证命令成功
1. 主库验证
删除批次至少应记录:
- 删除任务 ID;
- 执行时间;
- 条件版本;
- 删除行数;
- 起止主键或时间范围;
- 是否存在法律保留;
- 失败原因;
- 操作者或调度身份。
删除后可以验证:
SELECT count(*)
FROM login_event
WHERE created_at < now() - interval '180 days'
AND legal_hold = false;
预期结果为 0,但这个查询只覆盖当前表和当前条件。它不能覆盖备份、日志和其他系统。
2. 反向验证下游
对每个下游系统维护删除状态,例如:
subject_id | target_system | requested_at | completed_at | status | error
验证目标不是“下游收到事件”,而是:
- 记录是否不再可查询;
- 搜索索引是否删除;
- 缓存是否失效;
- 统计或特征表是否仍含可识别关联;
- 失败任务是否进入重试;
- 恢复演练后是否会重新暴露原值。
3. 用计数时要认识到聚合泄露
删除前后对比总行数可以发现明显问题,但不能作为唯一证据。因为:
- 数据可能被转移到另一张表;
- 记录可能被匿名化但仍保留关联键;
- 视图可能缓存旧结果;
- 备份仍可能含原值;
- 计数相同不代表主体数据相同。
验证应针对数据对象、传播副本和可识别关联分别设计。
十、常见错误与失败表现
错误一:把加密当作最小化
数据库磁盘加密或字段加密可以降低未授权读取风险,但没有解决:
- 应用是否真的需要该字段;
- 查询结果是否进入日志;
- 数据是否复制到更多系统;
- 到期后是否删除。
如果应用拥有解密密钥,应用被攻破后原值仍可能暴露。
错误二:把哈希当作匿名化
对邮箱执行:
SHA256(email)
并不能保证安全。攻击者可以对常见邮箱地址批量计算哈希并匹配。加盐可以提高攻击成本,密钥哈希或 HMAC 可以减少公开预计算风险,但稳定哈希仍可能被用于跨表关联,也不必然消除个人数据属性。
错误三:只脱敏展示层,不控制导出和接口
前端显示 ******5678,但后端接口仍返回完整手机号,或者导出任务绕过视图读取原表,结果仍然是暴露。
应沿着完整数据流检查:
数据库查询 → ORM 对象 → API 响应 → 日志 → 消息 → 缓存 → 导出
错误四:只做软删除
软删除适合业务恢复和引用保留,但不等于物理删除。需要删除时,必须继续处理软删除行、索引、派生表和备份。
错误五:先导出 ID,稍后无条件删除
示例:
10:00 查询出到期 ID
12:00 逐个 DELETE BY id
两小时内数据可能被更新、冻结或改变留存状态。删除语句应重新检查资格条件,并尽量缩短预选与删除之间的时间。
错误六:用整表扫描执行每日清理
失败表现通常包括:
- 删除任务运行时间越来越长;
- 复制延迟增加;
- WAL 或 binlog 暴增;
- 锁等待和连接堆积;
- 表空间没有立即下降。
应结合索引、批量提交、时间分区和数据库负载窗口设计,而不是只增加调度频率。
错误七:忘记错误数据和调试数据
无效手机号、注册失败请求、异常堆栈、SQL 参数、消息死信队列同样可能含个人数据。数据分类和留存策略必须覆盖“失败路径”,否则攻击者或运维人员可能从日志中获得比主表更多的信息。
十一、数据库层面的控制边界
数据库是重要控制点,但不是唯一控制点。
数据库通常能直接控制的内容
- 表、列、视图的访问权限;
- 行级访问策略(具体能力依赖引擎和配置);
- 事务内的一致性;
- 外键和约束;
- 审计扩展或日志能力;
- 数据库内的删除、归档和分区操作;
- 备份接口与恢复流程的一部分。
数据库通常不能单独保证的内容
- 应用是否收集了不必要字段;
- 应用是否把查询参数写入日志;
- 消息队列和搜索系统是否删除;
- 操作系统快照是否过期;
- 第三方 SaaS 是否保留导出文件;
- 恢复环境是否重新暴露旧数据;
- 人员是否通过管理员权限绕过业务流程。
因此,应把隐私控制映射到系统边界:
采集端:字段和目的控制
应用层:接口输出和业务授权
数据库:权限、约束、事务和删除
消息系统:事件幂等与重放策略
分析系统:粒度、聚合和查询审计
备份系统:保留、恢复和密钥生命周期
运维系统:日志、工单和操作审计
十二、一个可落地的端到端设计
下面用“客户账号”说明一条完整路径。
1. 在线模型
CREATE TABLE app_user (
user_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email text NOT NULL,
phone text,
account_status text NOT NULL CHECK (
account_status IN ('active', 'disabled', 'pending_delete')
),
created_at timestamptz NOT NULL DEFAULT now(),
last_activity_at timestamptz NOT NULL DEFAULT now(),
deletion_requested_at timestamptz,
legal_hold boolean NOT NULL DEFAULT false
);
CREATE UNIQUE INDEX app_user_email_uq
ON app_user (lower(email));
这里需要注意:
email的唯一性是业务约束,不代表所有角色都可以读取它;last_activity_at的含义必须固定,否则留存计算会失真;legal_hold不应由普通业务用户随意修改;pending_delete是业务状态,不等于已完成删除。
2. 删除资格
假设账号删除请求后保留 30 天用于撤销,且没有法律保留:
SELECT user_id
FROM app_user
WHERE account_status = 'pending_delete'
AND deletion_requested_at <
now() - interval '30 days'
AND legal_hold = false;
如果业务要求“最后活动后 2 年删除”,则条件可能还要包含:
AND last_activity_at < now() - interval '2 years'
两个期限的关系必须由业务规则明确:是满足任一条件即可删除,还是必须同时满足,不能由实现人员自行推断。
3. 删除与关联数据
若订单表保留 user_id 作为业务关联,删除用户行时可能触发外键冲突。此时有几种不同语义:
ON DELETE CASCADE:连订单一起删除,适合订单本身也不需保留的场景;ON DELETE SET NULL:保留订单但解除主体关联;- 保留不可识别的业务主体占位记录;
- 将订单中的个人字段匿名化,同时保留必要的交易事实。
不能因为删除方便就默认使用级联删除。级联删除可能破坏财务、审计或履约记录;反过来,保留外键也可能继续保留可识别关联。
4. 删除任务
在事务内再次确认状态:
BEGIN;
WITH candidates AS (
SELECT user_id
FROM app_user
WHERE account_status = 'pending_delete'
AND deletion_requested_at <
now() - interval '30 days'
AND legal_hold = false
ORDER BY user_id
LIMIT 500
)
DELETE FROM app_user AS u
USING candidates AS c
WHERE u.user_id = c.user_id
AND u.account_status = 'pending_delete'
AND u.legal_hold = false
RETURNING u.user_id;
COMMIT;
这段 SQL 只删除主表。订单、地址、客服工单、消息和仓库中的关联数据仍需要各自处理。
生产任务还应考虑:
- 并发删除任务是否会重复选取相同记录;
- 是否需要
FOR UPDATE SKIP LOCKED,以及当前引擎和事务语义是否满足需求; - 下游事件是否在同一事务中记录;
- 删除失败时是否回滚;
- 部分下游失败时如何重试和告警。
SKIP LOCKED 可用于多个清理工作者减少相互等待,但它可能跳过暂时锁住的记录,因此任务必须可重复运行,不能把“本轮未选中”误认为“无需删除”。
十三、生产取舍:保留、匿名化还是删除
三种动作的语义不同:
删除
让数据不再作为可用记录存在。适合原始数据已无业务必要、且没有保留义务的情况。
匿名化
保留统计或研究价值,但降低主体关联能力。需要评估重识别风险,尤其是准标识符、稀有记录和外部数据关联。
降级保存
删除高风险原值,仅保留低精度或不可逆派生值。例如删除完整生日,只保留年龄段;删除原手机号,只保留国家和运营商统计信息。
降级保存也可能保留个人数据。例如稳定哈希仍可关联同一主体,不能因为“看起来不是原文”就跳过分类和权限评估。
一个合理的决策顺序是:
是否仍有明确目的?
├─ 否 → 删除
└─ 是
是否必须保留原值?
├─ 否 → 聚合、降精度或假名化
└─ 是 → 严格权限、加密、审计和期限控制
这个顺序体现了最小化优先:安全措施应降低必要数据的暴露风险,而不是为不必要的数据永久保留寻找理由。
十四、如何审计这套机制是否真的有效
一次完整检查应同时问以下问题:
- 数据目录是否能指出每个敏感字段的目的和责任人?
- 是否有字段实际上没有被业务使用,却一直被收集?
- 测试库、数仓、搜索索引和日志是否含有原值?
- 应用角色能否直接读取原表?
- 脱敏是否会因错误信息、过滤条件或计数接口被绕过?
- 留存起算时间是否来自可信字段?
- 删除任务是否可重复、可恢复、可观测?
- 删除条件是否在实际删除时再次检查?
- CDC、消息队列、缓存和仓库是否处理删除事件?
- 备份恢复后是否仍会恢复已删除数据?
- 法律保留是否有明确创建、解除和审计流程?
- 是否能提供某次删除的执行记录和验证证据?
测试不应只做“查询视图看到星号”。还应使用低权限账号尝试:
- 读取基表;
- 访问导出接口;
- 查询异常路径;
- 读取日志和缓存;
- 重放删除事件;
- 在并发更新下验证最终状态;
- 从备份恢复到隔离环境后检查删除清单。
结语
数据库隐私治理的核心不是某一个函数、某一条 DELETE,而是把数据从产生到消失的每个状态和传播边界都定义清楚:
- 分类决定风险和控制强度;
- 最小化决定系统根本不应持有什么;
- 脱敏决定不需要原值的角色看到什么;
- 留存决定何时仍有理由保存;
- 删除决定到期后如何从主库、派生数据和恢复路径中消除或不可用化。
真正可验证的治理方案,应能回答三类问题:
为什么有这条数据?
谁在什么条件下能看到它?
到期后如何证明它已经不再被系统使用?
如果只能回答其中一部分,数据库安全控制仍然是不完整的。
系列导航与关联阅读
- 系列入口:数据库完整学习路线:从关系模型、事务索引到分布式与向量检索
- 上一篇:数据质量与治理:血缘、口径、约束、校验、责任人和变更审计
- 下一篇:ETL 与 ELT 数据管道:批流处理、幂等、补数、校验和可观测
- 延伸:数据库安全治理:最小权限、加密、审计、脱敏与注入防护
官方资料
本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。

评论
0 条讨论