Velox:数据界的超级发动机
核心思想
想象一下,现在有很多汽车品牌,比如 Presto、Spark、PyTorch 等。它们每个都有自己独特的外观、操作系统和驾驶体验(这就是所谓的"控制面"),但它们都需要一个强大的发动机来驱动轮子(这就是"数据面"或"执行引擎")。
在 Velox 出现之前,每个"汽车品牌"都在自己造发动机。结果就是:
- 重复造轮子:大家都在做类似的事情,浪费人力物力。
- 标准不统一:A 家的发动机零件(比如一个叫
substr
的函数)和 B 家的不能通用,甚至连操作方式都不一样(比如一个从0开始计数,一个从1开始),让司机(数据工程师)非常头疼。
Meta 公司也遇到了这个问题,内部有十几个处理数据的系统,各有各的"发动机",非常混乱。于是他们想:"我们能不能打造一个业界最顶尖、性能最强、并且标准化的发动机核心,让所有系统都能用?"
这个标准化的"发动机核心",就是 Velox。
Velox 是什么?
Velox是Meta开发的扩展性强,高性能的C++执行引擎加速库,提供可重用、可扩展、高性能且与方言无关的数据处理组件。
- 它是一个 C++ 库:不是一个能直接使用的完整软件,而是一套工具箱,或者说是一堆高性能的"发动机零件"。
- 它是"执行引擎":它只负责最核心、最繁重的计算任务。你给它一个明确的指令(优化好的查询计划),它就用最快的速度在单台机器上把数据计算出来。
- 它性能极高:主要因为它采用了"向量化(Vectorization)"技术。你可以理解为,传统计算是一个一个处理数据,而向量化是把数据一批一批(像一个向量)地处理,速度快得多。
- 它不做什么:它不负责跟用户直接打交道。它看不懂 SQL 语言,也不知道如何把一个任务拆分到整个集群去执行。这些工作由 Presto、Spark 等上层应用负责。
总结一下:Velox 专注于做一件事并把它做到极致——在单台机器上高效地执行数据计算任务。
为什么需要 Velox
一个由各种专用数据引擎(用于 SQL、机器学习、流处理等)组成的、杂乱无章的生态系统。它所列出的后果一针见血:
- 重复的工程投入:这是最核心的经济驱动因素。Meta 意识到其内部团队都在重复构建功能相似的组件(如数据结构、连接算法、过滤逻辑)。这是极其低效的。
- 不一致的用户体验:
substr()
的例子堪称完美。当同一个函数在不同系统中的行为不一致时,会导致难以察觉的错误,浪费开发人员的时间,并使数据治理成为一场噩梦。 - 拖慢创新速度:如果你想增加一个新的硬件加速器(如 GPU)或一个新的压缩算法,你必须在十几个不同的系统中分别实现。而一个统一的引擎可以让你在一个地方创新,然后让所有系统受益。
Velox 的方法是"解构"数据系统。它认为"控制平面"(Control Plane,负责 SQL 解析、优化、任务调度)可以与"数据平面"(Data Plane,负责实际的计算执行)分离。Velox 的目标就是成为业界顶级的、标准化的数据平面。
这正《可组合数据管理系统宣言》(The Composable Data Management System Manifesto) 的核心思想。然而,Velox 并非推动这一理念的唯一项目。要理解 Velox,你必须了解它的同行:
- Apache Arrow:这可以说是该领域最重要的项目。Arrow 致力于标准化内存中的列式数据格式。它的目标是成为数据表示的"世界语",从而消除数据在不同系统(例如从 Spark 到 Pandas)之间移动时昂贵的序列化/反序列化开销。
- Velox vs. Arrow:它们是互补关系,而非竞争对手。Velox "与 Arrow 兼容",这是一个至关重要的特性。Arrow 是数据格式(好比"集装箱标准"),而 Velox 是计算引擎(好比"处理集装箱内货物的机械臂")。Velox 与 Arrow 的紧密结合,使其能够与 Arrow 生态中的其他系统实现零拷贝(zero-copy)的数据交换。
- Substrait.io:这个项目将"可组合化"的理念又向前推进了一步。它旨在标准化查询计划(Query Plan)本身。想象一下,如果 Presto、Spark 和 DuckDB 都能输出同一种格式的查询计划,那么你就可以将这个计划喂给任何兼容的执行引擎,比如 Velox 或 Arrow DataFusion。
Velox 的真正的力量来自于它顺应了由 Arrow 开创的更广泛的行业趋势。它的意义不仅是为 Meta 创建一个内部共享库,更是构建一个能够在一个更大的、即插即用的生态系统中充当"CPU"角色的核心组件。
Velox 的"家谱"和背景
Velox 的技术渊源 能帮助我们理解它为什么这么厉害:
- 思想源头:向量化执行的思想很早就有了,像 MonetDB、VectorWise 都是这个领域的先驱。
- 业界巨头都在用:
- Snowflake 的创始人就来自 VectorWise 团队,Snowflake 的成功证明了这条路的正确性。
- Databricks 为了追赶 Snowflake,也专门挖人做了个类似的 C++ 向量化引擎叫 Photon。
- DuckDB 是一个非常流行的嵌入式分析数据库,也深度使用了向量化技术。
- Velox 的诞生:Meta 挖来了业界的大牛(比如曾在 Google 和 DuckDB 工作过的 Orri Erling),借鉴了各种先进思想,并结合内部海量的应用场景,最终打造出了 Velox。 Velox 是集大成者,站在了巨人的肩膀上。
执行计划和语法解析能否复用
理论上,执行计划和语法解析确实有大量的逻辑可以复用,并且已经有项目在尝试将它做成一个公共库。和 Velox 诞生的理由如出一辙:
- 重复造轮子:就像每个系统都在实现自己的
Join
算法一样,每个系统(Presto, Spark, ClickHouse...)也都在实现自己的 SQL 解析器和查询优化器。这同样是巨大的工程浪费。 - 不一致性:不同系统的 SQL 方言、支持的函数、优化行为都略有不同,给用户带来了困惑和迁移成本。
如果能有一个统一的、可插拔的“查询计划中心”公共库,那将是数据系统领域的又一次巨大进步。这个领域已经有两个非常重要的明星项目:
1. Apache Calcite: 数据库领域的“瑞士军刀”或“大脑移植套件”
Calcite 是目前最成熟、最成功的尝试。它是一个功能极其强大的、可插拔的查询处理框架。
它能做什么?
- SQL 解析 (Parsing):将 SQL 字符串解析成抽象语法树 (AST)。
- SQL 验证 (Validation):检查表、字段、函数是否存在,类型是否匹配。
- 查询优化 (Optimization):这是 Calcite 的核心。它提供了一个巨大的、可扩展的优化规则库(包括基于规则的 RBO 和基于成本的 CBO),可以将一个逻辑执行计划,优化成一个高效的物理执行计划。
- 数据源集成:它还能与多种数据源对话。
谁在用?
大量顶级项目都在使用 Calcite 作为它们的“大脑”,例如:Apache Flink, Apache Hive, Apache Drill, Google BigQuery 等。
Calcite 就像一个“大脑移植套件”,它让新的数据系统可以不必从零开始构建自己复杂的查询优化器,而是直接集成 Calcite 这个成熟的大脑。
2. Substrait: 数据查询计划的“通用蓝图”或“PDF格式”
Substrait 是一个更新、更专注的项目。它不做优化,只做一件事:定义一种标准的、与语言无关的、可序列化的格式,用来描述一个数据查询计划。
- 它是什么?
您可以把它想象成查询计划领域的“PDF”或“Protobuf”。它提供了一个通用的“蓝图”格式。 - 它解决了什么问题?
在有 Substrait 之前,Spark 的查询计划只有 Spark 自己认识,Presto 的查询计划也只有 Presto 自己认识。如果 Spark 想把一部分计算任务交给 Velox,就需要一个“翻译官”(比如 Gluten)。 - 未来的理想世界:
一个系统(比如用 Calcite)可以生成一个标准的 Substrait 计划,然后这个计划可以被任何支持 Substrait 的后端(如 Velox、Arrow DataFusion)直接执行,无需任何翻译。
为什么这部分如此之难?为什么 Velox 没有做?
既然有 Calcite 这样的项目,为什么不是所有人都用它?为什么 Velox 决定不碰这一块?因为将“控制面”(解析和优化)完全通用化,比“数据面”(执行)要面临更多、更棘手的挑战:
SQL 方言的“巴别塔”问题
SQL 远非一个统一的标准。PostgreSQL, MySQL, T-SQL, Spark SQL 各有各的语法、函数和特性。一个通用的解析器需要兼容所有这些方言,或者让用户自己去扩展,这是一个巨大的工程。优化与“上下文”的强绑定
一个好的查询优化器不是在真空中工作的。它需要大量的“上下文信息”(元数据)来做出最佳决策。- 数据统计信息:表A有多大?列B的基数(不重复值的数量)是多少?数据是排序的吗?有没有索引?这些信息对于决定 Join 的顺序、选择 Join 的算法至关重要。
- 数据源特性:这个数据源支不支持谓词下推?
这些元数据通常存储在 Hive Metastore、Iceberg Metadata 等外部系统中。一个通用的优化器需要设计一套复杂的接口去获取和理解所有这些信息。
物理计划与“执行引擎”的深度耦合
最终的物理执行计划,必须为底层的执行引擎“量身定做”。- 一个为 Spark 的 RDD 和 Shuffle 机制优化的计划,和一个为 Velox 的 Push-based Pipeline 优化的计划,可能是完全不同的。
- 优化器需要深刻理解执行引擎的每一个
Operator
的成本和特性,才能生成最优的“乐高积木组合图”。
历史遗留与工程现实
像 Spark 和 Presto 这样的成熟系统,已经投入了数年甚至十数年的时间,打造了自己极其复杂的、与自身系统深度绑定的优化器。想把它们替换成 Calcite,是一项伤筋动骨的、风险极高的“大脑移植手术”。
结论:Velox 的战略选择
面对这些巨大的挑战,Velox 团队做出了一个非常务实且专注的战略选择:先集中所有精力,把“数据面”(执行层)做到世界第一。
Velox 的哲学是成为一个极致高效的组件。它定义了清晰的边界:“你们(Presto, Spark, 或任何其他系统)负责把用户的意图(SQL)翻译成一个优化好的、具体的执行计划。只要你把这个计划交给我,我就能用业界最快的速度把它执行出来。”
这是一种解耦的思想。Velox 专注于它最擅长的事情,同时,它也通过支持 Substrait 这样的标准,为未来更加“可组合”的数据系统铺平了道路。在理想的未来,用户或许真的可以自由组合:
[某个SQL解析器]
-> [Calcite优化器]
-> [Substrait计划]
-> [Velox执行引擎]
总结
通俗比喻 | 专业术语 | 解释 |
---|---|---|
汽车品牌 (Presto, Spark) | 数据管理系统 | 用户直接使用的工具,负责整体协调。 |
汽车的控制系统 (方向盘, GPS) | 控制面 (Control-Plane) | 负责解析用户指令 (如SQL)、优化任务、任务分发。 |
标准化的发动机核心 | Velox (执行引擎) | 负责具体的数据计算,是性能的心脏。 |
发动机的内部运作 | 数据面 (Data-Plane) | 数据的实际处理、计算、过滤、聚合等。 |
所以,Velox 的目标是统一数据处理系统的"发动机",让开发者不再需要重复发明轮子,同时为用户提供更一致、更高效的数据处理体验。它代表了现代数据系统向"模块化"和"可组合"方向发展的一个重要趋势。
Velox 如何工作
让我们来分析是什么让Velox 的关键组件如此高效和现代化。
Type & Vector (数据基石)
- 它是什么:一个用于定义数据类型(数字、字符串、Map、Array)的系统,以及一个与 Arrow 兼容的、内存中的列式数据布局。
- 为何重要:这里最重要的特性是"多种编码格式"(Dictionary, RLE 等)。高性能数据库不仅仅是存储原始数据,它们会智能地压缩数据。例如,一列国家名称可以被存储为小整数(即字典编码),这使得比较和聚合操作快上几个数量级。能够在不解码的情况下,直接在这些编码数据上执行计算,是高级引擎的标志。
Expression Eval (计算核心)
- 它是什么:在其向量化数据上执行计算(例如
a + b * 5
)的引擎。 - 为何重要:除了标准的向量化,其关键特性是"编码感知评估 (encoding-aware evaluation)"。这与上一点直接相关。一个聪明的引擎知道,在一个经过字典编码的列上筛选
country = 'USA'
,它只需找到 'USA' 对应的整数编码,然后对数据执行一次超高速的整数比较即可。这避免了代价高昂的字符串操作。
Functions (工具箱)
- 它是什么:一个标准函数的库(如
substr
,avg
等)。 - 为何重要:与 Presto 和 Spark 的兼容性。这不是一个技术细节,而是一个至关重要的"生态采纳策略"。通过确保函数行为完全一致,Velox 让现有团队可以轻松迁移他们的工作负载,而无需重写所有查询。为添加新函数(UDF)提供的简单接口,对于机器学习等特殊用例也至关重要。
Operators (构建模块): 数据处理的“乐高积木”
这些是构成一个查询计划(Query Plan)的物理执行单元。每一个算子都像一块功能独特的乐高积木,接收来自上游算子的一批数据(Vector),处理完后,再把结果(一个新的 Vector)交给下游算子。
我们来介绍几个最核心的“积木”:
TableScan (数据源扫描)
职责:这是任何查询流水线的起点。它的工作是从
Connector
(仓库)那里获取原始数据,把它们转换成 Velox 内部的列式格式(Vector)。比喻:工厂流水线的第一个工人,负责从仓库货架上把原材料(比如一整块钢板)搬到传送带上。
Filter (过滤器)
职责:接收一批数据,并根据一个返回“是/否”的条件函数,决定哪些行应该被保留,哪些应该被丢弃。
比喻:流水线上的质检员。他拿着一个模板(条件函数),检查传送带上的每一个零件,不符合尺寸的就直接扔掉。
Project (投影/转换器)
职责:对输入的每一行数据,执行一个或多个计算函数,生成新的列。这可以是简单的列选择(比如只保留
name
和age
列),也可以是复杂的计算(比如SELECT price * 1.1 AS new_price
)。比喻:流水线上的加工工人。他可以给零件上色(
upper(name)
),也可以把两个零件组合成一个新的(first_name + last_name
),或者只是改变一下零件的摆放位置。
Aggregation (聚合器)
职责:这是最复杂的算子之一。它接收大量数据,按指定的键(
GROUP BY
key)进行分组,然后对每一组应用一个聚合函数(如SUM
,COUNT
,AVG
)。它内部通常会使用一个高效的HashTable
来存储中间结果。比喻:流水线末端的打包站。工人把传送带上所有相同颜色的零件(
GROUP BY
key)都归拢到一起,然后计算每种颜色的零件总共有多少个(COUNT(*)
),或者总重量是多少(SUM(weight)
)。
HashJoin (哈希连接器)
职责:用于执行
JOIN
操作。它接收来自两条不同流水线的数据(比如orders
表和customers
表),根据连接键(customer_id
)进行匹配。它会先用其中一条流水线的数据构建一个高效的HashTable
(Build side),然后再用另一条流水线的数据去探测(Probe)这个HashTable
,快速找到匹配的行。比喻:一个高科技的“配对站”。它先把所有A流水线送来的螺母(Build side)按尺寸整理到一个巨大的、带索引的零件盒(
HashTable
)里。然后,当B流水线送来一个螺丝(Probe side)时,它能瞬间在零件盒里找到与之匹配的螺母。
算子 (Operator) 和函数 (Function) 的关系
函数 (Function): 是一个纯粹的计算单元,是“数学家”。
它只关心“计算逻辑”。比如
+
函数只关心如何把两个数字相加;upper()
函数只关心如何把字符串变大写。它不关心数据从哪里来,要到哪里去,也不关心自己被谁调用。它就是一个只懂得计算的“工具人”。
算子 (Operator): 是一个数据处理的“车间”或“工作站”,是“车间主任”。
它关心的是“数据流转和处理流程”。比如
Filter
算子这个车间,它的工作流程是:接收一批数据,对每一行数据调用一个返回布尔值的函数(比如is_greater_than(age, 30)
),然后根据函数返回的true
或false
决定是保留还是丢弃这行数据。Aggregation
算子这个车间,它的工作流程是:接收一批数据,根据group by
的 key 把它们分组,然后对每一组调用sum()
、avg()
等聚合函数,最后输出结果。
结论与对比 Flink:
关系:算子(车间)在其工作流程中,会调用一个或多个函数(工具人)来完成具体的计算任务。
与 Flink 的相似之处:这个模型和 Flink 完全一致。Flink 的
Map
,Filter
,KeyBy
等都是算子(Operator),而您在map()
或filter()
方法里写的 Lambda 表达式或传入的MapFunction
,就是具体执行计算的函数(Function)。与 Flink 的不同之处:
层级:Flink 是一个完整的、分布式的流处理框架,它的算子包含了网络数据传输、状态管理(State)、容错(Checkpoint)等复杂的分布式逻辑。
Velox 是一个单机的、C++ 的执行引擎库。它的算子更纯粹,只关心在单台机器的内存里,如何最高效地组织数据流和调用计算函数,不包含分布式协调的逻辑。
Connectors (连接器): 进口码头和卸货吊机
职责:负责读取外部存储系统上的数据,并将其解码成 Velox 内部可以理解的
Vector
格式。为何重要:数据世界有各种各样的存储格式(Parquet, ORC, CSV...)和存储系统(S3, HDFS, 本地文件...)。
Connector
就像一个多功能的“卸货吊机”,它认识各种不同类型的“集装箱”(文件格式),知道如何打开它们,并把里面的货物(数据)高效地搬运到工厂的传送带上。
Serializers (序列化器): 出口码头和打包机
职责:负责将 Velox 在内部处理完成的数据(
Vector
),编码成外部系统(如 Presto, Spark)或网络能够理解的格式。为何重要:当 Velox 作为 Presto 或 Spark 的一个执行引擎时,它计算出的结果需要返回给 Presto/Spark 的上层任务。
Serializer
就像一个“打包机”,它把 Velox 生产好的Vector
,打包成符合 Presto 或 Spark 网络传输协议的“包裹”(如 PrestoPage 或 UnsafeRow 格式)。比喻:工厂生产好的发动机,需要运送到另一个品牌的总装车间。打包机(
Serializer
)会把这个发动机装进一个符合对方物流标准的、带缓冲垫的箱子里,确保对方能顺利接收和拆包。
Resource Management & Fuzzer (生产就绪性)
- 它是什么:用于管理内存、线程和磁盘溢出(spilling)的工具,外加一个复杂的测试框架。
- 这是区分一个"原型"和一个"生产级系统"的关键。一个无法优雅处理内存耗尽(spilling)或没有经过严格边界情况测试(fuzzing)的引擎,是无法用于关键任务的。包含 Fuzzer(模糊测试工具)这一项,强烈表明了该项目对稳定性和正确性的承诺。
Velox Vector
可以将 Velox Vector 理解为一个"智能的列式数据容器"。它不是简单地把一堆数据堆在内存里,而是用非常聪明的方式来组织、压缩和访问这些数据,从而实现极致的计算性能。
Apache Arrow
格式是它的基础,这保证了它能和现代数据生态中的其他工具(如 Spark, Pandas)高效地交换数据,就像使用了国际标准的集装箱一样。我们来逐一拆解这些"智能"体现在哪里。
基础与复杂类型:从积木到城堡
FlatVector (扁平向量)
- 这是最基础的积木。你可以把它想象成电子表格中最普通的一列。它有一个地方存数据(
value buffer
),还有一个地方用"位图"标记哪个单元格是空的(null buffer
)。简单直接,是很多操作的最终形态。
- 这是最基础的积木。你可以把它想象成电子表格中最普通的一列。它有一个地方存数据(
ArrayVector, MapVector, RowVector (复杂/嵌套类型)
- 这些是用来搭建"数据城堡"的。
- ArrayVector:当一列的每个单元格本身就是一个列表时使用。例如,一列"用户标签",每个用户可以有多个标签
['VIP', '新用户', '高价值']
。 - MapVector:当每个单元格是一个键值对集合时使用。例如,一列"设备属性",每个单元格可能是
{'os': 'iOS', 'version': '15.5'}
。 - RowVector:当每个单元格是一个结构体(或一行记录)时使用。例如,一列"订单信息",每个单元格包含
{'order_id': 123, 'price': 99.9}
。 - 关键点:这些复杂类型的子元素(如 Array 的元素,Map 的键和值)本身也是 Vector!这种嵌套能力让 Velox 可以高效地处理任意复杂的半结构化数据(如 JSON)。
RowVector
和 MapVector
从表面上看,它们都像是键值对(Key-Value)的集合。
核心区别: RowVector
的"钥匙"是固定的,而 MapVector
的"钥匙"是灵活的。
RowVector (Struct): 结构固定的"档案盒"
可以把一个 RowVector
想象成一个标准化的员工档案盒。
- Schema 固定:档案局规定了,每个档案盒里必须有且只有这几项:
{姓名: ___, 年龄: ___, 部门: ___}
。这个结构是预先定义好的,对所有员工都一样。 - Key 是元数据:
姓名
、年龄
、部门
这些"键"(Key),是档案盒这个类型本身的一部分。它们是模板,是 schema,不是数据。 - Value 是数据:你往里面填写的"张三"、"35"、"销售部"才是真正的员工数据。
- 类型固定:
姓名
这一项永远是字符串,年龄
永远是数字。
回到例子:{'order_id': 123, 'price': 99.9}
这是一个完美的 RowVector
场景。因为每一笔订单,都必然包含 order_id
和 price
这两个字段。这个结构是稳定、可预测的。你不会遇到一笔订单有 order_id
却没有 price
,或者突然多出来一个 discount_code
字段(discount_code
应该是订单表的另一个独立的列,而不是这个结构体的一部分)。
MapVector (Map): 自由组合的"标签袋"
可以把一个 MapVector
想象成一个给商品贴标签的"标签袋"。
- Schema 灵活:你可以在这个袋子里放任意多的标签,每个标签都是一个键值对。
- Key 是数据:
'os'
,'version'
,'brand'
,'color'
这些"键",本身就是数据的一部分。它们不是预先定义好的。 - 内容可变:
- 商品A(一台iPhone)的标签袋里可能是:
{'os': 'iOS', 'version': '15.5', 'color': 'black'}
- 商品B(一台安卓电视)的标签袋里可能是:
{'os': 'Android', 'brand': 'Sony', 'resolution': '4K'}
- 商品C(一件T恤)的标签袋里可能是:
{'material': 'cotton', 'size': 'L'}
- 商品A(一台iPhone)的标签袋里可能是:
- 类型约束:
MapVector
也有约束,但比RowVector
宽松:袋子里所有标签的"键"必须是同一种类型(比如都是字符串),所有标签的"值"也必须是同一种类型(比如也都是字符串)。
回到例子:{'os': 'iOS', 'version': '15.5'}
这是一个典型的 MapVector
场景。因为"设备属性"千变万化。你无法预先定义一个包含所有可能性(操作系统、版本、品牌、分辨率、内存大小...)的固定结构。用 Map 来存储这种非结构化的、可变的属性是最合适的。
总结对比
特性 | RowVector (Struct / 档案盒) | MapVector (Map / 标签袋) |
---|---|---|
结构 (Schema) | 固定的,预先定义的 | 灵活的,动态的 |
键 (Key) | 是元数据,是类型的一部分,所有行都一样 | 是数据的一部分,每行都可以不一样 |
好比 | C语言的 struct / 一张有固定表头的表格 | Python的 dict / 一个自由的属性包 |
适用场景 | 数据结构一致、可预测(如:坐标 {x, y, z} ) | 数据结构不固定、多变(如:用户自定义标签、事件属性) |
一句话总结:
- 当你明确知道一个组合数据有哪些字段、分别是什么类型时,用
RowVector
。 - 当你只是想存一堆不确定的、可变的键值对时,用
MapVector
。
编码:数据压缩的艺术
编码是 Velox 性能优越的核心秘诀之一。它不是傻乎乎地存原始数据,而是根据数据特点选择最高效的"压缩"方式。
DictionaryVector (字典编码)
- 解决的问题:当一列有大量重复值时(例如"国家"列,几亿行数据里可能只有两百多个国家)。
- 工作原理:它会创建一个"字典"(比如
{'中国': 0, '美国': 1, ...}
),然后原始数据列只存储对应的整数[0, 1, 0, 0, 1, ...]
。 - 巨大优势:
- 省空间:存整数远比存长字符串省内存。
- 计算快:比较两个整数的速度,比比较两个字符串快几个数量级。
ConstantVector (常量编码)
- 这是字典编码的极致。如果一列的所有值都完全相同,Velox 就只存这一个值一次,然后标记一下"这一整列都是这个值"。
StringView:为字符串处理量身打造的"黑科技"
处理字符串是数据库性能的一大瓶颈。Velox 的 StringView
是一个极其精巧的设计,专门用来加速字符串操作。你可以把它想象成一个"字符串的索引卡",每张卡片大小固定(16字节)。
它是如何工作的?
- 短字符串内联 (Inline):如果要存的字符串很短(不超过12个字符),直接把整个字符串写在这张卡片上。访问它时,CPU 能在缓存里直接找到,速度飞快(这就是"缓存局部性")。
- 长字符串指针:如果字符串太长,卡片上就存一个"指针",告诉 CPU 去内存的哪个"书架"上找这个完整的字符串。
- 前缀 (Prefix):卡片上还会写着字符串的前4个字符作为"前缀"。
这带来了什么好处?
- 快速比较/排序:当你要比较两个字符串时,可以先比较卡片上的4字节前缀。如果前缀都不同,就根本不需要去"书架"上取完整的长字符串了。对于高选择性的过滤(比如
WHERE name LIKE '张%'
),这能过滤掉海量数据。 - 零拷贝操作:当你要做
substr()
(取子串) 操作时,Velox 不会傻乎乎地去复制一个新的、更短的字符串。它只是创建一张新的索引卡,让上面的指针指向原字符串的中间某个位置,并修改长度。这几乎没有成本,快得惊人。 - 乱序写入:因为每张"卡片"大小都一样,所以可以不按顺序写入。这在并行计算中非常有用。
- 快速比较/排序:当你要比较两个字符串时,可以先比较卡片上的4字节前缀。如果前缀都不同,就根本不需要去"书架"上取完整的长字符串了。对于高选择性的过滤(比如
C++17 标准库引入了 std::string_view
,它和 Velox 的 StringView
在名字和基本理念上都很相似,但它们的设计目标和内部实现有着天壤之别。
简单来说:std::string_view
是一个通用工具,而 velox::StringView
是一个为数据库性能而生的特种兵器。
它们的核心共同点是:都提供对一个已存在字符串的"非拥有视图"(non-owning view),避免了不必要的内存分配和数据拷贝。它们本身都不"拥有"字符串数据,只是一个指向数据的"引用"或"窗口"。
但区别才是关键所在,这些区别完全是为了在数据计算场景下榨干硬件性能而设计的。
特性 | velox::StringView (数据库特种兵器) | std::string_view (通用工具) |
---|---|---|
主要目标 | 在列式数据处理中实现极致的性能 | 提供一个通用的、安全的、非拥有的字符串引用 |
数据存储 | 可以内联短字符串 (SSO) | 永远不会存储字符串数据,总是指向外部 |
内置前缀 | 是,内置4字节前缀用于快速比较 | 否,没有这个概念 |
大小 | 固定16字节 | 通常是16字节(指针+长度),但非强制 |
使用场景 | 高性能查询引擎、数据转换、聚合、排序 | 日常C++编程、API接口、字符串传递 |
区别详解:为什么这些细节如此重要?
1. 内联短字符串 (Small String Optimization - SSO) - 最大的区别
- Velox 如何做:
velox::StringView
结构体本身大小是固定的16字节。如果一个字符串长度不超过12字节,它会被直接存放在这16字节的结构体内部。 - std 如何做:
std::string_view
永远只是一个指针和一个长度。数据永远在别处。 - 为什么重要?
在数据库负载中,有大量的短字符串(如国家代码 'USA', 'CHN';状态 'OK', 'FAIL';性别 'Male', 'Female')。- 缓存友好:当 Velox 处理这些短字符串时,CPU 在读取
StringView
结构体的同时,就已经把字符串数据本身读入了高速缓存。不需要再根据指针去主内存的另一个地址读取数据。这个"一步到位"的操作极大地提升了缓存命中率,是巨大的性能优势。 - 减少指针跳转:避免了一次内存解引用(pointer dereferencing),这在循环处理亿万行数据时,节省的时间非常可观。
- 缓存友好:当 Velox 处理这些短字符串时,CPU 在读取
2. 内置前缀 (Embedded Prefix)
- Velox 如何做:对于长于4字节的字符串,
velox::StringView
会把字符串的前4个字符也存入其16字节的结构体中。 - std 如何做:没有这个概念。
- 为什么重要?
这是为数据库中最常见的操作——比较和排序——量身定做的优化。- 快速失败:当需要比较两个字符串时(例如
WHERE name = 'Christopher'
),引擎可以先比较这两个StringView
的4字节前缀。如果前缀都不相等,就可以立刻断定这两个字符串不相等,根本无需再去内存中加载完整的、可能很长的字符串内容。在高选择性的过滤条件下,这个优化可以过滤掉绝大部分数据,效果惊人。
- 快速失败:当需要比较两个字符串时(例如
3. 固定大小 (Fixed Size)
- Velox 如何做:
velox::StringView
严格固定为16字节。 - std 如何做:大小依赖于实现,虽然在64位系统上通常也是16字节。
- 为什么重要?
一个包含velox::StringView
的列(Vector),在内存中就是一个连续的、由固定大小的块组成的数组。这使得内存布局非常规整,CPU可以进行高效的向量化操作(SIMD),并且可以轻松地进行乱序写入(并行计算时,不同线程可以同时写入这个数组的不同位置而互不干扰),这对于并行执行引擎至关重要。
执行层面的优化:聪明的计算策略
LazyVector (懒加载)
- 核心思想:"拖延症"是美德。
- 工作原理:当一个操作需要某一列数据时(比如从S3的Parquet文件里读取),Velox 不会立刻把整列数据都读到内存里。它只创建一个
LazyVector
作为占位符。只有当计算真正需要访问这一列的某几行时,它才去把那几行数据加载进来。 - 巨大优势:在一个查询中,如果一个
WHERE
条件过滤掉了99%的行,那么那些被过滤掉的行对应的很多列,就根本不需要从磁盘/网络读取了,极大地节省了 I/O。
DecodedVector (通用翻译器)
- 解决的问题:前面有 Flat, Dictionary, Constant 等各种编码,写算法的人难道要为每一种编码都写一套逻辑吗?太麻烦了。
- 工作原理:
DecodedVector
像一个"通用翻译器",它提供一个统一的接口,无论底层数据是哪种编码,它都能让上层算法感觉自己操作的是一个简单的 FlatVector。它把复杂性隐藏了起来。
SelectiveVector (选择向量/过滤掩码)
- 核心思想:传递"中选名单",而不是"中选的人"。
- 工作原理:当一个过滤器(如
WHERE price > 100
)执行后,它不会立即把所有符合条件的行复制到一个新地方。它会生成一个SelectiveVector
,你可以把它想象成一个"位图"或"中选名单",标记了哪些行号是符合条件的。 - 巨大优势:后续的操作(比如聚合
GROUP BY
)会接收这个"名单",并且只对名单上的行进行计算。这在多个过滤操作串联时,避免了反复地、大规模地在内存中移动数据,效率极高。Photon 用position list
(只记录行号的列表),是另一种实现方式,但思想是相通的。
Velox Function 核心解析
可以把每一个 Velox Function
想象成计算器上的一个独立按键:
+
按键 →Plus
Function√
按键 →Sqrt
Functionsin
按键 →Sin
FunctionUPPER
按键 → 字符串大写转换 Function
核心职责:对给定的输入数据,执行单一、具体的计算任务,并返回结果。
(每个Function都是专注自己领域的"专家")
工作流程解析
用户指令阶段
SQL示例:SELECT upper(name) FROM users WHERE age + 1 > 30
上层引擎处理
Presto等引擎将SQL解析为计算计划,将表达式计算任务交给Velox:- 表达式A:
upper(name)
- 表达式B:
age + 1
- 表达式A:
Velox表达式引擎工作
"指挥官"的决策过程:- 识别需要调用的Function(如
upper
和+
) - 准备输入数据(整列数据批量处理)
- 识别需要调用的Function(如
Function执行阶段
- 批量处理数据(如100万条记录)
- 返回计算结果给引擎
架构设计优势
可扩展性
- 支持自定义Function开发(如
log_normalize()
) - 无需修改核心引擎即可扩展功能
- 支持自定义Function开发(如
极致优化
每个Function都包含高级优化:- SIMD向量化计算
- 多种数据编码处理
- 空值检查快速路径
角色对比总结
维度 | 表达式计算引擎 | Velox Function |
---|---|---|
角色 | 指挥官/调度者 | 专家/执行者 |
工作重点 | 表达式解析、流程控制 | 单一计算任务执行 |
优化方向 | 整体执行效率 | 特定计算极致优化 |
扩展性 | 提供调用框架 | 支持无限功能扩展 |
Velox Function 就是构成Velox强大计算能力的"可插拔积木块"。
Velox Function: "手动挡" vs "自动挡"的函数开发
可以将 Velox 的函数框架理解为提供了两种开发模式:
"手动挡"模式: VectorFunction
开发者需要像一个经验丰富的老司机,手动处理所有细节来榨干性能:
手动内存管理:
- 代码开头需要自己判断结果向量
result
是否已经分配 - 如果没有,还要智能地尝试复用输入向量的内存 (
isVectorWritable
) - 实在不行才去申请新内存 (
ensureWritable
)
- 代码开头需要自己判断结果向量
手动处理数据编码:
- 代码中间有大量的
if/else
分支 - 用来判断输入向量是
FlatEncoding
(普通数组)还是ConstantEncoding
(常量) - 针对不同的编码组合,需要写不同的、最高效的循环代码
- 代码中间有大量的
手动循环和空值处理:
- 需要自己调用
applyToSelected
来遍历有效的行 - 并手动处理空值
- 需要自己调用
优点:能让你对内存、数据编码、执行路径进行像素级的精细控制,在特定场景下可以实现理论上的最高性能。
缺点:极其复杂、容易出错、开发效率极低。就像开手动挡赛车,大部分人都会把事情搞砸。
"自动挡"模式: Simple Function Interface
Velox 团队深知"手动挡"的痛苦,所以他们提供了 Simple Function Interface
这个"超级自动变速箱"。
开发者做什么?
你只需要提供一个最纯粹、最简单的函数核心逻辑,比如:struct MyPlusFunction {// 只需要告诉 Velox,给你两个 double,如何返回一个 doubledouble call(double a, double b) {return a + b;} };
框架做什么?
这个"自动挡"框架会自动帮你处理掉所有"手动挡"模式下的脏活累活:屏蔽编码细节:
- 你写的
call
函数接收的永远是简单的double
- 至于这个
double
是从FlatVector
来的,还是从DictionaryVector
解码来的,框架已经帮你处理好了
- 你写的
自动选择快速路径 (Fast Path):
- 框架会自动检测输入数据
- 如果发现数据里没有空值 (
null-free
),它会切换到一个没有空值检查的分支,避免了不必要的判断,从而提速 - 如果处理字符串,发现全是 ASCII 字符,它会切换到更快的 ASCII 处理路径
自动零拷贝输出:
- 当你写一个
substr
函数时,框架足够聪明 - 知道不需要分配新内存来存储子字符串
- 它会自动帮你创建高效的
StringView
来引用原始数据
- 当你写一个
自动内存管理:
- 你完全不用关心结果向量的分配和复用
结论:Simple Function Interface
是 Velox 推荐的、99% 的场景下都应该使用的开发方式。它让开发者可以专注于业务逻辑,同时免费享受到 Velox 框架底层的大量性能优化。
Expression Eval: 运筹帷幄的"智能指挥官"
如果说 Velox Function
是一个个具体的"士兵"(比如加法兵、求字符串长度兵),那么 Expression Evaluation
引擎就是调度这些士兵的"智能指挥官"。它的工作不是亲自去打仗,而是制定最高效的作战计划。
阶段一:编译 (Compilation) - 制定作战计划
在真正开始计算数据之前,指挥官会先分析收到的"作战指令"(即表达式树),并用各种方法优化它。
公共表达式消除 (CSE)
strpos(upper(a), 'FOO') > 0 OR strpos(upper(a), 'BAR') > 0
- 易懂解释:
- 指挥官发现,两个任务都需要把
a
列的士兵先"变成大写 (upper
)" - 他会下令:"
upper(a)
这个动作,所有人只做一次!把结果存起来,给后续两个任务共用。" - 这避免了重复劳动
- 指挥官发现,两个任务都需要把
常量折叠 (Constant Folding)
upper(a) > upper('Foo')
- 易懂解释:
- 指挥官看到
upper('Foo')
,发现这根本不需要等开战再算,现在就能确定结果是'FOO'
- 于是他直接把指令修改为
upper(a) > 'FOO'
- 这就是"预计算",把所有能提前算的东西都算好
- 指挥官看到
自适应重排 (Adaptive Conjunct Reordering)
AND(a, b, c, d, e)
- 易懂解释:
- 想象一个场景,你要从一大群人里找出"身高超过1米8、体重超过200斤、并且会说法语"的人
- 笨方法:按顺序检查每个人
- 聪明方法(自适应):
- 指挥官会先试着检查一下
- 他发现,"身高超过1米8"这个条件一下就能淘汰掉90%的人,而且检查身高很快
- 而"会说法语"这个条件检查起来很慢(得一个个问),而且淘汰率不高
- 决策:
- 指挥官会下令,永远先用"身高"这个条件去过滤
- 因为它"性价比"最高——用最短的时间干掉了最多的不合格者
- 这就是自适应重排的精髓:动态地找出并优先执行最高效的过滤条件
阶段二:求值 (Evaluation) - 高效执行战斗
计划制定好后,指挥官开始指挥士兵高效地执行计算。
剥离 (Peeling) - 抓重点
- 核心思想:当处理经过字典编码的数据时,只对字典里的那几个不重复的值进行计算
- 原文例子:一个颜色列有100万行,但其实只有3种颜色
['red', 'green', 'blue']
- 易懂解释:
- 如果要计算
upper(颜色)
,指挥官不会让士兵们把100万行颜色一个个转成大写 - 他会说:"你们只需要把字典里的这3个词
['red', 'green', 'blue']
变成['RED', 'GREEN', 'BLUE']
就行了 - 然后我们用原来的索引,就能瞬间得到100万行的大写结果"
- 这把百万次计算,变成了3次计算
- 如果要计算
记忆化 (Memoization) - 记住上次的结果
- 核心思想:这是"剥离"的延伸。如果指挥官发现,下一批100万行数据,用的还是
['red', 'green', 'blue']
这个字典 - 易懂解释:
- 他会直接说:"别算了,上次
upper
这几个词的结果我们已经存起来了,直接拿来用!" - 这在处理分批次但数据模式相似的数据流时,能极大地提升性能
- 他会直接说:"别算了,上次
- 核心思想:这是"剥离"的延伸。如果指挥官发现,下一批100万行数据,用的还是
Velox 向量化
向量化的核心思想是批处理,即一次性处理一大批数据,而不是一个一个地处理。这就像用一个巨大的扫描仪一次性扫完整个购物车,而不是一件一件商品地扫码。
Tight-Loop (紧凑循环): 相信"自动挡"
- 核心思想:编写最简单、最干净的循环代码,让编译器(可以想象成一个经验丰富的"车间主任")能看懂并自动进行优化。
- 易懂解释:你给车间主任一个非常简单重复的任务,比如"把这一千个螺丝都拧紧"。主任非常擅长优化这种任务,他可能会自己想出办法,比如一次拧四个(循环展开),或者用一个电动工具同时拧一排(SIMD指令)。你不需要教他怎么做,只要把任务描述得足够简单,他就能发挥到最好。这就是 Velox 的首选策略:相信编译器的自动向量化能力。
手动展开和 SIMD: "手动挡"介入
- 核心思想:在某些编译器不够聪明的关键场景(比如 HashTable 的探测),Velox 的开发者会亲自下场,编写"手动挡"代码来压榨性能。
- 手动进行循环展开。不让循环一次处理一个元素,而是一次性处理四个 (
state1
,state2
,state3
,state4
)。 - 为什么这么做?
这可以帮助 CPU 更好地进行指令流水线化,隐藏内存访问的延迟。当 CPU 在等待state1
的数据从内存中加载时,它可以同时开始处理state2
的指令,从而让硬件始终保持忙碌,最大化利用率。这就像一个熟练的厨师,在等锅烧热的时候,已经开始切下一份菜了。
HashTable: 一个自我优化的"智能档案柜"
HashTable
是数据库中实现 JOIN
和 GROUP BY
的核心部件,其性能直接决定了查询的快慢。Velox 的 HashTable
是一个被武装到牙齿的"智能档案柜"。
智能之一:自适应模式
这个档案柜会根据你存入的文件类型,自动选择最高效的整理方式:
- 少量、连续编号的文件? -> 直接用数组当架子,按编号放就行,查找最快。
- 可以用数字代表的文件? -> 把所有文件都转成一个64位的标准数字ID,按ID整理。
- 复杂、无法简化的文件? -> 才使用最通用的哈希+完整比较模式。
智能之二:为 CPU 缓存量身打造的"抽屉" (Bucket)
- 核心思想:档案柜的每一个"抽屉"(Bucket)不大不小,正好是 128 字节。这个大小是精心设计的,正好能装满 CPU 的两个缓存行(Cache Line)。
- 为什么重要?
CPU 从主内存(大仓库)取数据到缓存(办公桌)是一次昂贵的操作。这个设计确保了,当 CPU 决定要看一个抽屉时,它能一次性把整个抽屉的所有信息都拿到办公桌上,不需要来回跑仓库。
智能之三:高效的"索引卡"和"并行扫描仪"
- 抽屉里有什么?
一个抽屉里不直接放笨重的"文件"(数据行指针),而是放了16张轻便的"索引卡"。每张卡片上只有:- Tag (标签):从文件哈希值里提取的一个字节,用于快速识别。
- Pointer (指针):一个6字节的指针,告诉你完整文件在大仓库的哪个位置。
- 如何查找?(Probe 过程)
这就是最精彩的部分:- 准备查找:你要找一个文件,先算出它的
Tag
。然后用一个叫 SIMD 的"并行扫描仪",把这个Tag
复制16份,准备好。同时,告诉 CPU "我马上要看XX号抽屉了,你先准备一下"(__builtin_prefetch
)。 - 并行扫描:打开抽屉,用"并行扫描仪"一次性比较你要找的
Tag
和抽屉里16张索引卡上的所有Tag
。 - 命中后处理:如果扫描仪发现有匹配的
Tag
,再根据那张卡片上的指针,去大仓库里取出真正的文件,进行最终的精确比对。
- 准备查找:你要找一个文件,先算出它的
这个过程把绝大多数比较都限制在了 CPU 缓存内部,用最高效的 SIMD 指令完成,速度极快。
Velox HashTable 设计
一个 Key 并没有被哈希到一个字节。那个字节的 Tag
只是完整哈希值的一个"指纹"或"摘要",而不是哈希值的全部。
想象一下,HashTable
是一栋专门设计用来快速找人的高科技大楼。
- 你要找的人 (Key):比如,你要找一个叫"张三"的人。这里的"张三"就是
Key
。 - 整栋大楼 (HashTable):就是整个数据结构。
- 大楼的楼层 (Bucket):大楼有很多层,每一层就是一个
Bucket
。 - 楼层里的房间 (Slot):每一层楼都有16个房间,每个房间就是一个
Slot
,可以住一个人。
现在,我们来看看找"张三"的完整流程:
第1步:计算完整的"身份证号" (Full 64-bit Hash)
首先,我们不能直接用"张三"这个名字去找,效率太低。我们需要把他转换成一个唯一的、数字化的身份凭证。
- 我们把"张三"这个
Key
放入一个哈希函数,生成一个完整的、64位的哈希值(比如0x123456789ABCDEF0
)。 - 这个64位的哈希值,才是"张三"在这栋大楼里唯一的、完整的"身份证号"。
第2步:确定他住在哪一层楼 (Find the Bucket)
大楼有成千上万层,我们怎么知道"张三"住在哪一层?
- 我们用他的完整身份证号(那个64位的哈希值)对大楼的总楼层数取模。例如
完整身份证号 % 总楼层数
。 - 计算结果告诉我们,"张三"住在第 589 层楼。
- 关键点:决定去哪一层楼,用的是完整的哈希值,而不是那个1字节的
Tag
。
第3步:提取"指纹"用于快速扫描 (Create the Tag)
现在我们来到了第 589 层楼。这一层有16个房间,我们不想一个一个敲门去问:"你是张三吗?" 这太慢了。我们需要一种更快的扫描方法。
- 于是,我们从"张三"的完整身份证号(
0x123456789ABCDEF0
)中,只提取最高位的8个比特(即1个字节)作为他的"指纹" (Tag)。比如,这个指纹是0x12
。 - 这栋大楼的设计是,每个房间门口的电子屏上,都显示着住户的"指纹"。
第4步:并行扫描本楼层的所有"指纹" (SIMD Tag Comparison)
现在,我们站在第 589 层的走廊里,手里有"张三"的指纹 0x12
。
- 我们拿出"并行扫描仪" (SIMD 指令),一次性扫描这一层楼16个房间门口的所有指纹。
- 这个扫描仪会立刻告诉我们:"第 3、7、15 号房间门口的指纹也是
0x12
!"
这就是那个1字节 Tag
的真正作用:它是一个过滤器,让我们可以在一瞬间排除掉绝大多数不相干的房间(比如门口指纹是 0xAA
, 0xBB
的房间),而不需要去打扰他们。
第5步:最终确认 (Full Key Comparison)
扫描仪告诉我们有3个房间的指纹匹配。但这不意味着他们都是"张三",因为不同的身份证号可能会有相同的指纹(这叫"哈希冲突")。
- 现在,我们才需要去依次敲开这3个房间的门。
- 我们不会直接问"你是张三吗?",因为比较完整的名字(尤其是长字符串)很慢。
- 房间里的人会先出示他的完整身份证号(完整的64位哈希值)。我们先比较身份证号。
- 如果连身份证号都一样(极小概率事件),我们才会拿出"张三"的原始档案(原始
Key
),进行最终的、最精确的身份比对。
所以:
1字节的 Tag
不是哈希的全部,它只是一个从完整哈希中提取出来的、用于在CPU缓存内进行超高速过滤的"指纹"。 真正的定位和最终确认,依赖的是完整的哈希值和原始的 Key。这个设计的全部目的,就是为了尽可能地避免昂贵的内存访问和完整的 Key
比较。
Velox Pipeline: 高效的"批处理流水线"
这是数据在不同算子(过滤、聚合、连接)之间流动的模式。
- 传统模型 (Iterator/Pull):像一个"点餐式"的流水线。最后一个工序(打包)的工人,对前一个工序(贴标签)的工人说:"给我一个产品"。贴标签的再对前一个(上色)说:"给我一个产品"... 这种方式充满了"请求-等待",效率低下。
- Velox 模型 (Push-based):像一个真正的"传送带式"流水线。第一个工序(切割)的工人,完成一大批产品后,直接把整批产品推到传送带上,送到下一个工序。数据以"批"为单位,顺畅地在流水线上传递。
优势:
- 开销小:大大减少了工序之间的"沟通成本"(函数调用开销)。
- CPU 亲和性好:每个工人可以专注于处理自己面前的一大批产品,他的工具和零件(数据和指令)可以一直放在手边(CPU 缓存),不用频繁地放下和拿起。
- 调度灵活:如果某个工序特别慢(比如等待油漆晾干,类似于等待磁盘I/O),工厂调度员(Velox 调度器)可以让他先"休息",把宝贵的"工作台"(CPU 线程)让给其他流水线使用。
内存管理:一个公平且强大的"城市供水系统"
Velox 的内存管理系统确保在有限的内存资源下,多个查询可以公平、稳定地运行,而不会导致系统崩溃。
- Memory Pool (每户的水表):每个查询任务就像一个"家庭",有自己独立的"水表树",可以精确追踪到每个算子(家庭成员)的用水量。
- Arbitrator (城市水务局局长):当整个城市的"总水库"(机器总内存)快要见底时,"局长"就登场了。它的职责是决定让哪个"家庭"暂时减少用水。
- Spilling (把水存到自家水缸):当"局长"决定让查询A减少用水时,查询A就会把一些不那么紧急的数据(比如
HashTable
的一部分)从内存(自来水管道)中写入到磁盘(自家的储水缸),这个过程就叫Spilling
。这样就把宝贵的公共内存资源释放出来,给更需要的查询使用。 - Allocator (标准化的管道配件厂):为了避免内存碎片化(各种零碎的、无法使用的小内存块),Velox 使用了
SizeClass
机制。这就像一个管道配件厂,它不生产任意尺寸的管道,只生产几种标准尺寸(4K, 8K, 1M...)。通过mmap
这种虚拟内存技术,它假装每种尺寸的管道库存都是无限的,但只在实际安装使用时才消耗真正的"钢材"(物理内存)。这既避免了浪费,又简化了管理。
Gluten + Velox: 给 Spark 换上"F1赛车引擎"
- Gluten 是什么? 它是一个"转接器"项目。
- 做什么? Spark 原本的计算引擎是基于 JVM (Java) 的,虽然功能强大,但在性能上不如 C++。Gluten 的作用就是把 Spark 的 Java 引擎替换成 Velox 这个用 C++ 编写的、性能怪兽级的引擎。
- 如何工作? Gluten 就像一个精密的"变速箱",它把 Spark 的"驾驶指令"(查询计划)翻译成 Velox 能听懂的语言,然后让 Velox 这个 F1 引擎去真正地执行计算,最后再把结果传回给 Spark。
GlutenWholeStageColumnarRDD
就是这个转接过程中的一个关键部件,它负责把整个计算任务块打包好,一次性交给 Velox 处理。