KWDB 跨模查询的真正价值,远不止“能查”

在工业物联网、设备运维、能源监测这类场景里,业务数据往往天然分成两类:一类是设备、型号、站点、组织、资产这类关系数据,另一类是电流、电压、温度、功率、告警这类时序数据。真正有价值的问题,往往都要把这两类数据放到一条 SQL 里一起回答。

比如想查“某型号设备最近 1 小时的电流变化”,既要先从关系表里筛出目标设备,又要到时序表里拿到对应采样数据;如果再叠加最新值、聚合、告警筛选、维表关联,查询复杂度马上就会上来。这个时候,跨模查询的意义就不是“能不能把两张表连起来”,而是能不能让这类分析既写得出来,也跑得稳定。

KWDB 的跨模查询,亮点不只是把关系数据和时序数据拼到一起,而是已经开始朝着“可实用、可优化、可扩展”的方向走。 从现有能力来看,它已经不只是一个概念功能,而是一项值得重点展开讲的核心能力。

在这里插入图片描述

一、跨模查询真正解决的是什么问题

单看语法,跨模查询好像只是多了一次 Join;但放到真实业务里,它解决的是“业务主数据”和“实时测点数据”长期割裂的问题。

  • 关系侧保存的是设备、型号、站点、组织、资产等业务信息
  • 时序侧保存的是电流、电压、温度、功率、状态等连续采样数据
  • 真正的分析往往同时依赖两边,例如“某类设备最近 24 小时的异常波动”“某站点下高负载设备的最新状态”“某批设备的最新指标与基础属性联查”

像下面这种查询,本质上就是很多行业场景里最常见的一类需求:

SELECT d.deviceID, dm.ModelName, ms.current
FROM rdb.Device AS d
INNER JOIN rdb.DeviceModel AS dm ON d.modelID = dm.modelID
INNER JOIN tsdb.MeterSuper AS ms ON d.deviceID = ms.deviceID;

看起来只有一条 SQL,但真正难的地方,不是“语法上能不能执行”,而是下面这些事怎么选:

  • 先扫 Device/DeviceModel,还是先扫 MeterSuper
  • 关系侧过滤条件能不能尽早传到时序侧
  • 时序侧能不能先做一部分聚合或裁剪,再回到关系引擎
  • Join 顺序和 Join 方法到底该不该完全交给 CBO

换句话说,跨模查询真正重要的地方,从来不是“把两类数据连起来”,而是把两类数据在一条查询链路里用好。

二、从现有能力看,KWDB 的跨模查询已经有三块不错的基础

1. 查询能力不是点到为止,而是覆盖了不少真实场景

从现有样例可以看出,KWDB 的跨模查询并不是只演示最基础的等值关联,而是已经覆盖了比较完整的一组查询形态:

  • INNER JOINLEFT/RIGHT/FULL JOIN
  • 相关子查询
  • 聚合分析
  • 最新值查询
  • 更复杂的嵌套 SQL

这意味着它要解决的不是“给出一个能跑通的 demo”,而是已经开始面向真实业务里的组合场景。

2. 优化思路已经不只是“查出来”,而是开始追求“查得更划算”

跨模查询最怕的,不是 SQL 写不出来,而是执行时把时序侧扫得太重。现有实现里已经能看到一些很实用的优化思路,比如:

  • lastrow 场景的特化优化
  • 只涉及 tag 列时,把扫描改成 tag-only scan
  • Project / Select / TSScan 组合下的扫描类型改写

这类优化的价值很直接:很多跨模查询最后慢,不是慢在 Join 本身,而是慢在时序表扫太多。能把“全时序数据扫描”改成“只扫 tag 表”或者“走特化扫描”,收益往往是立竿见影的。

3. 已经有了面向复杂查询的优化基础

从现有实现还能看出,KWDB 并不是把跨模查询简单塞进普通关系查询流程里,而是已经开始从查询识别、统计信息、计划模式这些层面做区分。时序表相关统计里,至少已经包含:

  • RowCount
  • PTagCount
  • SortDistribution

其中 PTagCount 很重要,因为跨模查询很多时候不是单纯看“多少行”,而是看“命中了多少个主标签实体”。

SortDistribution 也很关键。它在过滤之后会继续更新有序/乱序相关统计,这说明时序场景下被考虑的已经不只是扫描行数,数据顺序本身也会影响代价判断。

另外,还能看到 InsideOutOutsideIn 这类计划模式。这至少说明当前实现已经不是“一律按普通 Join 处理”,而是知道跨模查询需要单独挑执行路线。

这类基础工作的意义很大,因为复杂跨模查询能不能跑得稳,很多时候靠的不是某一条规则,而是优化器能不能意识到:这不是一条普通查询,而是一条关系数据和时序数据共同参与的查询。

也正因为这些基础已经有了,后面再谈哪些能力最值得继续打磨,才更有意义。

三、如果继续把这项能力做强,最值得投入的是这 6 个点

下面这 6 点,是当前最影响跨模查询性能上限的几个方向。

1. Join 能力边界现在还是偏保守

kwbase/pkg/sql/opt/memo/memo.go 里,checkJoincheckOtherJoincheckLookupJoin 这几段代码的核心思路比较一致:

  • 先判断子树能不能在 TS 引擎里执行
  • 很多 Join 本身不尝试放到 TS 引擎里做
  • 更多是通过 Synchronizer 把结果拉回关系引擎继续处理

这套做法的好处是稳。先把正确性兜住,功能也能落地。

但代价也很明显:一旦查询跨到了 Join 这一步,当前实现整体上更像“两个引擎拼一下”,而不是“两个引擎一起优化”。

这里最直接的损失有两个:

  • 关系侧的过滤条件不容易尽早变成时序侧的实体裁剪
  • 时序侧本来能先做掉的一部分工作,可能因为过早切回关系引擎而丢掉

这里不建议一上来就追求“全 Join 下推”,那样范围太大,也容易把事情做重。更现实的顺序应该是:

  1. 先做关系侧结果集到主标签集合的裁剪
  2. 再做小维表场景下的半连接/Bloom Filter 过滤
  3. 最后再考虑更受限的等值 Join 下推

也就是说,先把“少扫数据”做好,再谈“在哪个引擎里做 Join”。

2. 多模查询的计划空间收得有点早

kwbase/pkg/sql/opt/xform/custom_funcs.go 里有 NoJoinManipulationNoLookupJoinManipulationNoMergeManipulation 这些逻辑。顺着看下去会发现,多模查询一旦进入某些计划模式判断后,后续 Join 变换空间会明显收缩。

再看 kwbase/pkg/sql/opt/xform/optimizer.go,像 countrows 这种场景还会进一步限制 InsideOut

这样做的原因并不难理解:多模查询比纯关系查询复杂得多,先把候选空间收住,能减少错误计划,也能降低优化复杂度。

但问题在于,这种收缩目前偏硬。很多时候还没真正进入完整的代价比较,好几个候选路径就先被剪掉了。

这里更合适的做法不是“完全放开”,而是把一部分硬限制改成软约束:

  • 明确不支持的物理路径,继续禁掉
  • 理论上能做、只是风险较高的路径,不要直接剪掉,给更高代价惩罚
  • 对小维表、强过滤、多等值条件这些比较稳的场景,允许保留更多 Join Reorder 空间

简单说,就是现在更像“先定路线,再估代价”;后面更理想的是“路线判断也参与代价比较”。

3. 代价模型里仍然有不少经验值写死了

kwbase/pkg/sql/opt/xform/coster.go 这块很值得重点关注。这里已经能看到一些比较明显的经验型估算,比如:

  • block filter 的收益先按 0.5
  • 某些只有 TagFilter 的场景,也按一个固定比例去压缩代价
  • 乱序数据相关代价有默认比例假设

这本身并没有问题。一个系统早期把代价模型搭起来,很多时候就是先靠经验值起步。

但跨模查询比较麻烦的一点是,查询形态差异太大,固定常数特别容易失真。举两个很典型的情况:

  • 同样是按设备维度过滤,命中 10 台设备和命中 10 万台设备,收益完全不是一个量级
  • 同样是 1 小时时间窗,不同表的乱序比例、活跃设备比例可能差很多

如果这些场景都靠 0.50.1 这种常数兜,最后选错计划并不奇怪。

所以这里最先该补的不是更复杂的公式,而是更像样的输入:

  • tag 过滤选择率
  • 主标签命中数
  • 时间窗内有效块比例
  • 乱序块比例
  • 命中设备集合占全体设备集合的比例

只要这几个输入能比现在更准,代价模型的稳定性就会明显提升。

4. 现在的统计信息,够做 TS 优化,但还不够做“跨模”优化

kwbase/pkg/sql/opt/memo/statistics_builder.go 现在已经能把 RowCountPTagCountSortDistribution 这些东西算起来,这对于单 TS 表优化已经很有用了。

但跨模查询比单表 TS 查询多出来一个关键问题:关系侧条件和时序侧数据分布之间,到底有没有相关性。

比如:

  • 某个 modelID 可能只占设备总数的 2%
  • 但这 2% 设备可能刚好是最活跃的一批
  • 也可能反过来,设备很多,但几乎没有新增时序数据

如果优化器只知道全表有多少设备、全表有多少时序行,却不知道某个关系条件过滤后到底会留下多少活跃实体、多少有效块,那跨模 Join 的代价就很容易估歪。

所以后面真正值得补的是“跨模相关性统计”,优先级甚至不低于继续加规则。比较有用的统计包括:

  1. 主标签和普通 tag 的联合统计
  2. 时间列在不同窗口下的分布统计
  3. 设备活跃度分布
  4. 关系 Join Key 到主标签的映射关系或相关性统计

这一步做完,跨模查询的计划质量会比单纯再加几条规则更稳。

5. DOP 现在更像“看机器给多少”,还不太像“看这条查询值不值得开这么多”

kwbase/pkg/sql/opt/memo/memo.go 里的 CalculateDop 会参考:

  • RowCount
  • PTagCount
  • 列宽
  • CPU 核数
  • 当前可用内存

这个方向本身没问题,至少它已经不是简单写死并行度了。

但如果从跨模查询全链路去看,它还是偏扫描视角。也就是说,它更像是在回答“TS 扫描阶段最多能开多大并行”,而不是“整条跨模查询开多大并行最划算”。

这里可能出现的情况是:

  • TS 扫描阶段并行度很高
  • 但后面要过 Synchronizer
  • 还要做 Join、聚合、结果归并
  • 最后整体收益没有跟着并行度同步上涨

所以后面可以把 DOP 分成两层:

  • 计划期给一个安全上限
  • 执行期根据真实输出规模和内存压力再做收敛

如果以后 InsideOut / OutsideIn 模式和 DOP 还能联动起来,这块的收益会更明显。

6. 现有优化更像“把 TS 子树优化好”,还没完全走到“跨引擎协同优化”

groupby.optmemo.gostatistics_builder.gocoster.go 这几块合起来看,会发现一个很明显的特点:当前 KWDB 在单 TS 子树上的优化其实已经做了不少事情。

比如:

  • lastrow
  • tag-only scan
  • 时间有序/乱序分布建模
  • 部分 window/diff 能力判断

这些都不是花架子,都是实打实会影响计划质量的东西。

但跨模查询真正往下走时,最关键的三件事现在还不算特别强:

  • 关系侧过滤能不能更深地传到时序侧
  • 时序侧预聚合或最新值结果能不能在 Join 后复用
  • Join 之后的列裁剪和聚合信息能不能反过来收缩时序扫描

所以对当前实现的判断是:它的单引擎局部优化基础已经不错,但跨引擎协同还在往前走的路上。

这不是坏事,反而说明方向是对的。现在最值得做的,不是推翻已有实现,而是把“引擎之间怎么互相给信息”这件事做深。

四、如果按工程投入和收益比来排优先级

如果站在工程投入和收益比上看,后续优化大致可以分成三段。

阶段 优先级 更适合先做什么 主要目标
第一阶段 P0 补统计信息,减少代价模型里的固定经验值 先把计划选错率降下来
第二阶段 P1 做关系侧到时序侧的实体裁剪、半连接过滤 先把扫描量压下来
第三阶段 P2 放宽部分多模 Join 搜索空间,逐步增强受限下推 再去吃更复杂查询的收益

这么排的原因很简单:

  • 现在最大的问题还不是“执行器完全不行”,而是“优化器有时不够敢选,也不够准”
  • 如果统计和代价都还不稳,就算贸然放开更多 Join 物理路径,也不一定能稳定收益
  • 先把裁剪和估算做好,后面再加跨引擎协同能力,路线会更稳

换句话说,先把“少扫”和“别选错”解决,再去追求“更激进的物理执行”。

五、KWDB 现在的跨模查询实现

综合现有实现来看,一个比较明确的感受是:KWDB 的跨模查询已经过了“有没有”这个阶段,现在进入的是“怎么做得更像一个成熟内核能力”的阶段。

它已经有:

  • 比较完整的跨模 SQL 样例
  • 多模查询类型识别
  • 一批实用的 TS 局部优化规则
  • 主标签和排序分布相关统计
  • 多模计划模式和执行计划拼接逻辑

但如果目标是让复杂业务里的跨模查询更稳定地跑快,那后面真正该继续啃的,还是这几件事:

  • 让统计信息更像跨模统计,而不是只像单表统计
  • 让代价模型少一点经验常数,多一点真实分布输入
  • 让计划空间别太早收死
  • 让关系侧和时序侧之间的信息传递更深入

总的来说,这不是一个“从 0 到 1”的问题了,更像一个“从 1 到 10”的问题。

这反而是好事。因为这说明 KWDB 的跨模查询已经有了继续打磨的基础,后面每往前走一步,都会更接近真正的工程收益,而不只是功能堆叠。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐