草庐IT

Explain:你见过这样的Sql吗?

萨科拉 2023-03-28 原文

上一篇我们讲到Mysql索引底层逻辑,为了了解后续sql知识,我们还是需要先学习一下相关“工具”得使用

一、Explain介绍

EXPLAIN是MySQl必不可少的一个分析工具,主要用来测试sql语句的性能及对sql语句的优化,或者说模拟优化器执行SQL语句。在select语句之前增加explain关键字,执行后MySQL就会返回执行计划的信息,而不是执行sql。

注意:如果from中包含子查询,仍会执行子查询,将结果放入到临时表中

Explain的用法还是很简单的,类似一个关键字,无需记住什么语法相关的东西,我们主要来看他的输出,接下来我们看一下他的常见输出并分情况进行讨论:

首先我们创建三张表并插入一些相关数据

-- ----------------------------

-- Table structure for user

-- ----------------------------

DROP TABLE IF EXISTS `user`;

CREATE TABLE `user` (

  `id` int(11) NOT NULL,

  `name` varchar(45) DEFAULT NULL,

  `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,

  `create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,

  PRIMARY KEY (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- ----------------------------

-- Records of user

-- ----------------------------

INSERT INTO `user` VALUES ('1', '小明', '2022-09-15 15:52:18', '2022-09-13 15:52:21');

INSERT INTO `user` VALUES ('2', '小红', '2022-09-14 15:52:35', '2022-09-06 15:52:41');

INSERT INTO `user` VALUES ('3', '小可', '2022-09-15 15:52:55', '2022-09-15 15:52:58');

INSERT INTO `user` VALUES ('4', '张三', '2022-09-13 15:53:13', '2022-09-14 15:53:17');

INSERT INTO `user` VALUES ('5', '李四', '2022-09-15 15:53:35', '2022-09-15 15:53:37');

INSERT INTO `user` VALUES ('6', '王五', '2022-09-15 15:53:47', '2022-09-15 15:53:49');

INSERT INTO `user` VALUES ('7', '小小', '2022-09-15 15:54:06', '2022-09-15 15:54:08');

-- ----------------------------

-- Table structure for address

-- ----------------------------

DROP TABLE IF EXISTS `address`;

CREATE TABLE `address` (

  `id` int(11) NOT NULL AUTO_INCREMENT,

  `name` varchar(45) DEFAULT NULL,

  PRIMARY KEY (`id`),

  KEY `idx_name` (`name`)

) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;

-- ----------------------------

-- Records of address

-- ----------------------------

INSERT INTO `address` VALUES ('2', '上海');

INSERT INTO `address` VALUES ('1', '北京');

INSERT INTO `address` VALUES ('3', '北京');

INSERT INTO `address` VALUES ('5', '南京');

INSERT INTO `address` VALUES ('6', '武汉');

INSERT INTO `address` VALUES ('4', '深圳');

INSERT INTO `address` VALUES ('7', '长沙');

-- ----------------------------

-- Table structure for user_address

-- ----------------------------

DROP TABLE IF EXISTS `user_address`;

CREATE TABLE `user_address` (

  `id` int(11) NOT NULL,

  `address_id` int(11) NOT NULL,

  `user_id` int(11) NOT NULL,

  `remark` varchar(255) DEFAULT NULL,

  PRIMARY KEY (`id`),

  KEY `idx_user_address_id` (`address_id`,`user_id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8;

首先来分析一下我们的表结构user表我们只有一个聚集索引,其实就是主键索引,那他的索引结构就只是id列,如下图所示:

 

 

 

address表不单单只有我们的聚集索引,也添加了一个二级索引,索引结构如下图所示:

 

user_address也不仅只有聚集索引,也有一个联合索引,相对应的列分别是address_id和user_id,索引结构如下图所示:

 

二、Explain中的列

1id

id列的编号是select的序列号,有几个select就有几个id,并且id的顺序是按select出现的顺序增长的。

id列越大执行优先级越高,id相同则从上向下执行,id为null最后执行。

2select_type

2.1、Simple:简单查询,查询不包含子查询和union

EXPLAIN SELECT * from `user` WHERE id = 1;

2.2、Primary:复杂查询中最外层的select

2.3、Subquery:包含在select中的子查询(不在from子句中)

2.4、Derived:包含在from子句中的子查询。Mysql会将结果存放到一个临时表中,也成为派生表(derived的英文含义)

EXPLAIN SELECT (SELECT 1 FROM `user` WHERE id = 1) FROM (SELECT * FROM address WHERE id =1) der;

注意:这里要先关闭一下mysql5.7新特性对衍生表的合并优化

set session optimizer_switch='derived_merge=off';

3Table

对应正在访问的哪一个表,显示的是表明或者是别命,可能是临时表或者union合并结果集如果是具体的表名,则表明从实际的物理表中获取数据,当然也可以是表的别命

表明是derived的形式,表明使用了id为N的查询产生的衍生表,如下图所示:

derived后面的id号为3,表明使用id为3的这个查询产生的衍生表,也就是(SELECT * FROM address WHERE id =1)这个查询语句结果集所在的的临时表

当有union result的时候,表名是union n1,n2等形式, n1,n2表示参与union的id

NULL:mysql能够在优化阶段分解查询语句,在执行阶段用不着在访问表或索引。例如:在索引列中取最小值,可以单独查找索引来完成,不需要在执行时访问表

上面的语句我们是通过主键id的方式来查找的,如果看过我们上一篇博客的读者就能够明白,此时直接查索引就可以了,找到最小的,无需查表。

4possible_keys

显示可能应用在这张表中的索引,一个或多个,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询实际使用

5key

实际使用的索引,如果为null,则没有使用索引,查询中若使用了覆盖索引,则该索引和查询的select字段重叠。

如果没有使用索引,则该列是null,如果想强制mysql使用或忽视possible_keys列中的索引,在查询中使用force index、ignore index。

基于4、5两个,有可能出现这种情况:possible_keys有值,key没有值,这种情况下有可能是因为分析的时候需要用索引,真正执行的时候发现不走索引的化还会快一点。

6Type

这一列b表示关联类型或者访问类型,即Mysql决定如何查找表中的行,查找数据行记录的大概范围。

依次从最优到最差分别为:

system>const>eq_ref>ref>range>index>ALL

一般来说,的保证查询达到range级别,最好达到ref。

1)NULL:mysql能够在优化阶段分解查询语句,在执行阶段用不着在访问表或索引。例如:在索引列中取最小值,可以单独查找索引来完成,不需要在执行时访问表

2)system:表只有一行记录(等于系统表),这是const类型的特例,平时不会出现

3)const:这个表至多有一个匹配行,

Const为常量的意思,他可能想要表达出查询的效率非常高,跟查一个常量式的,用我们的唯一索引,或者主键的时候,因为无重复值,所以查询效率非常高

4)eq_ref:多表连接中使用primary key或者 unique key作为关联条件,使用唯一性索引进行数据查找,也就是被关联表上的关联列走的是主键或者唯一索引这可能是在const之外最好的联接类型了,简单的select查询不会出现这种type

5)ref:使用了非唯一性索引进行数据的查找或者非唯一性索引的部分前缀

5.1)简单查询(非唯一索引)

5.2)关联表查询,idx_user_address_id是address_id和user_id的联合索引,这里使用到了user_address的左边前缀address_id部分

6)ref_or_null:对于某个字段即需要关联条件,也需要null值的情况下,查询优化器会选择这种访问方式,简单得来说就是二级索引等值查询也能搜索到值为null得行。(注意,此时在表中添加了一行为null得数据)

7)index_merge:在查询过程中需要多个索引组合使用

8)range:表示利用索引查询的时候限制了范围,在指定范围内进行查询,这样避免了index的全索引扫描,适用的操作符: =, <>, >, >=, <, <=, IS NULL, BETWEEN, LIKE, 或者 IN()

9)index:全索引扫描这个比all的效率要好,主要有两种情况,一种是当前的查询时覆盖索引,即我们需要的数据在索引中就可以索取,或者是使用了索引进行排序,这样就避免数据的重排序,一般是扫描某个二级索引,这种扫描不会从索引树根节点开始快速查找,而是直接对二级索引的叶子节点遍历和扫描,速度还是比较慢的,这种查询一般为使用覆盖索引,二级索引一般比较小,所以这种通常比All快一些

 

那么这里提出一个问题,address表中其实有两个索引,一个是主键索引,一个是二级索引idx_name,那么为什么这里会使用二级索引而不是主键索引呢?

打开address表你会发现。。Address只有两个字段一个id,一个name,而idx_name这个索引树中就包含了name和id,而要查的字段都存在于idx_name索引树中,mysql有一个这样的优化原则,凡是我查找结果集的分析我查找出来,如果这个结果集的几个字段在我们所有索引里面都有,他会优先选择二级索引去查,因为二级索引小不管是主键索引还是二级索引都是从叶子节点的第一个开始找,遍历到最后一个 

 11)all:全表扫描,扫描你的聚簇索引的所有叶子节点,一般情况下出现这样的sql语句而且数据量比较大的话那么就需要增加索引来进行优化了。

 

7key_len

这一列显示了mysql表示索引中使用的字节数,通过这个值可以算出具体使用了索引中的哪些列,举例来说,user_address的联合索引idx_user_address_id由address_id和user_id两个int列组成,并且每个int是4字节。通过结果中的key_len=4可推断出查询使用了第一个列address_id来执行索引查询,在不损失精度的情况下长度越短越好。

从上图可以看出这个查询用到了idx_user_address_id这个联合索引,这个索引有两个字段,但我们这个查询语句其实就用到了address_id,key_len是4,address_id是int类型,也就是4字节当两个都用到的时候,相应的key_len也就变成了8

Key_len计算规则如下:

字符串:

      Char(n):n字节长度

      Varchar(n):如果是utf-8,则长度3n+2字节,加的2字节用来存储字符串长度

数值类型:

      Tinyint:1字节

      Smallint:2字节

      Int:4字节

      Bigint:8字节

时间类型:

      Date:3字节

      Timestamp:4字节

      Datatime:8字节

如果字段允许为null,需要1字节记录是否为null

索引的最大长度是768字节,当字符串过长时,mysql会做一个类似左前缀索引的处理,将前半部分的字符提取出来左索引

 

8ref

显示索引的哪一列被使用了,如果可能的话,是一个常数

这一列显示了在key列记录的索引中,表查找值所用到的列或常量,常见的有:const(常量),字段名(例如:address.id)

9rows

这一列是mysql估计要读取并检测的行数 ,注意这个不是结果集里面的行数,她只是一个预估值

10Extra

这一列展示的是额外信息。常见的重要值如下:

1)using index:使用覆盖索引

覆盖索引定义:mysql执行计划explain结果里的key有使用索引,如果select后面查询的字段都可以从这个索引的树中获取,这种情况一般说用到了覆盖索引,extra里一般都有using index;覆盖索引一般针对的辅助索引,整个查询结果只通过辅助索引就能拿到结果,不需要通过辅助索引树找到主键,再通过主键索引树里获取其他字段值

 

 2)using where:使用where语句来处理结果,并且查询的列未被索引覆盖

 3)using index condition:查询的列不完全被索引覆盖,where条件中是一个前导列的范围;

4)using temporary:mysql需要创建一张临时表来处理查询。出现这种情况一般是要进行优化的,首先是想到用索引来优化

user.name没有索引,此时创建了临时表来distinct

address.name建立了idx_name索引,此时查询时extras是using index,没有用临时表

5)using filesort:将用外部排序而不是索引排序,索引较小时从内存排序,否则需要在磁盘完成排序。这种情况下一般也是要考虑使用索引来优化的

actor.name未创建索引,会浏览actor整个表,保存排序关键字name和对应的id,然后排序name并检索行记录,

 address.name建立了idx_name索引,此时查询时extra是using index

 6)Select tables optimized away:使用某些聚合函数(比如max、min)来访问存在索引的某个字段是

 

 

 

附:

1、派生表

派生表,是用于存储子查询产生的结果的临时表,这个子查询特指 FROM 子句 里的子查询,如果是出现在其它地方的子查询,就不叫这个名字了,所以本质上来说,派生表也是临时表。

2、物化表

物化表,也是用于存储子查询产生的结果的临时表,这个子查询特指 WHERE 子句中查询条件里的子查询。

前导列:

前导列,就是在创建复合索引语句的第一列或者连续的多列。比如通过:CREATE INDEX comp_ind ON table1(x, y, z)创建索引,那么x,xy,xyz都是前导列,而yz,y,z这样的就不是。

3、dual表

Dual表其实就是一个虚表,你可以在没有表的情况下指定这个虚拟的表名。

4、int(1)、int(2)、int(3)...int(10)有什么区别?

不知道大家对于上面ken_len列的计算是否有疑问,为什么像int这类数据类型就是4字节,而不是int(num)中的num字节接下来就给大家解释一下。

Mysql中int是占4个字节,那么对于无符号的int,最大值就是2^32-1 = 4294967295

int后面的数字不能表示字段的长度,int(num)一般加上zerofill才有效果,它能够实现当该字段不足num位时补0的效果。

下面我们给出一个例子:

首先我们创建一个测试表

CREATE TABLE `intTest` (
  `id` int(1) ZEROFILL UNSIGNED NOT NULL,
    `test` int(1) UNSIGNED ZEROFILL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

测试表中id列,test列是一个无符号整型,且设置的是int(1),那么他是不是意味着不能插入4294967295这个4字节的最大数字,接下来我们就插入4294967295试一下:

 

ALTER TABLE `intTest` add COLUMN `test1` int(3) ZEROFILL;

向test1中插入负数

INSERT INTO `inttest` VALUES(2,2,-2);

此时会发生报错;因为小编的mysql处于严格模式下

SET sql_mode ="NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION";

修改模式,再次插入

 

可以看到,插入负数的时候会存储为0,mysql会自动设置该列为无符号整数。

zerofill默认为int(10),int默认为int(11)

下面我们做一个示例:

ALTER TABLE `intTest` add COLUMN `test2` int ZEROFILL;
ALTER TABLE `intTest` add COLUMN `test3` int;

上面就可以证明这一点。

不仅仅是int类型,上面提到的整数类型皆是如此。

 

有关Explain:你见过这样的Sql吗?的更多相关文章

  1. Hive SQL 五大经典面试题 - 2

    目录第1题连续问题分析:解法:第2题分组问题分析:解法:第3题间隔连续问题分析:解法:第4题打折日期交叉问题分析:解法:第5题同时在线问题分析:解法:第1题连续问题如下数据为蚂蚁森林中用户领取的减少碳排放量iddtlowcarbon10012021-12-1212310022021-12-124510012021-12-134310012021-12-134510012021-12-132310022021-12-144510012021-12-1423010022021-12-154510012021-12-1523.......找出连续3天及以上减少碳排放量在100以上的用户分析:遇到这类

  2. sql - 查询忽略时间戳日期的时间范围 - 2

    我正在尝试查询我的Rails数据库(Postgres)中的购买表,我想查询时间范围。例如,我想知道在所有日期的下午2点到3点之间进行了多少次购买。此表中有一个created_at列,但我不知道如何在不搜索特定日期的情况下完成此操作。我试过:Purchases.where("created_atBETWEEN?and?",Time.now-1.hour,Time.now)但这最终只会搜索今天与那些时间的日期。 最佳答案 您需要使用PostgreSQL'sdate_part/extractfunction从created_at中提取小时

  3. ruby-on-rails - 没有这样的文件或目录 - 用 Mini Magick 识别 - 2

    在我让另一个人重做我的前端UI之前,我的Rails应用程序运行平稳。我已经尝试解决此错误3天了。这是错误:Nosuchfileordirectory-identifyExtractedsource(aroundline#59):575859606162@post=Post.find(params[:id])authorize@postif@post.update_attributes(post_params)flash[:notice]="Postwasupdated."redirect_to[@topic,@post]else{"utf8"=>"✓","_method"=>"patc

  4. sql - 在 Rails Console for PostgreSQL 的表中显示数据 - 2

    我找到了这样的东西:Rails:Howtolistdatabasetables/objectsusingtheRailsconsole?这一行没问题:ActiveRecord::Base.connection.tables并返回所有表但是ActiveRecord::Base.connection.table_structure("users")产生错误:ActiveRecord::Base.connection.table_structure("projects")我认为table_structure不是Postgres方法。如何列出Postgres数据库的Rails控制台中表中的所有

  5. ruby - 防止SQL注入(inject)/好的Ruby方法 - 2

    Ruby中防止SQL注入(inject)的好方法是什么? 最佳答案 直接使用ruby?使用准备好的语句:require'mysql'db=Mysql.new('localhost','user','password','database')statement=db.prepare"SELECT*FROMtableWHEREfield=?"statement.execute'value'statement.fetchstatement.close 关于ruby-防止SQL注入(inject

  6. ruby-on-rails - 像 "has_one"这样的 Rails 方法调用是如何工作的? - 2

    我是PHP开发人员,目前我正在学习Rails(3),当然还有Ruby。Idon'twanttobelieveinmagic因此,我尽可能多地了解Rails“背后”发生的事情。我发现有趣的是ActiveRecord模型中的方法调用,如has_one或belongs_to。我试图重现它,并提供了一个天真的例子:#has_one_test_1.rbmoduleFooclassBasedefself.has_oneputs'Willitwork?'endendendclassModel2如我所料,只要运行这个文件就会输出“Willitwork?”。在搜索Rails源代码时,我找到了负责的函数:

  7. ruby - 奇怪的 ruby​​ for 循环行为(为什么这样做有效) - 2

    defreverse(ary)result=[]forresult[0,0]inaryendresultendassert_equal["baz","bar","foo"],reverse(["foo","bar","baz"])这行得通,我想了解原因。有什么解释吗? 最佳答案 如果我使用each而不是for/in重写它,它看起来像这样:defreverse(ary)result=[]#forresult[0,0]inaryary.eachdo|item|result[0,0]=itemendresultendforainb基本上就

  8. ruby-on-rails - 如何在 Rails 中的不同数据库上执行直接 SQL 代码 - 2

    我正在编写一个Rails应用程序,它将监视某些特定数据库的数据质量。为了做到这一点,我需要能够对这些数据库执行直接SQL查询——这当然与用于驱动Rails应用程序模型的数据库不同。简而言之,这意味着我无法使用通过ActiveRecord基础连接的技巧。我需要连接的数据库在设计时是未知的(即:我不能将它们的详细信息放在database.yaml中)。相反,我有一个模型“database_details”,用户将使用它来输入应用程序将在运行时执行查询的数据库的详细信息。因此与这些数据库的连接实际上是动态的,细节仅在运行时解析。 最佳答案

  9. ruby-on-rails - 为什么 Rails 使用像 link_to 这样的辅助方法而不是 <a href...>? - 2

    我正在学习Rails,我注意到Rails不断地使用诸如link_to之类的辅助方法,而不是仅仅使用普通的html。现在我可以理解为什么他们会使用他们会使用一些辅助方法,但我不明白为什么他们更喜欢辅助方法而不是直接编码html。为什么Rails更喜欢辅助方法而不是您必须手动编写html?为什么Rails团队做出这样的设计选择? 最佳答案 在Rails应用程序中,通常使用URL方法和内容方法生成链接,例如这绝对比将它们放入中更易于管理手动标记。">(您正在使用路由器生成这些URL,对吗?如果您硬编码/users/1并决定稍后将其设为/u

  10. sql - Rails:使用 Postgres 创建对象时重复 ActiveRecord::RecordNotUnique? - 2

    我正在使用Rails4应用程序,它需要创建大量对象以响应来自另一个系统的事件。当我调用create!时,主键列上出现非常频繁的ActiveRecord::RecordNotUnique错误(由PG::UniqueViolation引起)我的模型之一。我在SO上找到了其他答案,建议挽救异常并调用retry:beginTableName.create!(data:'here')rescueActiveRecord::RecordNotUnique=>eife.message.include?'_pkey'#Onlyretryprimarykeyviolationslog.warn"Retr

随机推荐