手写一个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库检测:这个也是正常的业务需求,代码没有兼容
搬砖多年终不得要领,遂载源码看之望得真经。

浙公网安备 33010602011771号