Presto的IN条件语句的实现逻辑是限定范围查询

问题描述

用presto连接kudu进行查询,在where条件中使用in语句时,如果是单个值效率很高,如:where name in ('jack');但如果是多个值时效率很慢,如:where name in ('jack', 'tom')

问题排查

1、kudu连接器的逻辑

我们知道kudu是十分擅长定位一个值的,这点可以从where name in ('jack')in单个值查询速度很快看得出来。

presto连接kudu进行查询时,是通过kudu的java api实现数据的过滤的,在presto源码KuduClientSession这个类中有如下代码:

    /**
     * translates TupleDomain to KuduPredicates.
     *
     * @return false if TupleDomain or one of its domains is none
     */
    private boolean addConstraintPredicates(KuduTable table, KuduScanToken.KuduScanTokenBuilder builder,
            TupleDomain<ColumnHandle> constraintSummary)
    {
        if (constraintSummary.isNone()) {
            return false;
        }
        else if (!constraintSummary.isAll()) {
            Schema schema = table.getSchema();
            for (TupleDomain.ColumnDomain<ColumnHandle> columnDomain : constraintSummary.getColumnDomains().get()) {
                int position = ((KuduColumnHandle) columnDomain.getColumn()).getOrdinalPosition();
                ColumnSchema columnSchema = schema.getColumnByIndex(position);
                Domain domain = columnDomain.getDomain();
                if (domain.isNone()) {
                    return false;
                }
                else if (domain.isAll()) {
                    // no restriction
                }
                else if (domain.isOnlyNull()) {
                    builder.addPredicate(KuduPredicate.newIsNullPredicate(columnSchema));
                }
                else if (domain.getValues().isAll() && domain.isNullAllowed()) {
                    builder.addPredicate(KuduPredicate.newIsNotNullPredicate(columnSchema));
                }
                else if (domain.isSingleValue()) {
                    KuduPredicate predicate = createEqualsPredicate(columnSchema, domain.getSingleValue());
                    builder.addPredicate(predicate);
                }
                else {
                    ValueSet valueSet = domain.getValues();
                    if (valueSet instanceof EquatableValueSet) {
                        DiscreteValues discreteValues = valueSet.getDiscreteValues();
                        KuduPredicate predicate = createInListPredicate(columnSchema, discreteValues);
                        builder.addPredicate(predicate);
                    }
                    else if (valueSet instanceof SortedRangeSet) {
                        Ranges ranges = ((SortedRangeSet) valueSet).getRanges();
                        Range span = ranges.getSpan();
                        Marker low = span.getLow();
                        if (!low.isLowerUnbounded()) {
                            KuduPredicate.ComparisonOp op = (low.getBound() == Marker.Bound.ABOVE)
                                    ? KuduPredicate.ComparisonOp.GREATER : KuduPredicate.ComparisonOp.GREATER_EQUAL;
                            KuduPredicate predicate = createComparisonPredicate(columnSchema, op, low.getValue());
                            builder.addPredicate(predicate);
                        }
                        Marker high = span.getHigh();
                        if (!high.isUpperUnbounded()) {
                            KuduPredicate.ComparisonOp op = (low.getBound() == Marker.Bound.BELOW)
                                    ? KuduPredicate.ComparisonOp.LESS : KuduPredicate.ComparisonOp.LESS_EQUAL;
                            KuduPredicate predicate = createComparisonPredicate(columnSchema, op, high.getValue());
                            builder.addPredicate(predicate);
                        }
                    }
                    else {
                        throw new IllegalStateException("Unexpected domain: " + domain);
                    }
                }
            }
        }
        return true;
    }

从上面代码可以看到,根据输入的条件的数值个数不同,会产生对应的KuduPredicate添加到KuduScanTokenBuilder中,最后生成KuduScanToken及对应的KuduSplit分发给各个presto节点进行运算。

所以需要进一步测试KuduPredicate的性能。

2、KuduPredicate性能测试

KuduClient client = null;
KuduScanner scanner = null;
try {
    client = new KuduClient.KuduClientBuilder("kudu-host-1,kudu-host-2").build();
    KuduTable table = client.openTable("test");
    // 测试用in list单个值
    KuduPredicate predicate = KuduPredicate.newInListPredicate(table.getSchema().getColumn("name"),Arrays.asList('jack'));
    // 测试用EQUAL单个值
    KuduPredicate predicate = KuduPredicate.newComparisonPredicate(table.getSchema().getColumn("name"),KuduPredicate.ComparisonOp.EQUAL, 'jack');
    // 测试用in list多个值
    KuduPredicate predicate = KuduPredicate.newInListPredicate(table.getSchema().getColumn("name"),Arrays.asList('jack', 'tom', 'marry'));
    KuduScanner.KuduScannerBuilder builder = client.newScannerBuilder(table).setProjectedColumnNames(columns);
    scanner = builder.addPredicate(predicate).build();
    getAndOutput(scanner);
} catch ......

经过测试,当只有一个值时,KuduPredicate.newInListPredicateKuduPredicate.newComparisonPredicate性能相近,当有两个值时,KuduPredicate.newInListPredicate的速度相较一个值的时候虽然会慢点,但依然十分优秀,但presto使用kudu连接器的时候为什么就这么慢呢?

3、解析KuduScanToken

上文说过,presto节点是通过KuduScanToken向kudu请求数据的,当查询完成后,我们可以通过presto的WebUI获取到这个token:http://{presto-web}/v1/query/{query_id}?pretty,搜索serializedScanToken,将得到的token进行解析

String str = "EhJkaW0uZGltX3lpc2RrX3VzZXIaFAgAE.........."; // your token here
Base64.Decoder decoder = Base64.getDecoder();
byte[] bytes = decoder.decode(str);

KuduClient client = new KuduClient.KuduClientBuilder("kudu-host-1,kudu-host-2").build();
KuduScanner scanner = KuduScanToken.deserializeIntoScanner(bytes, client);

通过ide的debug功能获取KuduScanner scanner这个对象其中的成员变量predicate,结果发现presto并不是使用KuduPredicate.newInListPredicate的in list来实现in查询,而是使用了RANGE的范围查询:

KuduScanner

in list的查询只会拉取很少量的数据,而范围查询所需要获取的数据就多很多,这也就解释了为什么在presto使用in语句有多个值的时候速率会慢那么多。

4、Presto为什么使用的是范围查询

先看源码部分:

ValueSet valueSet = domain.getValues();
if (valueSet instanceof EquatableValueSet) {
    ......
}
else if (valueSet instanceof SortedRangeSet) {
    ......
}

presto根据ValueSet决定使用in list条件还是范围条件,那ValueSet是在哪里实例化的呢?我们看ValueSet这个类:

    static ValueSet of(Type type, Object first, Object... rest)
    {
        if (type.isOrderable()) {
            return SortedRangeSet.of(type, first, rest);
        }
        if (type.isComparable()) {
            return EquatableValueSet.of(type, first, rest);
        }
        throw new IllegalArgumentException("Cannot create discrete ValueSet with non-comparable type: " + type);
    }

当条件里有多个值的时候会进入这个方法,如果字段类型是可排序的,则会优先使用SortedRangeSet,之后才会使用EquatableValueSet,而近乎所有的基本类型的isOrderable方法返回的都是true,也就是说基本上只要是使用in条件语句带上多个值,那就会使用SortedRangeSet,相对应的kudu也就使用了RANGE范围查询去拉取数据。

那为什么Presto要有这样的逻辑呢?我的理解是:Presto作为一个对大量数据进行查询的引擎,默认每次查询从连接器中获取数据的体量都会是很大的,而连接器五花八门,presto不敢保证每个连接器都具备快速精准定位数据的能力,presto更相信自己基于内存的数据筛选和计算能力,因此presto只给了连接器一个范围条件,让连接器进行粗滤筛选,自己来做最终的精准定位。不知道我这想法对不对,如果有不同的观点也欢迎跟我一起探讨一下。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值