概述
最近在看 Presto 源码的过程中经常看到一个类名: TupleDomain , 不得不说这个命名真是糟糕透顶,光看名字完全不知道它是干什么的,很不表意,但是 Presto 源码里面很多地方又用到了它,不搞懂的话真是如鲠在喉,因此还是下决心啃完它的代码,给大家分享一下。
TupleDomain 所在的位置是 presto-spi 模块里面的 predicate 包里面,这整个包里面的类都是为了实现这个 TupleDomain 而存在的,这个包里面主要类的关系图如下:
TupleDomain相关类的关系图
在正式分析 TupleDomain 之前我们要先把这些辅助类讲清楚,我们从其中最关键的 ValueSet 以及它的几个实现讲起。
ValueSet
ValueSet 这个类名取得还算表意, 它表示的是值的集合,或者更准确的说是变量的取值范围,我们来看看它的核心方法:
    /**
     * 这个 ValueSet 里面值的类型
     * @return
     */
    Type getType();    /**
     * 不匹配任何值
     * @return
     */
    boolean isNone();    /**
     * 匹配任何值
     * @return
     */
    boolean isAll();    /**
     * 是否匹配给定的值
     * @param value
     * @return
     */
    boolean containsValue(Object value);    /**
     * 获取这个 ValueSet 里面所有离散的值(针对不能排序的类型)
     */
    default DiscreteValues getDiscreteValues()
    {        throw new UnsupportedOperationException();
    }    /**
     * 获取这个 ValueSet 里面所有的取值范围(针对可以排序的类型)
     */
    default Ranges getRanges()
    {        throw new UnsupportedOperationException();
    }这个接口定义了一些方法,比如判断这个 ValueSet 里面是不是空空如也( isNone() ); 判断这个 ValueSet 是不是包含所有的值( isAll() ); 是不是包含某个具体的值( containsValue() )等等。
最后两个接口方法 getDiscreteValues() 和 getRanges() 我觉得设计得很不好,把具体实现的方法放到了接口上面来了: getDiscreteValues 只对离散的值类型才有意义,而 getRanges 只对可排序的值类型有意义。
有的同学可能会问,如果只是表示值的集合为什么不用JDK里面自带的List, Set? 因为这个
ValueSet表示的是取值范围,而不会把真正的所有的值都保存在里面,比如它可以表示取值是所有的int:ValueSet.all(INTEGER)但是其实它没有保存所有的int,它保存的只是描述信息而已。
ValueSet 本身是一个接口,它有三个实现:
AllOrNoneValueSet
EquatableValueSet
SortedRangeSet
我们分别来分析一下。
AllOrNoneValueSet
从名字可以看出来,它表示要么匹配所有的值(All), 要么什么都不匹配(None), 如前所述,这个类里面并没有保存任何实际的元素,只是保存了一个标志位:
public class AllOrNoneValueSet
        implements ValueSet{    private final Type type;    private final boolean all;
}EquatableValueSet
EquatableValueset 针对的是离散型的值,所谓的离散型的值在Presto的语境里面指的是那些只实现了 equalTo 和 hash 方法的,而没有实现 compareTo 方法的类型。
/** * Are the values in the specified blocks at the * specified positions equal? */ boolean equalTo(Block leftBlock, int leftPosition, Block rightBlock, int rightPosition); /** * Calculates the hash code of the value at the * specified position in the specified block. */ long hash(Block block, int position); /** * Compare the values in the specified block at the * specified positions equal. */ int compareTo(Block leftBlock, int leftPosition, Block rightBlock, int rightPosition);
通俗点说离散是指:两个值A、B,我们能够知道它们相等还是不相等,但是不知道它们哪个大。这种 ValueSet 里面保存的就真的是具体的值了。不过除了具体的值,它还保存了一个 boolean 字段 whilelist 用来表示这些具体的值是要包含的还是要排除的, 看它的核心字段定义:
public class EquatableValueSet
        implements ValueSet{    private final Type type;    /**
     * 白名单还是黑名单
     */
    private final boolean whiteList;    /**
     * 具体的值
     */
    private final Set<ValueEntry> entries;
}SortedRangeSet
SortedRangeSet 应该是这几种实现里面最重要的了,它表示的是那些可以排序的( Orderable )值类型的取值范围,而我们平时碰到的几乎都是可以排序的类型,比如数字、字符串、日期等等。它也是这几种实现里面最复杂的。涉及到好多类,我们从低向上一个个介绍:
Marker
Maker 表示的是坐标轴上的位置,但是它又不是一个具体的点,而是表示跟一个具体的点的关系,比如  在3的上面 ,在9的下面 ,  跟10重合 。
Marker
看它的核心代码:
public final class Marker
        implements Comparable<Marker>{    public enum Bound
    {
        BELOW,   // lower than the value, but infinitesimally close to the value
        EXACTLY, // exactly the value
        ABOVE    // higher than the value, but infinitesimally close to the value
    }    /**
     * Marker所代表的数值的类型, bigint? int? short?
     */
    private final Type type;    /**
     * 具体的值
     */
    private final Optional<Block> valueBlock;    /**
     * 跟指定值的关系. 大于?小于?等于
     */
    private final Bound bound;
}它还可以表示一些特殊的概念,比如 在最大的值下面 ,  在最小的值上面 , 其实也就是类型无穷大,无穷小的概念啦。比如看它怎么表示无穷大:
public static Marker upperUnbounded(Type type){
    requireNonNull(type, "type is null");    return create(type, Optional.empty(), Bound.BELOW);
}其中值是没有的( Optional.empty() ), 因为不能存在一个无穷大的确定的值嘛;而跟这个值的关系是 BELOW ,也就是说在一个不存在的值的下面 -- 无穷大, 好像不是那么的自然哦,但表达的就是这么个意思。
Range
Range 表示变量在一维坐标上的取值范围,有了 Marker 的帮助,表示一个取值范围就非常的简单了。我们说到范围,那就一定有左边界和右边界了,Range 就是这么设计的,看它里面的字段:
public final class Range{    /** 左边界 */
    private final Marker low;    /** 右边界 */
    private final Marker high;
}它里面还定义了工具方法来快速创建取值范围,比如 大于等于3 , 用它的API来表示非常简单:
Range.greaterThanOrEqual(BIGINT, 3)
而它的内部实现也很简单:
new Range(Marker.exactly(BIGINT, 3), Marker.upperUnbounded(BIGINT));
左边界是一个 完全等于3的Marker , 而右边界表示的是一个没有边界的一个特殊 Marker 。
说回 SortedRangeSet
说完这么多铺垫,终于可以说说 SortedRangeSet 了,其实从名字就可以看出它的实现了,它内部其实就是一堆排好序的 Range 对象的集合,表示一个变量的多个取值区间的集合。看它的工厂方法,也是让你传一堆 Range 进去:
    /**
     * Provided Ranges are unioned together to form the SortedRangeSet
     */
    static SortedRangeSet of(Range first, Range... rest)
    {
        List<Range> rangeList = new ArrayList<>(rest.length + 1);
        rangeList.add(first);        for (Range range : rest) {
            rangeList.add(range);
        }        return copyOf(first.getType(), rangeList);
    }用一张图来总结下 Marker, Range 和 SortedRangeSet 的关系:
Marker, Range 和 SortedRangeSet
再谈 ValueSet
说完 ValueSet 的三种不同实现我们再说回 ValueSet 本身,我们平时编码其实不会接触到这三种实现的,ValueSet里面暴露了一些工厂方法,我们使用这些工厂方法就好了:
    static ValueSet none(Type type) {        // 省略实现
    }    static ValueSet all(Type type) {        // 省略实现        
    }    static ValueSet of(Type type, Object first, Object... rest) {        // 省略实现        
    }    static ValueSet copyOf(Type type, Collection<Object> values) {        // 省略实现        
    }    static ValueSet ofRanges(Range first, Range... rest) {        // 省略实现
    }这些工厂方法的背后会自动调用合适的实现来返回。
非常喜欢这种设计策略,让用户尽可能的少关心实现细节。(但是要读懂实现原理的话,还是要所有代码都看一遍 :( )
Domain
理解了 ValueSet 之后,理解 Domain 就简单了,Domain 只是把 ValueSet 简单包装了一下,没有提供太多新的东西,但是原作者起了两个完全不一样的名字,徒增理解的成本。
TupleDomain
最后说到我们的主角 TupleDomain 了,它是用来表达 table 里面各个字段的约束条件、取值范围的。比如我们有下面的表:
create table person ( id int, name varchar(1023), age int)
我们要获取其中 年龄大于20岁,并且名字叫Jack的人 ,用TupleDomain 来表达大概是这样的:
// 定义两个ColumnColumnHandle nameColumn = new TestingColumnHandle("name");
ColumnHandle ageColumn = new TestingColumnHandle("age"); 
// 构造PredicateTupleDomain<ColumnHandle> predicate = TupleDomain.withColumnDomains(
     ImmutableMap.of(         // 名字只能取一个值(因此叫Single Value): Jack                     
         nameColumn, Domain.singleValue(VARCHAR, "Jack"),         // 年龄要大于等于20(greaterThanOrEqual)
         idColumn, Domain.create(  
             ValueSet.ofRanges(Range.greaterThanOrEqual(BIGINT, 20)), false
         )
     )
);TupleDomain 内部其实就维护了一个字段名到对应的Domain的映射关系,表示一个表里面多个字段的取值约束条件。
public final class TupleDomain<T>{    private final Optional<Map<T, Domain>> domains;
}有了前面关于 Domain 、ValueSet 的介绍,TupleDomain 的源码应该不用多介绍了。
有同学可能会疑问,饶了这么大的弯子,搞得这么复杂就是为了表达一个 where 条件,为什么不直接用类似SQL里面的 Where 条件的语法来表达,既简单又直接?
我的理解是这样的: 首先 Presto 是一个可以查询异构数据源的引擎,它不止支持关系型数据库,也支持非关系型数据库比如文件存储,文件存储系统可不识别什么 SQL 的 Where语法哦;而且即使是关系型数据库,不同的数据库的语法也不一样,因此一种中立的表达数据约束条件的 DSL 还是蛮有必要的。另外 Presto 里面不止要能够表达这个数据约束条件,而且在各种优化器的规则里面还需要能够对这个数据约束条件进行转换、替换、修改,因此必须要用一种命令式的方式来表达,而不能用SQL那样的声明式方式来表达。
读到这里我觉得 TupleDomain 这个不表意的名字完全可以用一些更常见、易懂的词来表达,比如
TablePredicate,ColumnPredicates,ColumnConditions等等,都比这个所谓的TupleDomain更容易懂,但是也不怪原作者啦,毕竟取名是一门艺术 :)
实际使用场景
讲了这么多的 TupleDomain, 它这么的厉害,那么在 Presto 里面哪些地方用到了呢?
TableScanNode里面有一个concurrentConstraint, 表示读取 table 数据时候的过滤条件,而这个Constraint里面的条件就是用TupleDomain来表达的。Presto SPI 里面定义的获取 table 的统计信息的API:
ConnectorMetadata.getTableStatistics,ConnectorMetadata.getLayouts的参数里面都有一个参数就是Constraint, 让 Presto 框架可以指定查询条件来获取统计数据。WindowFilterPushDown这条优化规则在优化的过程中也使用TupleDomain对过滤条件进行计算、转换。
总结
取名是一门艺术活,不好的取名会极大的浪费代码阅读者的时间、增加理解的难度。但是瑕不掩瑜,TupleDomain 作为一个命令式的表达表里面取值条件的 DSL,设计得还是蛮巧妙、蛮有结构层次感的,代码实现也很有意思,学到了不少。
作者:xumingmingv
链接:https://www.jianshu.com/p/4d4bc347df80