乐趣区

关于sql优化:技术分享-explain-formatjson-详解

作者:胡呈清

爱可生 DBA 团队成员,善于故障剖析、性能优化,集体博客:https://www.jianshu.com/u/a95…,欢送探讨。

本文起源:原创投稿

* 爱可生开源社区出品,原创内容未经受权不得随便应用,转载请分割小编并注明起源。

explain format=json 能够打印具体的执行计划成本,上面两个示例将通知你如何查看老本输入,以及如何计算成本。

表构造如下:

mysql> show create table sbtest1\G
*************************** 1. row ***************************
       Table: sbtest1
Create Table: CREATE TABLE `sbtest1` (`id` int(11) NOT NULL AUTO_INCREMENT,
  `k` int(11) NOT NULL DEFAULT '0',
  `c` char(120) COLLATE utf8mb4_bin NOT NULL DEFAULT '',
  `pad` varchar(90) COLLATE utf8mb4_bin DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4316190 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin

## 留神 sbtest3 无主键
mysql> show create table sbtest3\G
*************************** 1. row ***************************
       Table: sbtest3
Create Table: CREATE TABLE `sbtest3` (`id` int(11) NOT NULL,
  `k` int(11) NOT NULL DEFAULT '0',
  `c` char(120) COLLATE utf8mb4_bin NOT NULL DEFAULT '',
  `pad` varchar(66) COLLATE utf8mb4_bin DEFAULT NULL,
  KEY `k_3` (`k`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin

示例 1

mysql> explain format=json select * from sbtest3 where id<100 and k<200\G
*************************** 1. row ***************************
EXPLAIN: {
  "query_block": {
    "select_id": 1,
    "cost_info": {"query_cost": "26.21"            ## 查问总成本},
    "table": {
      "table_name": "sbtest3",        ## 表名
      "access_type": "range",         ## 拜访数据的形式是 range,即索引范畴查找
      "possible_keys": ["k_3"],
      "key": "k_3",                   ## 应用索引
      "used_key_parts": ["k"],
      "key_length": "4",
      "rows_examined_per_scan": 18,   ## 扫描 k_3 索引的行数:18(满足特定条件时应用 index dive 可失去真履行数)"rows_produced_per_join": 5,    ## 在扫描索引后估算满足 id<100 条件的行数:5
      "filtered": "33.33",            ## 在扫描索引后估算满足其余条件 id<100 的数据行占比
      "index_condition": "(`sbtest`.`sbtest3`.`k` < 200)",     ## 索引条件
      "cost_info": {
        "read_cost": "25.01",         ## 这里蕴含了所有的 IO 老本 + 局部 CPU 老本
        "eval_cost": "1.20",          ## 计算扇出的 CPU 老本
        "prefix_cost": "26.21",       ##read_cost+eval_cost
        "data_read_per_join": "4K"
      },
      "used_columns": [
        "id",
        "k",
        "c",
        "pad"
      ],
      "attached_condition": "(`sbtest`.`sbtest3`.`id` < 100)"
    }
  }
}

eval_cost

这个很简略,就是计算扇出的 CPU 老本。利用条件 k<200 时,须要扫描索引 18 行,这里 18 是准确值(index dive),而后优化器用了一种叫启发式规定(heuristic)的算法估算出其中满足条件 id<100 的比例为 33.33%,进行 18*33.33% 次计算的 CPU 老本等于 18*33.33%*0.2=1.2,这里 0.2 是老本常数(即 row_evaluate_cost)。

留神:rows_examined_per_scan*filtered 才是扇出数,不能简略的用 rows_produced_per_join 来示意。

read_cost

这里蕴含了所有的 IO 老本 +(CPU 老本 – eval_cost)。咱们先看下这个 SQL 的总成本应该怎么算:

拜访二级索引 k_3 的老本:

  • IO 老本 = 1*1.0

查问优化器粗犷的认为读取索引的一个范畴区间的 I/O 老本和读取一个页面是雷同的,这个 SQL 中 k 字段的筛选范畴只有 1 个:k < 200,而读取一个页面的 IO 老本为 1.0(即 io_block_read_cost);

  • CPU 老本 = 18*0.2

从 k 索引中取出 18 行数据后,理论还要再计算一遍,每行计算的老本为 0.2。

而后因为 select * 以及 where id<100 须要的数据都不在索引 k_3 中,所以还须要回表,回表老本:

  • IO 老本 = 18*1.0

从索引中取出满足 k<200 的数据一共是 18 行,所以 = 18*1.0

  • CPU 老本 = 18*0.2

从这 18 行残缺的数据中计算满足 id<100 的数据,所以也须要计算 18 次。

总成本 = 1*1.0+18*0.2+18*1+18*02=26.2。因为 eval_cost 算的是扇出的 CPU 老本:18*33.33%*0.2,所以 read_cost = 回表的 CPU 老本 - eval_cost,也能够这么算 rows_examined_per_scan*(1-filtered)*0.2

示例 2

mysql> explain format=json select t1.id from sbtest1 t1 join sbtest3 t3 \
on t1.id=t3.id and t3.k<200 and t3.id<100\G
*************************** 1. row ***************************
EXPLAIN: {
  "query_block": {
    "select_id": 1,
    "cost_info": {"query_cost": "33.41"                      ## 查问总成本},
    "nested_loop": [                            ##join 算法:NLJ
      {
        "table": {
          "table_name": "t3",                   ##t3 是驱动表
          "access_type": "range",               ## 拜访数据的形式是 range,即索引范畴查找
          "possible_keys": ["k_3"],
          "key": "k_3",                         ## 应用的索引:k_3
          "used_key_parts": [                   ## 索引字段:k
            "k"
          ],
          "key_length": "4",
          "rows_examined_per_scan": 18,         ##k_3 索引扫描行数:18
          "rows_produced_per_join": 5,          ##(估算值)扫描索引 18 行后,满足条件 id<200 的行数
          "filtered": "33.33",                  ##(估算值)扫描索引 18 行后,满足条件 id<200 的数据占扫描行数的比例,即驱动表扇出
          "index_condition": "(`sbtest`.`t3`.`k` < 200)",
          "cost_info": {
            "read_cost": "25.01",              ## 这里蕴含了所有的 IO 老本 + 局部 CPU 老本
            "eval_cost": "1.20",               ## 计算扇出的 CPU 老本
            "prefix_cost": "26.21",            ## 驱动表的总成本:read_cost+eval_cost
            "data_read_per_join": "4K"
          },
          "used_columns": [
            "id",
            "k"
          ],
          "attached_condition": "(`sbtest`.`t3`.`id` < 100)"
        }
      },
      {
        "table": {
          "table_name": "t1",                  ##t1 为被驱动表
          "access_type": "eq_ref",             ## 关联查问时拜访驱动表形式是通过主键或惟一索引的等值查问
          "possible_keys": ["PRIMARY"],
          "key": "PRIMARY",                    ## 应用索引为主键
          "used_key_parts": [                  ## 索引字段为 id
            "id"
          ],
          "key_length": "4",
          "ref": ["sbtest.t3.id"],
          "rows_examined_per_scan": 1,         ## 关联查问时,每次扫描被驱动表 1 行数据(应用主键)"rows_produced_per_join": 5,         ## 被驱动表须要查问的次数,不是精确的驱动表扇出数
          "filtered": "100.00",                ## 满足关联条件数据占扫描行数的比例,被驱动表上看这个没啥意义
          "using_index": true,
          "cost_info": {                       ## 驱动表扇出数:rows_examined_per_scan*filtered, 即 18*33.33%= 6 行
            "read_cost": "6.00",               ## 单次查问被驱动表的 IO 老本 * 驱动表扇出数。6*1.0=6,1.0 为老本常数
            "eval_cost": "1.20",               ## 单次查问被驱动表的 CPU 老本 * 驱动表扇出数。6*0.2=1.2,0.2 位老本常数
            "prefix_cost": "33.41",            ## 查问总成本 = 驱动表的总成本 + 被驱动表的 (read_cost+eval_cost)
            "data_read_per_join": "5K"
          },
          "used_columns": ["id"]
        }
      }
    ]
  }
}

join 查问的总成本计算公式简化: 连贯查问总成本 = 拜访驱动表的老本 + 驱动表扇出数 * 单次访问被驱动表的老本 。explain 执行打算详解 1 中有解释 filtered 在关联查问中的重要性。

在下面示例中:拜访驱动表的老本 = 26.21,驱动表扇出数 = 18*33.33% = 6,单次访问驱动表的老本 = 1.0+0.2
总成本 =26.21+6(1.0+0.2)=33.41

留神:驱动表和被驱动表的 read_cost、eval_cost 代表不一样的老本。

退出移动版