Druid SQL模块除了用于进行SQL重写之外,最近发现了一个新的应用场景,即用来做一些简单SQL的分析预警。对于简单SQL,根据语句中部分传入参数值,进行预警通知,例如,

select * from foobar order by id limit 10000 offset 0;

语句中limit参数可能传入一个很大的数值,如果能进行分析,那么就可以做到预警。

获取到上述limit对应的值并不困难,在之前提及SQL重写问题时就处理过limit offset的问题。针对这个问题,解决思路也是类似的,即在遍历SQL解析之后的语法树时获取目标值。

处理limit offset值之外,能够预想到的一些可能分析有,

  • 某具体字段对应的参数值
  • IN子句元素数量

当然,SQL语句本身可能包含复杂的运算或是函数调用,对于这类场景暂时就是无能为力的。但把能做的事情做了,多少也还是有用的。

为了实现上述目标,需要自定义Visitor进行数据获取,获取到数据之后采取怎样的行动策略就很方便了。

public class AnalyseVisitor extends MySqlOutputVisitor {

    private List<Pair<String, String>> cmpInfo = new ArrayList<>();
    private List<Pair<String, String>> inInfo = new ArrayList<>();
    private String limit;
    private String offset;

    public AnalyseVisitor(Appendable appender) {
        super(appender);
    }

    public boolean visit(SQLBinaryOpExpr expr) {
        if (isIdentifier(expr.getLeft()) && isValue(expr.getRight())) {
            cmpInfo.add(Pair.of(expr.getLeft().toString(), expr.getRight().toString()));
        }
        return super.visit(expr);
    }

    public boolean visit(SQLInListExpr expr) {
        if (isIdentifier(expr.getExpr())) {
            inInfo.add(Pair.of(expr.getExpr().toString(), String.valueOf(expr.getTargetList().size())));
        }
        return super.visit(expr);
    }

    public boolean visit(SQLLimit expr) {
        limit = expr.getRowCount().toString();
        if (expr.getOffset() != null) {
            offset = expr.getOffset().toString();
        }
        return super.visit(expr);
    }

    private boolean isIdentifier(SQLExpr expr) {
        return expr.getClass() == SQLIdentifierExpr.class;
    }

    private boolean isValue(SQLExpr expr) {
        Class<?> clazz = expr.getClass();
        return clazz == SQLVariantRefExpr.class
                || clazz == SQLIntegerExpr.class
                || clazz == SQLNumberExpr.class
                || clazz == SQLCharExpr.class
                || clazz == SQLBooleanExpr.class;
    }
}

重载visitor函数,针对不同参数分别进行处理。上述示例选了SQLBinaryOpExpr、SQLInListExpr、SQLLimit。如果有更复杂的场景,那么也是类似根据情况去进行定义。

之后再使用上述Vistor,就可以获取到想要的数据了,例如,

String sql = "select * from foobar where id = 1 and value in (1, 2, 3) order by id limit 1000 offset 0";
AnalyseVisitor visitor = new AnalyseVisitor(new StringBuilder());
SQLUtils.parseSingleMysqlStatement(sql).accept(visitor);
// [(id,1)]
System.out.println((visitor).getCmpInfo());
// [(value,3)]
System.out.println((visitor).getInInfo());
// 1000
System.out.println((visitor).getLimit());
// 0
System.out.println((visitor).getOffset());

可以看出,至少在这个SQL例子中是获取到了目标数据的。再复杂一些的SQL,Visitor可能也需要再进一步去完善,覆盖遗漏的一些条件处理。不过整体方向上应该差不多。如果能在运行时进行这样的处理,不论是预警还是变换或是统计等等行动就都有可能去实施了。