SQL 解析引擎深度剖析:大数据平台的"隐形心脏"
SQL 解析引擎深度剖析:大数据平台的"隐形心脏"
上一篇文章聊了一站式大数据平台的架构演进。有朋友留言问:"你说数据网关需要 SQL 解析和改写能力,这到底是怎么实现的?"
好问题。SQL 解析引擎是大数据平台里最被低估的模块——用户感知不到它的存在,但平台 80% 的核心能力都建立在它之上:
- 数据血缘追踪?靠解析 SQL 中的表引用关系
- 列级权限控制?靠在 SQL 中注入过滤条件
- SQL 自动优化?靠分析和改写执行计划
- 多引擎路由?靠 SQL 方言翻译
- AI 自然语言转 SQL?靠生成符合语法的 SQL 再校验
这篇文章就来深入拆解 SQL 解析引擎的核心技术。内容会比较硬核,适合想深入理解底层原理的架构师和高级开发者。
一、SQL 处理的四个阶段
一条 SQL 从用户输入到最终执行,经历四个阶段:
用户 SQL → [词法分析] → [语法分析] → [语义分析] → [优化改写] → 执行引擎
Lexer Parser Analyzer Optimizer
每个阶段都有对应的核心技术,我们逐一拆解。
阶段一:词法分析(Lexing / Tokenization)
词法分析器把 SQL 字符串切分成一个个 Token(词法单元)。
举个例子:
SELECT user_id, count(*) FROM orders WHERE status = 'paid' GROUP BY user_id
经过词法分析后变成:
[SELECT] [user_id] [,] [count] [(] [*] [)] [FROM] [orders]
[WHERE] [status] [=] ['paid'] [GROUP] [BY] [user_id]
每个 Token 有类型标注:
SELECT→ 关键字 (Keyword)user_id→ 标识符 (Identifier)'paid'→ 字符串字面量 (String Literal)=→ 操作符 (Operator)
关键技术点:
-
关键字与标识符的冲突处理
SQL 有大量保留关键字(SELECT, FROM, WHERE...),但用户可能用这些词作为表名或列名(比如
order是关键字,但也是常见表名)。不同的 SQL 方言处理方式不同:- MySQL:用反引号
`order` - PostgreSQL/Hive:用双引号
"order" - SQL Server:用方括号
[order]
一个跨方言的 SQL 解析器需要同时支持所有这些引用方式。
- MySQL:用反引号
-
Unicode 和多字节字符
中文场景下,表名和列名可能是中文(
用户表.手机号)。词法分析器需要正确处理 UTF-8 编码的标识符。 -
注释处理
SQL 支持多种注释风格:
-- 单行注释、/* 多行注释 */、MySQL 特有的# 注释。解析器需要正确忽略注释内容,同时保留注释中可能的 Hint 信息(如/*+ MAPJOIN(t1) */)。
阶段二:语法分析(Parsing)
语法分析器把 Token 序列转化为 抽象语法树(AST, Abstract Syntax Tree)。
上面那条 SQL 的 AST 大致长这样:
QueryStatement
├── SelectList
│ ├── Column: user_id
│ └── FunctionCall: count(*)
├── FromClause
│ └── Table: orders
├── WhereClause
│ └── BinaryExpression
│ ├── Column: status
│ ├── Operator: =
│ └── Literal: 'paid'
└── GroupByClause
└── Column: user_id
AST 是整个 SQL 引擎的核心数据结构。 后续的语义分析、优化改写、血缘追踪,全部基于 AST 进行。
主流的语法分析器生成工具:
| 工具 | 语言 | 特点 | 使用者 |
|---|---|---|---|
| ANTLR4 | Java/多语言 | 最成熟的 LL(*) 解析器生成器 | Spark SQL, Presto, Hive |
| JavaCC | Java | 老牌 LL(k) 解析器 | Calcite, Flink SQL |
| Bison/Yacc | C/C++ | LALR(1) 解析器 | PostgreSQL, MySQL |
| 手写递归下降 | 任意 | 灵活但工作量大 | ClickHouse, CockroachDB |
ANTLR4 已经成为大数据 SQL 解析的事实标准。 Spark SQL、Presto/Trino、Hive 都使用 ANTLR4 定义 SQL 语法。
用 ANTLR4 定义一个简单的 SELECT 语句语法:
grammar SimpleSql;
query
: SELECT selectList FROM tableName (WHERE expression)? (GROUP BY columnList)?
;
selectList
: selectItem (',' selectItem)*
;
selectItem
: expression (AS? alias=IDENTIFIER)?
;
tableName
: IDENTIFIER ('.' IDENTIFIER)*
;
expression
: IDENTIFIER # columnRef
| literal # literalExpr
| functionName '(' (expression (',' expression)*)? ')' # functionCall
| expression op=('=' | '<' | '>' | '<=' | '>=') expression # comparison
| expression AND expression # andExpr
| expression OR expression # orExpr
;
// 词法规则
SELECT : [sS][eE][lL][eE][cC][tT] ;
FROM : [fF][rR][oO][mM] ;
WHERE : [wW][hH][eE][rR][eE] ;
// ... 更多关键字
IDENTIFIER : [a-zA-Z_][a-zA-Z0-9_]* ;
实际生产中的 SQL 语法远比这复杂。 Spark SQL 的 ANTLR 语法文件超过 3000 行,覆盖了子查询、窗口函数、CTE、LATERAL VIEW、自定义 DDL 等上百种语法结构。
语法分析中的难点:
-
歧义消解
SQL 语法中存在大量歧义。比如
SELECT a(b)——a(b)是函数调用还是列名后跟括号表达式?需要通过优先级规则和上下文来消解。 -
错误恢复
用户写的 SQL 经常有语法错误。一个好的解析器不应该在第一个错误处就崩溃,而应该尽可能解析剩余部分,给出尽可能精确的错误位置和修复建议。
ANTLR4 提供了
DefaultErrorStrategy和BailErrorStrategy两种错误处理策略。在 IDE 场景下,通常使用DefaultErrorStrategy来实现"带红线的实时语法检查"。 -
语法扩展
每个大数据引擎都会在标准 SQL 基础上增加自己的语法扩展。比如 Hive 的
LATERAL VIEW EXPLODE、Spark 的TRANSFORM、Presto 的UNNEST。一个跨引擎的 SQL 解析器需要能灵活地加载不同的语法插件。
阶段三:语义分析(Semantic Analysis)
语法分析只检查"SQL 的结构是否合法",语义分析检查"SQL 的含义是否正确"。
典型的语义检查包括:
-
表和列的存在性校验
SELECT user_name FROM orders -- orders 表有 user_name 列吗?这需要查询元数据目录(Catalog)。在分布式环境下,元数据访问可能涉及远程调用,是性能瓶颈之一。优化手段包括:
- 元数据本地缓存 + 失效通知
- 批量预加载(解析到 FROM 子句时,一次性加载所有相关表的元数据)
-
类型检查与隐式转换
SELECT user_id + '1' FROM users -- int + string,是否自动转换?不同引擎的隐式类型转换规则差异巨大。MySQL 会把
'1'转成整数,PostgreSQL 会报错。跨引擎平台需要明确定义类型转换规则。 -
作用域解析
SELECT a.id, b.name FROM (SELECT id FROM users) a JOIN (SELECT name, id FROM profiles) b ON a.id = b.id语义分析器需要维护一个作用域栈,正确解析子查询中的列引用。当子查询嵌套多层时,作用域解析的复杂度急剧上升。
-
聚合函数校验
SELECT user_id, count(*), status -- status 没有在 GROUP BY 中,是否合法? FROM orders GROUP BY user_idMySQL 允许(取随机值),PostgreSQL/Hive 不允许。这又是一个方言差异。
阶段四:优化改写(Query Optimization)
这是 SQL 引擎最核心、最复杂的部分。优化器的目标是:在语义等价的前提下,找到执行成本最低的执行计划。
两大优化范式:
1. 基于规则的优化(RBO, Rule-Based Optimization)
通过预定义的规则对 AST 进行变换。常见规则包括:
-
谓词下推(Predicate Pushdown):把 WHERE 条件尽可能推到数据源层面执行,减少数据传输量
-- 优化前 SELECT * FROM (SELECT * FROM orders) t WHERE t.status = 'paid' -- 优化后(谓词下推到子查询) SELECT * FROM (SELECT * FROM orders WHERE status = 'paid') t -
列裁剪(Column Pruning):只读取实际使用的列
-- 优化前:读取 orders 表的所有列 SELECT user_id FROM orders -- 优化后:只读取 user_id 列(对列式存储格式如 Parquet 效果巨大) -
常量折叠(Constant Folding)
-- 优化前 SELECT * FROM orders WHERE 1 + 1 = 2 -- 优化后 SELECT * FROM orders WHERE true -- 进一步优化为无条件扫描 -
Join 重排序(Join Reordering)
-- 优化前:大表 JOIN 大表 JOIN 小表 SELECT * FROM big_a JOIN big_b ON ... JOIN small_c ON ... -- 优化后:先 JOIN 小表,减少中间结果集 SELECT * FROM big_a JOIN small_c ON ... JOIN big_b ON ...
2. 基于成本的优化(CBO, Cost-Based Optimization)
RBO 的规则是"一刀切"的,但实际场景中,最优的执行计划取决于数据的统计信息。CBO 通过收集和利用统计信息(表大小、列基数、数据分布等)来估算不同执行计划的成本,选择成本最低的方案。
核心概念:
执行计划成本 = CPU 成本 + IO 成本 + 网络传输成本
统计信息采集:
-- Hive 收集统计信息
ANALYZE TABLE orders COMPUTE STATISTICS;
ANALYZE TABLE orders COMPUTE STATISTICS FOR COLUMNS;
-- 收集后存储的信息包括:
-- - 表级:行数、数据大小、文件数
-- - 列级:最大值、最小值、空值数、不同值数(NDV)、直方图
CBO 的经典算法是 Calcite 的 Volcano/Cascades 优化框架:
- 生成所有逻辑等价的执行计划(通过规则变换)
- 对每个执行计划估算成本
- 使用动态规划或分支定界找到最优计划
实际工程中的坑:
- 统计信息经常过时或不准确,导致 CBO 选错执行计划
- Join 多表时搜索空间爆炸(N 张表的 Join 有 N! 种排列),需要剪枝
- 用户自定义函数(UDF)的成本无法准确估算
二、Apache Calcite:SQL 引擎的"瑞士军刀"
如果你要设计一个大数据平台的 SQL 层,Apache Calcite 几乎是绕不开的选择。
Calcite 不是一个数据库,而是一个SQL 解析和优化框架。它提供了:
- SQL 解析器(基于 JavaCC)
- 关系代数表示
- 规则优化框架(基于 Volcano/Cascades 模型)
- 适配器层(可以对接任意数据源)
谁在用 Calcite?
| 项目 | 使用 Calcite 的部分 |
|---|---|
| Apache Hive | SQL 优化器 (CBO) |
| Apache Flink | SQL 解析和优化 |
| Apache Druid | SQL 层 |
| Apache Kylin | SQL 层 |
| Presto/Trino | 部分灵感来源(但自研优化器) |
| DataWorks | SQL 血缘解析 |
Calcite 的核心架构:
SQL 字符串
│
▼
┌───────────┐
│ SqlParser │ ← JavaCC 生成的 SQL 解析器
└─────┬─────┘
│ SqlNode (AST)
▼
┌───────────┐
│ SqlValidator│ ← 语义验证 + 类型推导
└─────┬─────┘
│ SqlNode (validated)
▼
┌───────────────┐
│ SqlToRelConverter│ ← AST → 关系代数
└─────┬─────────┘
│ RelNode (逻辑计划)
▼
┌───────────┐
│ Planner │ ← 优化器(RBO + CBO)
└─────┬─────┘
│ RelNode (物理计划)
▼
┌───────────┐
│ 执行引擎 │ ← 由各个项目自己实现
└───────────┘
用 Calcite 实现一个简单的 SQL 血缘追踪:
public class SqlLineageAnalyzer {
public Set<TableLineage> analyze(String sql) {
// 1. 解析 SQL
SqlParser parser = SqlParser.create(sql);
SqlNode sqlNode = parser.parseQuery();
// 2. 遍历 AST,提取表引用
Set<String> sourceTables = new HashSet<>();
Set<String> targetTable = new HashSet<>();
if (sqlNode instanceof SqlInsert) {
SqlInsert insert = (SqlInsert) sqlNode;
targetTable.add(insert.getTargetTable().toString());
extractSourceTables(insert.getSource(), sourceTables);
} else if (sqlNode instanceof SqlSelect) {
extractSourceTables(sqlNode, sourceTables);
}
// 3. 构建血缘关系
return buildLineage(sourceTables, targetTable);
}
private void extractSourceTables(SqlNode node, Set<String> tables) {
if (node instanceof SqlIdentifier) {
tables.add(node.toString());
} else if (node instanceof SqlJoin) {
SqlJoin join = (SqlJoin) node;
extractSourceTables(join.getLeft(), tables);
extractSourceTables(join.getRight(), tables);
} else if (node instanceof SqlSelect) {
SqlSelect select = (SqlSelect) node;
extractSourceTables(select.getFrom(), tables);
// 递归处理子查询
if (select.getWhere() != null) {
extractSubQueries(select.getWhere(), tables);
}
}
// ... 处理更多 SqlNode 类型
}
}
实际生产中的血缘分析远比这复杂,需要处理:
- 列级血缘(
target.col_a来自source1.col_x + source2.col_y) - 动态 SQL(SQL 中包含变量,需要运行时解析)
- 存储过程和 UDF 中的隐式引用
- 视图展开
三、SQL 方言翻译:跨引擎的关键难题
一站式大数据平台往往需要支持多个计算引擎(Hive、Spark、Presto、Doris...),但每个引擎的 SQL 方言都有差异。
方言差异的几个典型案例:
1. 类型系统差异
-- Hive:STRING 类型
CREATE TABLE t1 (name STRING);
-- MySQL:VARCHAR 类型
CREATE TABLE t1 (name VARCHAR(255));
-- PostgreSQL:TEXT 类型
CREATE TABLE t1 (name TEXT);
2. 函数差异
-- 获取当前时间
-- MySQL: NOW()
-- Hive: CURRENT_TIMESTAMP()
-- Presto: NOW() -- 但返回类型不同
-- 字符串拼接
-- MySQL: CONCAT(a, b)
-- Hive: CONCAT(a, b) 或 a || b
-- Presto: a || b 或 CONCAT(a, b)
-- 日期格式化
-- MySQL: DATE_FORMAT(dt, '%Y-%m-%d')
-- Hive: DATE_FORMAT(dt, 'yyyy-MM-dd')
-- Presto: DATE_FORMAT(dt, '%Y-%m-%d') -- 格式符和 MySQL 一样但行为不同
3. 语法差异
-- 分页查询
-- MySQL: LIMIT 10 OFFSET 20
-- Presto: LIMIT 10 OFFSET 20 (但性能特性不同)
-- Oracle: ROWNUM <= 30 AND ROWNUM > 20 (传统写法)
-- 建表语句
-- Hive 独有的 PARTITIONED BY, STORED AS, ROW FORMAT
CREATE TABLE orders (
id BIGINT,
amount DECIMAL(10,2)
) PARTITIONED BY (dt STRING)
STORED AS PARQUET;
-- 这在 MySQL/PostgreSQL 中完全不合法
4. 语义差异(最危险)
-- GROUP BY 行为
SELECT dept, name, count(*) FROM employees GROUP BY dept;
-- MySQL (ONLY_FULL_GROUP_BY 关闭时): 允许,name 取随机值
-- Hive/Presto: 报错,name 必须在 GROUP BY 中或聚合函数中
-- NULL 比较
SELECT * FROM t WHERE col != 1;
-- 在所有引擎中,col 为 NULL 的行都不会被返回
-- 但 MySQL 的 <=> 操作符可以比较 NULL
SQL 方言翻译的实现方案:
方案一:基于 AST 变换
源方言 SQL → 解析为 AST → AST 变换规则 → 生成目标方言 SQL
这是最主流的方案。核心挑战是变换规则的完备性——每增加一对源-目标方言组合,就需要一套完整的变换规则。
开源工具推荐:
- SQLGlot(Python):目前最完善的开源 SQL 方言翻译工具,支持 20+ 方言
- jOOQ(Java):商业级 SQL 构建和翻译库
- Calcite SqlDialect:Calcite 内置的方言转换能力
方案二:中间表示(IR)方案
源方言 SQL → 解析 → 标准 IR(关系代数) → 生成 → 目标方言 SQL
这是 Calcite 的做法。好处是只需要实现 N 个 "方言→IR" 和 N 个 "IR→方言" 的转换器,而不是 N² 对方言间的直接翻译。
方案三:大模型翻译(2024 年新趋势)
用大语言模型直接做 SQL 方言翻译。效果比预期好,特别是处理复杂的方言特定语法时。但问题是:
- 不可靠——偶尔会生成语法正确但语义错误的 SQL
- 不可解释——无法精确追踪翻译逻辑
- 延迟高——不适合实时场景
实际落地中,最佳实践是规则翻译为主、大模型为辅:简单的翻译用确定性规则,复杂的边角情况用大模型兜底,但需要人工确认。
四、SQL 权限注入:安全架构的关键环节
前面提到,数据网关通过 SQL 改写来实现细粒度权限控制。这里展开讲讲具体实现。
行级权限注入
假设权限规则是:"用户 A 只能查看 dept = 'finance' 的数据"。
-- 用户提交的原始 SQL
SELECT * FROM employees WHERE salary > 10000
-- 权限引擎改写后
SELECT * FROM employees WHERE salary > 10000 AND dept = 'finance'
实现步骤:
- 解析原始 SQL 为 AST
- 在 AST 的 WHERE 子句中注入权限条件
- 如果涉及子查询或 JOIN,需要在正确的位置注入(不能在 JOIN 之后注入,会改变语义)
- 将改写后的 AST 重新生成 SQL
复杂场景:
-- 原始 SQL 包含子查询和 JOIN
SELECT a.name, b.salary
FROM (SELECT * FROM employees) a
JOIN departments b ON a.dept_id = b.id
WHERE b.budget > 100000
-- 权限注入需要在子查询内部
SELECT a.name, b.salary
FROM (SELECT * FROM employees WHERE dept = 'finance') a -- 注入在这里
JOIN departments b ON a.dept_id = b.id
WHERE b.budget > 100000
列级脱敏
假设 phone 列需要脱敏,规则是"只显示前三位和后四位"。
-- 原始 SQL
SELECT name, phone FROM users
-- 脱敏改写后
SELECT name, CONCAT(SUBSTR(phone, 1, 3), '****', SUBSTR(phone, -4)) AS phone FROM users
实现上的挑战:
- 脱敏函数需要适配不同的 SQL 方言(SUBSTR 的参数在不同引擎中可能不同)
- 当脱敏列出现在 WHERE、JOIN、GROUP BY 中时,行为需要特殊处理
- 嵌套脱敏(A 视图引用 B 表的脱敏列,A 视图的下游不应看到原始值)
五、AI + SQL:大模型时代的 SQL 引擎新能力
2024 年以来,大模型给 SQL 引擎带来了几个革命性的新能力:
1. Text-to-SQL(自然语言转 SQL)
用户说:"帮我查一下上个月每个部门的销售总额,按金额从高到低排列"
系统生成:
SELECT
department,
SUM(amount) AS total_sales
FROM sales
WHERE sale_date >= DATE_ADD(CURRENT_DATE, INTERVAL -1 MONTH)
AND sale_date < CURRENT_DATE
GROUP BY department
ORDER BY total_sales DESC
技术实现的关键:
- 表结构信息(Schema)作为 Prompt 的一部分输入大模型
- 生成的 SQL 必须经过解析器验证语法正确性
- 通过 EXPLAIN 检查执行计划的合理性
- few-shot 示例提高生成准确率
当前的瓶颈:
- 复杂多表 JOIN 的准确率仍然不够高(业界 benchmark 约 70-85%)
- 业务语义理解依赖上下文("活跃用户"在不同公司的定义不同)
- 安全风险——大模型可能生成 DELETE、DROP 等危险操作
2. SQL 智能优化
把慢 SQL 和执行计划丢给大模型,让它给出优化建议。
输入:
SQL: SELECT * FROM orders o JOIN users u ON o.user_id = u.id WHERE o.create_time > '2025-01-01'
执行计划显示:orders 表全表扫描,耗时 300s
大模型输出:
1. orders 表的 create_time 列缺少索引/分区,建议按 create_time 分区
2. SELECT * 读取了所有列,建议只选择必要列
3. 如果 users 表远小于 orders 表,可以考虑 MAPJOIN
3. SQL 错误诊断
输入:
SQL: SELECT user_id, count(*) FROM orders GROUP BY 1 HAVING count(*) > avg(amount)
错误信息:HAVING 子句中 avg(amount) 引用了非聚合列
大模型输出:
问题分析:HAVING 子句中的 avg(amount) 没有问题(它是聚合函数),
但真正的问题是 avg(amount) 计算的是 GROUP BY 后每组的平均值,
这可能不是你想要的语义。
修复建议:
1. 如果要筛选"订单数 > 该用户平均金额":当前写法在语义上是正确的
2. 如果要筛选"订单数 > 全局平均金额":
SELECT user_id, count(*)
FROM orders
GROUP BY user_id
HAVING count(*) > (SELECT avg(amount) FROM orders)
六、实战:从零搭建一个 SQL 解析服务
如果你要为大数据平台搭建一个 SQL 解析和改写服务,推荐的技术栈是:
┌─────────────────────┐
│ SQL Parse Service │
│ │
SQL + Context ──→ │ ┌─────────────────┐ │ ──→ 解析结果 / 改写后的 SQL
│ │ ANTLR4 Parser │ │
│ │ (多方言支持) │ │
│ └────────┬────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Calcite │ │
│ │ (语义分析+优化) │ │
│ └────────┬────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ 改写规则引擎 │ │
│ │ (权限/脱敏/翻译) │ │
│ └─────────────────┘ │
└─────────────────────┘
关键设计决策:
- 解析器选择:多方言场景用 ANTLR4(语法文件可独立维护),单引擎场景直接用 Calcite Parser
- 元数据对接:通过 Calcite 的 Schema/Table/Column 接口对接平台的元数据中心
- 规则管理:改写规则做成可配置的,支持热加载
- 性能优化:AST 缓存(相同 SQL 模板只解析一次)、并行解析
七、写在最后
SQL 解析引擎是大数据平台最"底层"的模块,但它的质量直接决定了平台的上限。一个好的 SQL 引擎能让数据网关的权限注入精准无误、让血缘追踪覆盖每一个字段、让 SQL 优化器选出最佳执行计划。
对于大数据平台架构师来说,深入理解 SQL 解析的原理不是"加分项",而是"必修课"。当你面对一个性能问题时,如果能打开执行计划看到优化器的决策逻辑,如果能理解 SQL 改写背后的 AST 变换规则,你就比 90% 的同行更有可能找到根因。
下一篇文章,我们聊聊大模型如何重塑大数据开发平台——从 AI 代码生成到智能运维,从 Text-to-SQL 到数据质量自动修复。AI 不是要取代大数据工程师,而是要把他们从重复劳动中解放出来。
本文为"大数据平台架构深度解析"系列的第二篇。

浙公网安备 33010602011771号