一个看起来并不复杂的查询,能把整个集群拖到超时,这是我早期在AIoT数据库项目测试里印象最深的事。客户现场的SQL大概长这样:select device_id, avg(value) from sensor_data where ts between '2025-01-01 00:00:00' and '2025-01-01 01:00:00' group by device_id, time_bucket('1m', ts)。数据量其实并没有夸张到离谱,也就几十亿个点,但查询就是慢,慢到业务方宁可把原始数据导进离线数仓重新算一遍,也不愿意在线跑。
后面我花了不少时间梳理 KaiwuDB 的分布式执行引擎演进过程,才发现这类慢并不是单纯靠加机器就能解决的。真正卡脖子的点经常藏在执行引擎自身的结构里:数据在节点之间怎么流转、乱序数据怎么跟历史数据合并、算子是逐行处理还是按批处理、聚合结果在分布式环境下能不能正确收敛。从最早的单机执行框架,到分布式调度与两阶段聚合,再到乱序感知、向量化执行,以及现在面向多模和 AIoT 场景的持续改造,这条演进路线才是把类似问题真正压下去的根本原因。
这篇文章我不会只讲 KaiwuDB 做了什么,更想把每个关键演进背后的“为什么”拆开来说清楚。包括为什么乱序数据会成为查询引擎的拦路虎,为什么向量化执行能带来翻倍甚至更多的性能提升,以及“多模”对执行引擎到底意味着什么。如果你也在做时序数据库、分布式 OLAP 或者大数据查询引擎相关的工作,这篇应该能给你一些能直接落地的参考。
1. 场景逼出来的演进——AIoT查询到底给执行引擎出了哪些难题
先想一个问题:为什么 AIoT 场景下的数据库,执行引擎不能直接照搬互联网公司那套分布式 OLAP 方案?很多人第一反应是“数据量大”,但 AIoT 的真正难点并不是数据量大这个单一维度,而是查询模式和数据特征组合在一起之后,对执行引擎形成了很多隐性的约束。
1.1 一条降采样 SQL,在分布式系统里经历了什么
我们先拆一条典型的时序降采样查询。假设有几千台风机,每台风机每隔几秒上报一条数据,落在表里就是一行带有时间戳、设备编号、多个 field 字段的记录。这个表在存储层通常会按照时间分区,每台设备的数据又按照时间戳排列。查询“最近 5 分钟每台风机的平均有功功率”时,执行引擎大致要做以下几件事:
第一步,把 SQL 解析成语法树,然后做逻辑计划和物理计划。这里的核心不是能不能把这个 SQL 跑通,而是怎么把计算尽量压到靠近数据的位置。分布式数据库里,如果协调节点只是简单地把全量数据扫上来再聚合,那海量数据在网络上搬运就会直接打垮集群。
第二步,时间范围裁剪。时序查询基本都会带时间条件,执行引擎要通过分区元数据跳过那些不包含目标时间的数据分片。这个动作做得好不好,直接影响扫描量。
第三步,按照分区或分片做本地预聚合。比如按设备编号和时间窗口计算局部的 avg 和 count,然后只把聚合后的中间结果发给协调节点。
第四步,协调节点做全局合并,把各个分片的部分聚合结果再聚合成最终结果。
这个流程看起来并不复杂,但有几个细节会让执行引擎的设计难度陡增。比如时序聚合函数里常见的 first 和 last,它们和 avg、sum 不一样,分布式场景下必须先保证每个分片内按时间排序,形成部分结果之后再按照正确的时间语义做合并。如果底层存储的数据乱序没处理好,这种“有序聚合”的结果就可能是错的。
再比如时间桶。time_bucket('1m', ts) 这种表达式看上去人畜无害,但在执行引擎内部,它决定了很多算子能否共享同一个分组桶。旧引擎如果对每一行数据单独计算时间桶,再一条一条塞进哈希表,性能会非常难看。这个问题一直到向量化重构之后才被妥善解决。
1.2 AIoT数据模型对执行引擎的三个特殊要求
抛开具体 SQL 不说,从执行引擎的角度看,AIoT 场景至少有三个和传统 OLAP 明显不同的地方。
第一个是“时间线规模”和“单时间线密度”高度不均衡。有的设备一年上报 10 亿个点,有的设备可能每天只有几条数据。时间线数量可能达到百万级别,但每一条时间线的数据分布很不均匀。执行引擎在并行度划分时,如果只是简单按行数分片,很容易出现某个分片重、某个分片轻的倾斜问题。
第二个是数据写入天然乱序。设备离线缓存回补、网络重传、手机断网后重新上报、业务侧手工修正历史数据,都会导致时间戳早于当前水位的数据在后面才到达。大多数分布式存储引擎为了写入吞吐会采用追加写,可追加写和“按时间有序查询”之间存在天然矛盾。乱序数据如果没有被专门处理,查询引擎就会在扫描阶段遇到大量跨级合并,读放大非常严重。
第三个是查询类型高度混合。时序数据库不是只能做降采样聚合,生产环境里大量查询是很轻量的点查,比如“查某台设备最近的状态值”;也有复杂的大跨度统计分析,比如“算过去一年所有设备每小时的 P95”。执行引擎必须同时照顾两种极端,不能为了点查的极致优化牺牲分析查询,也不能为了分析性能把点查链路搞得过于笨重。
理解这几个特殊要求,才能明白 KaiwuDB 的执行引擎为什么一步步演化出分布式调度、乱序合并、向量化执行、多模融合这些能力。下面我按演进阶段来展开。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 引擎演进的起点——分布式执行框架里先定下来的那几件事
KaiwuDB 并不是从零开始直接做一个“天顶星”级别的引擎。和很多时序数据库团队一样,早期主要解决的是能不能把原来跑在单机上的时序能力,平滑地扩展到多节点。当时我调研过一些开源时序数据库的架构,最大的感受是:很多项目的存储层已经做得挺扎实,但 SQL 执行层和分布式的协同调度往往比较薄,如果只是简单做一层代理,复杂查询就会变成“把所有数据拉到代理节点再算”。
2.1 SQL到分布式计划:哪些计算能下推,哪些必须上拉
分布式执行引擎的第一步,是要搞清楚 SQL 算子中哪些可以推到数据节点执行,哪些必须在协调节点汇总后再处理。
理论上最简单的划分依据是算子的“交换属性”:
- 过滤算子基本都可以下推,而且越靠近存储层越好。
- 投影算子只要不包含依赖多行数据的表达式,也能下推。
- 聚合算子适合做两阶段:先在各数据节点做本地聚合,再把聚合中间态上拉到协调节点做合并。注意,这里的中间态不只是数值,也可能是一组状态,比如
percentile需要维护的直方图结构。 - 排序和 join 是最难下推的。跨节点排序必然涉及到全局归并,join 则可能需要在执行计划里插入重分区算子。
KaiwuDB 在早期分布式版本里选择了一条比较务实的路线:把时间范围裁剪和标签过滤尽量压到存储扫描层,把本地聚合作为默认的一阶段执行方式,只有遇到 order by 或者跨节点 join 时才引入代价较高的重分布算子。
实际跑测试的时候我发现,这套思路能不能生效,很依赖分区粒度。如果表按照时间先分成大区,再按哈希分成多个分片,执行引擎才能真正做到按分片并行扫描。假如分区粒度太粗,比如一整年的数据都在同一个物理分片里,那么不管执行计划写得多漂亮,物理上都没有并行度可言。
2.2 拉取模型还是推送模型:一个影响整个引擎形态的选择
在执行引擎的 Pipeline 模型上,很多团队会纠结该用“拉取模型(Pull)”还是“推送模型(Push)”。我在看 KaiwuDB 演进资料时明显感觉到,它们早期走的是更稳的拉取路线,而不是一步到位上推送模型。
拉取模型的典型代表是火山模型(Volcano):上层算子调用下层算子的 Next() 接口,一层一层往下拉数据,再由上层算子做处理后返回给更上层。这种模型的优点是实现简单、内存可控、方便实现 LIMIT 这类短路操作;缺点是每一行数据都要经过多次虚函数调用,CPU 开销很大,而且不适合向量化。
推送模型则是让上游算子主动把数据推给下游,更贴近现代数据仓库的 pipeline 思路,但对背压控制、内存管理要求很高。分布式场景下,如果某个节点处理慢,推送模型很容易把整个流水线堵死,处理不好会造成内存暴涨。
从架构演进的角度看,先基于拉取模型跑通分布式能力,让上层 SQL 语义、分区裁剪、两阶段聚合这些功能稳定下来,是一个比较明智的选择。也正是因为早期采用了这种相对规整的迭代器抽象,后面在做向量化重构时,才有机会把“行迭代器”替换成“批量列迭代器”,而不需要把整个 SQL 层推倒重来。
不过拉取模型也有代价。我记得自己第一次在一个跨节点的 group by 查询上看到执行计划时有点困惑,明明数据量不大,为什么还要在协调节点建一张很大的哈希表。后来才意识到,分布式聚合如果没有使用可合并的聚合中间态,就只能把所有数据组织成一张大哈希表,这种写法本质上没有把分布式的好处用起来,反而让协调节点成了瓶颈。后面版本里对聚合状态做了更多优化,才逐渐缓解掉这个问题。
3. 乱序数据这个分水岭:写入侧的“脏乱差”让查询引擎交了学费
执行引擎做得再快,如果底层给的数据是不一致的,做出来的结果也没有意义。在时序场景里,乱序数据就是那个最容易让上层计算“失真”的变量。KaiwuDB 的演进过程中,乱序数据的处理是一个很明显的分水岭。我甚至觉得,能否把乱序数据处理好,是判断一个时序数据库到底“懂不懂时序”的重要标准。
3.1 乱序数据为什么会让查询性能恶化到不可控
很多做关系型数据库出身的人,对数据乱序的感知并不强。关系表里的记录没有天然顺序,数据先来后到并不影响查询结果。但时序数据不一样,它的核心语义就是“时间序列”——同一个设备、同一批标签下,数据必须按时间戳排好,first、last、rate、delta 这类计算才有意义。
乱序数据进入系统之后,通常会发生这样的问题。存储层为了保证查询时能按时间顺序扫描,会把写入的数据切分成大小固定的有序数据块。正常情况下可以直接把新数据追加到最新的块里。但如果来了一条时间戳很早的数据,它就不能简单地追加到块尾,否则块内部的有序性就被破坏了。
最简单的做法是把乱序数据写到单独的乱序区域,等积累到一定量后,再和原来有序的数据块做一次合并压缩(Compaction)。从存储角度看,这个方案中等简单,但从查询引擎角度看,事情就变麻烦了:查询某个时间范围时,老数据可能同时存在于“有序主存储”和“乱序缓冲”两个位置。查询引擎必须把两边的数据都扫描出来,然后把同一条时间线上的数据按时间戳合并起来,才能返回正确结果。
如果乱序数据长期不整理,乱序区域的数据块会越来越多。查询一个时间窗口时,执行引擎生成的迭代器数量会急剧膨胀,本来一遍扫描就能完成的事情,最后变成几十个迭代器的归并。结果就是 CPU 消耗暴增,查询延迟变得很不稳定。
3.2 对齐窗口、乱序缓冲与查询侧的时间线合并机制
KaiwuDB 在乱序数据上的解法,我把它概括为“写入侧分窗口缓冲,查询侧时间线合并”。
写入侧,不是把每一条乱序数据都立刻引发一次文件合并,而是先把乱序数据放进一个独立的缓冲区。缓冲区按照时间窗口切分,这里会有类似“对齐窗口(Aligned Window)”和“水印(Watermark)”的概念。系统维护一个时间水位线:晚于水位线的数据可以被安全地写入当前打开的时间窗口;早于水位线的数据则说明对应的窗口已经关闭,需要进入乱序补偿路径。
窗口是否关闭是一个非常关键的设计决策。假如窗口关得太早,后面再来一条属于该窗口的数据就无法被正常处理;假如窗口关得太晚,数据在内存缓冲区里停留过久,又会影响查询新鲜度和写入吞吐。实际落地时,通常会结合写入延迟的统计分布来定。如果网络环境里 99.9% 的数据延迟都在 10 秒以内,那窗口等待时间设置在几十秒量级是相对稳妥的。
查询侧,执行引擎需要对主存储扫描路径和乱序缓冲扫描路径做一层“时间线合并”封装。从执行引擎的视角看,一次扫描不是简单地从一个迭代器里取数据,而是同时打开多个有序迭代器,然后按照时间戳和写入版本进行归并。这个归并逻辑不是通用的多路归并那么简单,还要处理相同时间戳的多个版本:到底取哪一条,取决于业务定义,可能是最新写入覆盖旧写入,也可能是根据某个字段做叠加。
这块我当时测试时踩过一个很典型的坑。如果合并层只是简单地把两条路径的数据拼接在一起,而不是严格按时间戳合并,first 或 last 这类函数得到的中间结果就会随着乱序缓冲刷盘而发生变化。也就是说,同一句查询在不同时间执行,返回结果可能不同。这不是随机抖动,而是因为查询层没有把时间线语义贯彻到底。后续版本中,KaiwuDB 把乱序合并下沉到扫描算子层,让分组聚合算子在拿到数据之前,就已经得到正确的单条时间线有序数据,这类问题才被根治。
3.3 乱序处理对查询执行器最直接的启发
乱序数据的问题看多了之后,我越来越体会到,执行引擎不能存储层完全分离。很多团队喜欢把接口抽象到“底层是排好序的”这个理想假设上,可真实世界里没有这个前提。乱序数据的占比一旦超过某个阈值,这种理想假设就会变成性能黑洞。
更合理的做法是让执行引擎具备“乱序感知能力”。具体来说,扫描算子需要知道当前扫描的数据是否存在乱序缓冲,必要时自动切换到多路合并模式。这种感知不是靠猜测,而是由存储引擎在元数据中标注清楚:哪些时间分区已经完全合并,哪些还没有。执行引擎拿到这些信息后,可以选择最优的扫描策略。
这个优化不需要改变 SQL 层语义,但对执行引擎内部算子的状态管理提出了更高要求。因为乱序合并可能让同一个分组的数据不是一次性到达,而是分批、乱序地到达。聚合算子如果固守“来一条算一条、算完就丢状态”的模式,就很难处理这种场景。必须把中间状态保留下来,并且在所有输入迭代器都结束之后才能输出最终结果。这也是为什么后来的向量化执行引擎在重构时,不能简单地把旧的聚合逻辑搬过去,而是要重写聚合状态管理。
4. 推倒重来的向量化执行引擎:从“一次一行”到“一次一批”
如果说乱序数据的演进解决的是“结果对不对”的问题,那向量化执行引擎的演进,解决的就是“快不快”的问题。这一阶段 KaiwuDB 基本是把执行引擎从火山模型整体搬到了批量向量模型上。公开资料里提到,他们采用列式存储内核,启用全新的执行器引擎,并且通过 CGO 优化了跨语言接口调用。这些动作是一套组合拳,不是孤立的重构。我分开说。
4.1 旧引擎的瓶颈为什么会卡在 CPU 而不是磁盘
很多人在优化分析查询时,第一反应是“磁盘 IO 太慢”,于是疯狂加缓存。但在列式存储已经普及的今天,扫描一个数据块的数据量往往并没有想象中那么大,真正拖后腿的反而是 CPU 的处理方式。
用火山模型执行一条 select avg(value) from table where ts between ... 时,每读取一行数据,都要做这样一系列动作:调用下层迭代器的 Next(),检查返回值类型,判断当前行是否满足过滤条件,如果满足则进入聚合逻辑,更新累加器。问题就出在这个“逐行”上。
每一次迭代器的调用都是一次虚函数调用;每判断一行数据的类型,都可能引发分支预测失败;每处理一条数据后,累加状态在内存中都是随机访问的,缓存命中率很低。当数据量从百万涨到亿级别时,这些 CPU 层面的开销会被放大到比磁盘 IO 更严重的程度。
这里可以用一个搬运的场景来类比。假设有一条流水线,工人需要把货物从入口搬到出口。火山模型的做法是:每来一件货物,就派一名工人全程跟着它走完所有工序,货物之间完全串行,每一道工序都要工人停下来确认“这件货接下来该去哪”。向量化模型的做法则是:先让一整批货物进入流水线,每道工序一次处理完一整排货物,工人不需要反复确认“下一件货是什么”,只需要批量操作同一类货物。CPU 的向量指令(SIMD)就是这个流水线上的“批量传送带”,可以让过滤、累加这种简单操作同时处理多份数据。
4.2 批量化算子:数据从“行”变成“列块”
KaiwuDB 在做向量化执行引擎重构时,核心动作是把算子的输入输出从“一行数据”变为“一批数据”。一批数据通常用一个列块(Column Block / Vector 数组)来表示,例如每次处理 1024 行。在这个结构下,执行引擎的数据流就不再是“行”的流动,而是“列块”的流动。
这个改变带来的直接好处有三个。第一,迭代器的调用次数从每行一次降低到每批一次,虚函数调用开销被分摊。第二,同一列的连续数据在内存中通常是紧凑存放的,扫描时能更充分地利用 CPU 缓存。第三,很多计算可以改成循环向量化,用 SIMD 指令同时对 8 个或 16 个数据做相同的运算。
但是这里也有一个非常重要的工程细节:并不是所有算子都能天然向量化。过滤算子可以向量化地批量判断条件,并生成一个选择向量;整型累加可以向量化地并行加;但像字符串匹配、正则表达式这类算子,向量化的收益就很不稳定。更麻烦的是,如果执行计划中出现了一个无法向量化的函数,整条 pipeline 都可能被“打断”回到逐行模式。
所以向量化执行引擎实际落地时,不只是换一个数据表示,还需要重新设计表达式求值框架。要尽可能把表达式构建成一组可以批量执行的运算节点,每个节点在编译期就确定好数据类型,执行时不再频繁做类型判断。这基本上等于把 SQL 执行层重写了一遍。
4.3 列式存储与查询执行链路的重写配合
向量化执行要发挥最大价值,底层存储必须配合。KaiwuDB 的时序引擎采用了列式存储内核,这个选择本身就是为了减少 IO 放大。设想一个宽表有几十个 field 字段,但查询只关心其中两个 field 的平均值。如果是行式存储,哪怕条件过滤后只剩两列,也必须把整行的所有列从磁盘读出来。列式存储则可以只读取需要的列,大幅减少磁盘 IO 和内存占用。
不过列式存储也给查询下推带来新的要求。以前面向行式存储,扫描算子一次拿一行,过滤后再投影出一行;列式存储下,扫描算子天然拿到的就是“某一列的连续一批值”,引擎需要把过滤条件的批量判断和“只读取命中列”的动作结合起来。具体做法通常是先读取条件列做过滤,得到一批满足条件的位置行号,再根据这些位置从目标列中取出对应的值。这个过程也适合做并行化,因为各列之间互不依赖,可以流水线推进。
从公开的信息看,KaiwuDB 在查询引擎里不仅实现了向量化执行,还做了基于 CGO 的接口调用加速。这一块可能很多应用层开发者不太熟悉,但对数据库这种底层系统很关键。上层语言(比如 Go/Rust 驱动层)和底层 C++ 存储引擎之间如果每次调用都要做一次成本很高的跨语言切换,大批量查询时的损耗会被放大。通过 CGO 精简调用路径,减少不必要的序列化拷贝,能让上层 SQL 计划、下层列式扫描之间的衔接更顺畅。这里想强调一个通用经验:向量化要解决的不只是单节点的算子性能,还包括所有跨模块边界的数据搬运开销。
4.4 一组值得记录的优化结果
KaiwuDB 官方公开过一次测试结果,在 17 项 SQL 功能测试中,有 12 项经过新执行引擎改写后的性能提升了 39% 以上。如果我没记错,部分复杂的过滤聚合查询提升幅度更明显,直接翻倍甚至更高的也有。
这个结果的分布其实很符合向量化引擎的典型表现。那些提升特别明显的,基本都是单表扫描、过滤、聚合、降采样这类时序高频查询,因为它们能完全跑在批量计算的“快车道”上。提升幅度相对有限的,往往是那些混合了复杂表达式、多表 join 或需要逐行回落的查询。这也在提醒后来者,不要期待向量化引擎能平均地拯救所有查询,一定要根据实际业务负载做针对性评估。
在复现类似优化时,我自己测下来觉得最有效的测试方式不是简单跑一遍总耗时,而是把查询拆成几个子阶段分别计时。重点看扫描阶段是否从逐行变成了批量读取,过滤阶段是否消除了大量类型分支,聚合阶段是否真的在频繁更新一个集中的哈希表。只要看这几个阶段的变化,就能快速判断向量化改造是否真正生效。
5. 多模与边缘场景:执行引擎的边界正在被重新定义
执行引擎做到向量化,并不是演进的终点。KaiwuDB 现在的定位已经不只是一个分布式时序数据库,而是朝着多模、分布式、AI 原生和云边端协同的方向演进。这里面的“多模”不是简单地把几种数据库拼在一起,而是让执行引擎在一个统一框架里处理关系数据、时序数据甚至 AI 推理请求。这个阶段的演进,很多问题还没有标准答案,但在做架构时如果能提前想清楚,会少走不少弯路。
5.1 多模数据库的一体化SQL,执行引擎怎么接
多模数据库的卖点是“用一种 SQL 入口操作多种数据模型”。举个例子,工业互联网场景里,设备状态数据可能存储在时序模型里,设备台账信息存储在关系模型里,AI 模型训练出来的故障标签可能又存储在向量模型里。业务分析时经常要一边查询设备最近的时序指标,一边关联设备档案,再结合模型推理结果判断设备状态。
这种查询如果分到三套数据库里做,就需要业务层自己维护跨库数据一致性和复杂的应用编排。多模数据库想解决这个问题,就要求执行引擎在生成执行计划时,能够感知不同数据模型之间的差异。比如一个表属于关系模型,底层扫描用的是传统的行存/列存结构;另一个表属于时序模型,底层天然带时间线和标签索引;同一条 SQL 里,它们的扫描路径是不同的。
对执行引擎架构影响最大的,是“内部行格式”的统一问题。关系表的一行和时序表的一条时间线记录,在元数据层面差别很大。关系表字段数量固定、类型相对规整;时序表则高度依赖标签维度,同一张表里可能还有多个 field 字段,甚至 field 集合还会随设备类型变化。如果执行引擎的算子在物理上锁死某一种数据布局,后续做多模融合就会非常痛苦。
比较好的演进方向是让执行引擎尽量抽象出统一的“算子接口”,但在物理算子内部保留对不同模型的特化路径。比如扫描算子对外都是产出一批列块,但内部可以有不同的实现:关系扫描走普通 ORC/Parquet 风格路径,时序扫描走“时间分区裁剪 + 标签索引 + 时间线合并”的路径。这样上层 SQL 优化规则不需要感知底层差异,多模结合的好处才能真正落地。
5.2 云边端协同下的查询下推和轻量化挑战
执行引擎的第二条新边界来自“云边端协同”。AIoT 数据不可能全部集中到中心云处理,很多判断必须在靠近设备的地方实时完成。比如一台风机异常预警,需要边端设备在一秒内基于最近几分钟的本地数据算出趋势,而不是上传到云端等结果再下发。
这种场景会把执行引擎逼向两个方向。一是支持“部分执行计划下推”:中心云生成一条完整的查询计划后,把能下沉到边缘节点的算子拆分出来,让边缘节点只执行本地扫描和本地预聚合,再把很小的聚合结果回传。二是执行引擎要变得更轻量:边缘节点的内存可能只有几百 MB,边端如果部署一套完整执行引擎,光是模块加载就可能把内存吃光。
做边端查询引擎时,我最深的体会是功能裁剪并不难,难的是保证“语义一致性”。边缘节点上执行的 avg 必须和中心节点执行的 avg 遵循完全相同的合并规则,否则边缘算完部分结果后,云端做二阶段合并时结果就可能对不上。这要求执行引擎的聚合框架从设计上就具备“部分聚合状态可序列化、可传输、可合并”的能力。这也是为什么向量化重构时,负责人会反复强调聚合算子状态管理要多花力气——它不只是单机优化的事,还直接决定分布式以及边端协同能不能做好。
还有一个容易被忽视的问题是可观测性。执行计划一旦被切分到云、边、端不同节点,每个节点处理的数据量、耗时、错误信息都必须能被统一追踪和展示。否则一条慢查询出现时,你根本分不清瓶颈是在边缘节点的扫描上、在数据传输链路上、还是在中心云的合并阶段。后续做执行引擎的人,如果一开始不把 trace 信息设计进算子接口,后面想补非常麻烦。
KaiwuDB 现在的目录里已经能看到这些方向的影子:多模一体化 SQL 解析与算子的融合、向量化执行引擎的继续迭代、和 AI 能力结合的内置算法算子。从一个技术观察者的角度看,这是一个典型的“场景驱动演进”案例:AIoT 逼着它在存储层解决乱序,在计算层解决向量化,在多模融合和边端部署上重塑执行引擎的边界。
最后一点个人的体会
回看这条演进之路,我最大的感受是:执行引擎的每一步改造,都必须有非常具体的业务痛点作为支撑,而不是为了追新架构而重构。乱序数据没有成为瓶颈时,你可能觉得“额外维护水印和乱序缓冲”是多余的;扫描场景没有达到一定数据量时,你可能觉得“向量化执行”收益不明显。但这些能力一旦缺失,数据规模和业务复杂度的提升会毫不留情地把短板暴露出来。
如果你现在也要做类似的分布式存储或查询引擎演进,我会建议先把观测体系和 benchmark 基线搭好。不要只看某个大查询的端到端时间,最好把一个查询拆成“扫描、过滤、聚合、传输、合并”几个阶段分别记录耗时,然后再去判断优化到底落在哪一层。另一个建议是,在做任何算子重构时,都要把“中间状态可合并”作为一等公民来设计,否则将来要做分布式或边缘计算时,会发现自己被早先的设计卡住。
这次梳理对我自己的帮助也很大。以前看数据库源码时,总是容易一头扎进某个算子或者某个存储结构的细节里,忽略了上层执行计划、底层数据分布、乱序处理策略、多模融合这些环节其实是互相咬合的整体。KaiwuDB 的演进过程,刚好提供了一个很完整的样本,让我对这些咬合关系有了更具体的认知。如果你也在研究分布式执行引擎,希望这篇写下来的演进复盘,能帮你少踩几个我踩过的坑。
