真正把YashanDB的数据质量搞上去,靠的不是事后补救,而是从设计、写入、清洗、监控全链路一起使劲。我接触YashanDB也有段时间了,刚开始踩了不少坑——表结构随便建、应用层不设防、重复数据堆成山,等报表跑出来才发现数字对不上,再回头一根根扒数据,那叫一个痛苦。后来我总结出5种最有效的方法,从源头到运维一层层堵住脏数据,实测下来数据准确率明显提升。这篇文章就把这些方法完整拆开讲,适合正在用YashanDB做业务系统、数仓项目的开发者和DBA参考,新手也能照着操作。
1. 从表结构设计入手,堵住脏数据入口
数据质量的第一道闸门不是代码,是表结构。很多数据问题(字段为空、格式混乱、超出范围)如果能在建表时用约束卡死,后面根本不会发生。YashanDB支持完整的关系模型约束,包括主键、唯一约束、非空、CHECK和默认值,但实际项目里能把这些用全的表少之又少。
1.1 非空、默认值与CHECK约束的精细化设计
先说最简单的。比如一张用户表,注册时间字段如果允许为空,后面统计“本月新增用户”时就会出现漏数。正确的做法是:业务上必然存在的字段一律加NOT NULL,并给一个合理的默认值。像创建时间,直接默认当前时间戳;状态字段,默认一个初始值。这样就算应用层漏传,数据库也能兜底。
CHECK约束是很多人忽略的利器。比如年龄字段,正常情况下是0到120之间,加一个CHECK(age BETWEEN 0 AND 120)就能挡住那些“年龄=9999”的妖怪数据。再比如性别字段,虽然不建议用CHECK代替枚举表,但至少可以限定范围。还有一个经典场景:订单金额必须大于0,如果业务上有优惠券导致0元订单也得记录,那可以规定金额大于等于0,但必须配合其他字段校验。这类约束看起来不起眼,却是在数据库层面挡住了大量无效数据。
CREATE TABLE user_profile ( user_id NUMBER PRIMARY KEY, user_name VARCHAR2(64) NOT NULL, age NUMBER(3) CHECK (age BETWEEN 0 AND 120), gender CHAR(1) DEFAULT 'U' CHECK (gender IN ('M', 'F', 'U')), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL );提示:YashanDB对CHECK约束的处理与主流数据库一致,如果后续需要调整约束,用ALTER TABLE ENABLE/DISABLE CONSTRAINT即可,不需要重建表。
1.2 主键、唯一约束与业务键的权衡
主键的作用不只是去重,更重要的是确保每一行记录都有唯一身份。但实际业务中,自然主键(比如身份证号)往往不稳定或会变更,这时候用自增序列做主键更稳妥,再另加唯一约束去保证业务键唯一。比如用户表,user_id是代理主键,而user_name或者手机号应该加唯一约束。这样既保证了每一行可被稳定引用,又避免了业务上重复注册。
这里要特别留意一个细节:唯一约束对NULL值不生效,也就是说,如果手机号允许为空,那么100个空手机号的记录不会被唯一约束拦截。所以如果业务要求“一个手机号只能注册一次”,但注册时手机号又可能为空,那就不能只靠唯一约束,得在应用层或触发器里做条件判断。YashanDB支持函数索引,可以对非空字段建立条件唯一索引,这个技巧很实用:
CREATE UNIQUE INDEX idx_user_mobile_unique ON user_profile (CASE WHEN mobile IS NOT NULL THEN mobile END);在Oracle和YashanDB里,这种基于CASE表达式的唯一索引可以做到“只对非空值去重”,既允许空值存在,又保证非空值不重复。这是我后来用得最多的手段之一。
2. 应用层与数据库层双重校验,让坏数据进不来
约束是最后一道防线,但如果我们能早早在应用层发现问题,就能减少数据库无谓的开销,也方便给出友好的错误提示。理想状态是:应用层先粗筛,数据库层再兜底,两层配合,坏数据几乎没有机会落库。
2.1 应用层参数校验的常见漏网点
很多后端团队只校验必填字段,却忽略了格式、长度、取值范围。举个例子,注册接口只校验了用户名非空,没校验用户名长度,结果一个用户把几千字的文本塞进去,数据库字段定义VARCHAR2(20)直接报ORA-12899类似的过长错误,或者被截断,导致数据失去意义。所以在应用层,至少要校验:长度、格式(邮箱、手机号、日期)、枚举值是否合法、数值范围是否合理。如果是Java后端,可以用Bean Validation注解;如果是Python,可以用Pydantic或者手写校验函数。原则是“疑罪从有”,拿不准的都校验一遍。
另一个容易被忽略的是批量导入场景。很多系统都提供Excel导入功能,如果导入前不校验,坏数据就会成批进入YashanDB。我的经验是:先解析文件到临时表,做完整校验,再通过MERGE或存储过程搬入正式表,校验不通过的行要生成错误清单反馈给用户,而不是直接丢弃。这样既保证了正式表的数据质量,又能让用户根据错误清单修改后重新导入。
2.2 触发器与存储过程实现复杂校验的时机
有些校验逻辑非常复杂,涉及跨表查询,比如“用户当天只能提交一次申请”。这种场景靠应用层容易漏,靠简单约束又办不到,用触发器比较合适。YashanDB支持BEFORE INSERT/UPDATE触发器,可以在写入前拦截。但要注意,触发器是事务里执行,会加大锁的持有时间,高并发下性能会受影响。因此触发器只应处理那些应用层无法保证的、必须原子校验的逻辑,不要把所有业务校验都塞进去。
如果项目里还有多个应用(Web端、后台任务、报表工具)都在写同一张表,各自应用层校验逻辑不统一的话,数据库层的触发器反而是最可靠的统一收口方案。我通常会在核心业务表上建一个“校验触发器 + 日志表”,记录拦截下来的非法数据,方便事后分析。日志表本身只增不改,不影响主业务流程。
CREATE OR REPLACE TRIGGER trg_check_daily_apply BEFORE INSERT ON apply_record FOR EACH ROW DECLARE v_cnt NUMBER; BEGIN SELECT COUNT(*) INTO v_cnt FROM apply_record WHERE user_id = :NEW.user_id AND TRUNC(apply_date) = TRUNC(:NEW.apply_date); IF v_cnt > 0 THEN RAISE_APPLICATION_ERROR(-20001, '同一用户当天只能申请一次'); END IF; END;注意:触发器里的RAISE_APPLICATION_ERROR会使整个写入事务回滚,应用层必须捕获并展示友好提示,避免给用户留下“系统崩溃”的印象。
3. 数据清洗与标准化:用SQL给数据“洗澡”
哪怕是已经上了约束和校验的表,历史数据里也难免有空格、全半角混淆、大小写不统一、日期格式错乱等问题。这些数据不洗,报表做出来就是脏的。清洗的工具有很多,但直接在YashanDB里用SQL处理是最直接、成本最低的。
3.1 去除空格、统一大小写与全半角转换
最常见的问题是字符串里的隐形脏字符。比如用户提交姓名时带了前后空格,或者中间出现多个空格;再比如输入法导致的全半角差异,看起来是一样的字,实际编码不同。处理方式很简单:先用TRIM去掉首尾空格,再用REPLACE把全角空格和全角标点替换成半角,最后用正则表达式把多个连续空格合并成一个。
UPDATE user_profile SET user_name = REGEXP_REPLACE( TRIM( REPLACE( REPLACE(user_name, ' ', ' '), -- 全角空格转半角 ',', ',' ) ), '\s{2,}', ' ' ) WHERE user_name != REGEXP_REPLACE( TRIM( REPLACE( REPLACE(user_name, ' ', ' '), ',', ',' ) ), '\s{2,}', ' ' );这段SQL的精髓在于WHERE条件里加了一个对比,只更新真正需要洗的行,避免触发无谓的日志写入和索引维护。数据量大时,这种“先对比再更新”的思路能省下大量redo和undo空间。
日期格式也是重灾区。有的系统存字符串‘2024/01/05’,有的是‘2024-01-05’,还有‘20240105’。清洗时需要统一转成标准日期。YashanDB支持TO_DATE和TO_CHAR,可以先识别格式再转换,也可以用CASE WHEN把不同格式逐一处理。碰到无法识别的值,先记到异常表里,不要直接丢,否则审计时说不清。
3.2 利用MERGE实现增量清洗与标准化
大批量清洗时,不建议直接UPDATE原表,除非表特别小。比较稳妥的做法是:先把需要清洗的数据抽到清洗临时表,做好各种转换和校验,再通过MERGE回写正式表。MERGE的好处是一次SQL既能更新已有行,又能插入新行,非常适合“清洗+补全”场景。YashanDB对MERGE的支持很完善,和Oracle语法一致。
MERGE INTO user_profile t USING cleaned_data s ON (t.user_id = s.user_id) WHEN MATCHED THEN UPDATE SET t.user_name = s.user_name, t.mobile = s.mobile WHEN NOT MATCHED THEN INSERT (user_id, user_name, mobile) VALUES (s.user_id, s.user_name, s.mobile);注意,MERGE的USING子句里如果出现重复的user_id,会导致“ORA-30926: unable to get a stable set of rows”之类的错误。所以在清洗临时表里,一定要先对关联键去重。清洗过程的每步都建议留日志,比如每个清洗规则命中了多少行、转换失败多少行,便于复盘。
3.3 数据标准化与字典表映射
还有一种需要清洗的情况是“同义不同值”。比如性别字段,有的系统存“男”,有的存“M”,有的存“1”,统计的时候就乱套了。解决思路是建立标准字典表,通过映射关系把老数据洗成标准码。字典表建议做成“源值 -> 标准值 -> 标准描述”的结构,并记录来源系统,方便以后溯源。
CREATE TABLE dict_gender_mapping ( source_value VARCHAR2(20) PRIMARY KEY, standard_code CHAR(1) NOT NULL, standard_desc VARCHAR2(20) NOT NULL ); INSERT INTO dict_gender_mapping VALUES ('男', 'M', '男性'); INSERT INTO dict_gender_mapping VALUES ('M', 'M', '男性'); INSERT INTO dict_gender_mapping VALUES ('1', 'M', '男性');清洗时把业务表和字典表做关联,更新标准码。如果遇到字典表里没有的源值,说明又出现了未预料的脏数据,要记录下来补充映射。标准化是一个持续迭代的过程,不可能一次性做完,所以清洗脚本和字典表本身也要纳入版本管理。
4. 去重与主数据治理:消灭“同一人,多条记录”
数据质量另一个大问题是重复。重复数据造成统计虚高、客户被重复触达、对账不平。去重不能简单“DELETE掉多余的”,要先搞清楚业务上怎么定义“重复”,以及保留哪一条。
4.1 基于业务键识别重复并保留有效记录
拿客户表举例,同一个客户可能因为导入来源不同,被录入了两次,姓名、手机号都相同,只是其中一条更新了地址。去重的第一步是先定义“重复”的判定规则:完全一致?还是手机号一致就算?判定规则不同,去重方案完全不同。实际业务里,往往需要用多个字段的组合,甚至加上模糊匹配(比如姓名相同且生日相同)来判断。
判定好重复后,第二步是决定保留哪一条。我的建议是:保留“信息最全”或“最后更新时间最近”的一条,并把其他记录的关联业务(订单、工单)逐步迁移到保留下来的主记录上。直接删除会导致外键报表丢失数据,所以正确做法是“合并”而不是“删除”。可以给每一条客户记录加一个master_id字段,指向最终保留的主记录,查询时统一用master_id聚合。
YashanDB中可以用ROW_NUMBER()窗口函数来给重复组编号,再批量标记:
UPDATE customer c SET master_id = ( SELECT keep_id FROM ( SELECT cust_id AS keep_id, ROW_NUMBER() OVER ( PARTITION BY mobile ORDER BY last_update_time DESC, cust_id ) AS rn FROM customer ) k WHERE k.cust_id = c.cust_id ) WHERE c.master_id IS NULL;这段SQL先按手机号分组,每组按最后更新时间倒序排,更新时间最新、id最小的那条rn=1,就把它作为该组的master_id。之后业务查询都以master_id为准,重复数据不再产生负面影响。
4.2 唯一索引配合合并流程防止重复再生
光清理存量重复还不够,必须防止新重复产生。最直接的办法就是给业务键加唯一索引。但业务场景往往复杂,比如“同一手机号允许出现多次,但只能有一个有效状态为‘正常’的”,这就不能用普通唯一索引。YashanDB支持函数索引和部分唯一索引(利用CASE表达式),可以实现这种条件唯一。
CREATE UNIQUE INDEX idx_customer_one_active ON customer (CASE WHEN status = 'ACTIVE' THEN mobile END);这个索引的逻辑是:当状态为ACTIVE时,对手机号去重;而状态为INACTIVE的历史记录不做限制。这样在合并老数据时可以先保留一条ACTIVE,其他置为INACTIVE,之后系统层面就再也不会出现两条有效客户了。这个方法我在实际项目中解决了“一人多卡”类型的问题,效果立竿见影。
4.3 导入与同步场景下的幂等控制
另一个重复来源是数据同步。比如从外部系统同步客户数据,因为网络原因任务跑了两遍,结果重复插入。解决方案是给同步表加一个“源系统ID+源表主键”的组合唯一索引,并采用INSERT ... ON CONFLICT或MERGE方式写入,而不是简单的INSERT。YashanDB兼容Oracle,MERGE是最常用的幂等同步手段。如果同步工具不支持MERGE,也可以在应用层先按条件查询再决定插入还是更新,但会有并发窗口,所以最稳的还是数据库层的唯一索引兜底。
注意:凡是涉及外部系统对接的接口,一定要让外部系统提供唯一业务键,否则数据质量无从谈起。
5. 数据质量监控与定期体检:防患于未然
前四步解决的是“如何防止脏数据产生”和“如何清理存量脏数据”,但数据质量不是一劳永逸的。业务变化、版本迭代、新系统接入都可能引入新的问题。因此,必须建立一套持续监控机制。
5.1 设计数据质量规则并定期跑批
我建议把数据质量规则沉淀成一张规则表,包含:表名、字段名、规则类型(非空、唯一、取值范围、正则、跨字段一致性)、阈值、负责人。然后写一个定期任务(比如每天凌晨)遍历执行这些规则,把跑批结果写入质量报告表。
例如,检查订单表里“订单金额为负数且状态不是已取消”的记录数:
INSERT INTO data_quality_report (check_date, rule_name, bad_count, details) SELECT CURRENT_DATE, 'order_amount_non_negative', COUNT(*), LISTAGG(order_id, ',') WITHIN GROUP (ORDER BY order_id) FROM orders WHERE amount < 0 AND status NOT IN ('CANCELED') HAVING COUNT(*) > 0;如果bad_count超过阈值,就触发告警,发邮件或者企业微信机器人通知。这类任务用数据库的JOB调度即可,YashanDB支持DBMS_SCHEDULER类似的调度能力,不需要额外搭一套调度平台。重点是规则要不断迭代,每发现一种新脏数据,就补充一条新规则。
5.2 审计日志与变更追踪
数据质量出问题,最后总要定位是哪个环节出的错。这时候审计日志就是救命稻草。建议对核心表开启审计,或者通过触发器记录关键字段的变更前值、变更后值、操作人、操作时间。YashanDB本身提供审计功能,可以配置对DDL和DML的审计,但业务级的“谁改了订单金额”还需要业务表里记录操作日志。
我自己的习惯是:核心业务表都带一个last_updated_by字段,每次更新必须由应用层传入当前用户。然后定期归档变更日志。遇到数据对不上时,按时间倒序查日志,很快就能定位到操作源头。没有审计日志,出了问题只能干瞪眼。
5.3 同步链路中的数据质量校验
如果环境里用了数据库同步工具(比如从业务库同步到数仓),同步过程中也可能引入数据质量问题,比如延迟导致先读旧数据、主键冲突、类型转换失败等。我的经验是,同步任务里必须加上“行数和校验和对比”这一环。每次同步完成后,对比源表和目标表的行数、关键字段的SUM值或MD5采样值,不一致立刻告警。YashanDB在数仓场景中可以和这些同步工具配合,但数据质量的最终责任还是在自己这边,不能指望同步工具自动保证。
SELECT COUNT(*), SUM(amount) FROM orders@dblink_src WHERE update_time > last_sync_time; SELECT COUNT(*), SUM(amount) FROM orders WHERE update_time > last_sync_time;两条语句的结果如果对不上,说明同步链路出了问题,需要查看同步任务日志。这里提醒一句,做数据迁移或同步前,一定要先做全量校验,再切增量。
6. 常见问题与排查技巧实录
最后整理一下我在YashanDB数据质量实践中遇到的高频问题,直接给解决方案。
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 唯一约束没拦住重复数据 | 重复字段中有NULL值 | 改用函数唯一索引,只对非空值去重 |
| UPDATE清洗时卡死或慢 | 没有先加WHERE条件,全表更新 | 先查询准备更新的行,用小批提交 |
| MERGE报错“unable to get a stable set of rows” | USING结果集中关联键重复 | 对USING结果用ROW_NUMBER去重 |
| CHECK约束不生效 | 字段在写入时被隐式类型转换 | 检查插入数据的类型,使用显式TO_NUMBER |
| 数据同步导致重复 | 同步任务重复执行 | 加源系统唯一键,使用MERGE幂等写入 |
| 报表数字偏大 | 关联多表时产生笛卡尔积 | 检查关联字段是否有唯一索引,先聚合再关联 |
| 触发器报错影响主业务 | 触发器内嵌套事务或复杂查询 | 精简触发器逻辑,改为应用层+约束组合 |
| 中文乱码导致校验失败 | 字符集不一致 | 统一数据库字符集为UTF-8,连接串指定编码 |
还有一个容易被坑的地方:使用DBLINK做跨库校验时,如果源库字符集和目标库不一致,中文长度可能对不上。解决办法是统一用LENGTHB或LENGTHC来对比,不要直接用LENGTH,因为有的字符集下LENGTH返回的是字符数,有的返回字节数。
最后再分享一个小技巧:数据质量问题不要只盯着生产库,测试环境同样要有数据质量规则。很多脏数据源头是在开发阶段业务逻辑不严谨导致的,如果能在测试阶段就发现规则漏洞,生产环境的数据质量会稳得多。我自己就是先写规则、再写业务代码,每次迭代先用规则过上版数据,发现异常直接打回开发修改,后面维护成本低很多。
数据质量这件事没有终点,它是一个持续运营的过程。但把上面这5种方法落实到位,至少能让日常数据维持在一个健康水平。你真去做了,就会发现很多让人头疼的“数据疑难杂症”,其实在源头就能避免。