One SQL to Rule Them All: An Efficient and Syntactically Idiomatic Approach to Management of Streams and Tables
一个SQL统治一切:一个高效且语法惯用的方式管理流和表 论文翻译
文中【】为译者注,()中英对照,或者原文的括号。
—— 2022.07.19 by zz
概述
实时数据分析和管理对于当今的企业来说越来越重要。 SQL 是这些努力的事实上的通用语言(de facto lingua franca),但对使用 SQL 进行强大的流分析和管理的支持仍然有限。 许多方法将其语义限制为缩减后的特性子集 和/或 需要一套非标准构造。 此外,使用事件时间戳来为根据事件实际发生的时间分析事件提供原生支持并不普遍,并且通常具有重要的限制。
我们提出了一个将健壮的流(robust streaming)集成到 SQL 标准中的三部分提案,即:(1)时变(time-varying)关系作为经典表和流数据的基础,(2)事件时间语义(event time semantic),(3)有限可选的扩展关键字集合来控制时变查询结果的物化。【(1) time-varying relations as a foundation for classical tables as well as streaming data, (2) event time semantics, (3) a limited set of optional keyword extensions to control the materialization of timevarying query results】 我们使用基于 Apache Calcite、Apache Flink 和 Apache Beam 中的实现中学到的示例和经验教训驱动和说明,展示了如何通过这些最少的添加来利用完整的标准 SQL 语义套件来执行健壮的流处理。
1 简介
本文的论点是,在开发支持现实世界流用例的大型开源框架的经验的支持下,SQL 语言和关系模型,在保持不变和做少量非侵入性扩展的情况下,可以非常有效地操作流式数据。
我们的动机有两个。首先,我们想分享我们在广泛使用的开源框架中进行流式处理时的观察、创新和经验教训。 其次,我们希望向更广泛的数据库社区通报我们正在与国际 SQL 标准化机构 [26] 开展的工作,以标准化流式 SQL 功能和扩展,并促进关于该主题的全球对话(我们在第 6 节讨论提议的扩展 )。
1.1 一个SQL处理流表
结合来看,表和流涵盖了业务运营(business operations)的关键范围(critical spectrum),从历史数据支持的战略决策到交互式分析中使用的近实时数据。 SQL 长期以来一直是查询和管理数据表的主导技术,并得到了数十年的研究和产品开发的支持。
我们相信,根据我们的经验和近 20 年来对流式 SQL 扩展的研究,以一致的方式使用相同的 SQL 语义是统一这两种数据模式的一种高效且优雅的方式:它简化了学习、采用和支持开发内聚的数据管理系统。 因此,我们的方法是提供一种统一的方法来使用相同的语义(semantic)管理表和数据流。
1.2 提议的贡献
基于从现有技术和我们自己的工作中获得的见解,我们在本文中提出了以下贡献:
时变关系(Time-varying relations):首先,我们提出时变关系作为 SQL 的通用基础。经典指向时间点的查询、持续更新的视图、新式流式查询(classic point-in-time queries, continuously updated views, and novel streaming queries)以此为基础。时变关系是指:一个随时间变化的关系,也可以被视为一个函数,将每个时间点映射到一个静态关系。至关重要的是,现有的全套 SQL 运算符在时变关系上仍然有效(通过自然的逐点应用程序(natural point-wise application)),以最小的认知开销提供最大的功能。 3.1 节详细探讨了这一点,6.2 节讨论了它与标准 SQL 的关系。
事件时间语义(Event time semantics):其次,我们提出了一个简洁的建议,用于启用健壮的事件时间流语义。我们提出的扩展非常合适的保留了所有现有的 SQL 语义。通过用时变关系作为底层原始概念,我们可以自由地结合经典 SQL 和事件时间扩展。第 3.2 节描述了必要的基础,第 6.2 - 6.4 节描述了我们提出的支持事件时间的扩展。
物化控制(Materialization control):第三,我们提出了一套适度(modest)的物化控制,为处理现代流用例的广度(breadth)提供必要的灵活性。
-
流物化(Stream materialization ):为了完成 流表对偶性(stream-table duality) ,我们建议允许可选地将查询输出呈现为对输出关系的 更改流(stream of changes) 。 这个流本身就是一个可以应用流式SQL的时变关系,可以用来展示系统在流方面如何区分流和表。 第 3.3.1 节概括地描述了流物化,第 6.5.1 节描述了我们将流物化添加到 SQL 的建议。
-
物化延迟(Materialization delay) :最后,我们提出了一个小但灵活的框架,用于延迟物化来自时变关系的不完整、推测性结果(incomplete, speculative results)。 我们的框架在即时更新的视图语义之外增加了有表现力(expressiveness)的 推送语义(push sematic )(例如,通知用例下,即时更新通常很难提供服务)以及大容量用例(例如,控制高吞吐量流中聚合合并的频率) )。 第 3.3.2 节详细讨论了物化控制,第 6.5.2 节介绍了我们的物化框架。
总而言之,我们相信这些贡献为在流式上下文中充分利用标准 SQL 奠定了坚实的基础,同时为经典时间点查询上下文中的健壮事件时间处理提供了额外的功能。
2 背景和相关工作
自 1990 年代以来,直接形式的流处理和流式 SQL 以及以复杂事件处理 (CEP) [18] 和连续查询 [11] 为幌子(guise)的数据流处理和流式 SQL 一直是数据库研究的活跃领域。 这些领域已经取得了重大进展,我们在此简要介绍与我们的方法相关的研究、工业和开源发展。
2.1 流式 SQL 历史
流处理的工作可以追溯到 1992 年 Tapestry [25] 系统的引入,该系统旨在使用称为 TQL [37] 的 SQL 子集对电子邮件和留言板文档进行基于内容的过滤。几年后,刘等人。介绍了 OpenCQ,这是一种由用户或应用程序指定的事件驱动的信息传递系统,其更新仅在不需要主动监控或干预(monitoring or interference)的指定触发器上发生 [30]。同一组开发了 CONQUER,这是一个更新/捕获系统,用于有效地监控 Web 上的连续查询,使用三层架构,旨在在各种结构化数据源之间共享信息 [29]。此后不久,NiagaraCQ 出现了,这是一种基于 XML-QL 的查询系统,旨在通过动态重组 (dynamic regrouping)[21] 将相似的连续查询分组在一起,从而解决连续查询的可伸缩性问题。 OpenCQ、CONQUER 和 NiagaraCQ 都支持大型网络(即 Internet)上的 到达(arrival)和基于计时器(timer-based)的查询。然而,Tapestry 和 OpenCQ 都没有解决多重查询优化问题,而且 NiagaraCQ 忽略了查询执行时间并且没有指定时间间隔 [27]。
2003 年,Arasu、Babu 和 Widom 推出了连续查询语言 (CQL),这是一种类似于 SQL 的声明性语言,由斯坦福大学的 STREAM 项目团队开发。 一种内聚语法或转换逻辑,用于处理流数据和静态数据,这项工作是第一个为流和关系上的通用、声明性连续查询引入精确语义的工作。 它形式化了流、可更新关系及其关系的一种形式; 此外,它为在关系查询语言概念之上构建的连续查询定义了抽象语义 [8, 9]。
2.1.1 CQL 运算符(CQL Operators.)。 CQL 定义了三类运算符:关系到关系、流到关系和关系到流。 核心运算符,关系到关系,使用类似于 SQL 的符号。 流到关系运算符使用窗口规范从流中提取关系,例如滑动(sliding)和翻转(tumbling)窗口。 关系到流运算符包括 Istream(插入流)、Dstream(删除流)和 Rstream(关系流)运算符 [7]。 具体来说,这三个特殊运算符定义如下:
(1) Istream(R) 包含所有 (r,T),其中 r 是 R 在 T 的一个元素,但不在 T - 1
(2) Dstream(R) 包含所有 (r,T),其中 r 是 R 在 T − 1 的一个元素,但不在 T
(3) Rstream(R) 包含所有 (r,T ) 其中 r 是 R 在时间 T 的一个元素 [39]
许多想法的核心在于这些运算符。 值得注意的是,时间是隐式(implicit)的。 STREAM 系统通过在接收时对其进行缓冲并以时间戳顺序将其呈现给查询处理器来容纳乱序数据,因此 CQL 语言不解决乱序数据的查询。
CQL 的一个重要限制是时间是指跟踪关系和流演变(evolution)的逻辑时钟,而不是被分析的数据中表达的时间,这意味着时间不是可以与其他数据一起观察和操作的首要(first-class)实体 .
2.1.2 其他发展。 Aurora 系统与 STREAM 几乎同时设计,将归档(archive)、跨接(spanning)和实时监控(real-time monitoring)应用程序组合到一个框架中。与 STREAM 一样,查询被构造为具有算子顶点和数据流边的有向无环图 [3]。 Aurora 被用作 Medusa(分布式流处理系统 [13] 的负载管理系统)和 Borealis(由 Brandeis、Brown 和 MIT 开发的流处理引擎)的查询处理引擎。 Borealis 使用 Medusa 的负载管理系统,并引入了一种新的方法来探索容错技术(结果修改和查询修改)和动态负载分配 [2]。这些系统的优化过程仍然没有考虑事件规范。 Aurora 的 GUI 提供了自定义运算符,旨在处理延迟或丢失的数据,具有四个特定的新功能:超时功能、无序输入处理、用户定义的可扩展性和重采样运算符(out-of-order input handling, user-defined extendibility, and a resampling operator) [34]。这些运算符部分基于线性代数/SQL,但也借鉴了 AQuery 和 SEQ [12]。
IBM 于 2008 年推出 SPADE,也称为 System S [24]; 这后来演变成 InfoSphere Streams,一个使用 SPL 的流分析平台,它自己有原生处理语言,允许事件时间注释。
2.2 当代流式系统
尽管流式 SQL 近 30 年来一直是一个活跃的研究领域,但流式处理本身最近受到了业界的关注,许多当前的流式系统都采用了某种形式的 SQL 功能。
Apache Spark Spark 的数据集 API 是一个建立在 Spark SQL 的优化器和执行引擎之上的高级声明式 API。数据集程序可以在有限数据或流数据上执行。数据集 API 的流式变体称为结构化流式处理 [10]。结构化流查询是增量评估的,默认情况下使用微批处理执行引擎进行处理,该引擎将数据流作为一系列小批量作业处理,并具有精确一次的容错保证。
KSQL Confluent 的 KSQL [28] 建立在 Kafka Streams 之上,Kafka Streams 是 Apache Kafka 项目的流处理框架。 KSQL 是围绕 Kafka Streams 的声明式包装器,并定义了一种自定义的类似 SQL 的语法来公开流和表的概念 [33]。 KSQL 专注于最终一致的物化视图语义。
Apache Flink [23] 具有两个关系 API,即 LINQ-style [31] Table API 和 SQL,后者已被阿里巴巴、华为、Lyft、Uber 等企业采用。两种 API 中的查询都被转换为通用的逻辑计划表示,并使用 Apache Calcite [19] 进行优化,然后作为批处理或流应用程序进行优化和执行。 Apache Beam [15] 最近增加了 SQL 支持,开发时仔细考虑了 Beam 统一有界和无界数据处理 [5]。 Beam 目前实现了本文提出的语义子集,并且许多提议的扩展都来自我们多年来使用 Beam 的经验。 Apache Calcite [19] 被广泛用作流式 SQL 解析器 和 计划器/优化器,特别是在 Flink SQL 和 Beam SQL 中。 除了 SQL 解析、计划和优化之外,Apache Calcite 还支持流处理语义,这些语义与 Flink 和 Beam 的方法一起影响了本文介绍的工作。
还有许多其他此类系统添加了某种程度的 SQL 或类似 SQL 的功能。 我们在这项工作中的新提议的一个关键区别在于,其他系统要么仅限于标准 SQL 的子集,要么绑定到专门的运算符。 此外,其他突出的实现并不完全支持健壮的事件时间语义,这是我们提议的基础。
在本文中,我们综合了在其中三个系统(Flink、Beam、Calcite)上工作的经验教训,并提出了一项新提案,用于扩展 SQL 标准以及流关系处理的最基本方面。
3 最小化流式 SQL 基础
我们对流式 SQL 的提议分为两部分。 在本节中,第一个是概念基础,列出了支持流操作基础的概念和实现技术。 第二部分,在第 6 节中,建立在这些基础之上,确定了标准 SQL 已经支持流的方式,并提出了对 SQL 的最小扩展,以便为其余概念提供强大的支持。 中间部分致力于通过示例和从我们的开源框架中吸取的经验教训来讨论(流式SQL)基础。
3.1 时变关系
在流(streaming)的上下文中,要考虑的关键附加维度是时间。在处理经典关系时,人们在单个时间点(at a single point in time)处理关系。在处理流式关系时,必须处理随着时间演变(evolve over time)的关系。我们建议明确说明 SQL 在时变关系或 TVR 上运行。
时变关系顾名思义:一种内容可能随时间变化的关系。这个想法与我们已经熟悉的可变数据库表兼容;对于这样一个表的消费者来说,它已经是一个时变关系。(事实上,AS OF SYSTEM TIME 结构已经在 SQL 标准中体现了时变关系的概念。)但是消费者没有能力根据关系如何随时间变化来观察或计算。一个传统的 SQL 查询或视图可以表达一个派生的时变关系,该关系与其输入同步演变(evolve):在每个时间点,它相当于在该时间点查询其输入。但是存在无法以这种方式表达的 TVR,如其中时间本身就是一个关键输入。
TVR 并不是一个新想法。它们在 [8, 9, 33] 中进行了探索。 TVR 的一个重要方面是它们可以以多种方式进行编码或物化,特别是作为一系列经典关系(用 CQL 术语来说是瞬时关系(instantaneous relations)),或作为一系列 INSERT 和 DELETE 操作。这两种编码相互对偶,对应于 Sax 等人很好地描述的表和流。 [33]。还有其他基于关系列属性的有用编码。例如,当聚合是可逆的时,TVR 的编码可能使用聚合差异(aggregation difference)而不是整个(记录)删除和添加。
我们关于 TVR 的主要贡献是表明 CQL 和 Streams 和 Tables 方法都远远不够:与其定义流和表的二元性,然后继续将两者视为有很大不同,我们应该利用这种二元性来发挥我们的优势.在先前的工作中陈述但未充分利用的关键见解是流和表是一个语义对象的两种表示。这并不是说表示本身不有趣 - 有一些用例可以在变化流本身上物化和操作 - 但这又是一个 TVR,可以统一处理。这里重要的是,随着时间的推移,关系的核心语义对象始终是 TVR,根据定义,它支持整套关系运算符,即使在涉及流数据的场景中也是如此。这很关键,因为这意味着任何理解足够 SQL 以解决非流式上下文中的问题的人仍然具有解决流式上下文中的问题所需的知识。
3.2 事件时间语义
我们的第二个贡献涉及事件时间语义。许多方法未能处理 事件时间(event time)和处理时间(processing time)的固有独立性。最简单的失败是假设数据是根据事件时间排序的。在存在移动应用程序、分布式系统,甚至只是分片归档数据的情况下,情况并非如此。即使数据按照事件时间排序,逻辑时钟或处理时钟的进程也与事件实际发生的时间尺度无关—— 一小时的处理时间与一小时的事件时间无关。必须明确说明事件时间才能获得正确的结果。
STREAM 系统将心跳(heartbeats)作为一项可选功能来缓冲乱序数据并将其按顺序提供给查询处理器。这引入了延迟以允许时间戳偏差。 Millwheel [4] 的处理基于水印(watermark),直接计算乱序数据以及有关输入被认为有多完整的元数据。这种方法在 Google 的 Cloud Dataflow [5] 中得到了进一步扩展,它开创了 Beam 和 Flink 采用的乱序处理模型。
KSQL [33] 中采用的方法也是按到达顺序处理数据。它的窗口语法绑定到系统提供的特定类型的事件时间窗口(不要与 SQL 窗口概念混淆。)实现(而不是允许通过 SQL 进行任意的声明式构造)。由于它缺乏对水印的支持,它不适合用于需要一些完整性概念的通知等用例,而是支持与轮询方法的最终一致性。我们相信需要更通用的方法来服务于所有流用例。
我们建议通过两个概念来支持事件时间语义:显式事件时间戳和水印。 总之,这些允许正确的事件时间计算,例如分组到事件时间的间隔(或窗口),以有效地表达和执行,而不会消耗无限的资源。
3.2.1 事件时间戳(Event Timestamps)。 为了在时变关系上执行健壮的流处理,关系的行应该在事件时间加上时间戳并进行相应的处理,而不是按照 到达顺序(arrival order) 或 处理时间(processing time)。
3.2.2 水印(Watermarks)。 水印是流处理中的一种机制,用于确定性地或启发式地定义带时间戳事件流的完整性时间边界(temporal margin of completeness)。 这些边界用于推断输入数据的完整性,这些数据输入到时间聚合中,从而允许这些聚合的输出物化,并且只有在聚合的输入数据足够完整时才释放资源。 例如,可以将水印与拍卖(auction)的结束时间进行比较,以确定所述拍卖的所有有效 出价(bid)何时到达,即使在事件可能严重无序到达的系统中也是如此。 一些系统提供配置以允许事件有足够的松弛时间(slack time)到达。
更正式地说,水印是从处理时间到事件时间的单调函数。 对于处理时间的每个时刻,水印指定事件时间戳,在该时间点输入被认为是完整的。 换句话说,如果在处理时间 y 观察到的水印具有事件时间 x 的值,则断言从处理时间 y 开始,所有未来记录的事件时间戳都会大于 x。【也就是说事件时间 x 之前的 event 不会再进入系统,已经稳定了】
3.3 物化控制
我们的第三个贡献涉及塑造关系物化的方式,提供对关系 如何呈现(how to rendered)以及行本身 何时物化(when to materialized)的控制。
3.3.1 流物化(Stream Materialization)。 如 [33] 中所述,流更改日志(Changelogs,后文又称变更日志)是一种节省空间的描述 TVR 随时间演变的方式。 更改日志捕获关系的两个版本之间的逐个元素差异,实际上编码了用于随时间改变关系的 INSERT 和 DELETE 语句的序列。 其表达了有关关系中的行随时间演变的元数据。 例如:添加或撤消(add or retract)哪些行、物化行的处理时间,以及给定事件时间间隔的行的修订索引(revision index)。(如需更详尽地了解可能遇到的变更日志元数据类型,请参阅 Beam Java 的 PaneInfo 类)
如果按照上面的建议专门处理 TVR,则在物化该 TVR 的面向流的视图以进行存储、传输或自省(storage, transmission, or introspection)(特别是检查有关流的元数据,例如是否更改是 添加的 或 撤销的)。 与将流变更日志视为与关系完全不同的对象(以及处理随时间变化的关系的主要结构)的其他方法不同,我们建议将变更日志表示为简单的另一种时变关系。 这样,它可以使用与正常关系相同的机制(machinery)进行操作。 此外,仍然可以使用标准 SQL(不需要特殊运算符)以声明方式将变更日志流视图转换回原始 TVR,同时还支持接下来描述的物化延迟。
3.3.2 物化延迟(Materialization Delay)。通过将输入表和流建模为时变关系,并将查询的输出建模为结果时变关系,将查询的输出定义为即时变化以反映任何新输入似乎很自然。但作为一种实现策略,这种方法效率极低,会为只对最终结果感兴趣的消费者产生大量不相关的更新。即使消费者为推测的(speculative)非最终结果做好了准备,也可能存在有用的最大频率。例如,对于人工操作员查看的实时仪表板,秒级的更新可能就足够了。对于为外部消费而存储或传输的顶级查询,输出物化发生的频率和原因是基本的业务逻辑。
毫无疑问,有许多有趣的方法可以指定何时需要物化。在第 6.5 节中,我们根据实际用例的经验提出了一个具体的建议。但重要的是用户有某种方式来表达他们的要求。
4 一个有驱动性的例子
为了说明第 3 节中的概念,本节检查流式 SQL 文献中的一个具体示例查询。我们展示了如何在查询中使用这些概念,然后在实际输入中遍历其语义。
以下示例来自 NEXMark 基准 [38],该基准旨在衡量流查询系统的性能。 NEXMark 基准扩展了 XMark 基准 [35] 并模拟了一个在线拍卖平台,用户可以在该平台上开始拍卖物品并为物品出价。 NEXMark 数据模型包含三个流:Person、Auction 和 Bid,以及一个包含项目详细信息的静态 Category 表。在 NEXMark 基准测试中,我们选择了查询 7,定义为:“查询 7 监控当前拍卖的最高价格物品。每十分钟,此查询返回最近十分钟内的最高出价(和相关的 itemid)。” [38]。这是一个连续评估的查询,它使用一个 bid 流作为输入,并产生一个从输入的有限窗口计算的聚合流作为输出。在展示基于普通 SQL 的解决方案之前,我们展示了一个用 CQL [8] 构建的变体 [17] 来定义
每十分钟,查询处理前十分钟的出价。 它计算最后十分钟(子查询)的最高价格,并使用该值选择最后十分钟的最高出价。 结果被附加到流中。 我们不会深入研究 CQL 方言的细节,但要注意一些我们不会在我们的提案中重现的方面:
CQL 明确了流和关系的概念,提供了将流转换为关系的运算符(在我们的示例中为 RANGE)和将关系转换为流的运算符(在我们的示例中为 Rstream)。我们的方法基于时变关系的单一概念,并不严格要求转换运算符。
时间是隐含的;分组到十分钟窗口取决于底层流作为元数据附加到行的时间戳。如第 3.2 节所述,STREAM 通过缓冲和提供给 CQL 的顺序支持乱序时间戳,以便事件时间间隔始终对应于流的连续部分。我们的方法是通过明确事件时间戳并利用水印来推理输入完整性来直接处理乱序数据。
整个查询的时间步调一致。没有明确的条件,即子查询中的窗口对应于主查询中的窗口。我们通过连接条件使这种关系显式化。相比之下,这里是使用我们建议的标准 SQL 扩展指定的查询(请注意,此处编写的查询是超越目前 Flink 和 Beam via Calcite 支持的查询; 我们将在第 6 节和附录 B 中讨论差异。)
此查询计算相同的结果,但使用我们对标准 SQL 提出的扩展(以及 2016 年的 SQL 标准功能)进行计算。值得注意的点:
bidtime 列保存 bid 发生的时间。与之前的查询相比,时间戳是显式数据。Bid 流中的行不按 bid 时间的顺序到达。
Bid 流被假定有一个水印,如第 3.2 节所述,估计 BidTime 的完整性(completeness)作为 bidtime 列中未来时间戳的下限。请注意,该要求不会影响查询的基本语义。可以在从Bid流中记录的表上评估相同的查询而无需水印,从而产生相同的结果。
Tumble 是一个表值(table-valued) 函数 [1],它将 bid 流中的每一行分配给包含 bidtime 的 10 分钟间隔。输出表 TumbleBid 具有与 Bid 相同的所有列,加上两个额外的列 wstart 和 wend,它们分别表示滚动窗口间隔的开始和结束。 wend 列包含时间戳并具有相关的水印,用于估计 TumbleBid 相对于 wend 的完整性。
GROUP BY TumbleBid.wend 子句是使用水印的地方。因为水印为 wend 提供了尚未见过的值的下限,所以它允许实现推断特定输入分组何时完成。这一事实可用于延迟结果的物化,直到已知聚合完成,或提供指示相同程度的元数据。
随着 Bid 关系随时间推移而演变,随着新事件的添加,此查询定义的关系也在演变。这与瞬时视图语义相同。我们没有使用管理此查询的物化的高级功能。
现在让我们将这个查询应用到一个具体的数据集来说明它是如何执行的。 由于我们对流数据感兴趣,因此我们不仅关心所涉及的数据,还关心系统何时意识到它们(处理时间),以及它们发生的时间,以及系统自己对输入完整性的理解 随着时间的推移,在事件时间域(即水印)中。 我们将使用的示例数据集如下:
这里,左列的时间包括系统内发生事件的处理时间。 右列描述了事件本身,它们要么是推进到事件时间点的水印,要么是插入到流中的 (bidtime, price, item)元组。
清单 2 中的示例 SQL 查询在 8:21 对该数据集执行时将产生以下结果(为简洁起见,省略了大部分查询正文):
这实际上与原始 CQL 查询提供的输出相同,但添加了显式窗口开始、窗口结束和事件发生时间戳。
但是,这是数据集的表视图,它在查询时捕获整个关系的时间点视图,而不是流视图。 如果我们在处理时间的早些时候执行这个查询,比如在 8:13,它看起来会非常不同,因为到那时只有一半的输入数据已经到达:
在第 6 节中,我们将描述如何编写一个查询来创建与原始 CQL 查询匹配的输出流,以及为什么我们的方法总体上更灵活。
5 实践中学到的教训
我们提出的 SQL 扩展是基于现有技术和相关工作的,并且源自在 Apache Calcite、Flink 和 Beam 的工作经验——这些开源框架在整个行业和其他开源框架中得到广泛采用。
在附录 B 中,我们描述了这三个框架的一般架构属性、采用的广度以及目前存在的流式实现。 尽管到目前为止的实现还没有达到我们在第 6 节中提出的完整建议,但它们是朝着正确方向迈出的一步,并且已经产生了有用的经验教训,为它的发展提供了信息。 在这里,我们总结了这些教训:
某些操作仅(有效地)对带水印的事件时间属性起作用。无论是代表用户执行聚合还是执行明显有状态的业务逻辑,实现者都必须有办法在无限输入上维持有限状态。事件时间语义,尤其是水印,是至关重要的。当水印足够推进(advanced)以至于不会再次访问该状态时,可以释放正在进行的聚合或有状态运算符的状态。
操作可以擦除事件时间属性的水印对齐。事件时间处理要求事件时间戳与水印对齐。由于事件时间戳作为常规属性公开,因此可以在任意表达式中引用它们。根据表达式,结果可能会或可能不会与水印对齐;在查询规划期间需要考虑这些情况。在某些情况下,可以通过调整水印来保持水印对齐,而在其他情况下,事件时间属性会失去其特殊属性。
时变关系可能具有多个事件时间属性。大多数具有事件时间处理功能的流处理系统仅支持带有水印的单个事件时间属性。当加入两个 TVR 时,可能会发生两个输入 TVR 的事件时间属性都保留在生成的 TVR 中。解决这种情况的一种方法是“保留(hold-back)”水印,以使所有事件时间属性保持对齐。
对于用户来说,推理事件时间属性可以做什么可能很困难。为了定义可以使用事件时间语义和推理有效执行的查询,需要在某些子句中的特定位置使用事件时间属性,例如作为 OVER 子句中的 ORDER BY 属性。这些位置并不总是很容易发现并且未能正确的使用事件时间属性,很容易导致具有非预期的非常昂贵的执行计划。
推理查询状态的大小有时是必要的。理想情况下,用户在使用 SQL 时不必担心内部问题。但是,当使用无界输入时,用户干预是有用的或有时是必要的。因此,我们需要考虑用户需要提供哪些元数据(属性插入或更新的活动间隔,例如 sessionId),以及如何向用户提供有关正在使用的状态的反馈,将物理计算与他们的查询相关联。
用户区分流式操作符和物化操作符很有用的。在 Flink 和 Beam 中,用户需要明确推理哪些算子可以产生更新结果,哪些算子可以消费更新结果,以及算子对事件时间属性的影响。这些低级别的考虑不适用于 SQL,并且在关系语义中没有自然的位置;我们需要与 SQL 配合良好的物化控制扩展。
更新的洪流(Torrents of update):对于高吞吐量的流,为所有派生值持续发布更新是非常昂贵的。通过 Flink 和 Beam 中的物化控制,这可以限制为更少和更多相关的更新。
6 扩展SQL标准
这里介绍的工作是标准化流式 SQL 和定义我们对其特性的新位置的初步努力的一部分。 在本节中,我们将首先简要讨论 SQL 已经支持流式处理的一些方式,然后我们将介绍我们提出的流式处理扩展。
6.1 SQL 中对流式处理的现有支持
今天存在的 SQL 已经包含对许多流相关方法的支持。虽然不足以涵盖所有相关的流用例,但它们为构建提供了良好的基础,特别是:
查询基于表快照:随着经典 SQL 表的演变,查询可以在其当前内容上执行。通过这种方式,随着时间的推移,SQL 已经很好地处理了关系,尽管只是在静态快照的上下文中。
物化视图:视图(语义上)和物化视图(物理上)将查询逐点映射到 TVR。在任何时候,视图都是当时应用于其输入的查询的结果。这是流处理中非常有用的初始步骤。
时态表(Temporal tables):时态表体现了时变关系的思想,并提供了通过 AS OF SYSTEM TIME 运算符查询过去任意时间点的表快照的能力。
MATCH RECOGNIZE:MATCH_RECOGNIZE 子句与 SQL:2016 [1] 一起添加。当与事件时间语义相结合时,此扩展与流式 SQL 高度相关,因为它支持一类新的流处理用例,即复杂事件处理和模式匹配 [18]。
6.2 时变关系、事件时间列和水印
不需要扩展来支持时变关系。今天存在的关系运算符已经自然地将一个时变关系映射到另一个时变关系。
为了在 SQL 中启用事件时间语义,关系可以在其模式列中包含事件时间戳。查询执行需要知道哪些列对应于事件时间戳以将它们与水印相关联,如下所述。列包含事件时间戳的元数据将作为模式的一部分或与模式一起存储。时间戳本身与任何其他数据一样在查询中使用,这与时间戳本身是元数据的 CQL 和隐式引用使用模式声明的事件时间属性的 KSQL 形成对比。
为了支持无界用例,水印也可用作标准 SQL 运算符的语义输入。这扩展了关系运算符的范围,以包括与时间无关的运算符,如第 6.5.2 节中所述。例如,可以仅基于水印的推进(advance)将行添加到输出关系,即使在输入关系中没有行改变时也是如此。
扩展 1(带水印的事件时间列(Watermarked event time column))。关系中的事件时间列是具有关联水印的 TIMESTAMP 类型的可区分列。与事件时间列相关联的水印由系统维护为整个关系的时变元数据,并提供可能添加到列的事件时间戳的下限。
6.3 对事件时间戳的分组
在无界流上进行处理时,当已知没有更多行对聚合有贡献时,以 SELECT ... GROUP BY ... 形式的查询中投影的聚合就完成了。如果没有扩展,就永远不知道是否有更多的输入有助于分组。在事件时间语义下,水印给出了完整性的度量,并且可以根据事件时间列确定分组何时完成。这对应于现在广泛使用的事件时间窗口(event time windowing)的概念。我们可以通过利用事件时间列和水印来适配 SQL。
扩展 2(按事件时间戳分组(Grouping on event timestamps))。当 GROUP BY 子句包含作为事件时间列的分组键时,任何键小于该列的水印的分组都被声明为完成,并且进一步的输入将有助于该组(实际上,可配置的数量通常需要允许延迟,但这种机制超出了本文的范围;有关更多详细信息,请参阅 [6] 的第 2 章)每个具有无限输入的 GROUP BY 子句都需要包含至少一个事件时间列作为一个分组键。
6.4 事件时间窗口函数
除非尝试查找同时发生的事件,否则很少按包含原始事件时间戳的事件时间列进行分组。相反,事件时间戳通常映射到分组完成之后的可区分(distinguished)结束时间。在第 4 节的示例中,出价时间戳被映射到包含它们的十分钟间隔的末尾。我们建议添加内置的表值函数,以增强与这些常见用例的额外事件时间戳列的关系(同时为未来额外的内置或自定义 TVF【table valued function,表值函数】 敞开大门)。
扩展 3(事件时间窗口函数(Event-time windowing functions))。添加(作为起点)内置表值函数 Tumble 和 Hop 【滚动 和 跳跃(一般称滑动)】,它们将关系和事件时间列描述符作为输入并返回与附加事件时间间隔列作为输出的关系,并为事件时间间隔建立约定列名。
Tumble 和 Hop 的调用和语义如下。 SQL 标准可能会考虑添加在流式应用程序中使用的其他有用的事件时间窗口函数,但这两个非常常见且具有说明性。为简洁起见,我们展示了缩写的函数签名并在散文中描述了参数,然后用示例调用进行说明。
6.4.1 Tumble。 滚动(或“固定”)窗口将事件时间划分为等间距的不相交的覆盖间隔。 Tumble 采用三个必需参数和一个可选参数:
Tumble ( data , timecol , dur , [ offset ])
• data 是一个表参数,可以是有事件时间列的任何关系。
• timecol 是一个列描述符,指示数据的哪个事件时间列应映射到滚动窗口。
• dur 是指定滚动窗口宽度的持续时间。
• offset(可选)指定滚动应该从不同于标准纪元开始的时刻开始。 Tumble 的返回值是一个包含所有数据列以及额外的事件时间列 wstart 和 wend 的关系。 这是第 4 节中示例中对 Bid 表的示例调用:
用户可以按 wstart 或 wend 分组; 两者都导致相同的分组,并且假设理想的水印传播,分组同时达到完整性。 例如,按 wend 分组:
6.4.2 Hop。 跳跃(或“滑动”)事件时间窗口将固定大小的间隔均匀地分布在事件时间上。 Hop 接受四个必需参数和一个可选参数。 所有参数都类似于 Tumble 的参数,除了 hopsize,它指定跳跃窗口的起点(和终点)之间的持续时间,允许重叠窗口(hopsize < dur,常见)或数据中的间隙(hopsize > dur, 很少有用)。
Hop 的返回值是一个包含所有数据列以及附加事件时间列 wstart 和 wend 的关系。 这是第 4 节中示例中对 Bid 表的示例调用:
用户可以按wstart或wend分组,效果相同,就像滚动窗口一样。 例如:
使用表值函数通过以下方式改进了当前的实现状态:
GROUP BY 是根据列的值对行进行真正的分组。 在 Calcite、Beam 和 Flink 中,GROUP BY HOP(...) 因导致多个输入行违反了关系语义。
所有窗口函数的更统一的表示法。 近乎平凡的 Tumble 具有与输入扩展 Hop 相同的一般形式,并且使用表值函数允许添加具有相似外观的各种更复杂的功能(例如日历窗口或会话化)。
引擎在如何实现这些表值函数方面具有灵活性。 根据下游物化要求,输出中的行可能会出现和消失。
6.5 物化控制
我们提案的最后一部分围绕物化控制,允许用户灵活地决定他们的 TVR 中的行如何(how)以及何时(when)物化。
6.5.1 流物化。物化的 how 方面围绕将 TVR 物化为表或流的选择。关系的长期默认设置是将它们物化为表。并且由于这种方法与将时间点关系与时变关系交换的想法完全兼容,因此无需围绕物化表进行任何更改。然而,在某些情况下,物化 TVR 的面向流的变更日志视图是可取的。
在这些情况下,我们需要某种方式向系统发出信号,表明关系的变更日志应该被物化。我们建议在查询中使用新的 EMIT STREAM 修饰符来执行此操作。回想一下清单 3 中的原始驱动性的查询结果,它呈现了示例查询的表格视图。通过在顶层添加 EMIT STREAM,我们物化了 TVR 的流更改日志,而不是关系本身的时间点快照:
注意,STREAM 版本中包含许多附加列:
• undo:该行是否是前一行的撤消。
• ptime:更改日志中行的处理时间偏移量。
• ver:相对于对应于同一事件时间窗口的不同修订版本(revision)的其他行版本该行的序列号。
当查询中存在聚合导致行随时间发生更改时,更改日志仅对行进行多个修订。
扩展 4(流物化(Stream Materialization))。 EMIT STREAM 产生一个时变关系,表示查询的经典结果的变化。除了经典结果的模式之外,更改流还包括指示列:该行是否是前一行的撤消,该行的更改日志处理时间偏移量,相对于同一事件时间分组的其他更改的序列号。
可以想象其他选项,例如允许物化增量而不是聚合,甚至是整个关系,如 CQL 的 Rstream。这些可以用额外的修饰符来指定,但超出了本文的范围。
就等于原始 CQL 查询的输出而言,STREAM 关键字是朝着正确方向迈出的一步,但它显然更冗长,在数据到达时捕获给定 10 分钟事件时间窗口的最高出价者的完整演变,而 CQL 版本在该窗口的输入数据完成后,每 10 分钟窗口仅提供一个答案。为了调整流输出以匹配 CQL 的行为(但适应无序输入数据),我们需要支持物化延迟。
6.5.2 物化延迟。物化的 when 方面围绕关系随时间演变的方式展开。标准方法是逐个记录的:当将诸如 INSERT 和 DELETE 之类的 DML 操作应用于关系时,这些更改会立即反映出来。然而,在处理关系中的聚合变化时,以某种方式延迟聚合的物化通常是有益的。多年来,我们观察到两种主要的常用延迟物化类别:完整性延迟和周期性延迟。
完整性延迟(Completeness delays):事件时间窗口化提供了一种将无界关系分割成有限时间块的方法,并且对于窗口聚合的最终一致性足够的用例,不需要进一步的扩展。但是,某些用例要求聚合仅在其输入完成时才物化,例如部分结果太不稳定而无法使用的查询,例如确定数字总和是偶数还是奇数的查询。即使作为表格使用,这些仍然受益于水印驱动的物化。
再次回忆一下清单 4 中的查询,我们在 8:13 查询了关系的表版本。该查询为每个窗口提供了部分结果,在处理时间的那个点捕获每个滚动窗口的最高价格商品。对于不希望显示此类部分结果的用例,我们建议使用 EMIT AFTER WATERMARK 语法以确保表视图只会物化输入数据完整的行。这样,我们在 8:13 的查询将返回一个空表:
如果我们在 8:16 再次查询,一旦水印通过第一个窗口的末尾,我们将看到第一个窗口的最终结果,但第二个窗口仍然没有:
然后如果我们在 8 点 21 分再次查询,在水印经过第二个窗口的末尾之后,我们将最终得到两个窗口的最终答案:
我们还可以使用 STREAM 物化来简洁地观察结果的演变,这类似于原始 CQL 查询会产生的结果:
将此与第 6.5.1 节中流式变更日志的演变进行比较,说明了与 AFTER WATERMARK 的区别:
• 每个窗口恰好有一行,每行包含最终结果。
• ptime 值不再对应于最大出价记录的到达时间,而是对应于水印超过给定窗口结束的处理时间。
延迟流物化的最常见示例是通知用例,其中轮询最终一致关系的内容是不可行的。 在这种情况下,将关系作为流使用更有用,该流仅包含已知输入数据完整的聚合。 这是原始 CQL 最高出价查询所针对的用例类型。
扩展 5(物化延迟:完整性(Materialization Delay: Completeness))。 当查询具有 EMIT AFTER WATERMARK 修饰符时,只会物化化结果中的完整行。
周期性延迟(Periodic delays):我们关心的第二个延迟物化用例围绕管理最终一致的 STREAM 变更日志的详细程度。 正如我们在上面看到的,默认的 STREAM 渲染会在关系中的任何行发生变化时提供更新。 对于大容量流,这样的变更日志可能非常冗长。 在这些情况下,通常需要限制关系中聚合的更新频率。 为此,我们建议在 EMIT 子句中添加 AFTER DELAY 修饰符,该修饰符指示在给定聚合发生更改后对物化施加延迟,例如:
在此示例中,每个窗口的多个更新被压缩在一起,每个更新都在从第一次更改到行的六分钟延迟内。
扩展 6(定期物化(Periodic Materialization))。 当查询有 EMIT AFTER DELAY d 时,行会以周期 d 物化(而不是连续)。
也可以将 AFTER DELAY 修饰符与 AFTER WATERMARK 修饰符结合起来,为部分结果行提供重复定期更新的早/准/晚模式 [6],然后是单个准时行,然后是重复定期更新任何迟到的行。
扩展 7(组合物化延迟(Combined Materialization Delay))。 当查询具有 EMIT AFTER DELAY d AND AFTER WATERMARK 时,行将在周期 d 和 完成时物化。
7 总结
流式 SQL 是随着时间的推移操纵关系的活动。 大量的流式 SQL 文献与现代流式社区最近的努力相结合,为基本的流式语义奠定了坚实的基础,但在可用性、灵活性和健壮的事件时间处理方面仍有改进的空间。 我们相信本文提出的三个贡献,(1) 时变关系的普遍使用,(2) 健壮的事件时间语义支持,以及 (3) 物化控制可以显着提高流式 SQL 的易用性 . 此外,他们将扩大可用运算符的清单,不仅包括当今标准 SQL 中可用的全套时间点关系运算符,而且还将语言的功能扩展到另外的运算符,这些运算符随着时间的推移而起作用,以描述(shape)何时以及如何进行 关系演变。
8 未来工作
扩展/自定义事件时间窗口:虽然第 6.4 节中提出的窗口 TVF 很常见,但 Beam 和 Flink 都提供了更多,例如传递闭包会话(连续活动的周期)、键控会话(keyed session)(具有公共会话标识符的周期,带有超时)和基于日历的窗口。经验还表明,预构建的解决方案永远不足以满足所有用例([6] 的第 4 章);最终,用户应该能够利用 SQL 的强大功能来描述他们自己的自定义窗口 TVF。
时间进度表达式(Time-progressing expressions):计算流尾部的视图很常见,例如计算最后一小时的出价。从概念上讲,这可以通过像 (bidtime > CURRENT_TIME - INTERVAL '1' HOUR) 这样的谓词来完成。但是,SQL 标准定义像 CURRENT_TIME 这样的表达式在查询执行时是固定的。因此,我们需要随着时间的推移而有进度的表达式。
对时态表的相关访问(Correlated access to temporal tables):流式 SQL 中的一个常见用例是使用特定时间点的时态表中的属性来丰富一个表,例如使用下订单时的货币汇率来丰富订单。目前,只能访问由固定文字 AS OF SYSTEM TIME 指定的临时版本。要为连接启用临时表,需要通过相关连接属性(correlated join attribute)访问表版本。
流式变更日志选项:正如在第 6.5.1 节中提到的,存在更多用于流物化的选项,并且应该扩展 EMIT 以支持它们。特别是,将流更改日志呈现为一系列增量。
嵌套 EMIT:虽然我们建议将 EMIT 的应用限制在查询的顶层,但可以为在任何嵌套查询级别允许 EMIT 的用处提出一个论据。探索这种变化将带来的额外能力和额外复杂性之间的权衡关系是值得的。
优雅演变(Graceful evolution):根据定义,流式查询存在很长一段时间,但软件从未完成或完美:发现错误,需求不断发展,随着时间的推移,长时间运行的查询必须改变。这些查询的状态性质对中间状态的演变提出了新的挑战。总之对于流社区,这仍然是一个未解决的问题,但它在更抽象的 SQL 领域中更重要。
更严格的语义正式定义:尽管我们尝试在适用的情况下对本文提出的概念进行半正式分析,但作为流社区,我们仍然缺乏对流含义的真正正式分析,尤其是在应用于某些事件时间处理的更微妙的方面,例如水印和物化控制。对现代流概念进行更严格的调查将是非常欢迎和有益的对本文的补充。