MYSQL与TiDB的执行计划

Wesley13
• 阅读 779

前言

这里采用了tpc-h一个数据库的数据量来进行查询计划的对比。并借助tpc-h中的22条查询语句进行执行计划分析。

mysql采用的是标准安装,TiDB采用的是单机测试版,这里的性能结果不能说明其性能差异

本文章主要目的是对比Mysql与TiDB在执行sql查询时的差异。

mysql版本5.7   TiDB版本v2.0.0-rc.4

准备阶段

数据导入TiDB后是缺少统计信息的:

SHOW STATS_META

MYSQL与TiDB的执行计划

可以手工进行统计信息的刷新

ANALYZE TABLE nation,region,part,supplier,partsupp,customer,orders,lineitem

刷新后再次查看SHOW STATS_META

MYSQL与TiDB的执行计划

首先选择Q17做为例子,进行查询

select
    sum(l_extendedprice) / 7.0 as avg_yearly
from
    lineitem,
    part
where
    p_partkey = l_partkey
    and p_brand = 'Brand#23' # 指定品牌。 BRAND=’Brand#MN’ ,M和N是两个字母,代表两个数值,相互独立,取值在1到5之间
    and p_container = 'MED BOX' # 指定包装类型。在TPC-H标准指定的范围内随机选择
    and l_quantity < (
        select
            0.2 * avg(l_quantity)
        from
            lineitem
        where
            l_partkey = p_partkey
    );

表结构

CREATE TABLE IF NOT EXISTS part  ( P_PARTKEY     INTEGER NOT NULL,
                          P_NAME        VARCHAR(55) NOT NULL,
                          P_MFGR        CHAR(25) NOT NULL,
                          P_BRAND       CHAR(10) NOT NULL,
                          P_TYPE        VARCHAR(25) NOT NULL,
                          P_SIZE        INTEGER NOT NULL,
                          P_CONTAINER   CHAR(10) NOT NULL,
                          P_RETAILPRICE DECIMAL(15,2) NOT NULL,
                          P_COMMENT     VARCHAR(23) NOT NULL,
              PRIMARY KEY (P_PARTKEY));

CREATE TABLE IF NOT EXISTS lineitem ( L_ORDERKEY    INTEGER NOT NULL,
                             L_PARTKEY     INTEGER NOT NULL,
                             L_SUPPKEY     INTEGER NOT NULL,
                             L_LINENUMBER  INTEGER NOT NULL,
                             L_QUANTITY    DECIMAL(15,2) NOT NULL,
                             L_EXTENDEDPRICE  DECIMAL(15,2) NOT NULL,
                             L_DISCOUNT    DECIMAL(15,2) NOT NULL,
                             L_TAX         DECIMAL(15,2) NOT NULL,
                             L_RETURNFLAG  CHAR(1) NOT NULL,
                             L_LINESTATUS  CHAR(1) NOT NULL,
                             L_SHIPDATE    DATE NOT NULL,
                             L_COMMITDATE  DATE NOT NULL,
                             L_RECEIPTDATE DATE NOT NULL,
                             L_SHIPINSTRUCT CHAR(25) NOT NULL,
                             L_SHIPMODE     CHAR(10) NOT NULL,
                             L_COMMENT      VARCHAR(44) NOT NULL,
                 PRIMARY KEY (L_ORDERKEY,L_LINENUMBER),
                 CONSTRAINT FOREIGN KEY LINEITEM_FK1 (L_ORDERKEY)  references orders(O_ORDERKEY),
                 CONSTRAINT FOREIGN KEY LINEITEM_FK2 (L_PARTKEY,L_SUPPKEY) references partsupp(PS_PARTKEY, PS_SUPPKEY));

part表是20万 ,而lineitem是600万,mysql在建立约束时,会自动创建一个索引LINEITEM_FK2(L_PARTKEY, L_SUPPKEY),而TiDB则不会

mysql的查询时间大概是1秒左右,TiDB的查询时间大概是30秒。

mysql的执行计划:

MYSQL与TiDB的执行计划

 mysql首先对part表进行了查询,由于经过where的处理20万数据已经被过滤到几百条了。再与lineitem关联,最后再处理子查询。

查询过程中借用索引,所以大大加快了查询速度。

TiDB的执行计划

MYSQL与TiDB的执行计划

TiDB的执行计划比较复杂,需要转换为查询树后,才能看到比较清楚

MYSQL与TiDB的执行计划

从下而上的执行,上层收到下层的数据处理后,再向上递交

84、85、86读取part表

74、75、76读取lineitem表

77 将两者进行join

54、49、56、55汇总lineitme表,并进行分组的平均值的计算

在72进行55和77进行融合和再过滤

71、70、20、15过滤汇总和计算,得到最终结果。

但由于part表是小表,对linetiem的两次扫描和计算都很浪费。所以性能不佳。

Mysql与TiDB的执行计划规则与解读

由于大家对Mysql和TiDB的执行计划规则不了解,所以解读会比较困难,但如果掌握了如何解读执行计划,能够理解数据库的执行方式以及进行对应的优化

下面学习一下,执行计划规则与解读,我们将分别学习两种数据库的执行计划,这样也有利于进行对比

Mysql执行计划

在SQL语句前添加EXPLAIN可以查询到对应SQL的执行计划,例如:EXPLAIN select * from part

执行计划共有12列

id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra

类别

可选值

解释说明

 id

 

标识执行的顺序,按数值大小进行执行,如果数值一样大,按排列顺序执行

id列为null的就表示这是一个结果集,不需要对其进行执行

 select_type 

 

查询类型

 

 SIMPLE

简单查询(不使用UNION或子查询)

 

 PRIMARY

最外层的SELECT语句

 

 UNION

在UNION结构中的第二个及以上的SELECT语句

 

 DEPENDENT UNION

在UNION结构中的第二个及以上的SELECT语句,依赖外层查询

 

 UNION RESULT

UNION的结果

 

 SUBQUERY

子查询中的第一个SELECT语句

 

DEPENDENT SUBQUERY

子查询中的第一个SELECT语句,依赖于外层查询

 

DERIVED

子查询中FROM后面的语句

 

MATERIALIZED

物化视图子查询

 

UNCACHEABLE SUBQUERY

查询结果没有被缓存且需要重新外层查询计算每行数据的子查询

 

UNCACHEABLE UNION

结构中第二个及之后的SELECT语句且没有生成查询缓存

table

 

被查询的表名

partitions

 

表的分区,若不是分区表该字段为null,如果是分区表则显示用到的分区名称

type

 

表连接的类型

性能按排列顺序从好至坏,除了all之外,其他的type都可以使用到索引

 

system

表中只有一行数据或者是空表,且只能用于myisam和memory表。

如果是Innodb引擎表,type列在这个情况通常都是all或者index

 

const

使用唯一索引或者主键,返回记录一定是1行记录的等值where条件时,通常type是const。其他数据库也叫做唯一索引扫描

 

eq_ref

出现在要连接过个表的查询计划中,驱动表只返回一行数据,且这行数据是第二个表的主键或者唯一索引,且必须为not null,

唯一索引和主键是多列时,只有所有的列都用作比较时才会出现eq_ref

 

ref

每次和之前的表做连接时,读取所有符合条件的索引值。

如果连接使用索引的最左边前缀字段,或者索引不是主键或UNIQUE索引,会用到这种连接方式,

也就是说如果连接不能基于每个符合连接条件的索引值选择出单独的一行,则会使用这种连接方式。

 

fulltext

使用FULLTEXT索引来建立连接

全文索引的优先级很高,若全文索引和普通索引同时存在时,mysql不管代价,优先选择使用全文索引

 

ref_or_null

连接类型类似ref,除此之外,MySQL会额外扫描出包含NULL值的行。这种连接方式通常用于有子查询的情形下。

 

unique_subquery

这种连接方式在某种情况下会代替eq_ref,如value IN (SELECT primary_key FROM single_table WHERE some_expr),

这种方式使用索引查询功能代替子查询,以获得更好的执行效率。

 

index_subquery

这种连接方式类似unique_subquery。它会代替IN子查询,但是它适用于非unique索引的子查询,

用于in形式子查询使用到了辅助索引或者in常数列表,子查询可能返回重复值,可以使用索引将子查询去重

如value IN (SELECT key_column FROM single_table WHERE some_expr)

 

range

使用索引扫描出指定范围的行。key字段指示使用的索引。key_len指示索引的最大长度。ref字段会显示NULL

常见于使用>,<,is null,between ,in ,like等运算符的查询中。

 

index_merge

使用索引合并的连接方式。在这种情况下,key字段会包含使用的索引,key_len包含使用索引的最长索引部分。

表示查询使用了两个以上的索引,最后取交集或者并集,常见and ,or的条件使用了不同的索引,

官方排序这个在ref_or_null之后,但是实际上由于要读取数个索引,性能可能大部分时间都不如range

 

index 

这种索引连接类型和ALL相同,除了索引树被扫描到。这会出现在两种情况下:一、如果该索引是一个覆盖索引查询,且只扫描出索引树。

在这种情况下,Extra字段会显示Using index。二、通过索引顺序来执行全表扫描。

 

all

全表扫描数据文件,然后再在server层进行过滤返回符合要求的记录。

possible_keys  

 

 可供选择的索引。可以有多个用逗号分隔

 key

 

实际选择的索引

select_type为index_merge时,这里可能出现两个以上的索引,其他的select_type这里只会出现一个

 key_len

 

用于处理查询的索引长度,如果是单列索引,那就整个索引长度算进去,

如果是多列索引,那么查询不一定都能使用到所有的列,具体使用到了多少个列的索引,这里就会计算进去,没有使用到的列,这里不会计算进去。

留意下这个列的值,算一下你的多列索引总长度就知道有没有使用到所有的列了。

要注意,mysql的ICP特性使用到的索引不会计入其中。另外,key_len只计算where条件用到的索引长度,而排序和分组就算用到了索引,也不会计算到key_len中。

 ref

 

如果是使用的常数等值查询,这里会显示const,如果是连接查询,被驱动表的执行计划这里会显示驱动表的关联字段

如果是条件使用了表达式或者函数,或者条件列发生了内部隐式转换,这里可能显示为func

例如:tpch.partsupp.PS_PARTKEY,tpch.partsupp.PS_SUPPKEY表示使用了partsupp表的两个字段与当前表的索引进行比较

 rows

 

执行计划中估算的扫描行数,不是精确值

 filtered 

 

表示存储引擎返回的数据在server层过滤后,剩下多少满足查询的记录数量的比例,注意是百分比,不是具体记录数。

 extra

 

查询的描述信息,种类非常多。这里只列一些常用的。一个查询中可以有多个种类,使用逗号进行分隔

 

using temporary

表示使用了临时表存储中间结果。临时表可以是内存临时表和磁盘临时表,执行计划中看不出来,需要查看status变量,used_tmp_table,used_tmp_disk_table才能看出来。

 

using where

表示存储引擎返回的记录并不是所有的都满足查询条件,需要在server层进行过滤。查询条件中分为限制条件和检查条件,

5.6之前,存储引擎只能根据限制条件扫描数据并返回,然后server层根据检查条件进行过滤再返回真正符合查询的数据。

5.6.x之后支持ICP特性,可以把检查条件也下推到存储引擎层,不符合检查条件和限制条件的数据,直接不读取,这样就大大减少了存储引擎扫描的记录数量。extra列显示using index condition

 

distinct

在select部分使用了distinc关键字

 

no tables used

不带from字句的查询或者From dual查询

 

using index

查询时不需要全表查询,直接通过索引就可以获取查询的数据。

 

using intersect

表示使用and的各个索引的条件时,该信息表示是从处理结果获取交集

 

using union

表示使用or连接各个使用索引的条件时,该信息表示从处理结果获取并集

 

using filesort

排序时无法使用到索引时,就会出现这个。常见于order by和group by语句中

 

firstmatch(tb_name)

5.6.x开始引入的优化子查询的新特性之一,常见于where字句含有in()类型的子查询。如果内表的数据量比较大,就可能出现这个

 

loosescan(m..n)

5.6.x之后引入的优化子查询的新特性之一,在in()类型的子查询中,子查询返回的可能有重复记录时,就可能出现这个

TiDB执行计划

TiDB的数据存储与ti-kv,而数据处理在ti-server分属与不同应用。ti-server需要数据时,需要先调用ti-kv进行扫描,然后再从ti-kv拿到数据。

在ti-server层面需要执行的过滤/汇总/分组,送到ti-kv扫描时去运行就可以减少传输的数据量,加快处理速度。这个操作称为“下推”

同Mysql一样,在SQL语句前添加EXPLAIN可以查询到对应SQL的执行计划,例如:EXPLAIN select * from part

执行计划共有6列

id | parents | children | task | operator info | count

类别

可选值

解释说明

id

 

operator 的 id,在整个执行计划中唯一的标识一个 operator。

id由两部分组成:操作类型+序号。

操作类型有很多种,也代表提供了不同的处理能力,如TableReader 和 TableScan等等

序号是创立执行计划时生成的,大小无作用,只是为了避免重复。

执行计划从上至下的方式运行,任务内可能会有并行,例如到多个ti-kv上提取数据

任务间也会有并行,具体看任务实现,union算子就会驱动所有的child同时执行。

而对于无关联的任务,可能就不会并行了。(未得到官方确认)

 parents

 

这个 operator 的 parent。目前的执行计划可以看做是一个 operator 构成的树状结构,

数据从 child 流向 parent,每个 operator 的 parent 有且仅有一个

 children

 

 这个 operator 的 children,也即是这个 operator 的数据来源

 task

 

 当前的执行计划在 task 级别的拓扑关系是一个 root task 后面可以跟许多 cop task,

root task 使用 cop task 的输出结果作为输入。

cop task 中执行的也即是 tidb 下推到 tikv 上的任务,每个 cop task 分散在 tikv 集群中,由多个进程共同执行

 

root

在tidb-server 上执行的任务

 

cop

在tikv上执行的任务

 count

 

 预计当前 operator 将会输出的数据条数,基于统计信息以及 operator 的执行逻辑估算而来

 operator info

 

 操作类型中会输出的明细信息,需要结合操作类型一起看

下面这个表格专门对operator相关内容进行说明

类别

操作类型

info信息样例值

解释说明

数据读

 

 

 

 

TableScan

 

在ti-kv上进行数据扫描

 

 

table:part

操作的表名,这里指的是操作part表

 

 

range:[-inf,+inf]

range的范围从-inf开始到+inf结束。如果没有开始或者结束使用,例如range:[,+inf]

 

 

keep order:false

是否进行排序:true排序,false不排序

 

TableReader

 

ti-server从ti-kv读取数据的操作

 

 

data:Selection_85

这里是指Ti-Server拿到ti-kv扫描结果Selection_85的数据

索引读

 

 

 

IndexReader

 

直接从索引中读取索引列,适用于 SQL 语句中仅引用了该索引相关的列或主键;

 

IndexLookUp

index:IndexScan_74,

table:TableScan_75

表示从索引中过滤部分数据,仅返回这些数据的 Handle ID,通过 Handle ID 再次查找表数据,

这种方式需要两次从 TiKV 获取数据。Index 的读取方式是由优化器自动选择的。

 

IndexScan

 

官网没有说明

 

 

table:partsupp, 

操作的表

 

 

index:PS_PARTKEY, PS_SUPPKEY

索引列

 

 

range:[,+inf]

范围

过滤

 

keep order:false

 排序

 

Selection

 

表示 SQL 语句中的选择条件,通常出现在 WHERE/HAVING/ON 子句中。

如果task为cop,则表示比较选择条件进行了下推。

 

 

eq(tpch.part.p_brand, Brand#23)

内容一般是选择的条件

包括:eg/le/lt/ge

映射

 

 

 

 

Projection

 

对应 SQL 语句中的 SELECT 列表,功能是将每一条输入数据映射成新的输出数据。

 

 

tpch.part.p_container, mul(0.2, 7_col_0)

一般是映射的字段列表

 

 

offset:0

 

 

 

count:10

 

聚集

Aggregation

 

 

对应 SQL 语句中的 Group By 语句或者没有 Group By 语句但是存在聚合函数,例如 count 或 sum 函数等。

 

HashAgg

 

是基于哈希的聚合算法,如果 Hash Aggregation 紧邻 Table 或者 Index 的读取算子,

则聚合算子会在 TiKV 端进行预聚合,以提高计算的并行度和减少网络开销。

 

 

group by:tpch.lineitem.l_partkey

分组 

 

 

funcs:avg(tpch.lineitem.l_quantity)

函数

 

StreamAgg

 

官方没有说明

 

 

funcs:sum(tpch.lineitem.l_extendedprice)

函数

联合

join

 

 

Hash Join 的原理是将参与连接的小表预先装载到内存中,读取大表的所有数据进行连接。
Sort Merge Join 会利用输入数据的有序信息,同时读取两张表的数据并依次进行比较。
Index Look Up Join 会读取外表的数据,并对内表进行主键或索引键查询

部分join方式还没有遇到过,暂时没有添加进来

 

IndexJoin

 

官方没有说明

 

 

inner join

 

 

 

index:IndexScan_74

inner key:tpch.lineitem.l_partkey

 

 

 

outer:TableReader_86 

outer key:tpch.part.p_partkey

 

 

HashLeftJoin

 

官方没有说明

 

 

inner join

 

 

 

left outer join

 

 

 

inner:HashAgg_55

 

 

 

equal:[eq(tpch.part.p_partkey, tpch.lineitem.l_partkey)]

 

Apply

 

 

用来描述子查询的一种算子,行为类似于 Nested Loop,即每次从外表中取一条数据,

带入到内表的关联列中,并执行,最后根据 Apply 内联的 Join 算法进行连接计算。

Apply 一般会被查询优化器自动转换为 Join 操作。用户在编写 SQL 的过程中应尽量避免 Apply 算子的出现。

 

暂时没有遇到过

 

 

其它

 

 

 

 

Sort

tpch.lineitem.l_returnflag:asc

排序处理,一般是字段名:asc(desc)

执行器的接口在executor.go文件中,实现一般是*Exec命名的

type executor interface {
    SetSrcExec(executor)
    GetSrcExec() executor
    ResetCounts()
    Counts() []int64
    Next(ctx context.Context) ([][]byte, error)
    // Cursor returns the key gonna to be scanned by the Next() function.
    Cursor() (key []byte, desc bool)
}

indexScan的实现

type indexScanExec struct {
    *tipb.IndexScan
    colsLen        int
    kvRanges       []kv.KeyRange
    startTS        uint64
    isolationLevel kvrpcpb.IsolationLevel
    mvccStore      MVCCStore
    cursor         int
    seekKey        []byte
    pkStatus       int
    start          int
    counts         []int64

    src executor
}

练习

联系解读一下,以下sql的执行计划

select
    sum(l_extendedprice * l_discount) as revenue # 潜在的收入增加量
from
    lineitem
where
    l_shipdate >= '1994-01-01' # DATE是从[1993, 1997]中随机选择的一年的1月1日
    and l_shipdate < date_add('1994-01-01', interval '1' year) # 一年内
    and l_discount between 0.06 - 0.01 and 0.06 + 0.01
    and l_quantity < 24; # QUANTITY在区间[24, 25]中随机选择

MYSQL与TiDB的执行计划

select
    100.00 * sum(case
        when p_type like 'PROMO%' # 促销零件
            then l_extendedprice * (1 - l_discount) # 某一特定时间的收入
        else 0
    end) / sum(l_extendedprice * (1 - l_discount)) as promo_revenue
from
    lineitem,
    part
where
    l_partkey = p_partkey
    and l_shipdate >= '1995-09-01' # DATE是从1993年到1997年中任一年的任一月的一号
    and l_shipdate < date_add('1995-09-01', interval '1' month);

MYSQL与TiDB的执行计划

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
Python3:sqlalchemy对mysql数据库操作,非sql语句
Python3:sqlalchemy对mysql数据库操作,非sql语句python3authorlizmdatetime2018020110:00:00coding:utf8'''
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这