【实战二】字段级血缘追踪:拆解 AST 迷局,精准穿透函数链与子查询

前言:按图索骥的艺术

在上一篇文章中,我们建好了一个完美的'仓库登记表'(Scope作用域 )。现在,我们要进入第二阶段 SqlAnalyzerListener,顺着 SELECT 子句,去查出每一个字段最底层的来源。

我们的目标是:不管用户在 SELECT 里写了多复杂的函数、子查询或表达式,引擎都要能剥开重重伪装,追踪到最底层的物理字段。

💡开源项目地址::Hermes-SQL

字段级血缘追踪初识

在 Hive SQL 语法中,SELECT 后面支持多达 11 种表达式。如果全放在一篇文章里讲完会非常臃肿,今天我们先啃下最核心的几块硬骨头:物理字段、函数嵌套与子查询。

📍 第一关:AST 的陷阱 ------ t1.id 去哪儿了?

普通物理字段 (tableOrColumn),正常情况下,物理字段名就在tableOrColumn节点下面,但是字段名一旦有前缀之后,情况就不一样了。

比如解析 SELECT t1.id FROM user t1 时,最早我按照惯性思维,以为能直接拿到完整字符串 t1.id。但结果却报错了。我仔细看了官方语法文件,发现在实际解析中,ANTLR 把它拆成了 TableOrColumn("t1") 和 DOT id_("id") 两个兄弟节点。

为什么 ANTLR 要这么设计? 因为它在兼容极其复杂的语法,比如 arr[0](数组下标)或 t1.struct_col.field

如何解? 既然它们是兄弟节点,我们就必须向上追溯它们共同的父节点 PrecedenceFieldExpressionContext(优先级字段表达式),直接将父节点的整体文本提取出来,从而获得完整的 t1.id

java 复制代码
// 核心代码:在 AstHelper 中向上回溯
if (node instanceof HiveParser.PrecedenceFieldExpressionContext) {

    HiveParser.PrecedenceFieldExpressionContext pfCtx = (HiveParser.PrecedenceFieldExpressionContext) node;

    // 判断是纯字段引用,不是函数
    if (pfCtx.atomExpression() != null && pfCtx.atomExpression().tableOrColumn() != null) {
        // 直接拿整体的 getText()
        String fullText = pfCtx.getText();   // 得到 "t1.id" 或 "id" 或 "arr[0]"
        ColumnRef candidate = parseTableOrColumn(fullText);
        results.add(candidate);
        return results;   // 关键:处理完直接返回,截断递归,防止重复提取
    }
}

📍 第二关:按图索骥 ------ 裸字段归属与歧义检测

当我们遇到 t1.id,直接去第一阶段建好的 Scope.aliasToTableMap 里找 t1,直接命中。

当我们遇到 SELECT id 这种裸字段,去 Scope.columnToTableMap 里找。因为有第一阶段元数据服务的加持,我们可以轻松拿到包含 id 的表。

但实际业务中,字段往往包裹在算术表达式(如 a.id + b.id)中。为此,我写了一个通用的递归提取器 AstHelper.extractColumnCandidates,只要递归命中 TableOrColumn 节点就抓取;如果遇到子查询节点,直接 return 阻断递归。这样就能把复杂表达式树里所有的物理字段全部揪出来。

高能预警 :如果多张表都有 id,结果列表会大于 1。这时引擎必须抛出警告,标记 isAmbiguous = true。在真实的生产事故中,多表 JOIN 漏加别名是导致数据混乱的最常见原因。

java 复制代码
// 核心逻辑:歧义检测
List<TableInfo> tableInfos = scope.columnToTableMap.get(columnRef.columnName);
if (tableInfos.size() == 1) {
    columnRef.originTable = tableInfos.get(0); // 唯一命中
} else if (tableInfos.size() > 1) {
    columnRef.isAmbiguous = true; // 多表同名,标记歧义警告!
}

📍 第三关:洋葱剥壳 ------ 函数链(FunctionChain)的构建

函数 (function_),当我们遇见单个函数或者函数嵌套时,比如UPPER(SUBSTR(name, 1, 3)),我们需要提取出 name 字段,同时记录它经过了哪些加工。

如何解? 这就要用到我们的函数栈 functionStack:

  1. 外层函数 UPPER 先入栈。
  2. 内层 SUBSTR 后入栈。
  3. 出栈时精准关联上对应的 parentFunction。
java 复制代码
// 核心代码:利用 functionStack 建立父子函数关系
FunctionInfo currentFunc = new FunctionInfo();
currentFunc.functionName = ctx.functionName().getText();

if (!functionStack.isEmpty()) {
    // 栈顶元素即为当前函数的父函数(外层函数)
    currentFunc.parentFunction = functionStack.peek(); 
    currentFunc.isNested = true;
}
functionStack.push(currentFunc); // 压栈

对于每个函数的参数(如 name),我抽象出了 FunctionArgument 模型,把参数类型分为(字段、常量、子查询、嵌套函数等),精准挂载到 arguments 列表里。

踩坑实录:上下文污染

在遍历窗口函数 OVER(PARTITION BY ...) 时,有一个极其隐蔽的坑。比如 ROW_NUMBER() OVER (PARTITION BY id ORDER BY age),ANTLR 在遍历 id 和 age 时,functionStack 的栈顶是 ROW_NUMBER,它会顺理成章地把 id 当成 ROW_NUMBER 的输入参数(Arguments)挂载上去!

为了解决这个问题,我增加了一个 isInsideWindowSpec 状态标记。在进入 OVER 子句时设为 true,拦截所有参数挂载操作,让这些字段只做血缘归属,不进参数列表。

📍 第四关:乾坤大挪移 ------ 子查询与虚拟表血缘穿透

这一关是解析器最容易翻车的地方,分为两种截然不同的子查询。

1. 列级子查询穿透 (subQueryExpression)

在前面第一阶段,对于 FROM 子查询 ,我们将它视作一个虚拟的 TableInfo 数据源。但是对于 SELECT 列级子查询,我们却不能这么处理。

两种子查询的入口完全不同! 列级子查询直接以 selectStatement 的形式包裹在 subQueryExpression 里,跳过了 queryStatementExpression 这一层。如果你用同样的条件去判断,直接漏抓!

graph TD FROM子查询subQuerySource --> A[queryStatementExpression] A --> B[selectStatement] SELECT列级子查询subQueryExpression -->B

如何解? 必须独立处理,用 selectItemStack(查询列栈)来判断当前是否正在解析某个 SELECT 列 ,如果是,就把内部的子查询归挂到当前SELECT 列 的 selectItemSubQueries 属性中,等子查询解析完毕后,向上层递归合并子查询的 sourceColumns 血缘。

2. UDTF 虚拟列穿透 (第一阶段数据源后的LATERAL VIEW)

业务场景:SELECT score FROM user LATERAL VIEW explode(arr) e AS score。

  • 第一阶段,score 被归到了虚拟表 e 中。
  • 第二阶段,当查到 score 来源是 e(虚拟表类型为 FUNCTION)时,启动 "血缘穿透" !顺着 e.inputSourceTable 找到底层的物理表 user,然后把 score 的最终血缘指向 user.arr。

📍 第五关:打造全景图 ------ transformType 的综合判定

数据处理的类型,就是引擎要给拿到的数据打标签。经过上面的解析,我们收集了物理字段 (sourceColumns)和函数链 (functionChain)。最后我们需要综合判定并打上标签:

标签类型 (TransformType) 判定条件 业务含义
DIRECT(直取) 没有函数链,且只有一个物理字段 原始数据直接输出
SIMPLE(简单加工) 经过了单个函数,或者经过了 UDTF表函数处理 轻度数据清洗
COMPLEX(复杂加工) 包含了子查询、CASE WHEN、嵌套函数 重度计算,迁移重灾区

这个标签在未来做脱敏策略和方言迁移时极其重要,它直接决定了我们需要重点关照哪些数据列(SelectItem)。

结语与预告

至此,普通物理字段、函数和函数嵌套、子查询的血缘已经被我们彻底扒光了!

但这还不够,真实业务中的大魔王还没出现。下一篇,我们将死磕高阶子句:CASE WHEN 的四种分支拆解,以及令人头皮发麻的窗口函数(Window Functions) !敬请期待😁。

相关推荐
goujunwe1 小时前
GEO 内容的“可摘录性”打磨技巧:段落、表格、小标题怎么写,方便大模型提取独立答案块
大数据·人工智能
小田学Python1 小时前
DeepSeek Harness 完整上手指南:安装部署 + 必装插件 + 用一周就回不去的功能
大数据·大模型·agent·ai工具·deepseek
.痞老板.1 小时前
mysql初阶--增删改查操作
数据库·mysql
成都蜀味缘餐饮技术培训学校5 小时前
手撕烤兔技术培训实战指南,秘制腌料配方比例与调制手法
大数据·经验分享·笔记·其他·微信公众平台
roman_日积跬步-终至千里5 小时前
AI 能让一个人成为团队吗?从数据中台到 OPC,重新理解软件工程能力
大数据·人工智能·软件工程
演艺大赏6 小时前
NiFi 数据流实战:FTP→HDFS 与 CSV→Parquet,附坏数据、漏文件与双写
大数据
lambo mercy7 小时前
AI智能租房助手
数据库
浮生若城8 小时前
MySQL:库与表的操作
数据库·mysql
薛定谔的悦8 小时前
光储充一体机从主控到执行单元的双层架构
数据库·mongodb·架构