数据完整性校验:别让脏数据毁了你的分析结果
数据完整性,说白了就是你的数据到底「靠谱不靠谱」。我记得刚带团队那会儿,有个同事跑了一周的分析模型,最后发现是外键关联错了,整批结果全部作废。嗯,从那以后,数据完整性校验就成了我每次流程里雷打不动的第一道关卡。
这一章咱们就聊聊四大校验维度:非空、唯一性、外键约束和数据一致性。每一个我都踩过坑,希望你能绕过去。
1. 非空校验:最基础也最容易翻车
非空校验看起来简单,但其实坑不少。你觉得一个字段不可能为空?我跟你讲,生产环境里什么都有可能。
我个人习惯是在数据库层面加 NOT NULL 约束,同时在应用层再校验一遍。双重保险,比单条腿走路靠谱。
来看看一些常见的非空校验代码:
# Python 版本:检查 DataFrame 中关键字段
import pandas as pd
def check_not_null(df, required_columns):
"""检查必须字段是否包含空值"""
null_report = {}
for col in required_columns:
null_count = df[col].isnull().sum()
null_report[col] = null_count
if null_count > 0:
print(f"[警告] 字段 {col} 存在 {null_count} 个空值")
return null_report
# 使用示例
sales_data = pd.read_csv("sales_2024.csv")
required = ["order_id", "customer_id", "amount"]
result = check_not_null(sales_data, required)
还有一个容易忽视的点:空字符串和 NULL 完全是两回事。我遇到过一张表,明明有值,但全是空字符串 '',一查是 ETL 脚本写错了。
-- SQL 层面:同时排除 NULL 和空字符串
SELECT COUNT(*) AS invalid_count
FROM orders
WHERE customer_name IS NULL
OR customer_name = ''
OR TRIM(customer_name) = '';
2. 唯一性校验:重复数据是慢性毒药
数据重复的危害,往往是延迟爆发的。你一开始可能觉得「多一条就多一条嘛」,等到做聚合分析、用户画像的时候,才发现结果全偏了。
唯一性校验的核心就是确保你的主键或者业务唯一标识不重复。我在这里习惯用「业务唯一键」的概念——比如用户邮箱、订单号,这些是业务上不允许重复的字段。
我自己一般先做一次全局去重检查:
-- 找出重复的 email
SELECT email, COUNT(*) AS cnt
FROM users
GROUP BY email
HAVING COUNT(*) > 1;
-- 查出具体的重复行
WITH dup_emails AS (
SELECT email
FROM users
GROUP BY email
HAVING COUNT(*) > 1
)
SELECT u.*
FROM users u
INNER JOIN dup_emails d ON u.email = d.email
ORDER BY u.email, u.created_at;
如果数据量很大,几千万行那种,GROUP BY 可能扛不住。我建议先做抽样,或者用分桶的方式去重:
-- 分桶去重检查(适用于超大数据表)
SELECT MOD(order_id, 100) AS bucket,
COUNT(DISTINCT order_id) AS unique_count,
COUNT(order_id) AS total_count
FROM orders
GROUP BY MOD(order_id, 100)
HAVING unique_count < total_count;
3. 外键约束校验:数据关系的护栏
外键约束,说白了就是保证「你的引用是存在的」。比如订单表里的 customer_id 必须能在用户表里找到,否则就是野指针。
我见过最离谱的情况是:一个运营人员手动删了一批「僵尸用户」,结果连带把近半年的订单数据全部变成孤儿,报表直接崩了。所以我现在做数据校验,一定会加入外键完整性检查。
一个典型的外键校验脚本:
-- 检查订单表的 customer_id 是否都在用户表中存在
-- 找出所有「孤儿记录」
SELECT o.order_id,
o.customer_id,
o.order_date
FROM orders o
LEFT JOIN customers c ON o.customer_id = c.customer_id
WHERE c.customer_id IS NULL;
-- 如果有结果返回,说明外键约束被破坏了
对于没有数据库外键约束(很多大数据平台不强制外键)的场景,我更建议做定时校验:
# Python 批处理外键校验
def check_foreign_key(df_child, child_key,
df_parent, parent_key, label="外键"):
"""批量检查外键完整性"""
child_set = set(df_child[child_key].dropna().unique())
parent_set = set(df_parent[parent_key].dropna().unique())
orphans = child_set - parent_set
if orphans:
print(f"[错误] {label}: 发现 {len(orphans)} 条孤儿记录")
print(f" 示例: {list(orphans)[:5]}")
return False
else:
print(f"[通过] {label}: 所有引用均有效")
return True
外键校验还有一个隐含的坑——级联删除。你真的确定你的 DELETE CASCADE 不会把不该删的数据也干掉吗?我建议每次做批量删除前,先跑一遍外键校验,看看会不会产生意想不到的级联效应。
4. 数据一致性问题:最难定位的幽灵
数据一致性,范围就大了。它可能是指同一个用户在两张表里的性别字段不一致,也可能是数据库和缓存之间的数据不同步,又或者是 OLAP 和 OLTP 之间的数对不上。
我遇到过的最头疼的一次:订单总金额在交易库和数仓里差了 300 多万。排查了三天,最后发现是交易库的汇率字段用了「买入价」,数仓却用了「中间价」。你看,这就是跨系统的一致性问题。
来看看我常用的几种一致性检查手段:
-- 1. 跨表一致性:对账类
SELECT t1.customer_id,
t1.total_amount AS trade_amount,
t2.settlement_amount AS settle_amount,
(t1.total_amount - t2.settlement_amount) AS diff_amount
FROM trade_system t1
JOIN settlement_system t2 ON t1.customer_id = t2.customer_id
WHERE ABS(t1.total_amount - t2.settlement_amount) > 0.01
ORDER BY diff_amount DESC;
-- 2. 字段间逻辑一致性
-- 例如:发货时间不应该早于下单时间
SELECT order_id,
order_time,
ship_time
FROM orders
WHERE ship_time < order_time;
我习惯把一致性校验分成三类,用表格整理一下,方便你对照自己的场景:
| 一致性类型 | 关注点 | 典型错误 |
|---|---|---|
| 引用一致性 | 子表字段与主表字段的值逻辑相符 | 订单引用已删除的用户 |
| 逻辑一致性 | 相同实体的属性在不同地方保持一致 | 用户表性别是男,日志表写女 |
| 时间一致性 | 时间字段遵循业务先后顺序 | 退款时间早于支付时间 |
| 汇总一致性 | 某个维度的汇总值与明细求和一致 | 月报表金额与日明细求和不同 |
这里我建议给每个一致性检查点定义一个「容忍阈值」。比如对账允许 0.01 元的误差,时间差异允许 5 秒的时钟偏差。别想着 100% 一致,现实中做不到,你只会累死自己。
说实话,数据完整性没有银弹。你设计的校验规则再严密,也架不住业务流程的诡异操作。所以我的态度是:把校验做成自动化流程的一部分,每天跑、每次导入跑、每次 ETL 跑,发现问题立刻报警。不要让脏数据在系统里过夜。