手写一个DQC(DQC简介及数据解析)

一:DQC核心流程

    Define:数据质检规则(指标)的定义。
            你要告警给谁,你要使用什么方式告警(邮件,即时消息),你的规则是什么(空值,波动)等
    Measure:数据质检任务的执行
            数据在哪存储:hive、mysql是基本的数据库、CK、kylin等
    Analyze:数据质检结果量化及可视化展示。
            分为两种情况:(一)不需要图形化界面,直接在调度里面进行bash配置,使用自定义代码解析(二)有图形化界面,可以操作和查看历史结果

二:DQC标准

    Accuracy:准确性。如是否符合表的加工逻辑。
    Completeness:完备性。如数据是否存在丢失。
    Timeliness:及时性。如表数据是否按时产生。
    Uniqueness:唯一性。如主键字段是否唯一。
    Validity:合规性。如字段长度是否合规、枚举值集合是否合规。
    Consistency:一致性。如表与表之间在某些字段上是否存在矛盾。

三:DQC规则

    1有效性
    字段长度有效、字段内容有效、字段数值范围有效、枚举值个数有效、枚举值集合有效
    2 唯一性
    对主键是否存在重复数据的监控指标。
    3 完整性
    字段是否为空或NULL、记录数是否丢失、记录数环比波动、录数波动范围、记录数方差检验、
    4 准确性
    数值同比、数值环比、数值方差检验、表逻辑检查
    5 一致性
    表级别一致性检查,外键检查
    6 时效性
    表级别质量监控指标,数据是否按时产出
    7数据剖析
    最大值检查、最小值检查、平均值检查、汇总值检查
    8 自定义规则检查
    用户写自定义SQL实现的监控规则
    从有效性、唯一性、完整性、准确性、一致性、时效性、数据剖析和自定义规则检查等几个维度对数据质量进行测量,但对于现在超级大的数据量级监控所有的数据是不符合成本效率的。
    因此,知道哪些数据为最关键的,对这些关键数据进行全链路的数据质量,这样有助于防止错误或揭示改进的机会。

总结:指定值、空值、外键规范、外键最大最小、行数统计、最大、最小、平均、用户自定义

四:样例代码

通用代码--参数接收类

日志解析类,主要解析命令行后面的参数

class DqcArgs{
    private String alter_user;   //告警人
    private String alter_type;   //告警方式

    private String date;         //需要运行的时间 默认dt=这个列

    private String table_name;    //需要监控的table
    private String where_column_name;   //需要限制条件的column的值,多个用|分割 必须和column_value对应  in(is null) inn (is not null)
    private String where_column_value;  //需要限制条件的column的值,多个用|分割,可以不写   where条件中使用

    private String column;            //需要监控的列
    private String express;           //需要监控的类型 eq(=) lt(<) gt(>)  le(<=) ge(>=) ne(!=)
    private String express_value;     //最终的结果
    private String aggregate_type;    //需要计算的类型 C count|D count distinct|M min|X max| A avg|S sum|uq(计算当前值是否唯一) fl(和昨天相比波动)
    private String sql;               //自定义sql语句

    public static DqcArgs parse_string_2_dqcargs(String[] args{

        DqcArgs dqcArgs = new DqcArgs();
        for (int i = 0; i < args.length; i++) {
            String[] pair = args[i].split("=");
            String name = pair[0].substring(1);
            String value = pair[1];
            switch (name) {
                case "alter_user":dqcArgs.setAlter_user(value);break;
                case "alter_type":dqcArgs.setAlter_type(value);break;
                case "date":dqcArgs.setDate(value);break;
                case "table_name":dqcArgs.setTable_name(value);break;
                case "column":dqcArgs.setColumn(value);break;
                case "where_column_name":dqcArgs.setWhere_column_name(value);break;
                case "where_column_value":dqcArgs.setWhere_column_value(value);break;
                case "express":dqcArgs.setExpress(value);break;
                case "express_value":dqcArgs.setExpress_value(value);break;
                case "aggregate_type":dqcArgs.setAggregate_type(value);break;
                default:System.out.println("无法解析参数!"+name);return null;
            }
        }
        return dqcArgs;
    }

    public String getAlter_user() {return alter_user;}
    public void setAlter_user(String alter_user{this.alter_user = alter_user;}
    public String getAlter_type() {return alter_type;}
    public void setAlter_type(String alter_type{this.alter_type = alter_type;}
    public String getDate() {return date;}
    public void setDate(String date{this.date = date;}
    public String getTable_name() {return table_name;}
    public void setTable_name(String table_name{this.table_name = table_name;}
    public String getWhere_column_name() {return where_column_name;}
    public void setWhere_column_name(String where_column_name{this.where_column_name = where_column_name;}
    public String getWhere_column_value() {return where_column_value;}
    public void setWhere_column_value(String where_column_value{this.where_column_value = where_column_value;}
    public String getColumn() {return column;}
    public void setColumn(String column{this.column = column;}
    public String getExpress() {return express;}
    public void setExpress(String express{this.express = express;}
    public String getExpress_value() {return express_value;}
    public void setExpress_value(String express_value{this.express_value = express_value;}
    public String getAggregate_type() {return aggregate_type;}
    public void setAggregate_type(String aggregate_type{this.aggregate_type = aggregate_type;}
    public String getSql() {return sql;}
    public void setSql(String sql{this.sql = sql;}

    @Override
    public String toString() 
{
        return String.format("DqcArgs{alter_user='%s', alter_type='%s', date='%s', table_name='%s',column='%s', where_column_name='%s', where_column_value='%s', express='%s', express_value='%s', aggregate_type='%s', sql='%s'}",
                alter_user, alter_type, date, table_name,column, where_column_name, where_column_value, express, express_value, aggregate_type, sql);
    }
}
通用代码--解析参数成为sql
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.io.BufferedReader;
import java.io.InputStreamReader;

public class Dqc {

    public static void main(String[] args) throws Exception {
        DqcArgs dqcArgs = DqcArgs.parse_string_2_dqcargs(args);
        //System.out.println(dqcArgs.toString());//将字符串解析成需要告警的参数
        String sql = concat_sql(dqcArgs);
        System.out.println(sql);//dqcArgs 拼接成的sql
    }
    public static String concat_sql(DqcArgs dqcArgs){
        StringBuilder stringBuilder = new StringBuilder("select ");

        switch (dqcArgs.getAggregate_type()) {
            case "C" :stringBuilder.append("count("+dqcArgs.getColumn()+") "+" as data,"+"count("+dqcArgs.getColumn()+") ");break;
            case "D" :stringBuilder.append("count(distinct "+dqcArgs.getColumn()+") "+" as data,"+"count(distinct "+dqcArgs.getColumn()+") ");break;
            case "M" :stringBuilder.append("min("+dqcArgs.getColumn()+") "+" as data,"+"min("+dqcArgs.getColumn()+") ");break;
            case "A" :stringBuilder.append("avg("+dqcArgs.getColumn()+") "+" as data,"+"avg("+dqcArgs.getColumn()+") ");break;
            case "S" :stringBuilder.append("sum("+dqcArgs.getColumn()+") "+" as data,"+"sum("+dqcArgs.getColumn()+") ");break;
            case "UQ":stringBuilder.append("count(distinct "+dqcArgs.getColumn()+") = "+"count("+dqcArgs.getColumn()+") ");break;
            case "FL":stringBuilder.append("count("+dqcArgs.getColumn()+")");break;
            default:System.out.println("请检查参数:aggregate_type 参数错误!"); System.exit(1);
        }
        if(!(dqcArgs.getAggregate_type().equals("UQ")) && !(dqcArgs.getAggregate_type().equals("FL"))){
            switch (dqcArgs.getExpress()) {
                case "eq" :stringBuilder.append("=");break;
                case "lt" :stringBuilder.append("<");break;
                case "gt" :stringBuilder.append(">");break;
                case "le" :stringBuilder.append("<=");break;
                case "ge" :stringBuilder.append(">=");break;
                case "ne":stringBuilder.append("!=");break;
                default:System.out.println("请检查参数:express 参数错误!");System.exit(1);
            }
            stringBuilder.append(dqcArgs.getExpress_value());
        }
        stringBuilder.append(" as result from "+dqcArgs.getTable_name());


        if(!(dqcArgs.getAggregate_type().equals("FL"))){//拼接dt=20230401的语句
            stringBuilder.append(" where dt in("+dqcArgs.getDate()+") ");
        }else{
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
            LocalDate resultDate = LocalDate.parse(dqcArgs.getDate(), formatter).minusDays(1);
            stringBuilder.append(" where dt in("+dqcArgs.getDate()+","+formatter.format(resultDate)+") ");
        }

        if(dqcArgs.getWhere_column_name().split("\\|").length != dqcArgs.getWhere_column_value().split("\\|").length){//拼接where的其他条件
            System.out.println("请检查参数:where_column_name  where_column_value 长度不相同!");
            System.exit(1);
        }
        int where_condition_length = dqcArgs.getWhere_column_name().split("\\|").length;
        String where_column_name[] = dqcArgs.getWhere_column_name().split("\\|");
        String where_column_value[] = dqcArgs.getWhere_column_value().split("\\|");

        for (int i = 0; i < where_condition_length; i++) {
            stringBuilder.append("and "+where_column_name[i]+"="+where_column_value[i]+" ");
        }
        stringBuilder.append("group by dt order by dt");
        return stringBuilder.toString();
    }
使用shell方式提交任务

缺点:
1.代码里面用的是进程方式提交任务,可以改成线程方式
2.结果多列的时候处理不好
优点:
1.结果简单的时候集成最快,环境依赖简单

    public static void dqc(String[] args)  throws IOException, InterruptedException {
        DqcArgs dqcArgs = DqcArgs.parse_string_2_dqcargs(args);
        //System.out.println(dqcArgs.toString());//将字符串解析成需要告警的参数
        String sql = concat_sql(dqcArgs);
        System.out.println(sql);//dqcArgs 拼接成的sql

        String[] result = submit_sql(sql);//提交SQL运行
        if(result == null){
            System.exit(1);
        }
        else{
            parse_result(result,dqcArgs,args);//解析结果,告警操作
        }
    }
    public static void parse_result(String[] result,DqcArgs dqcArgs,String[] args{
        StringBuilder alter_string = new StringBuilder("dqc ");
        for (int i = 0; i < args.length; i++) {
            alter_string.append(args[i]+" ");
        }
        if((dqcArgs.getAggregate_type().length()==1 || dqcArgs.getAggregate_type().equals("UQ"))&& result[0].contains("true")){//判断 C D M A S
            String data = result[0].replaceAll("true","");
            if(dqcArgs.getAggregate_type().equals("UQ")){
                alter_string.append("当前值不唯一!");
            }else{
                alter_string.append("检测值为:"+dqcArgs.getExpress_value()+"当前值为:"+data);
            }
            System.out.print(alter_string.toString());//告警操作  接入邮箱,消息告警就可以了
        }
        if(dqcArgs.getAggregate_type().equals("FL")){//判断波动的情况

            Double yesterday = Double.parseDouble(result[0]);
            Double today = Double.parseDouble(result[1]);

            if((today-yesterday)/yesterday>=Double.parseDouble(dqcArgs.getExpress_value())){
                alter_string.append("检测值为:"+dqcArgs.getExpress_value()+"当前值为:"+(today-yesterday)/yesterday);
                System.out.print(alter_string.toString());//告警操作  接入邮箱,消息告警就可以了
            }
        }
    }
    public static String[] submit_sql(String sql) throws IOException, InterruptedException {
        ProcessBuilder processBuilder = new ProcessBuilder("spark-sql","-S","--name","debug","--master","yarn","--deploy-mode","client","--num-executors","1","--executor-memory","4G","--executor-cores","2","--driver-memory","1G","--conf","spark.dynamicAllocation.enabled=true","-e",sql);
        Process process = processBuilder.start();
        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
        String redult_array[] = new String[2];
        int redult_index = 0;
        String line = "";
        while ((line = reader.readLine()) != null) {
            redult_array[redult_index] = line;
            redult_index+=1;
        }
        int exitCode = process.waitFor();
        if(exitCode == 0){
            return redult_array;
        }
        else{
            System.out.println("sparksql 执行错误!");
            return null;
        }
    }
连接OLAP工具提供的API接口

缺点:
1.环境依赖严重,需要有对应的OLAP环境
优点:
1.JDBC方式代码难度不高,结果方便遍历

    public static void jdbc_olap(String[] args) throws Exception { //使用jdbc olap的方式去运行sql
        //java -cp ".:/root/wxl/data/presto-jdbc-0.245.jar" Dqc
        String driver="com.facebook.presto.jdbc.PrestoDriver";
        String url="jdbc:presto://ip:8889/hive/adm";
        String username="hive";
        String password="";

        Class.forName(driver);
        Connection conn = DriverManager.getConnection(url, username, password);// 建立数据库连接
        Statement stmt = conn.createStatement(); // 执行SQL语句
        ResultSet rs = stmt.executeQuery("select count(distinct gaid) = count(gaid)  as result from adm.adm_user_di where dt in(20230424) and a=1 and b=1 and c=1 group by dt order by dt");
        while (rs.next()) {// 处理查询结果
            String data = rs.getString("result");
            System.out.println("result: " + data);
        }
        rs.close();// 关闭资源
        stmt.close();
        conn.close();
    }
连接spark-beeline

优点:
1.spark-beeline方式连接spark,但是我感觉连接的是hive

    public static void jdbc_hive(String[] args) throws Exception {//使用jdbc hive的方式去运行sql  但是这种好像是使用hive的方式 而且spark参数受限制于默认参数
        //java -cp ".:/opt/apps/HIVE/hive-3.1.3-hadoop3.1-1.0.2/lib/下面的jar全部拉过来,一个一个的找太麻烦" Dqc
        String driver="org.apache.hive.jdbc.HiveDriver";
        String url="jdbc:hive2://localhost:10000/tranadm";//spark.master=spark://localhost:7077 还有一个这个参数,但是我的环境实在试不了
        String username="hive";
        String password="";

        Class.forName(driver);
        Connection conn = DriverManager.getConnection(url, username, password);// 建立数据库连接
        Statement stmt = conn.createStatement(); // 执行SQL语句
        stmt.execute("SET hive.execution.engine=spark");
        ResultSet rs = stmt.executeQuery("select count(distinct gaid) = count(gaid)  as result from adm.adm__di where dt in(20230424) group by dt order by dt");
        while (rs.next()) {// 处理查询结果
            String data = rs.getString("result");
            System.out.println("result: " + data);
        }
        rs.close();// 关闭资源
        stmt.close();
        conn.close();

    }

这个代码遗留了几个地方没有处理,或者说还有下面几个可能会需要升级的地方:
1.直接传递sql的方式没有处理,这种比较简单直接调用就可以
2.支持配置表+数据表的检测:这一块我理解应该是说是表本身的业务逻辑,你这个表本身就应该只能产生维表或者配置表中的数据,如果数据不在维表或者配置表里面说明你逻辑处理有问题。最终这个可能还是需要根据业务情况来处理
3.不支持mysql库检测:这个也是正常的业务需求,代码没有兼容

posted @ 2022-05-23 20:56  Kotlin  阅读(1924)  评论(0编辑  收藏  举报
Live2D