sql的处理流程
一条SQL语句的处理流程包含**解析(Parser)、解析(Analyzer)、优化(Optimizer)、执行(Execution)**过程。
-
Parser:将Sql字符串String解析为一个**抽象语法树/AST(abstract syntax tree)**。
- 词法分析:拆分字符串,得到关键词、数值常量、字符串常量、运算符等token
- 语法分析:将token组成AST node,最终得到一个AST
- 实现:递归下降(ClickHouse) ,Flex和Bison (PostgreSQL),JavaCC (Flink),Antlr (Presto, Spark)
-
Analyzer:会遍历整个AST,并对AST上的每个节点进行数据类型的绑定以及函数绑定,然后根据元数据信息Catalog对数据表中的字段进行解析。
- 检查并绑定Database, Table, Column等元信息
- SQL的合法性检查,比如min/max/avg的输入是数值
- AST -> Logical Plan
-
Logical Plan:逻辑地描述SQL对应的分步骤计算操作——算子( operator )
-
例子:
-
SELECT country.name, SUM(weblog.bytes) as total FROM country INNER JOIN geoip ON country.id = geoip.country_id INNER JOIN weblog ON geoip.host = weblog.host WHERE weblog.reply = "200" and weblog.host is not null GROUP BY country.name ORDER BY total LIMIT 10
-
Optimizer:主要分为RBO和CBO两种优化策略,其中RBO是基于规则优化,CBO是基于代价优化
- SQL是一种声明式语言,用户只描述做什么,没有告诉数据库怎么做。
- 目标:找到一个正确且执行代价最小的物理执行计划。
- 查询优化器是数据库的大脑,最复杂的模块,很多相关问题都是NP的。
- 一般SQL越复杂,Join的表越多,数据量越大,查询优化的意义就越大,因为不同执行方式的性能差别可能有成百上千倍。
查询优化器的分类
遍历树顺序划分
Top-down Optimizer
-
从目标输出开始,由上往下遍历计划树,找到完整的最优执行计划
-
例子: Volcano/Cascade,SQL Server
Bottom-up Optimizer
- 从零开始,由下往上遍历计划树,找到完整的执行计划
- 例子: System R,PostgreSQL,IBM DB2
优化方法划分
Rule-based Optimizer RBO
- 根据关系代数等价语义,重写查询
- 基于启发式规则优化
- 会访问表的元信息(catalog),不会涉及具体的表数据(data)
Cost-based Optimizer ( CBO )
- 使用一个模型估算执行计划的代价,选择代价最小的执行计划
基于规则的优化策略实际上就是对语法树进行一次遍历,模式匹配能够满足特定规则的节点,再进行相应的等价转换。
RBO
谓词下推(Predicate Pushdown)
谓词下推就是将过滤操作下推到join之前进行,之后再进行join的时候,数据量将会得到显著的减少,join耗时必然降低。
左边是经过解析后的语法树,语法树中两个表先做join,之后再使用age>10进行filter。join算子是一个非常耗时的算子,耗时多少一般取决于参与join的两个表的大小,如果能够减少参与join两表的大小,就可以大大降低join算子所需的时间。
select *
from table1 a
join table2 b on a.id=b.id
where a.age>20 and b.cid=1
优化成
select *
from (select * from table1 where age>20) a
join (select * from table2 where cid=1) b
on a.id=b.id
常量累加(Constant Folding)
常量累加就是比如计算x+(100+80)->x+180,虽然是一个很小的改动,但是意义巨大。如果没有进行优化的话,每一条结果都需要执行一次100+80的操作,然后再与结果相加。优化后就不需要再次执行100+80操作。
select 100+80 as id from table1
优化成
select 180 as id from table1
列值裁剪(Column Pruning)
列值裁剪是当用到一个表时,不需要扫描它的所有列值,而是扫描只需要的id,不需要的裁剪掉。这一优化一方面大幅度减少了网络、内存数据量消耗,另一方面对于列式存储数据库来说大大提高了扫描效率。
select a.name, a.age, b.cid
from (select * from table1 where age>20) a
join (select * from table2 where cid=1) b
on a.id=b.id
优化成
select a.name, a.age, b.cid
from (select name, age, id from table1 where age>20) a
join (select id, cid from table2 where cid=1) b
on a.id=b.id
主流RBO实现一般都有几百条基于经验归纳得到的优化规则 优点:实现简单,优化速度快 缺点:不保证得到最优的执行计划
- 单表扫描:如果查询的数据分布非常不均衡,索引扫描(随机I/O)可能不如全表扫描(顺序I/O)。
- Join的实现: Hash Join vs. SortMerge Join
- 两表Hash Join :用小表构建哈希表如何识别小表 ?
- 多表Join:
- 哪种连接顺序是最优的?
- 是否要对每种组合都探索?
- N个表连接,仅仅是left-deep tree就有差不多N!种连接顺序
- 例子: N= 10->总共3, 628, 800个连接顺序
CBO
使用一个模型估算执行计划的代价,充分考虑了数据本身的特点(如大小、分布)以及操作算子的特点(中间结果集的分布及大小)及代价,从而更好的选择执行代价最小的物理执行计划。
-
执行计划的代价等于所有算子的执行代价之和
-
通过RBO得到(所有)可能的等价执行计划
算子代价包含CPU,内存,磁盘I/O,网络I/O等代价
- 和算子输入数据的统计信息有关:输入、输出结果的行数,每行大小...
- 叶子算子Scan :通过统计原始表数据得到
- 中间算子:根据一定的推导规则 ,从下层算子的统计信息推导得到
- 和具体的算子类型,以及算子的物理实现有关
- 例子: Spark Join算子代价= weight * row_ count + (1.0 - weight) * size
CBO基本原理
-
基于代价优化(CBO)原理是计算所有执行路径的代价,并挑选代价最小的执行路径。问题转化为:如何计算一条给定执行路径的代价
-
计算给定路径的执行代价,只需要计算这条路径上每个节点的执行代价,最后相加即可。问题转化为:如何计算其中任意一个节点的执行代价
-
计算任意节点的执行代价,只需要知道当前节点算子的代价计算规则以及参与计算的数据集(中间结果)基本信息(数据量大小、数据条数等)。问题转化为:如何计算中间结果的基本信息以及定义算子代价计算规则
-
算子代价计算规则是一种死的规则,可定义。而任意中间结果基本信息需要通过原始表基本信息顺着语法树一层一层往上推导得出。问题转化为:如何计算原始表基本信息以及定义推导规则
很显然,上述过程是思维过程,真正工程实践是反着由下往上一步一步执行,最终得到代价最小的执行路径。现在再把它从一个个零件组装起来:
-
首先采集原始表基本信息
-
再定义每种算子的基数评估规则,即一个数据集经过此算子执行之后基本信息变化规则。这两步完成之后就可以推导出整个执行计划树上所有中间结果集的数据基本信息
-
定义每种算子的执行代价,结合中间结果集的基本信息,此时可以得出任意节点的执行代价
-
将给定执行路径上所有算子的代价累加得到整棵语法树的代价
-
计算出所有可能语法树代价,并选出一条代价最小的