Agile Query 中的 SQL 编译器
Agile Query 的 SQL 编译器在架构上与标准编译器相似,但其核心差异在于它们编译的目标不同。传统编 译器负责将源代码转换成操作系统或 CPU 架构能够识别的指令
(例如,机器代码或 LLVM 的中间表示,再由 LLVM 编译器统一处理),然后由操作系统和 CPU 直接执行,或者对于某些动态语言,由运行时环境进行解释和执行。相比之下,
Agile Query 的 SQL 编译器将源代码直接编译成数据库能够直接解释和执行的 SQL 代码。
传统编译器需要适配不同 CPU 架构的指令集,而 Agile Query 的编译器则需适配不同数据库的 SQL 语法以及各自的执行引擎优化规则,以确保生成的 SQL 能在目标数据库中高效运行。
下图是 Agile Query 编译器的内部结构和处理过程:
- 词法解析、语法解析是编译器的基础过程,与传统编译器类似,负责将文本文件解析为 AST(抽象语法树)。在此基础上,语义解析则进一步为 AST 实例填充状态,赋予其特定的行为和逻辑。
- 标准函数库包含数据库提供的常用函数,如聚合函数和标量处理函数,而高级函数库则由 Agile Query 提供,封装了与领域更贴合的功能,以及对不同数据库函数语法的适配。
- 中间代码生成相当于传统编译器中的 IR(中间表示)。大多数传统编译器采用 DAG(有向无环图)存储状态,Agile Query 也使用树形结构存储状态信息。这些状态包括投影、过滤、排序等关键操作。
- 目标代码生成阶段是一个复杂的逻辑处理过程。此阶段,编译器解析查询所涉及的表,依据关系代数的计算规则拆分子查询,并最终将所有子查询图合并,构造出完整的查询逻辑。
Agile Query 编译器的目标是简化标准 SQL 的底层复杂逻辑。正如高级编程语言的发展历程,每一代新语言的出现都在降低编程的技能门槛,让开发者能 够摆脱对底层技术细节的过度依赖,
专注于解决领域内的问题。在一些业务系统的 SQL 编程,除了各类统计指标的开发,开发者还需要面对下列技术细节:
- 多对多关联分析:多对多场景在领域实体关系中非常常见,例如:客户与订单的关系、商品与品类的关系。当这些表在进行关联分析时,常常会遇到过度计算(over-counting 或 double-counting)
的问题(例如:统计每个客户购买的商品数据或品类数量)。为了解决这些问题,通常需要数据工程师通过优化 SQL 结构进行手动调整。
- 子查询:在实际数据分析工作中,嵌套聚合、统计维度对齐等查询需求都需要子查,然而,目前主流的 ORM 框架通常不支持生成包含子查询的 SQL。如果尝试通过预计算来替代子查询,
不仅实现复杂度增加,开发成本也往往远高于直接编写子查询。
上述问题是数据分析工程中最为常见的问题,也是数据分析工作始终离不开数据工程师和程序员的重要原因。随着数据分析需求的不断变化,数据工程师的日常工作变得越来越重复且繁琐。
一个看似普通的数据分析需求往往需要多个部门协同完成,流程冗长,效率低下。数据工程师不仅需要负责数据的清洗与治理,还要应对各部门的各种数据分析需求。这种情况导致数据团队规模日益庞大,
使得数据分析这一常见且刚需的业务需求成本高昂,使用门槛也随之提高。
Agile Query 正是在这样的背景下应运而生。通过算法和规则自动生成查询 SQL,它显著提升了数据分析需求的开发效率。数据工程师只需专注于单个指标的计算逻辑,
而无需处理指标与其他维度或指标间的关联查询(由数据分析师或运营人员自由组合)。这不仅大幅减少了数据工程师的开发工作量,也加快了数据分析师和运营人员获取分析结果的速度,
大幅提升整体效率。
数据分析需求分类
在生成 SQL 之前,Agile Query 首先对数据分析需求的方式或模式进行分类,力求覆盖所有可能的数据分析需求。随后,根据不同的数据分析方法设计相应的 SQL 生成逻辑。
这正是上文中目标代码生成的核心设计理念。其中最关键的步骤是针对查询的合并,这是手工编写 SQL 时最容易被忽视的部分。Agile Query 通过分析数据分析需求涉及的表,
识别并合并可整合的查询逻辑,从而减少数据库从磁盘加载的数据量。这不仅优化了资源使用,还大幅提升了整体查询效率。Agile Query 对数据分析需求分为八个大类:
- 简单聚合:例如:
AVG(Price)
、SUM(Revenue)
、COUNT_DISTINCT(Users)
、这些聚合指标通常以指标卡片的形式出现
- 聚合值的算术运算:例如,利润可以定义为
SUM(Revenue) - SUM(Cost) - SUM(Commissions)
。或者,客单价可以定义为:SUM(Sales) / COUNT(Orders)
- 需要 JOIN 的聚合:例如:计算不同品类的销售额可以定义为:
SUM_IF(Product = '牛奶', Sales)
,Product 和 Sales 分别在品类表和订单表中,
当计算上述指标时,需要 JOIN 操作,典型场景:同时计算每个月牛奶、咖啡的销售额
- 窗口函数的指标:例如:移动平均(Moving Averages),累计(Cumulative Sum),典型场景:计算一年内每月利润的累计值,能够体现出随时间变化,每个月利润的结余
- 嵌套聚合:例如:月均销售额,月均利润,内层聚合需要
SUM(Sales, TO_MONTH(Sales Date))
,外层聚合为 AVG
,完整的形式为:
AVG(SUM(Sales, TO_MONTH(Sales Date)))
- 多维度聚合:典型场景:计算每个品类的销售额和客户数据,这类查询属于比较复杂的查询,因为品类表和客户表之间为多对多的关联关系,一个客户可以购买多个品类,
一个品类也可以被多个客户购买
- 跨维度计算:典型场景:同时计算每个商品的销售额占总体销售的比例和占每个品类的比例,这类查询通常需要多个不同层级的 维度对销售额进行聚合,
然后将多个聚合值拉平为同一维度,再进行算术运算
- 分类或标签:当分析对象数量众多时,很难以单个实体的形式进行数据分析,例如:客户维度的分析,当客户数据很少时,可以以客户的维度进行分析,但当客户数据有几万、十几万时,
就很难以单个客户的维度进行分析,通常会以某种方式对客户进行分类,例如:将单位时间范围内的销售额切分为多个区间,以销售额区间的形式进行客户数据的分析。
- 时间序列对比:时间序列对比在大都数行业的数据分析中者至关重要,例如:销售额的同环比或自由时间区间的对比,或者再结合其它维度的指标增长率等。
上述方法需求覆盖了大部分的数据分析需求,Agile Query 针对上述分析需求提供了不同的函数进行处理,标准聚合提供了与标准 SQL 同名的函数,但实际的聚合逻辑完全不同,
为非标准聚合提供了高级分析函数,例如:分类或标签的函数为
SEGMENT
函数。
与其它产品的比较