肤浅的聊聊关联子查询,数据集链接,TiDB代码,关系代数,等等

本章涉及的内容是TiDB的计算层代码,就是咱们编译完 TiDB 后在bin目录下生成的 tidb-server 的可执行文件,它是用 go 实现的,里面对 TiPD 和 TiKV实现了Mock,能够单独运行;node

用explain语句能够看到一条sql在TiDB中生成的最终执行计划,例如:咱们有一条关联子查询: select * from t1 where t1.a in (select t2.a from t2 where t2.b = t1.b);mysql

tidb> explain select * from t1 where t1.a in (select t2.a from t2 where t2.b = t1.b);
+--------------------------+-------+------+--------------------------------------------------------------------------------------------+
| id                       | count | task | operator info                                                                              |
+--------------------------+-------+------+--------------------------------------------------------------------------------------------+
| HashLeftJoin_9           | 4.79  | root | semi join, inner:TableReader_15, equal:[eq(test.t1.b, test.t2.b) eq(test.t1.a, test.t2.a)] |
| ├─TableReader_12         | 5.99  | root | data:Selection_11                                                                          |
| │ └─Selection_11         | 5.99  | cop  | not(isnull(test.t1.a)), not(isnull(test.t1.b))                                             |
| │   └─TableScan_10       | 6.00  | cop  | table:t1, range:[-inf,+inf], keep order:false, stats:pseudo                                |
| └─TableReader_15         | 5.99  | root | data:Selection_14                                                                          |
|   └─Selection_14         | 5.99  | cop  | not(isnull(test.t2.a)), not(isnull(test.t2.b))                                             |
|     └─TableScan_13       | 6.00  | cop  | table:t2, range:[-inf,+inf], keep order:false, stats:pseudo                                |
+--------------------------+-------+------+--------------------------------------------------------------------------------------------+

sql算子差很少是这个样子的:sql

上面是一个物理查询计划树,顶层算子是Join算子,t1表和t2表的数据关联操做用的是Hash Join,由于只返回左表(Outer Plan)的数据,因此用了左链接(Left Join);  Join条件是 t1.b = t2.b,t1.a = t2.a,其中内表数据取自 TableReader_15---就是t2表的数据,t2表做为 Inner Plan的数据;数据库

下层左右两边的算子相似,都是TableReader接了Selection算子,Selection算子负责过滤掉空数据;底层算子是2个最基本的扫表算子(TableScan),分别扫 t1 和 t2 的数据,返回给上层算子;session

 

TiDB代码中,explain语句和select语句相似,都是下面这样的处理逻辑:架构

    clientConn.handleQuery---处理mysql客户端来的请求
        TiDBContext.Execute
                session.execute
                    session.ParseSQL---解析SQL
                    Compiler.Compile---遍历SQL语法树,生成逻辑计划树和物理计划树
                    session.executeStatement
        clientConn.writeResultset
            clientConn.writeChunks
                ResultSet.Next---Next函数驱动数据行的获取
                clientConn.writePacket---将数据写回客户端

 explain语句结果生成的代码在这里:oracle

    ExplainExec.Next
        ExplainExec.generateExplainInfo
            Explain.RenderResult
                Explain.explainPlanInRowFormat---遍历物理执行计划树,格式化输出

 

Explain.explainPlanInRowFormat 会从根节点开始递归的访问PhysicalPlan和子树,遍历物理执行计划树,生成explain的结果集;框架

 

explain显示的信息是最终优化生成的执行计划;函数

 

咱们接着来看看中间生成的执行计划是怎样的:oop

sql的解析和生成逻辑计划树的代码在这里:

func Optimize(ctx sessionctx.Context, node ast.Node, is infoschema.InfoSchema) (plannercore.Plan, error) {
    //根据sql语法树(ast.Node)生成逻辑执行计划(LogicalPlan)
    func (b *PlanBuilder) Build(node ast.Node) (Plan, error) {
        func (b *PlanBuilder) buildExplainPlan(targetPlan Plan, format string, analyze bool, execStmt ast.StmtNode) (Plan, error) {
            func (b *PlanBuilder) buildSelect(sel *ast.SelectStmt) (p LogicalPlan, err error) {
    // DoOptimize optimizes a logical plan to a physical plan.
    //将逻辑执行计划树(LogicalPlan)转为物理执行计划树(PhysicalPlan)
    func DoOptimize(flag uint64, logic LogicalPlan) (PhysicalPlan, error) {

buildExplain 调用 buildSelect 来生成 select 语句的逻辑执行计划树 (LogicalPlan),接着调用DoOptimize来进行优化;好比相似这样的优化:对关联子查询进行改写,应用关系代数的一些规则,将 in子句 转为 semi join,由于 semi join 能够有多种方式进行高效的数据集链接操做;

而后,咱们看看优化以前的执行计划:

这个执行计划大致是这样的,执行计划的根节点是一个投影算子(Projection),取表 t1 的两个列 t1.a 和 t1.b;根节点下面是一个 Apply 算子,Apply 算子是为了知足关联子查询的须要,子查询语句中用到了外面的结果集,就像咱们示例的 sql,子查询里面的选择算子是 t2.b = t1.b,t1.b 就是关联到外部执行计划的列;

Apply关联了两个算子,一个是上图中左边的DataSource,这个DataSource算子是对t1的扫表操做;另外一个算子是上图中的 下面那个 LogicalProjection 算子,这个算子下面接了选择算子(LogicalSelection),至关于执行 t1.b = t2.b 的操做,而后是扫表算子,对 t2 进行扫表;

相对于咱们这条语句,Apply算子大概是这样的执行步骤:

        从扫表算子(TableScan)中获取一条 t1 的数据;

        从投影算子(Projection)获取一条 t2 的数据;

                投影算子调用选择算子(Selection);

                        选择算子拿的外部关联的数据 t1.a;

                        选择算子对 t2 扫表,条件是 t2.b = t1.b,获取 t2.a;其中,t1.b 相对于Apply 算子是外部计划的列;

        执行一个标量算子,判断 t1.a = t2.a;无论 t2.a 的记录有多少,只要有一条知足,即成功;

能够看到,对关联子查询,须要执行嵌套循环(NestedLoop),对 t1 的每条记录循环,传给 Apply算子, Apply算子再用这条记录去 t2 表对每条记录执行循环;

而咱们看到 explain 语句显示的最终执行计划,是用 semi join 来改写的,t1 和 t2 用 semi join 来链接,链接条件是 t1.a = t2.a and t1.b = t2.b,链接方式是左半链接;

说的直白一点就是:t1 和 t2 作 天然链接获取左表数据,而后对结果集去重;

semi join有不少种策略,mysql支持差很少5种 Semi Join;

 

TiDB执行计划优化代码在这里:

// DoOptimize optimizes a logical plan to a physical plan.
func DoOptimize(flag uint64, logic LogicalPlan) (PhysicalPlan, error) {
    ......
	logic, err := logicalOptimize(flag, logic)
    ......
	physical, err := physicalOptimize(logic)
    ......
	finalPlan := postOptimize(physical)
	return finalPlan, nil
} 

调用 PlanBuilder.buildSelect 生成的逻辑计划(LogicalPlan),会传到 DoOptimize 进行优化,DoOptimize主要作了两件事:

一件事作基于规则的优化(RBO),应用关系代数规则,对关系代数进行等价改写,生成更优的执行计划,例如:一些简单的关系代数转换---谓词下推,常量折叠,子查询展开,投影合并等等;或者一些更高级的关系代数转换,子查询转 semi join,子查询转各类链接,等等;基本上涉及到数据集链接或者有子集嵌套的sql优化起来难度要大不少;要应用到的关系代数规则也更多;

另外一件事是作基于代价的优化(CBO),这个我也不懂 😄;

 

在传统数据库如 mysql, oracle中,这些优化作完以后已是极限了,可是在NewSQL中,还有更多的优化空间,例如:谓词下推,列剪裁这种简单的规则,能够从计算节点下推到KV节点上;再好比:NewSQL中的KV是多副本和Range分片的,这样能够将计算分布在不一样的节点上;

传统数据库都是基于关系代数设计的,因此对集合链接,集合条件过滤操做很是友好;可是随着数据的增加,咱们在数据库架构上不得不使用分片来提升海量数据的读写能力;

分片能够得到优良的单条数据的读写能力,可是失去了数据库的事务性;比起牺牲事务性,更大的问题是牺牲了传统的关系代数运算,分片以后,多个表的数据链接变得异常困难,对于子查询再嵌套子查询,几乎是没法完成的任务;

为了折衷,大部分企业在分片的数据库集群下游接了一个OLAP集群,来知足传统意义上的关系代数操做;

 

关系代数最先是由早期的SQL提出的;

后来各类大数据平台Hadoop,HBase和 MapReduce 计算框架出现后,碰到不少数据集和数据过滤的问题,不得不经过关系代数来解决,因而人们将目光转回到了传统的关系代数上,Hive,Calcite这种支持SQL的外挂引擎出现了,这些引擎实现了SQL的解析和规则优化,咱们只须要将算子应用到底层的数据存储就能够了;

 

 

 

结尾;

附:关系代数几个经常使用的符号,这些符号能够对应到SQL里面的算子,算子优化是经过关系代数的等价转换来进行的:

σF(R):对集合R选择;--- 至关于 where t1.b=t2.b;

ΠA(R):对集合R投影;--- 至关于 select t1.a, t1.b; 

R×S:链接;这是关系代数最重要的操做,链接有不少种,天然链接,左链接,右链接,笛卡尔积,Semi Join 等等,子查询这种场景一般被转换为了各类链接;

R A⊗ E:Apply,对R中的每条记录,代入E中进行运算,而后把记录作各类链接;

相关文章
相关标签/搜索