Presto源码分析之TupleDomain

概述

最近在看 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的语境里面指的是那些只实现了 equalTohash 方法的,而没有实现 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, RangeSortedRangeSet 的关系:

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 来表达大概是这样的:

// 定义两个Column
ColumnHandle nameColumn = new TestingColumnHandle("name");
ColumnHandle ageColumn = new TestingColumnHandle("age"); 
// 构造Predicate
TupleDomain<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;
}

有了前面关于 DomainValueSet 的介绍,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,设计得还是蛮巧妙、蛮有结构层次感的,代码实现也很有意思,学到了不少。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270

推荐阅读更多精彩内容

  • 第2章 基本语法 2.1 概述 基本句法和变量 语句 JavaScript程序的执行单位为行(line),也就是一...
    悟名先生阅读 4,059评论 0 13
  • 有情皆孽,无人不冤 人是活在网络之中的,既不是影响链的中心,也不能独立于世不受影响。 所以当我们用线性思维去解读一...
    尋租阅读 384评论 0 2
  • 文|阿左 摇滚,用眼睛观看,是热烈;用耳朵倾听,是强劲;用肌肤触碰,是激情;而用内心体察,却是无尽的寂寞… 舞台之...
    AZ雕刻孤独AZ阅读 351评论 8 7
  • 梦, 晨醒, ...
    星星点辰阅读 188评论 0 0
  • javascript目前看来仍是世界上最流行的语言,不管在web、服务端还是客户端都有广泛的应用,很多跨平台方案也...
    树下老男孩阅读 4,459评论 3 29