MySQL优化器特性(八)索引范围扫描成本计算

俊达2年前技术文章888

range

执行计划中的range表示索引范围扫描。索引范围扫描的执行过程大致如下:

1、根据where条件中索引字段的条件,定位到索引结构中的第一条满足条件的记录。

2、根据索引中记录的rowid,到表中获取数据。处理其它条件。

3、访问索引结构中的下一条记录,继续处理。直到满足range条件的所有记录都处理完成。

一条SQL中可能存在多个range范围(通过or条件相连, 如select * from tab where k1 in (a,b,c)就有3个range范围),需要依次处理每一个range。


range.png


range查询有几种不同的情况,mysql在计算成本时有所差异:

情况1:非覆盖索引。就是上面描述的那种情况。

情况2:覆盖索引,非主键。这种情况下,只需要扫描索引,不需要回表获取数据。

情况3:在主键上进行范围扫描。和覆盖索引类似。但是mysql在计算执行计划的成本时和覆盖索引有所差异。


一个例子

这个例子中,查询使用了索引范围扫描(range),cost为16.01,访问的记录数为35行。

mysql> explain format=tree select * from test_ror where a <= 1;
+----------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN                                                                                                                          |
+----------------------------------------------------------------------------------------------------------------------------------+
| -> Index range scan on test_ror using idx_a over (NULL < a <= 1), with index condition: (test_ror.a <= 1)  (cost=16.01 rows=35)
 |
     
explain format=json select * from test_ror where a <= 1;

{
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "16.01"
    },
    "table": {
      "table_name": "test_ror",
      "access_type": "range",
      "possible_keys": [
        "idx_a"
      ],
      "key": "idx_a",
      "used_key_parts": [
        "a"
      ],
      "key_length": "5",
      "rows_examined_per_scan": 35,
      "rows_produced_per_join": 35,
      "filtered": "100.00",
      "index_condition": "(`test`.`test_ror`.`a` <= 1)",
      "cost_info": {
        "read_cost": "12.51",
        "eval_cost": "3.50",
        "prefix_cost": "16.01",
        "data_read_per_join": "840"
      },
      "used_columns": [
        "id",
        "a",
        "b",
        "c"
      ]
    }
  }
}



成本分为两部分,read_cost 12.51,eval_cost 3.5。从上一篇文章的分析中,我们知道eval_cost通过公式得到:

row_eval_cost = rows * row_evaluation_cost = 35 * 0.1 = 3.5


相关成本计算在函数test_quick_select中进行。optimizer_trace中记录了优化器评估各个访问路径的成本,本文中我们会大量使用optimizer_trace中显示的信息来验证成本计算公式。

表扫描成本


sql: select * from test_ror where a <=1

"range_analysis": {
    "table_scan": {
        "rows": 105,
        "cost": 12.85
    }



表扫描成本由几部分组成:

  • row_evaluate_cost + 1:rows * row_evaluate_cost + 1 =  105*0.1 + 1 = 11.5

  • table_scan_cost:in_mem_pages * memory_block_read_cost + disk_pages * io_block_read_cost = 1*0.25 + 0*1 = 0.25

  • 常数 1.1

上面3个部分相加,得到表扫描的成本为 11.5 + 0.25 + 1.1 = 12.85。



索引范围扫描成本评估

评估索引范围扫描的成本,需要先评估满足索引条件的记录数,然后根据记录数来评估索引范围扫描的成本。

我们先看一下索引成本计算的公式,后续再来看如何评估记录数。

索引范围扫描成本估算的代码调用路径:get_key_scans_params -> check_quick_select -> multi_range_read_info_const

 if (*flags & HA_MRR_INDEX_ONLY)
      *cost = index_scan_cost(keyno, static_cast<double>(n_ranges),
                              static_cast<double>(total_rows));
    else
      *cost = read_cost(keyno, static_cast<double>(n_ranges),
                        static_cast<double>(total_rows));
    cost->add_cpu(
        cost_model->row_evaluate_cost(static_cast<double>(total_rows)) + 0.01);


索引扫描分两种情况,

如果是覆盖索引,则计算index_scan_cost,具体计算方式已经在前面介绍了。

如果是非覆盖索引,则计算读取索引和表的成本。

索引范围扫描的总成本为IO成本加上解析行的CPU成本,再加上常数0.01

cost = index_scan_cost(ranges, rows) + row_evaluate_cost(rows) + 0.01  (覆盖索引)

cost = read_cost(ranges, rows) + row_evaluate_cost(rows) + 0.01  (非覆盖索引)



非覆盖索引

读取非覆盖索引的成本公式:

io_cost = read_time(index, ranges, rows) * page_read_cost(1.0)

成本跟range的数量和记录数都有关系。

Cost_estimate handler::read_cost(uint index, double ranges, double rows) {
  const double io_cost =
      read_time(index, static_cast<uint>(ranges), static_cast<ha_rows>(rows)) *
      table->cost_model()->page_read_cost(1.0);
  Cost_estimate cost;
  cost.add_io(io_cost);
  return cost;
}


计算方式:

在我们的测试SQL中(select * from test_ror whrere a <=1),ranges = 1, rows = 35

io_cost = (1 + 35) * 0.25

eval_cost = 35 * 0.1

cost = io_cost + eval_cost = (1 + 35) * 0.25 + 35 * 0.1 + 0.01 = 9 + 3.5 + 0.01 = 12.51



最终cost


对于range查询,最终的cost为

scan_total_cost = scan_read_cost + row_evaluate_cost(prefix_rowcount * rows_after_filtering)

    = 12.51 + (1 * 35) * 0.1 = 12.51 + 3.5  = 16.01


相关代码如下:

make_join_plan -> choose_table_order -> greedy_search -> best_extension_by_limited_search -> best_access_path

 double scan_read_cost = calculate_scan_cost(
        tab, idx, best_ref, prefix_rowcount, found_condition, disable_jbuf,
        &rows_after_filtering, &trace_access_scan);

    /*
      We estimate the cost of evaluating WHERE clause for found
      records as row_evaluate_cost(prefix_rowcount * rows_after_filtering).
      This cost plus scan_cost gives us total cost of using
      TABLE/INDEX/RANGE SCAN.
    */
    const double scan_total_cost =
        scan_read_cost +
        cost_model->row_evaluate_cost(prefix_rowcount * rows_after_filtering);


相关文章

MySQL运维实战(2.3)MySQL的权限体系和一个例子

mysql权限按授权范围分为3大类全局权限。全局权限是用于管理系统模块的权限。跟具体的数据库或对象无关。授权时需要指定为*.*数据库权限对象权限对于具体的数据库对象的权限,如表、字段级别的权限。MyS...

MySQL优化器特性(三)表关联之BKA(Batched Key Access)优化

MySQL优化器特性(三)表关联之BKA(Batched Key Access)优化

单表range查询时,可以使用MRR优化,先对rowid进行排序,然后再回表查询数据。在表关联的时候,也可以使用类似的优化方法,先根据关联条件取出被关联表的rowid,将rowid缓存在join bu...

MySQL运维实战之备份和恢复(8.6)将数据库恢复到指定时间点

恢复到指定时间点使用全量备份和增量备份文件,都只能将数据库恢复到备份结束的时间。通过binlog,可以将数据库恢复到任意时间点(前提是备份和该时间点之间的binlog都存在)。找到时间点对应的binl...

MySQL运维实战(4.7) SQL_MODE之ANSI_QUOTES

默认情况下,mysql使用反引号(`)作为标识符的引号。使用mysql关键字作为表名、字段名会报语法错误,这时可以加上反引号( `),避免报错。设置ANSI_QUOTES后,使用双引号(")...

MySQL运维实战(1.1)安装部署:使用RPM进行安装部署

MySQL运维实战(1.1)安装部署:使用RPM进行安装部署

我们在生产环境部署mysql时,一般很少使用rpm。用rpm或或者其他包管理器安装mysql,好处是安装简单,而且很多系统可能都自带了某个版本的mysql。但是使用RPM安装也存在一些缺点:1、rpm...

MySQL运维实战(5.2) MySQL charset基本概念

mysql多字符集mysql支持多字符集。一个数据库中可以存储不同字符集的数据,一个表的不同字段可以使用不同的字符集。mysql> show character s...

发表评论    

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。