【笔记】SQL调优指南—执行计划介绍

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云原生数据库 PolarDB 分布式版,标准版 2核8GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 本文介绍如何使用查询执行计划并介绍一些基本的算子含义和实现。

算子介绍

含义 物理算子
下发给DN的算子 LogicalView,LogicalModifyView,PhyTableOperation, IndexScan
连接(Join) BKAJoin,NLJoin,HashJoin,SortMergeJoin,HashSemiJoin,SortMergeSemiJoin,MaterializedSemiJoin
排序 MemSort,TopN, MergeSort
聚合(Group By) HashAgg,SortAgg
数据重分布或者聚合 Exchange Gather
过滤 Filter
投影 Project
求并集 Union
设置结果集输出行数(Limit/Offset...Fetch) Limit
窗口函数 OverWindow

LogicalView

LogicalView是从存储层MySQL数据源拉取数据的算子,类似于其他数据库中的TableScan或IndexScan,但支持更多的下推。LogicalView中包含下推的SQL语句和数据源信息,更像一个视图。其中下推的SQL可能包含多种算子,如Project、Filter、聚合、排序、Join和子查询等。下述示例为您展示EXPLAIN中LogicalView的输出信息及其含义:


mysql> explain select * From sbtest1 where id > 1000;
Gather(concurrent=true)
   LogicalView(tables="[0000-0031].sbtest1_[000-127]", shardCount=128, sql="SELECT * FROM `sbtest1` WHERE (`id` > ?)")

LogicalView 的信息由三部分构成:

  • tables:存储层MySQL对应的表名,以英文句号(.)分割,英文句号(.)之前是分库对应的编号,之后是表名及其编号,如[000-127]表示表名编号从000到127的所有表。
  • shardCount:需访问的分表总数,该示例会访问从000到127共计128张分表。
  • sql:下发至存储层MySQL的SQL模版,PolarDB-X在执行时会将表名替换为物理表名,参数化的常量问号(?)替换成实际参数,详情请参见执行计划管理

IndexScan

IndexScan和LogicalView一样也是表示从存储层MySQL数据源拉取数据的算子,扫描的是索引表。下述示例为您展示EXPLAIN中IndexScan的输出信息及其含义:


mysql> explain select * from sequence_one_base where integer_test=1;
|
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| IndexScan(tables="DRDS_POLARX1_QATEST_APP_000000_GROUP.gsi_sequence_one_index_3a0A_01", sql="SELECT `pk`, `integer_test`, `varchar_test`, `char_test`, `blob_test`, `tinyint_test`, `tinyint_1bit_test`, `smallint_test`, `mediumint_test`, `bit_test`, `bigint_test`, `float_test`, `double_test`, `decimal_test`, `date_test`, `time_test`, `datetime_test`, `timestamp_test`, `year_test`, `mediumtext_test` FROM `gsi_dml_sequence_one_index_index1` AS `gsi_dml_sequence_one_index_index1` WHERE (`integer_test` = ?)") |

作为谓词条件对索引表做裁剪,所以实际上我们会去生成IndexScan算子,表示只会扫描gsi_sequence_one_index索引表一个分片。上述SQL正常应该扫描sequence_one_base表,由于integer_test不是分区键,需要扫描sequence_one_base所有的分片。但是由于sequence_one_base表在integer_test列上存在全局二级索引gsi_sequence_one_index,将

Gather

Gather将多份数据合并成同份数据。上面的例子中,Gather将各个分表上查询到的数据合并成一份。Gather通常出现在LogicalView上方,表示收集合并各个分表的数据。

Exchange

Exchange是一个逻辑算子,本身不对计算过程中的数据做计算,只是将输入的数据做重分布后,输出给下游算子。一般重分布策略分为

  • SINGLETON: 将上游多份数据进行合并输出,这种重分布策略等价与Gather
  • HASH_DISTRIBUTED: 将上游输入的数据按照某些列做repartition,常见于包含Join和Agg的执行计划中。
  • BROADCAST_DISTRIBUTED: 将上游相同一份数据分发成多份,广播给下游多个节点,主要应用于MPP执行计划中。

MergeSort

MergeSort即归并排序算子,表示将有序的数据流进行归并排序,合并成一个有序的数据流。例如:


mysql> explain select * from sbtest1 where id > 1000 order by id limit 5,10; 
MergeSort(sort="id ASC", offset=?1, fetch=?2)
LogicalView(tables="[0000-0031].sbtest1_[000-127]", shardCount=128, sql="SELECT * FROM `sbtest1` WHERE (`id` > ?) ORDER BY `id` LIMIT (? + ?)")

MergeSort算子包含三部分内容:

  • sort:表示排序字段以及排列顺序,id ASC表示按照ID字段递增排序,DESC表示递减排序。
  • offset:表示获取结果集时的偏移量,例子中被参数化了,实际值为5。
  • fetch:表示最多返回的数据行数。与offset类似,同样是参数化的表示,实际对应的值为10。

Project

Project表示投影操作,即从输入数据中选择部分列输出,或者对某些列进行转换(通过函数或者表达式计算)后输出,当然也可以包含常量。


mysql> explain select '你好, DRDS', 1 / 2, CURTIME(); 

Project(你好, DRDS="_UTF-16'你好, DRDS'", 1 / 2="1 / 2", CURTIME()="CURTIME()")

Project的计划中包括每列的列名及其对应的列、值、函数或者表达式。

Filter

Filter表示过滤操作,其中包含一些过滤条件。该算子对输入数据进行过滤,若满足条件,则输出,否则丢弃。如下是一个较复杂的例子,包含了以上介绍的大部分算子。


mysql> explain select k, avg(id) avg_id from sbtest1 where id > 1000 group by k having avg_id > 1300;
Filter(condition="avg_id > ?1")
Project(k="k", avg_id="sum_pushed_sum / sum_pushed_count")
SortAgg(group="k", sum_pushed_sum="SUM(pushed_sum)", sum_pushed_count="SUM(pushed_count)")
MergeSort(sort="k ASC")
LogicalView(tables="[0000-0031].sbtest1_[000-127]", shardCount=128, sql="SELECT `k`, SUM(`id`) AS `pushed_sum`, COUNT(`id`) AS `pushed_count` FROM `sbtest1` WHERE (`id` > ?) GROUP BY `k` ORDER BY `k`")

WHERE id > 1000中的条件没有对应的Filter算子,是因为这个算子最终被下推到了LogicalView中,可以在LogicalView的SQL中看到WHERE (id > ?) 。

Union All与Union Distinct

顾名思义,Union All对应UNIONALL,Union Distinct对应UNIONDISTINCT。该算子通常有2个或更多个输入,表示将多个输入的数据合并在一起。例如:


mysql> explain select  From sbtest1 where id > 1000 union distinct select  From sbtest1 where id < 200;
UnionDistinct(concurrent=true)
Gather(concurrent=true)
LogicalView(tables="[0000-0031].sbtest1_[000-127]", shardCount=128, sql="SELECT * FROM `sbtest1` WHERE (`id` > ?)")
Gather(concurrent=true)
LogicalView(tables="[0000-0031].sbtest1_[000-127]", shardCount=128, sql="SELECT * FROM `sbtest1` WHERE (`id` < ?)")

LogicalModifyView

LogicalView表示从底层数据源获取数据的算子,与之对应的,LogicalModifyView表示对底层数据源的修改算子,其中也会记录一个SQL语句,该SQL可能是INSERT、UPDATE或者DELETE。


mysql> explain update sbtest1 set c='Hello, DRDS' where id > 1000;
LogicalModifyView(tables="[0000-0031].sbtest1_[000-127]", shardCount=128, sql="UPDATE `sbtest1` SET `c` = ? WHERE (`id` > ?)"


mysql> explain delete from sbtest1 where id > 1000;
LogicalModifyView(tables="[0000-0031].sbtest1_[000-127]", shardCount=128, sql="DELETE FROM `sbtest1` WHERE (`id` > ?)")

LogicalModifyView查询计划的内容与LogicalView类似,包括下发的物理分表,分表数以及SQL模版。同样,由于开启了执行计划缓存,对SQL做了参数化处理,SQL模版中的常量会用?替换。

PhyTableOperation

PhyTableOperation表示对某个物理分表直接执行一个操作。


说明 通常情况下,该算子仅用于INSERT语句。但当路由分发分到一个分片时,该算子也会出现在SELECT语句中。


mysql> explain insert into sbtest1 values(1, 1, '1', '1'),(2, 2, '2', '2');
PhyTableOperation(tables="SYSBENCH_CORONADB_1526954857179TGMMSYSBENCH_CORONADB_VGOC_0000_RDS.[sbtest1_001]", sql="INSERT INTO ? (`id`, `k`, `c`, `pad`) VALUES(?, ?, ?, ?)", params="`sbtest1_001`,1,1,1,1")
PhyTableOperation(tables="SYSBENCH_CORONADB_1526954857179TGMMSYSBENCH_CORONADB_VGOC_0000_RDS.[sbtest1_002]", sql="INSERT INTO ? (`id`, `k`, `c`, `pad`) VALUES(?, ?, ?, ?)", params="`sbtest1_002`,2,2,2,2")

示例中,INSERT插入两行数据,每行数据对应一个PhyTableOperation算子。PhyTableOperation算子的内容包括三部分:

  • tables:物理表名,仅有唯一一个物理表名。
  • sql:SQL模版,该SQL模版中表名和常量均被参数化,用?替换,对应的参数在随后的params中给出。
  • params:SQL模版对应的参数,包括表名和常量。

执行计划介绍

一条SQL进入到PolarDB-X分布式数据库后,经过解析优化,会生成一颗可运行的执行计划。该执行计划是按照算子执行过程中的依赖关系组成。一般通过执行计划树,可以窥探SQL在数据库内部是如何高效运行的。为了方便理解,这里罗列几个例子。

示例1


mysql> explain select count(*) from lineitem group by L_LINESTATUS;

| HashAgg(group="L_LINESTATUS", count()="SUM(count())") |
| Exchange(distribution=hash[0], collation=[]) |
| LogicalView(tables="[000000-000003].lineitem_[00-15]", shardCount=16, sql="SELECT `L_LINESTATUS`, COUNT() AS `count()` FROM `lineitem` AS `lineitem` GROUP BY `L_LINESTATUS`")

Exchange: 汇总LogicalView返回的数据,按照L_LINESTATUS字段做重分布式,输出给下游算子;由于group by 的列和表lineitem分区键不对齐,group by是没法完全下推给DN执行。所以group by会拆分成两阶段,将partition agg下推给DN,先做部分聚合;然后在CN层将数据做重分布式,再做一次最终的聚合,输出结果。

示例2


mysql> explain select * from lineitem, orders where L_ORDERKEY= O_ORDERKEY;
|
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| HashJoin(condition="O_ORDERKEY = L_ORDERKEY", type="inner") |
| Exchange(distribution=hash[0], collation=[]) |
| LogicalView(tables="[000000-000003].lineitem_[00-15]", shardCount=16, sql="SELECT `L_ORDERKEY`, `L_PARTKEY`, `L_SUPPKEY`, `L_LINENUMBER`, `L_QUANTITY`, `L_EXTENDEDPRICE`, `L_DISCOUNT`, `L_TAX`, `L_RETURNFLAG`, `L_LINESTATUS`, `L_SHIPDATE`, `L_COMMITDATE`, `L_RECEIPTDATE`, `L_SHIPINSTRUCT`, `L_SHIPMODE`, `L_COMMENT` FROM `lineitem` AS `lineitem`") |
| Exchange(distribution=hash[0], collation=[]) |
| LogicalView(tables="[000000-000003].orders_[00-15]", shardCount=16, sql="SELECT `O_ORDERKEY`, `O_CUSTKEY`, `O_ORDERSTATUS`, `O_TOTALPRICE`, `O_ORDERDATE`, `O_ORDERPRIORITY`, `O_CLERK`, `O_SHIPPRIORITY`, `O_COMMENT` FROM `orders` AS `orders`")

示例2是典型的两张表做关联(join),由于两表分区键没对齐,所有join没有下推,整个执行是将两个表数据都扫描出来,在CN层做关联计算。

  • LogicalView: 扫描表数据。
  • Exchange: 汇总LogicalView返回的数据,分布按照关联条件的列做重分布式,输出给下游Join算子。
  • HashJoin: 接受两边输入,通过构建HashTable的方式来计算关联结果。

示例3


mysql> explain select * from lineitem, orders where L_LINENUMBER= O_ORDERKEY;
| Gather(concurrent=true) |
| LogicalView(tables="[000000-000003].lineitem_[00-15],orders_[00-15]", shardCount=16, sql="SELECT `lineitem`.`L_ORDERKEY`, `lineitem`.`L_PARTKEY`, `lineitem`.`L_SUPPKEY`, `lineitem`.`L_LINENUMBER`, `lineitem`.`L_QUANTITY`, `lineitem`.`L_EXTENDEDPRICE`, `lineitem`.`L_DISCOUNT`, `lineitem`.`L_TAX`, `lineitem`.`L_RETURNFLAG`, `lineitem`.`L_LINESTATUS`, `lineitem`.`L_SHIPDATE`, `lineitem`.`L_COMMITDATE`, `lineitem`.`L_RECEIPTDATE`, `lineitem`.`L_SHIPINSTRUCT`, `lineitem`.`L_SHIPMODE`, `lineitem`.`L_COMMENT`, `orders`.`O_ORDERKEY`, `orders`.`O_CUSTKEY`, `orders`.`O_ORDERSTATUS`, `orders`.`O_TOTALPRICE`, `orders`.`O_ORDERDATE`, `orders`.`O_ORDERPRIORITY`, `orders`.`O_CLERK`, `orders`.`O_SHIPPRIORITY`, `orders`.`O_COMMENT` FROM `lineitem` AS `lineitem` INNER JOIN `orders` AS `orders` ON (`lineitem`.`L_LINENUMBER` = `orders`.`O_ORDERKEY`)") |

示例3也是典型的两张表做关联,由于两表分区键对齐,所有join下推到各个分片的DN来执行,上层的CN节点只需要通过Gather算子将DN返回的结果汇总输出。

示例4


mysql> explain select * from gsi_dml_unique_multi_index_base where integer_test=1;                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Project(pk="pk", integer_test="integer_test", varchar_test="varchar_test", char_test="char_test", blob_test="blob_test", tinyint_test="tinyint_test", tinyint_1bit_test="tinyint_1bit_test", smallint_test="smallint_test", mediumint_test="mediumint_test", bit_test="bit_test", bigint_test="bigint_test", float_test="float_test", double_test="double_test", decimal_test="decimal_test", date_test="date_test", time_test="time_test", datetime_test="datetime_test", timestamp_test="timestamp_test", year_test="year_test", mediumtext_test="mediumtext_test") |
| BKAJoin(condition="pk = pk", type="inner") |
| IndexScan(tables="DRDS_POLARX1_QATEST_APP_000000_GROUP.gsi_dml_unique_multi_index_index1_a0ol_01", sql="SELECT `pk`, `integer_test`, `varchar_test`, `char_test`, `bit_test`, `bigint_test`, `double_test`, `date_test` FROM `gsi_dml_unique_multi_index_index1` AS `gsi_dml_unique_multi_index_index1` WHERE (`integer_test` = ?)") |
| Gather(concurrent=true) |
| LogicalView(tables="[000000-000003].gsi_dml_unique_multi_index_base_[00-15]", shardCount=16, sql="SELECT `pk`, `blob_test`, `tinyint_test`, `tinyint_1bit_test`, `smallint_test`, `mediumint_test`, `float_test`, `decimal_test`, `time_test`, `datetime_test`, `timestamp_test`, `year_test`, `mediumtext_test` FROM `gsi_dml_unique_multi_index_base` AS `gsi_dml_unique_multi_index_base` WHERE ((`integer_test` = ?) AND (`pk` IN (...)))") |
| HitCache:true

这个例子很有意思,SQL本身只是带有谓词的简单查询,结果从执行计划看是两表做关联(BKAJoin)。主要是gsi_dml_unique_multi_index_base在列上integer_test有全局二级索引,命中索引可以减少扫描代价,但这个索引并不是覆盖索引,所以需要有回表操作。

  • IndexScan: 根据integer_test=1扫描出索引表gsi_dml_unique_multi_index_index1_a0ol_01数据。
  • BKAJoin: 收集IndexScan的结果,通过该算子和主表gsi_dml_unique_multi_index_base做回表关联,获取其他列值。

关于BKAJoin更多的介绍可以查看《分布式数据库如何实现 Join》


说明 通常情况下,通过查询执行计划,可以查看到是否命中了全局二级索引等信息。但是对于下推部分的SQL,还可以通过explain execute 指令,获取物理SQL在DN上的执行情况,比如是否命中了DN的局部索引。

相关文章
|
存储 NoSQL 关系型数据库
什么是列式存储,一文秒懂
云栖号资讯:【点击查看更多行业资讯】在这里您可以找到不同行业的第一手的上云资讯,还在等什么,快来! 从数据存储讲起 我们最先接触的数据库系统,大部分都是行存储系统。大学的时候学数据库,老师让我们将数据库想象成一张表格,每条数据记录就是一行数据,每行数据包含若干列。
什么是列式存储,一文秒懂
|
Java 数据安全/隐私保护 Spring
新一代开源配置中心 - Apollo
Apollo(阿波罗)是携程框架部门研发的配置管理平台,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性。服务端基于Spring Boot和Spring Cloud开发,打包后可以直接运行,不需要额外安装Tomcat等应用容器。
27910 0
|
8月前
|
数据可视化 前端开发 JavaScript
地图可视化的艺术:深入比较Mapbox、OpenLayers、Leaflet和Cesium,不同场景下应如何选择地图库
选择合适的地图库取决于项目的需求、团队的技术栈以及预算等因素。简单来说,新手可以从leaflet入手;GIS开发使用openlayers会更顺手一些;mapbox适应大多数2D和2.5D场景,可视化效果好,但是不开源;cesium更侧重于3D场景。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出
|
Python
Python实现hellokitty
Python实现hellokitty
558 0
|
算法 C++
2022年第十三届蓝桥杯大赛C/C++语言B组省赛题解
2022年第十三届蓝桥杯大赛C/C++语言B组省赛题解
289 5
|
测试技术
《 嵌入式系统设计与实践》一一1.2 嵌入式系统开发
本节书摘来自华章出版社《 嵌入式系统设计与实践 》一 书中的第1章,第1 . 节,作者:Elecia White 著 ,更多章节内容可以访问云栖社区“华章计算机”公众号查看
4851 0
|
Android开发 iOS开发
如何在 Gmail 中将所有电子邮件标记为已读
如何在 Gmail 中将所有电子邮件标记为已读
|
存储 Cloud Native 关系型数据库
云原生点亮数据上云之路 | 数据库全面进入云原生+分布式时代
随着企业数字协同成为常态,数据类型和计算的复杂性空前增加,AI覆盖的场景越来越丰富,如何用好大数据就显得十分关键。对于企业来讲,数据已经成为一种资产,只有让数据更智能,让数据流动起来,才能够真正地发挥数据的价值。为此,阿里云构建了基于数据生产、存储、分析、计算和应用的一体化平台。下面让我们正式进入阿里云端世界的核心去看一看,开启第二篇章:融和·全栈数据工厂。
4386 0
云原生点亮数据上云之路 | 数据库全面进入云原生+分布式时代
|
安全 物联网 区块链
阿里云农产品区块链溯源服务为农产品建立信任体系
农产品溯源系统,全程商品种养殖管理记录,为农产品建立信任体系,消费者扫码了解商品全部溯源信息,消除安全担忧,获取消费者信任.
阿里云农产品区块链溯源服务为农产品建立信任体系
|
弹性计算 运维 负载均衡
一文读懂 - 云上用户如何灵活应用定制化网络服务
在将传统数据中心业务迁移上云的过程中,如何将云下基于不同业务场景和设备角色灵活变化的网络配置基于云上网络统一服务能力进行转换,用户及其业务架构通常会面临诸多的挑战。阿里云混合云网络技术团队和阿里云网络产品团队自主创新研发的【开放网络服务平台】(简称:ONSP)构建在阿里云飞天洛神网络系统上,实现了飞天洛神网络和三方网络生态的充分融合,从而优化了企业客户生态服务的体验,更好的帮助客户迁云上云。
2000 0
一文读懂 - 云上用户如何灵活应用定制化网络服务