未记录事实核验 · 状态:维护中

记录仅说明核验时间与维护状态,不保证适用于所有环境;使用前请留意正文中的版本、适用条件与风险提示。

warning 适用边界与维护说明

本文适合具备Oracle SQL/PLSQL基础的读者审阅银行数据质量规则配置、结果留存、调度监控、清洗及度量的综合设计,不是可直接编译运行或上线的完整方案。本次仅作全文结构与代码静态审阅,未进行数据库运行验证或外部事实核验:失败记录游标仍含非法动态SQL,业务表及报告、告警、清洗日志对象定义缺失,执行失败统计、零分母处理和趋势判断仍有缺陷。余额核对须明确期初余额、流水覆盖、核对时点及币种;关闭账户规则只检查关闭后新增的待处理交易;证件、手机号和邮箱规则仅作有限格式筛查,不代表真实性验证。自动清洗可能将原值覆盖为NULL、改变地址含义或提交缺少日志的修改,负余额冻结也可能影响合法透支,仅宜在隔离测试数据和明确业务授权条件下讨论。生产应用还需完善敏感信息脱敏、日志访问控制、动态规则执行权限及事务保护,本文不构成数据准确性或监管合规保证。

Oracle银行数据质量管理

数据质量管理概述

数据质量管理是银行数据治理的核心组成部分,直接影响业务决策、风险控制和监管合规。在Oracle数据库环境中,数据质量管理体系可用于检查和改善银行数据的准确性、完整性、一致性和时效性;本文展示简化的规则与实现思路,不保证数据质量或监管合规。

1. 数据质量维度

准确性(Accuracy)

  • 数据值正确反映真实世界的实体或事件
  • 符合业务规则和约束条件

完整性(Completeness)

  • 必要的数据字段都有值
  • 记录数量符合预期

一致性(Consistency)

  • 同一数据在不同系统中保持一致
  • 数据格式和编码标准统一

时效性(Timeliness)

  • 数据更新及时
  • 历史数据保留适当

有效性(Validity)

  • 数据符合定义的格式和范围
  • 通过各种验证规则

数据质量检查框架

1. 数据质量规则定义表

-- 数据质量规则配置表
CREATE TABLE dq_rule_config (
    rule_id VARCHAR2(20) PRIMARY KEY,
    rule_name VARCHAR2(100) NOT NULL,
    rule_type VARCHAR2(20) NOT NULL, -- COMPLETENESS, ACCURACY, CONSISTENCY, VALIDITY
    table_name VARCHAR2(100) NOT NULL,
    column_name VARCHAR2(100),
    rule_description VARCHAR2(500),
    check_sql CLOB,
    threshold_warning NUMBER(5,2) DEFAULT 95, -- 告警阈值
    threshold_error NUMBER(5,2) DEFAULT 90,   -- 错误阈值
    is_active CHAR(1) DEFAULT 'Y',
    create_time DATE DEFAULT SYSDATE,
    update_time DATE DEFAULT SYSDATE
);

-- 数据质量检查结果表
CREATE TABLE dq_check_result (
    result_id NUMBER PRIMARY KEY,
    rule_id VARCHAR2(20),
    check_time DATE,
    total_records NUMBER,
    passed_records NUMBER,
    failed_records NUMBER,
    pass_rate NUMBER(5,2),
    status VARCHAR2(20), -- PASS, WARNING, ERROR
    error_details CLOB,
    check_duration NUMBER(10,3), -- 检查耗时(秒)
    FOREIGN KEY (rule_id) REFERENCES dq_rule_config(rule_id)
);

-- 数据质量问题明细表
CREATE TABLE dq_issue_detail (
    issue_id NUMBER PRIMARY KEY,
    result_id NUMBER,
    table_name VARCHAR2(100),
    column_name VARCHAR2(100),
    record_id VARCHAR2(100),
    issue_type VARCHAR2(50),
    issue_description VARCHAR2(1000),
    current_value VARCHAR2(1000),
    expected_value VARCHAR2(1000),
    severity VARCHAR2(20), -- LOW, MEDIUM, HIGH, CRITICAL
    create_time DATE DEFAULT SYSDATE,
    FOREIGN KEY (result_id) REFERENCES dq_check_result(result_id)
);

-- 创建序列
CREATE SEQUENCE dq_result_seq START WITH 1 INCREMENT BY 1;
CREATE SEQUENCE dq_issue_seq START WITH 1 INCREMENT BY 1;

2. 银行业务数据质量规则

-- 插入银行业务数据质量规则
BEGIN
    -- 客户信息完整性检查
    INSERT INTO dq_rule_config VALUES (
        'CUST_COMPLETENESS_01',
        '客户基本信息完整性检查',
        'COMPLETENESS',
        'customer_info',
        'customer_name,id_card,phone',
        '检查客户姓名、身份证号、手机号是否为空',
        'SELECT customer_id FROM customer_info WHERE customer_name IS NULL OR id_card IS NULL OR phone IS NULL',
        98, 95, 'Y', SYSDATE, SYSDATE
    );
    
    -- 身份证号格式检查
    INSERT INTO dq_rule_config VALUES (
        'CUST_VALIDITY_01',
        '身份证号格式有效性检查',
        'VALIDITY',
        'customer_info',
        'id_card',
        '检查身份证号是否符合18位格式规范',
        'SELECT customer_id FROM customer_info WHERE id_card IS NOT NULL AND NOT REGEXP_LIKE(id_card, ''^[0-9]{17}[0-9X]$'')',
        99, 97, 'Y', SYSDATE, SYSDATE
    );
    
    -- 手机号格式检查
    INSERT INTO dq_rule_config VALUES (
        'CUST_VALIDITY_02',
        '手机号格式有效性检查',
        'VALIDITY',
        'customer_info',
        'phone',
        '检查手机号是否符合11位格式规范',
        'SELECT customer_id FROM customer_info WHERE phone IS NOT NULL AND NOT REGEXP_LIKE(phone, ''^1[3-9][0-9]{9}$'')',
        99, 97, 'Y', SYSDATE, SYSDATE
    );
    
    -- 账户余额准确性检查
    INSERT INTO dq_rule_config VALUES (
        'ACCT_ACCURACY_01',
        '账户余额准确性检查',
        'ACCURACY',
        'account_info',
        'balance',
        '检查账户余额与交易记录计算结果是否一致',
        'SELECT ai.account_id FROM account_info ai WHERE ai.balance != (
            SELECT NVL(SUM(CASE WHEN tl.trans_type IN (''DEPOSIT'', ''TRANSFER_IN'') THEN tl.amount
                              WHEN tl.trans_type IN (''WITHDRAW'', ''TRANSFER_OUT'') THEN -tl.amount
                              ELSE 0 END), 0)
            FROM transaction_log tl WHERE tl.account_id = ai.account_id AND tl.status = ''SUCCESS'')',
        95, 90, 'Y', SYSDATE, SYSDATE
    );
    
    -- 交易金额有效性检查
    INSERT INTO dq_rule_config VALUES (
        'TRANS_VALIDITY_01',
        '交易金额有效性检查',
        'VALIDITY',
        'transaction_log',
        'amount',
        '检查交易金额是否为正数且在合理范围内',
        'SELECT trans_id FROM transaction_log WHERE amount <= 0 OR amount > 100000000',
        99, 95, 'Y', SYSDATE, SYSDATE
    );
    
    -- 账户状态一致性检查
    INSERT INTO dq_rule_config VALUES (
        'ACCT_CONSISTENCY_01',
        '账户状态一致性检查',
        'CONSISTENCY',
        'account_info',
        'status',
        '检查关闭账户是否还有未完成的交易',
        'SELECT ai.account_id FROM account_info ai WHERE ai.status = ''CLOSED'' 
         AND EXISTS (SELECT 1 FROM transaction_log tl WHERE tl.account_id = ai.account_id 
                     AND tl.status = ''PENDING'' AND tl.trans_time > ai.close_date)',
        100, 95, 'Y', SYSDATE, SYSDATE
    );
    
    COMMIT;
END;
/

数据质量检查引擎

1. 核心检查引擎

-- 数据质量检查包
CREATE OR REPLACE PACKAGE pkg_data_quality AS
    
    -- 执行单个规则检查
    PROCEDURE execute_rule_check(
        p_rule_id IN VARCHAR2,
        p_result_id OUT NUMBER
    );
    
    -- 执行批量规则检查
    PROCEDURE execute_batch_check(
        p_rule_type IN VARCHAR2 DEFAULT NULL,
        p_table_name IN VARCHAR2 DEFAULT NULL
    );
    
    -- 生成数据质量报告
    PROCEDURE generate_quality_report(
        p_start_date IN DATE,
        p_end_date IN DATE,
        p_report_type IN VARCHAR2 DEFAULT 'SUMMARY'
    );
    
    -- 修复数据质量问题
    PROCEDURE fix_quality_issues(
        p_issue_type IN VARCHAR2,
        p_auto_fix IN BOOLEAN DEFAULT FALSE
    );
    
END pkg_data_quality;
/

CREATE OR REPLACE PACKAGE BODY pkg_data_quality AS
    
    -- 执行单个规则检查
    PROCEDURE execute_rule_check(
        p_rule_id IN VARCHAR2,
        p_result_id OUT NUMBER
    ) AS
        v_rule_config dq_rule_config%ROWTYPE;
        v_failed_count NUMBER := 0;
        v_total_count NUMBER := 0;
        v_pass_rate NUMBER;
        v_status VARCHAR2(20);
        v_start_time TIMESTAMP;
        v_end_time TIMESTAMP;
        v_duration NUMBER;
        v_error_details CLOB;
        
        CURSOR c_failed_records IS
            SELECT column_value as failed_id FROM TABLE(
                CAST(MULTISET(
                    SELECT REGEXP_SUBSTR(result_column, '[^,]+', 1, level)
                    FROM (
                        SELECT LISTAGG(subquery_result, ',') WITHIN GROUP (ORDER BY 1) as result_column
                        FROM (
                            EXECUTE IMMEDIATE v_rule_config.check_sql
                        )
                    )
                    CONNECT BY REGEXP_SUBSTR(result_column, '[^,]+', 1, level) IS NOT NULL
                ) AS sys.odcivarchar2list)
            );
    BEGIN
        v_start_time := SYSTIMESTAMP;
        
        -- 获取规则配置
        SELECT * INTO v_rule_config
        FROM dq_rule_config
        WHERE rule_id = p_rule_id AND is_active = 'Y';
        
        -- 获取表总记录数
        EXECUTE IMMEDIATE 'SELECT COUNT(*) FROM ' || v_rule_config.table_name
        INTO v_total_count;
        
        -- 执行检查SQL获取失败记录数
        BEGIN
            EXECUTE IMMEDIATE 'SELECT COUNT(*) FROM (' || v_rule_config.check_sql || ')'
            INTO v_failed_count;
        EXCEPTION
            WHEN OTHERS THEN
                v_error_details := 'SQL执行失败: ' || SQLERRM;
                v_failed_count := -1;
        END;
        
        -- 计算通过率
        IF v_total_count > 0 AND v_failed_count >= 0 THEN
            v_pass_rate := ((v_total_count - v_failed_count) / v_total_count) * 100;
        ELSE
            v_pass_rate := 0;
        END IF;
        
        -- 确定状态
        IF v_failed_count = -1 THEN
            v_status := 'ERROR';
        ELSIF v_pass_rate >= v_rule_config.threshold_warning THEN
            v_status := 'PASS';
        ELSIF v_pass_rate >= v_rule_config.threshold_error THEN
            v_status := 'WARNING';
        ELSE
            v_status := 'ERROR';
        END IF;
        
        v_end_time := SYSTIMESTAMP;
        v_duration := EXTRACT(DAY FROM (v_end_time - v_start_time)) * 86400
                    + EXTRACT(HOUR FROM (v_end_time - v_start_time)) * 3600
                    + EXTRACT(MINUTE FROM (v_end_time - v_start_time)) * 60
                    + EXTRACT(SECOND FROM (v_end_time - v_start_time));
        
        -- 插入检查结果
        INSERT INTO dq_check_result (
            result_id, rule_id, check_time, total_records,
            passed_records, failed_records, pass_rate, status,
            error_details, check_duration
        ) VALUES (
            dq_result_seq.NEXTVAL, p_rule_id, SYSDATE, v_total_count,
            v_total_count - v_failed_count, v_failed_count, v_pass_rate, v_status,
            v_error_details, v_duration
        ) RETURNING result_id INTO p_result_id;
        
        -- 记录问题明细(仅在有失败记录且数量不太多时)
        IF v_failed_count > 0 AND v_failed_count <= 1000 THEN
            FOR rec IN c_failed_records LOOP
                INSERT INTO dq_issue_detail (
                    issue_id, result_id, table_name, column_name,
                    record_id, issue_type, issue_description, severity
                ) VALUES (
                    dq_issue_seq.NEXTVAL, p_result_id, v_rule_config.table_name,
                    v_rule_config.column_name, rec.failed_id, v_rule_config.rule_type,
                    v_rule_config.rule_description, 
                    CASE WHEN v_status = 'ERROR' THEN 'HIGH' ELSE 'MEDIUM' END
                );
            END LOOP;
        END IF;
        
        COMMIT;
        
    EXCEPTION
        WHEN NO_DATA_FOUND THEN
            RAISE_APPLICATION_ERROR(-20001, '规则不存在或已禁用: ' || p_rule_id);
        WHEN OTHERS THEN
            ROLLBACK;
            RAISE;
    END execute_rule_check;
    
    -- 执行批量规则检查
    PROCEDURE execute_batch_check(
        p_rule_type IN VARCHAR2 DEFAULT NULL,
        p_table_name IN VARCHAR2 DEFAULT NULL
    ) AS
        v_result_id NUMBER;
        v_check_count NUMBER := 0;
    BEGIN
        FOR rule_rec IN (
            SELECT rule_id
            FROM dq_rule_config
            WHERE is_active = 'Y'
            AND (p_rule_type IS NULL OR rule_type = p_rule_type)
            AND (p_table_name IS NULL OR table_name = p_table_name)
            ORDER BY rule_id
        ) LOOP
            BEGIN
                execute_rule_check(rule_rec.rule_id, v_result_id);
                v_check_count := v_check_count + 1;
                
                DBMS_OUTPUT.PUT_LINE('规则 ' || rule_rec.rule_id || ' 检查完成,结果ID: ' || v_result_id);
                
            EXCEPTION
                WHEN OTHERS THEN
                    DBMS_OUTPUT.PUT_LINE('规则 ' || rule_rec.rule_id || ' 检查失败: ' || SQLERRM);
            END;
        END LOOP;
        
        DBMS_OUTPUT.PUT_LINE('批量检查完成,共执行 ' || v_check_count || ' 个规则');
    END execute_batch_check;
    
    -- 生成数据质量报告
    PROCEDURE generate_quality_report(
        p_start_date IN DATE,
        p_end_date IN DATE,
        p_report_type IN VARCHAR2 DEFAULT 'SUMMARY'
    ) AS
        v_report CLOB;
        v_total_checks NUMBER;
        v_pass_count NUMBER;
        v_warning_count NUMBER;
        v_error_count NUMBER;
        v_avg_pass_rate NUMBER;
    BEGIN
        -- 获取统计数据
        SELECT 
            COUNT(*),
            SUM(CASE WHEN status = 'PASS' THEN 1 ELSE 0 END),
            SUM(CASE WHEN status = 'WARNING' THEN 1 ELSE 0 END),
            SUM(CASE WHEN status = 'ERROR' THEN 1 ELSE 0 END),
            AVG(pass_rate)
        INTO v_total_checks, v_pass_count, v_warning_count, v_error_count, v_avg_pass_rate
        FROM dq_check_result
        WHERE check_time BETWEEN p_start_date AND p_end_date;
        
        -- 生成报告头部
        v_report := '数据质量检查报告' || CHR(10) ||
                   '报告期间: ' || TO_CHAR(p_start_date, 'YYYY-MM-DD') || 
                   ' 至 ' || TO_CHAR(p_end_date, 'YYYY-MM-DD') || CHR(10) ||
                   '生成时间: ' || TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS') || CHR(10) ||
                   RPAD('=', 50, '=') || CHR(10) || CHR(10);
        
        -- 添加汇总信息
        v_report := v_report ||
                   '检查汇总:' || CHR(10) ||
                   '  总检查次数: ' || v_total_checks || CHR(10) ||
                   '  通过次数: ' || v_pass_count || ' (' || 
                   ROUND(v_pass_count * 100 / v_total_checks, 2) || '%)' || CHR(10) ||
                   '  警告次数: ' || v_warning_count || ' (' || 
                   ROUND(v_warning_count * 100 / v_total_checks, 2) || '%)' || CHR(10) ||
                   '  错误次数: ' || v_error_count || ' (' || 
                   ROUND(v_error_count * 100 / v_total_checks, 2) || '%)' || CHR(10) ||
                   '  平均通过率: ' || ROUND(v_avg_pass_rate, 2) || '%' || CHR(10) || CHR(10);
        
        -- 按规则类型统计
        v_report := v_report || '按规则类型统计:' || CHR(10);
        FOR type_rec IN (
            SELECT 
                rc.rule_type,
                COUNT(*) as check_count,
                AVG(cr.pass_rate) as avg_pass_rate,
                SUM(CASE WHEN cr.status = 'ERROR' THEN 1 ELSE 0 END) as error_count
            FROM dq_check_result cr
            JOIN dq_rule_config rc ON cr.rule_id = rc.rule_id
            WHERE cr.check_time BETWEEN p_start_date AND p_end_date
            GROUP BY rc.rule_type
            ORDER BY rc.rule_type
        ) LOOP
            v_report := v_report ||
                       '  ' || type_rec.rule_type || ': ' ||
                       '检查次数=' || type_rec.check_count ||
                       ', 平均通过率=' || ROUND(type_rec.avg_pass_rate, 2) || '%' ||
                       ', 错误次数=' || type_rec.error_count || CHR(10);
        END LOOP;
        
        -- 详细报告
        IF p_report_type = 'DETAIL' THEN
            v_report := v_report || CHR(10) || '详细检查结果:' || CHR(10);
            FOR detail_rec IN (
                SELECT 
                    cr.rule_id,
                    rc.rule_name,
                    cr.check_time,
                    cr.pass_rate,
                    cr.status,
                    cr.failed_records
                FROM dq_check_result cr
                JOIN dq_rule_config rc ON cr.rule_id = rc.rule_id
                WHERE cr.check_time BETWEEN p_start_date AND p_end_date
                AND cr.status IN ('WARNING', 'ERROR')
                ORDER BY cr.check_time DESC
            ) LOOP
                v_report := v_report ||
                           '  规则: ' || detail_rec.rule_name || CHR(10) ||
                           '    检查时间: ' || TO_CHAR(detail_rec.check_time, 'YYYY-MM-DD HH24:MI:SS') || CHR(10) ||
                           '    通过率: ' || detail_rec.pass_rate || '%' || CHR(10) ||
                           '    状态: ' || detail_rec.status || CHR(10) ||
                           '    失败记录数: ' || detail_rec.failed_records || CHR(10) || CHR(10);
            END LOOP;
        END IF;
        
        -- 输出报告
        DBMS_OUTPUT.PUT_LINE(v_report);
        
        -- 保存报告到表
        INSERT INTO dq_quality_report (
            report_id, report_date, report_type, report_content, create_time
        ) VALUES (
            dq_report_seq.NEXTVAL, TRUNC(SYSDATE), p_report_type, v_report, SYSDATE
        );
        
    END generate_quality_report;
    
    -- 修复数据质量问题
    PROCEDURE fix_quality_issues(
        p_issue_type IN VARCHAR2,
        p_auto_fix IN BOOLEAN DEFAULT FALSE
    ) AS
        v_fixed_count NUMBER := 0;
    BEGIN
        IF p_issue_type = 'PHONE_FORMAT' THEN
            -- 修复手机号格式问题
            FOR phone_rec IN (
                SELECT customer_id, phone
                FROM customer_info
                WHERE phone IS NOT NULL 
                AND LENGTH(phone) = 11
                AND SUBSTR(phone, 1, 1) = '1'
                AND NOT REGEXP_LIKE(phone, '^1[3-9][0-9]{9}$')
            ) LOOP
                IF p_auto_fix THEN
                    -- 自动修复:去除非数字字符
                    UPDATE customer_info 
                    SET phone = REGEXP_REPLACE(phone_rec.phone, '[^0-9]', '')
                    WHERE customer_id = phone_rec.customer_id;
                    
                    v_fixed_count := v_fixed_count + 1;
                ELSE
                    DBMS_OUTPUT.PUT_LINE('客户 ' || phone_rec.customer_id || 
                                       ' 手机号格式异常: ' || phone_rec.phone);
                END IF;
            END LOOP;
            
        ELSIF p_issue_type = 'DUPLICATE_ID_CARD' THEN
            -- 处理重复身份证号
            FOR dup_rec IN (
                SELECT id_card, COUNT(*) as dup_count
                FROM customer_info
                WHERE id_card IS NOT NULL
                GROUP BY id_card
                HAVING COUNT(*) > 1
            ) LOOP
                DBMS_OUTPUT.PUT_LINE('发现重复身份证号: ' || dup_rec.id_card || 
                                   ', 重复次数: ' || dup_rec.dup_count);
                
                -- 重复身份证号需要人工处理,不能自动修复
            END LOOP;
            
        ELSIF p_issue_type = 'NEGATIVE_BALANCE' THEN
            -- 处理负余额账户
            FOR neg_rec IN (
                SELECT account_id, balance
                FROM account_info
                WHERE balance < 0
                AND status = 'ACTIVE'
            ) LOOP
                IF p_auto_fix THEN
                    -- 自动冻结负余额账户
                    UPDATE account_info
                    SET status = 'FROZEN',
                        freeze_reason = '余额为负自动冻结'
                    WHERE account_id = neg_rec.account_id;
                    
                    v_fixed_count := v_fixed_count + 1;
                ELSE
                    DBMS_OUTPUT.PUT_LINE('账户 ' || neg_rec.account_id || 
                                       ' 余额为负: ' || neg_rec.balance);
                END IF;
            END LOOP;
        END IF;
        
        IF p_auto_fix THEN
            COMMIT;
            DBMS_OUTPUT.PUT_LINE('自动修复完成,共修复 ' || v_fixed_count || ' 条记录');
        END IF;
        
    END fix_quality_issues;
    
END pkg_data_quality;
/

数据质量监控和预警

1. 实时监控触发器

-- 实时数据质量监控触发器
CREATE OR REPLACE TRIGGER trg_realtime_dq_monitor
AFTER INSERT OR UPDATE ON customer_info
FOR EACH ROW
DECLARE
    v_issue_count NUMBER := 0;
    v_issues VARCHAR2(1000) := '';
BEGIN
    -- 检查手机号格式
    IF :NEW.phone IS NOT NULL AND NOT REGEXP_LIKE(:NEW.phone, '^1[3-9][0-9]{9}$') THEN
        v_issue_count := v_issue_count + 1;
        v_issues := v_issues || '手机号格式错误; ';
    END IF;
    
    -- 检查身份证号格式
    IF :NEW.id_card IS NOT NULL AND NOT REGEXP_LIKE(:NEW.id_card, '^[0-9]{17}[0-9X]$') THEN
        v_issue_count := v_issue_count + 1;
        v_issues := v_issues || '身份证号格式错误; ';
    END IF;
    
    -- 检查邮箱格式
    IF :NEW.email IS NOT NULL AND NOT REGEXP_LIKE(:NEW.email, '^[A-Za-z0-9._%-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$') THEN
        v_issue_count := v_issue_count + 1;
        v_issues := v_issues || '邮箱格式错误; ';
    END IF;
    
    -- 如果发现问题,记录到实时监控表
    IF v_issue_count > 0 THEN
        INSERT INTO dq_realtime_alert (
            alert_id, table_name, record_id, issue_count,
            issue_description, alert_time
        ) VALUES (
            dq_realtime_seq.NEXTVAL, 'customer_info', :NEW.customer_id,
            v_issue_count, v_issues, SYSDATE
        );
    END IF;
END trg_realtime_dq_monitor;
/

2. 定时监控调度

-- 创建定时数据质量检查作业
BEGIN
    DBMS_SCHEDULER.CREATE_JOB (
        job_name => 'DQ_DAILY_CHECK_JOB',
        job_type => 'PLSQL_BLOCK',
        job_action => 'BEGIN pkg_data_quality.execute_batch_check(); END;',
        start_date => SYSTIMESTAMP,
        repeat_interval => 'FREQ=DAILY; BYHOUR=2; BYMINUTE=0; BYSECOND=0',
        enabled => TRUE,
        comments => '每日凌晨2点执行数据质量检查'
    );
    
    -- 创建周报生成作业
    DBMS_SCHEDULER.CREATE_JOB (
        job_name => 'DQ_WEEKLY_REPORT_JOB',
        job_type => 'PLSQL_BLOCK',
        job_action => 'BEGIN pkg_data_quality.generate_quality_report(SYSDATE-7, SYSDATE, ''DETAIL''); END;',
        start_date => SYSTIMESTAMP,
        repeat_interval => 'FREQ=WEEKLY; BYDAY=MON; BYHOUR=8; BYMINUTE=0; BYSECOND=0',
        enabled => TRUE,
        comments => '每周一上午8点生成数据质量周报'
    );
END;
/

数据清洗和标准化

1. 数据清洗函数

-- 数据清洗函数包
CREATE OR REPLACE PACKAGE pkg_data_cleansing AS
    
    FUNCTION clean_phone_number(p_phone VARCHAR2) RETURN VARCHAR2;
    FUNCTION clean_id_card(p_id_card VARCHAR2) RETURN VARCHAR2;
    FUNCTION clean_email(p_email VARCHAR2) RETURN VARCHAR2;
    FUNCTION standardize_address(p_address VARCHAR2) RETURN VARCHAR2;
    FUNCTION clean_amount(p_amount VARCHAR2) RETURN NUMBER;
    
END pkg_data_cleansing;
/

CREATE OR REPLACE PACKAGE BODY pkg_data_cleansing AS
    
    FUNCTION clean_phone_number(p_phone VARCHAR2) RETURN VARCHAR2 AS
        v_cleaned VARCHAR2(32767);
    BEGIN
        IF p_phone IS NULL THEN
            RETURN NULL;
        END IF;
        
        -- 去除所有非数字字符
        v_cleaned := REGEXP_REPLACE(p_phone, '[^0-9]', '');
        
        -- 检查长度和格式
        IF LENGTH(v_cleaned) = 11 AND SUBSTR(v_cleaned, 1, 1) = '1' THEN
            RETURN v_cleaned;
        ELSE
            RETURN NULL; -- 无法清洗的返回NULL
        END IF;
    END clean_phone_number;
    
    FUNCTION clean_id_card(p_id_card VARCHAR2) RETURN VARCHAR2 AS
        v_cleaned VARCHAR2(32767);
    BEGIN
        IF p_id_card IS NULL THEN
            RETURN NULL;
        END IF;
        
        -- 转换为大写并去除空格
        v_cleaned := UPPER(TRIM(p_id_card));
        
        -- 检查格式
        IF REGEXP_LIKE(v_cleaned, '^[0-9]{17}[0-9X]$') THEN
            RETURN v_cleaned;
        ELSE
            RETURN NULL;
        END IF;
    END clean_id_card;
    
    FUNCTION clean_email(p_email VARCHAR2) RETURN VARCHAR2 AS
        v_cleaned VARCHAR2(100);
    BEGIN
        IF p_email IS NULL THEN
            RETURN NULL;
        END IF;
        
        -- 转换为小写并去除空格
        v_cleaned := LOWER(TRIM(p_email));
        
        -- 检查格式
        IF REGEXP_LIKE(v_cleaned, '^[A-Za-z0-9._%-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$') THEN
            RETURN v_cleaned;
        ELSE
            RETURN NULL;
        END IF;
    END clean_email;
    
    FUNCTION standardize_address(p_address VARCHAR2) RETURN VARCHAR2 AS
        v_standardized VARCHAR2(500);
    BEGIN
        IF p_address IS NULL THEN
            RETURN NULL;
        END IF;
        
        v_standardized := TRIM(p_address);
        
        -- 标准化常见地址词汇
        v_standardized := REPLACE(v_standardized, ' ', ' '); -- 全角空格转半角
        v_standardized := REGEXP_REPLACE(v_standardized, '\s+', ' '); -- 多个空格合并
        v_standardized := REPLACE(v_standardized, '路口', '路');
        v_standardized := REPLACE(v_standardized, '大厦', '大厦');
        v_standardized := REPLACE(v_standardized, '小区', '小区');
        
        RETURN v_standardized;
    END standardize_address;
    
    FUNCTION clean_amount(p_amount VARCHAR2) RETURN NUMBER AS
        v_cleaned VARCHAR2(50);
        v_amount NUMBER;
    BEGIN
        IF p_amount IS NULL THEN
            RETURN NULL;
        END IF;
        
        -- 去除货币符号和千分位分隔符
        v_cleaned := TRIM(p_amount);
        v_cleaned := REPLACE(v_cleaned, '¥', '');
        v_cleaned := REPLACE(v_cleaned, '$', '');
        v_cleaned := REPLACE(v_cleaned, ',', '');
        v_cleaned := TRIM(v_cleaned);
        
        -- 转换为数字
        BEGIN
            v_amount := TO_NUMBER(v_cleaned);
            RETURN v_amount;
        EXCEPTION
            WHEN OTHERS THEN
                RETURN NULL;
        END;
    END clean_amount;
    
END pkg_data_cleansing;
/

2. 批量数据清洗

-- 批量数据清洗过程
CREATE OR REPLACE PROCEDURE batch_data_cleansing AS
    v_processed_count NUMBER := 0;
    v_cleaned_count NUMBER := 0;
BEGIN
    DBMS_OUTPUT.PUT_LINE('开始批量数据清洗...');
    
    -- 清洗客户信息表
    FOR cust_rec IN (
        SELECT customer_id, phone, id_card, email, address
        FROM customer_info
        WHERE update_time < SYSDATE - 1 -- 只处理一天前的数据
    ) LOOP
        DECLARE
            v_new_phone VARCHAR2(11);
            v_new_id_card VARCHAR2(18);
            v_new_email VARCHAR2(100);
            v_new_address VARCHAR2(500);
            v_has_changes BOOLEAN := FALSE;
        BEGIN
            v_processed_count := v_processed_count + 1;
            
            -- 清洗手机号
            v_new_phone := pkg_data_cleansing.clean_phone_number(cust_rec.phone);
            IF v_new_phone != cust_rec.phone OR (v_new_phone IS NULL AND cust_rec.phone IS NOT NULL) THEN
                v_has_changes := TRUE;
            END IF;
            
            -- 清洗身份证号
            v_new_id_card := pkg_data_cleansing.clean_id_card(cust_rec.id_card);
            IF v_new_id_card != cust_rec.id_card OR (v_new_id_card IS NULL AND cust_rec.id_card IS NOT NULL) THEN
                v_has_changes := TRUE;
            END IF;
            
            -- 清洗邮箱
            v_new_email := pkg_data_cleansing.clean_email(cust_rec.email);
            IF v_new_email != cust_rec.email OR (v_new_email IS NULL AND cust_rec.email IS NOT NULL) THEN
                v_has_changes := TRUE;
            END IF;
            
            -- 清洗地址
            v_new_address := pkg_data_cleansing.standardize_address(cust_rec.address);
            IF v_new_address != cust_rec.address THEN
                v_has_changes := TRUE;
            END IF;
            
            -- 如果有变更,更新记录
            IF v_has_changes THEN
                UPDATE customer_info SET
                    phone = v_new_phone,
                    id_card = v_new_id_card,
                    email = v_new_email,
                    address = v_new_address,
                    update_time = SYSDATE
                WHERE customer_id = cust_rec.customer_id;
                
                v_cleaned_count := v_cleaned_count + 1;
                
                -- 记录清洗日志
                INSERT INTO data_cleansing_log (
                    log_id, table_name, record_id, 
                    original_values, cleaned_values, cleansing_time
                ) VALUES (
                    cleansing_log_seq.NEXTVAL, 'customer_info', cust_rec.customer_id,
                    'phone:' || cust_rec.phone || '|id_card:' || cust_rec.id_card || 
                    '|email:' || cust_rec.email || '|address:' || cust_rec.address,
                    'phone:' || v_new_phone || '|id_card:' || v_new_id_card || 
                    '|email:' || v_new_email || '|address:' || v_new_address,
                    SYSDATE
                );
            END IF;
            
            -- 每1000条提交一次
            IF MOD(v_processed_count, 1000) = 0 THEN
                COMMIT;
                DBMS_OUTPUT.PUT_LINE('已处理 ' || v_processed_count || ' 条记录');
            END IF;
            
        EXCEPTION
            WHEN OTHERS THEN
                DBMS_OUTPUT.PUT_LINE('处理客户 ' || cust_rec.customer_id || ' 时出错: ' || SQLERRM);
        END;
    END LOOP;
    
    COMMIT;
    
    DBMS_OUTPUT.PUT_LINE('批量数据清洗完成');
    DBMS_OUTPUT.PUT_LINE('总处理记录数: ' || v_processed_count);
    DBMS_OUTPUT.PUT_LINE('清洗记录数: ' || v_cleaned_count);
    DBMS_OUTPUT.PUT_LINE('清洗率: ' || ROUND(v_cleaned_count * 100 / v_processed_count, 2) || '%');
    
END batch_data_cleansing;
/

数据质量指标和度量

1. 质量指标计算

-- 数据质量指标视图
CREATE OR REPLACE VIEW v_data_quality_metrics AS
SELECT 
    'customer_info' as table_name,
    'completeness' as metric_type,
    'customer_name' as column_name,
    ROUND((COUNT(*) - COUNT(CASE WHEN customer_name IS NULL THEN 1 END)) * 100.0 / COUNT(*), 2) as score,
    COUNT(*) as total_records,
    COUNT(CASE WHEN customer_name IS NULL THEN 1 END) as null_count
FROM customer_info
UNION ALL
SELECT 
    'customer_info',
    'validity',
    'phone',
    ROUND(COUNT(CASE WHEN phone IS NOT NULL AND REGEXP_LIKE(phone, '^1[3-9][0-9]{9}$') THEN 1 END) * 100.0 / 
          COUNT(CASE WHEN phone IS NOT NULL THEN 1 END), 2),
    COUNT(CASE WHEN phone IS NOT NULL THEN 1 END),
    COUNT(CASE WHEN phone IS NOT NULL AND NOT REGEXP_LIKE(phone, '^1[3-9][0-9]{9}$') THEN 1 END)
FROM customer_info
UNION ALL
SELECT 
    'account_info',
    'consistency',
    'balance',
    ROUND(COUNT(CASE WHEN ai.balance = (
        SELECT NVL(SUM(CASE WHEN tl.trans_type IN ('DEPOSIT', 'TRANSFER_IN') THEN tl.amount
                          WHEN tl.trans_type IN ('WITHDRAW', 'TRANSFER_OUT') THEN -tl.amount
                          ELSE 0 END), 0)
        FROM transaction_log tl 
        WHERE tl.account_id = ai.account_id AND tl.status = 'SUCCESS'
    ) THEN 1 END) * 100.0 / COUNT(*), 2),
    COUNT(*),
    COUNT(*) - COUNT(CASE WHEN ai.balance = (
        SELECT NVL(SUM(CASE WHEN tl.trans_type IN ('DEPOSIT', 'TRANSFER_IN') THEN tl.amount
                          WHEN tl.trans_type IN ('WITHDRAW', 'TRANSFER_OUT') THEN -tl.amount
                          ELSE 0 END), 0)
        FROM transaction_log tl 
        WHERE tl.account_id = ai.account_id AND tl.status = 'SUCCESS'
    ) THEN 1 END)
FROM account_info ai;

2. 质量趋势分析

-- 数据质量趋势分析函数
CREATE OR REPLACE FUNCTION analyze_quality_trend(
    p_days IN NUMBER DEFAULT 30
) RETURN CLOB AS
    v_result CLOB;
    v_current_score NUMBER;
    v_previous_score NUMBER;
    v_trend VARCHAR2(20);
BEGIN
    v_result := '数据质量趋势分析(最近' || p_days || '天)' || CHR(10) || CHR(10);
    
    -- 分析各个质量维度的趋势
    FOR metric_rec IN (
        SELECT DISTINCT rule_type FROM dq_rule_config WHERE is_active = 'Y'
    ) LOOP
        -- 获取最近平均分数
        SELECT AVG(pass_rate)
        INTO v_current_score
        FROM dq_check_result cr
        JOIN dq_rule_config rc ON cr.rule_id = rc.rule_id
        WHERE rc.rule_type = metric_rec.rule_type
        AND cr.check_time >= SYSDATE - 7;
        
        -- 获取之前平均分数
        SELECT AVG(pass_rate)
        INTO v_previous_score
        FROM dq_check_result cr
        JOIN dq_rule_config rc ON cr.rule_id = rc.rule_id
        WHERE rc.rule_type = metric_rec.rule_type
        AND cr.check_time BETWEEN SYSDATE - p_days AND SYSDATE - 7;
        
        -- 判断趋势
        IF v_current_score > v_previous_score + 2 THEN
            v_trend := '上升 ↗';
        ELSIF v_current_score < v_previous_score - 2 THEN
            v_trend := '下降 ↘';
        ELSE
            v_trend := '平稳 →';
        END IF;
        
        v_result := v_result || 
                   metric_rec.rule_type || ': ' ||
                   '当前=' || ROUND(v_current_score, 2) || '%, ' ||
                   '之前=' || ROUND(v_previous_score, 2) || '%, ' ||
                   '趋势=' || v_trend || CHR(10);
    END LOOP;
    
    RETURN v_result;
END analyze_quality_trend;
/

本文以简化示例展示Oracle银行数据质量管理中的检查规则、自动化监控、数据清洗和质量度量机制,目标是为业务决策和风险控制提供数据质量支持;实际效果取决于业务口径、数据覆盖、实现正确性和运行控制,不能据此保证银行数据达到高质量标准。