科普文:软件架构数据库系列之【MySQL优化器之统计信息、直方图梳理】

377 篇文章 1 订阅
119 篇文章 2 订阅

概叙

关于MySQL统计信息现状,可参考:科普文:软件架构数据库系列之【MySQL统计信息的现状和发展】-CSDN博客

如上图所示,MySQL作为C/S架构的RDBMS,在客户端发送一条SQL命令后,要经过如上过程,完成服务端mysqld进程对SQL命令的处理。

TCP/IP网络通信可参考:

科普文:Java基础系列之【以太网和以太网交换机】_java中交换机-CSDN博客

科普文:Java基础系列之【互联网基石:TCP/IP四层模型】_tcp和wss-CSDN博客

科普文:Java基础系列之【互联网基石:TCP/IP四层模型各层协议介绍】-CSDN博客

科普文:Java基础系列之【互联网基石:TCP/IP四层模型下数据帧是如何传输的】-CSDN博客

MySQL的C/S架构可参考:科普文:软件架构数据库系列之【MySQL和服务器】-CSDN博客

客户端的ORM可参考:科普文:Java基础mybatis系列之【mybatis框架中SQL 执行流程分析】-CSDN博客

实战:MyBatis适配多种数据库:MySQL、Oracle、PostGresql等_mybatis oracle-CSDN博客

科普文:深入理解Mybatis-CSDN博客

SQL命令可参考:科普文:软件架构数据库系列之【MySQL数据库SQL命令和SQL标准梳理】-CSDN博客

具体梳理如下:

客户端:mysql客户端命令、jdbc/odbc等应用程序。

连接器:处理客户端连接、进行权限验证、线程池管理等。负责监听和管理客户端的连接以及线程处理等。每一个连接到MySQL服务器的请求都会被分配一个连接线程。连接线程负责与客户端的通信,接受客户端发送的命令并且返回服务器处理的结果。


分析器(解析器):SQL词法与语法分析,例如语义和语法的分析和检查,以及对象访问权限检查等。生成SQL解析树。


优化器:依据SQL解析树、统计信息、数据字典,在CBO优化器的综合抉择下生成SQL的执行计划。利用数据库的统计信息决定SQL语句的最佳执行方式。使用索引还是全表扫描的方式访问表,多表连接的实现方式等。优化器是决定查询性能的关键组件,而数据库的统计信息是优化器判断的基础。


执行器:依据优化器生成的SQL执行计划,执行SQL,通过存储引擎接口从存储引擎将数据加载到内存进行处理,并将最终SQL执行结果返回给连接器。这一过程有Nestedloop/Hash join等表连接、index merge、ICP、MRR等优化策略参与具体的数据处理操作。根据执行计划调用相应的执行模块获取数据,并返回给客户端。对于MySQL而言,会根据表的存储引擎调用不同的接口获取数据。如果数据已经被缓存,可以直接从缓冲区获取。


存储引擎:存储数据、主要负责和操作系统、磁盘之间的IO操作。存储引擎是对底层物理数据执行实际操作的组件,为服务器层提供各种操作数据的API。MySQL支持插件式的存储引擎,包括InnoDB、MyISAM、Memory等。

详细的MySQL体系结构,参考:

科普文:软件架构数据库系列之【MySQL5.6体系结构】_mysql 5.6 存储结构-CSDN博客

科普文:软件架构数据库系列之【图解MySQL】-CSDN博客

科普文:软件架构数据库系列之【MySQL存储引擎InnoDB】-CSDN博客

科普文:软件架构数据库系列之【MySQL前世今生及其体系结构概叙】_mysql8.0 8.1 8.2 8.3-CSDN博客

分析器(解析器)可参考:科普文:软件架构数据库系列之【MySQL的sql_mode参数】-CSDN博客

优化器可参考:科普文:软件架构数据库系列之【MySQL解析器和优化器】_mysql解析器优化器-CSDN博客

科普文:软件架构数据库系列之【MySQL查询优化器中的优化策略optimizer_switch】-CSDN博客

优化器策略可参考:

科普文:软件架构数据库系列之【MySQL查询优化器中的优化策略optimizer_switch--ICP索引下推】_查询优化器 optimizer-CSDN博客

科普文:软件架构数据库系列之【MySQL查询优化器中的优化策略optimizer_switch--MRR 优化器】-CSDN博客

科普文:软件架构数据库系列之【MySQL查询优化器中的优化策略optimizer_switch-- Index Merge 索引合并 优化器】-CSDN博客

科普文:软件架构数据库系列之【MySQL查询优化器中的优化策略optimizer_switch--Hash Join和BNL(Block Nested Loop)优化器】-CSDN博客

执行器可参考:

科普文:软件架构数据库系列之【MySQL8新特性--执行器之火山模型(Iterator Model)】-CSDN博客

科普文:软件架构数据库系列之【经典的火山模型执行引擎Vastbase】-CSDN博客

科普文:软件架构数据库系列之【MySQL 8.0 Server层源码概叙】作者 | 道客-CSDN博客

执行计划可参考:

科普文:软件架构数据库系列之【MySQL控制查询优化器Hints】-CSDN博客

科普文:软件架构数据库系列之【MySQL执行计划Extra梳理】-CSDN博客

实战:搞懂SQL执行流程、SQL执行计划解读和SQL优化_sql的执行流程-CSDN博客

实战:万字小结MySQL慢原因分析_mysql 数据库突然变慢-CSDN博客

MySQL的内存模型和磁盘模型可参考:

科普文:软件架构数据库系列之【MySQL5.7的InnoDB引擎存储结构分析:buffer+disk】_mysql 5.7 innodb存储引擎架构-CSDN博客

科普文:软件架构数据库系列之【MySQL 中的 7 种日志介绍】_mysql5.7 redo日志的类型-CSDN博客

科普文:软件架构数据库系列之【详解InnoDB重做日志Redlog】-CSDN博客

科普文:软件架构数据库系列之【详解InnoDB双写(Doublewrite Buffer)】-CSDN博客

科普文:软件架构数据库系列之【详解InnoDB事务日志(redo log和undo log) 】_redolog和undolog-CSDN博客

科普文:软件架构数据库系列之【详解InnoDB逻辑序列号LSN (log sequence number)】_log sequence number (lsn)-CSDN博客

科普文:软件架构数据库系列之【图解InnoDB恢复recovery过程】_innodb-tools-CSDN博客

科普文:软件架构数据库系列之【详解InnoDB恢复recovery过程】-CSDN博客

上面基本将SQL命令经过的环节都梳理了一边,包括:网络通信、ORM框架、以及整个MySQL体系的组件。这里面还缺少了很重要的环节“索引”和“锁”,后面将会详细说明。其实上面的梳理原本是想在“SQL命令执行流程”中做梳理的,这里就先放一部分出来。

SQL命令最简单可以分成读写两种命令:select作为读命令、DDL/DML作为写命令。这两种SQL命令虽然都要经过如上图所示的环节,但是在具体执行上还是有很大区别,稍后再详说。

MySQL执行SQL过程中,优化器使用基于CBO(成本的优化方式Cost-based Optimization),以SQL语句/SQL解析树作为输入,利用内置的成本模型和数据字典信息以及存储引擎的统计信息决定使用哪些步骤实现执行计划。

本文主要介绍这一过程中CBO的依据统计信息、直方图。

优化器

优化器是数据库的一个核心子系统,你也可以把他理解为MySQL数据库中的一个核心模块或者一个核心功能模块。

优化器也是基于特定模型、各种配置和统计信息进行选择,因此也不可能总是获得最佳执行方式。

MySQL Server可以分为两部分:服务器层以及存储引擎层。其中,优化器工作在服务器层,位于存储引擎API之上。

优化器的工作过程从语义上可以分为四个阶段:

  1. 逻辑转换,包括否定消除、等值传递和常量传递、常量表达式求值、外连接转换为内连接、子查询转换、视图合并等;
  2. 优化准备,例如索引ref和range访问方法分析、查询条件扇出值(fan out,过滤后的记录数)分析、常量表检测;
  3. 基于成本优化,包括访问方法和连接顺序的选择等;
  4. 执行计划改进,例如表条件下推、访问方法调整、排序避免以及索引条件下推。

1.逻辑转换

MySQL优化器首先可能会以不影响结果的方式对查询进行转换,转换的目标是尝试消除某些操作从而更快地执行查询。

显然,查询条件中的1=1是完全多余的。没有必要为每一行数据都执行一次计算;删除这个条件也不会影响最终的结果。执行EXPLAIN语句之后,通过SHOW WARNINGS命令可以查看逻辑转换之后的SQL语句,从上面的结果可以看出1=1已经不存在了。

下表列出了一些逻辑转换的示例:

SHOW WARNINGS命令输出中的Message显示了优化器如何限定查询语句中的表名和列名、应用了重写和优化规则后的查询语句以及优化过程的其他信息。

目前只有SELECT语句相关的额外信息可以通过SHOW WARNINGS语句进行查看,其他语句(DELETE、INSERT、REPLACE 和UPDATE)显示的信息为空。

2.优化准备

这个需要通过optimizer_trace优化器跟踪来进一步了解。

优化器跟踪输出主要包含了三个部分:

  • join_preparation,准备阶段,返回了字段名扩展之后的SQL语句。对于1=1这种多余的条件,也会在这个步骤被删除;
  • join_optimization,优化阶段。其中condition_processing中包含了各种逻辑转换,等值传递(equality_propagation)。另外constant_propagation表示常量传递,trivial_condition_removal表示无效条件移除;
  • join_execution,执行阶段。

优化器跟踪还可以显示其他基于成本优化的过程,后续我们还会使用该功能。关闭优化器跟踪的方式如下:SET optimizer_trace="enabled=off";

mysql> SET optimizer_trace="enabled=on";
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t1 where id>1 and id<5 and f1=f2;
+----+----+----+------+
| id | f1 | f2 | c1   |
+----+----+----+------+
|  2 |  2 |  2 |    2 |
|  3 |  3 |  3 |    3 |
|  4 |  4 |  4 |    4 |
+----+----+----+------+
3 rows in set (0.01 sec)

mysql> select * from information_schema.optimizer_trace\G
*************************** 1. row ***************************
                            QUERY: select * from t1 where id>1 and id<5 and f1=f2
                            TRACE: {
  "steps": [
    {
      "join_preparation": {  // 准备阶段、完成SQL改写。
        "select#": 1,
        "steps": [
          {
            "expanded_query": "/* select#1 */ select `t1`.`id` AS `id`,`t1`.`f1` AS `f1`,`t1`.`f2` AS `f2`,`t1`.`c1` AS `c1` from `t1` where ((`t1`.`id` > 1) and (`t1`.`id` < 5) and (`t1`.`f1` = `t1`.`f2`))"
          }
        ]
      }
    },
    {
      "join_optimization": {
        "select#": 1,
        "steps": [
          {
            "condition_processing": {
              "condition": "WHERE",
              "original_condition": "((`t1`.`id` > 1) and (`t1`.`id` < 5) and (`t1`.`f1` = `t1`.`f2`))",
              "steps": [
                {
                  "transformation": "equality_propagation",
                  "resulting_condition": "((`t1`.`id` > 1) and (`t1`.`id` < 5) and multiple equal(`t1`.`f1`, `t1`.`f2`))"
                },
                {
                  "transformation": "constant_propagation",
                  "resulting_condition": "((`t1`.`id` > 1) and (`t1`.`id` < 5) and multiple equal(`t1`.`f1`, `t1`.`f2`))"
                },
                {
                  "transformation": "trivial_condition_removal",
                  "resulting_condition": "((`t1`.`id` > 1) and (`t1`.`id` < 5) and multiple equal(`t1`.`f1`, `t1`.`f2`))"
                }
              ]
            }
          },
          {
            "substitute_generated_columns": {
            }
          },
          {
            "table_dependencies": [
              {
                "table": "`t1`",
                "row_may_be_null": false,
                "map_bit": 0,
                "depends_on_map_bits": [
                ]
              }
            ]
          },
          {
            "ref_optimizer_key_uses": [
            ]
          },
          {
            "rows_estimation": [
              {
                "table": "`t1`",
                "range_analysis": {
                  "table_scan": {
                    "rows": 9991,
                    "cost": 1007.45
                  },
                  "potential_range_indexes": [
                    {
                      "index": "PRIMARY",
                      "usable": true,
                      "key_parts": [
                        "id"
                      ]
                    }
                  ],
                  "setup_range_conditions": [
                  ],
                  "group_index_skip_scan": {
                    "chosen": false,
                    "cause": "not_group_by_or_distinct"
                  },
                  "skip_scan_range": {
                    "potential_skip_scan_indexes": [
                      {
                        "index": "PRIMARY",
                        "usable": false,
                        "cause": "query_references_nonkey_column"
                      }
                    ]
                  },
                  "analyzing_range_alternatives": {
                    "range_scan_alternatives": [
                      {
                        "index": "PRIMARY",
                        "ranges": [
                          "1 < id < 5"
                        ],
                        "index_dives_for_eq_ranges": true,
                        "rowid_ordered": true,
                        "using_mrr": false,
                        "index_only": false,
                        "in_memory": 1,
                        "rows": 3,
                        "cost": 0.560834,
                        "chosen": true
                      }
                    ],
                    "analyzing_roworder_intersect": {
                      "usable": false,
                      "cause": "too_few_roworder_scans"
                    }
                  },
                  "chosen_range_access_summary": {
                    "range_access_plan": {
                      "type": "range_scan",
                      "index": "PRIMARY",
                      "rows": 3,
                      "ranges": [
                        "1 < id < 5"
                      ]
                    },
                    "rows_for_plan": 3,
                    "cost_for_plan": 0.560834,
                    "chosen": true
                  }
                }
              }
            ]
          },
          {
            "considered_execution_plans": [
              {
                "plan_prefix": [
                ],
                "table": "`t1`",
                "best_access_path": {
                  "considered_access_paths": [
                    {
                      "rows_to_scan": 3,
                      "access_type": "range",
                      "range_details": {
                        "used_index": "PRIMARY"
                      },
                      "resulting_rows": 3,
                      "cost": 0.860834,
                      "chosen": true
                    }
                  ]
                },
                "condition_filtering_pct": 100,
                "rows_for_plan": 3,
                "cost_for_plan": 0.860834,
                "chosen": true
              }
            ]
          },
          {
            "attaching_conditions_to_tables": {
              "original_condition": "((`t1`.`f2` = `t1`.`f1`) and (`t1`.`id` > 1) and (`t1`.`id` < 5))",
              "attached_conditions_computation": [
              ],
              "attached_conditions_summary": [
                {
                  "table": "`t1`",
                  "attached": "((`t1`.`f2` = `t1`.`f1`) and (`t1`.`id` > 1) and (`t1`.`id` < 5))"
                }
              ]
            }
          },
          {
            "finalizing_table_conditions": [
              {
                "table": "`t1`",
                "original_table_condition": "((`t1`.`f2` = `t1`.`f1`) and (`t1`.`id` > 1) and (`t1`.`id` < 5))",
                "final_table_condition   ": "((`t1`.`f2` = `t1`.`f1`) and (`t1`.`id` > 1) and (`t1`.`id` < 5))"
              }
            ]
          },
          {
            "refine_plan": [
              {
                "table": "`t1`"
              }
            ]
          }
        ]
      }
    },
    {
      "join_execution": {
        "select#": 1,
        "steps": [
        ]
      }
    }
  ]
}
MISSING_BYTES_BEYOND_MAX_MEM_SIZE: 0
          INSUFFICIENT_PRIVILEGES: 0
1 row in set (0.01 sec)

mysql> SET optimizer_trace="enabled=off";
Query OK, 0 rows affected (0.00 sec)

mysql>

3.基于成本优化

MySQL优化器采用基于成本的优化方式,简化的步骤如下:

  1. 为每个操作指定一个成本;
  2. 计算每个可能的执行计划各个步骤的成本总和;
  3. 选择总成本最小的执行计划。

为了找到最佳执行计划,优化器需要比较不同的查询方案。随着查询中表的数量增加,可能的执行计划会呈现指数级增长;因为每个表都可能使用全表扫描或者不同的索引访问方法,连接查询可能使用任意顺序。对于少量表的连接查询(通常少于7到10个)可能不会产生问题,但是更多的表可能会导致查询优化的时间比执行时间还要长。

所以优化器不可能遍历所有的执行方案,一种更灵活的优化方法是允许用户控制优化器在查找最佳查询计划时的遍历程度。一般来说,优化器评估的计划越少,则编译查询所花费的时间就越少;但另一方面,由于优化器忽略了一些计划,因此可能找到的不是最佳计划。

控制优化程度

MySQL提供了两个系统变量,可以用于控制优化器的优化程度:

  • optimizer_prune_level, 基于返回行数的评估忽略某些执行计划,这种启发式的方法可以极大地减少优化时间而且很少丢失最佳计划。因此,该参数的默认设置为1;如果确认优化器错过了最佳计划,可以将该参数设置为0,不过这样可能导致优化时间的增加。
  • optimizer_search_depth,优化器查找的深度。如果该参数大于查询中表的数量,可以得到更好的执行计划,但是优化时间更长;如果小于表的数量,可以更快完成优化,但可能获得的不是最优计划。例如,对于12、13个或者更多表的连接查询,如果将该参数设置为表的个数,可能需要几小时或者几天时间才能完成优化;如果将该参数修改为3或者4,优化时间可能少于1分钟。该参数的默认值为62;如果不确定是否合适,可以将其设置为0,让优化器自动决定搜索的深度。

设置成本常量

MySQL优化器计算的成本主要包括I/O成本和CPU成本,每个步骤的成本由内置的“成本常量”进行估计。另外,这些成本常量可以通过系统数据库(mysql)中的server_cost和engine_cost两个表进行查询和设置。

成本优化器:MySQL :: MySQL 8.4 Reference Manual :: 10.9.5 The Optimizer Cost Model

统计信息:MySQL :: MySQL 8.4 Reference Manual :: 10.9.6 Optimizer Statistics 

server_cost中存储的是常规服务器操作的成本估计值:

cost_value为空表示使用default_value。其中,

  • disk_temptable_create_costdisk_temptable_row_cost代表了在基于磁盘的存储引擎(例如InnoDB)中使用内部临时表的评估成本。增加这些值会使得优化器倾向于较少使用内部临时表的查询计划。
  • key_compare_cost代表了比较记录键的评估成本。增加该值将导致需要比较多个键值的查询计划变得更加昂贵。例如,执行filesort排序的查询计划比通过索引避免排序的查询计划相对更加昂贵。
  • memory_temptable_create_costmemory_temptable_row_cost代表了在MEMORY存储引擎中使用内部临时表的评估成本。增加这些值会使得优化器倾向于较少使用内部临时表的查询计划。
  • row_evaluate_cost代表了计算记录条件的评估成本。增加该值会导致检查许多数据行的查询计划变得更加昂贵。例如,与读取少量数据行的索引范围扫描相比,全表扫描变得相对昂贵。

engine_cost中存储的是特定存储引擎相关操作的成本估计值:

engine_name表示存储引擎,“default”表示所有存储引擎,也可以为不同的存储引擎插入特定的数据。cost_value为空表示使用default_value。其中,

  • io_block_read_cost代表了从磁盘读取索引或数据块的成本。增加该值会使读取许多磁盘块的查询计划变得更加昂贵。例如,与读取较少块的索引范围扫描相比,全表扫描变得相对昂贵。
  • memory_block_read_cost与io_block_read_cost类似,但是它表示从数据库缓冲区读取索引或数据块的成本。
mysql> explain format=json select * from t1 where id>1 and id<1000 \G;
*************************** 1. row ***************************
EXPLAIN: {
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "200.14"
    },
    "table": {
      "table_name": "t1",
      "access_type": "range",
      "possible_keys": [
        "PRIMARY"
      ],
      "key": "PRIMARY",
      "used_key_parts": [
        "id"
      ],
      "key_length": "4",
      "rows_examined_per_scan": 998,
      "rows_produced_per_join": 998,
      "filtered": "100.00",
      "cost_info": {
        "read_cost": "100.34",
        "eval_cost": "99.80",
        "prefix_cost": "200.14",
        "data_read_per_join": "23K"
      },
      "used_columns": [
        "id",
        "f1",
        "f2",
        "c1"
      ],
      "attached_condition": "((`testdb`.`t1`.`id` > 1) and (`testdb`.`t1`.`id` < 1000))"
    }
  }
}
1 row in set, 1 warning (0.00 sec)

ERROR:
No query specified

mysql> explain format=json select * from t1 where id>1 and id<10 \G;
*************************** 1. row ***************************
EXPLAIN: {
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "1.86"
    },
    "table": {
      "table_name": "t1",
      "access_type": "range",
      "possible_keys": [
        "PRIMARY"
      ],
      "key": "PRIMARY",
      "used_key_parts": [
        "id"
      ],
      "key_length": "4",
      "rows_examined_per_scan": 8,
      "rows_produced_per_join": 8,
      "filtered": "100.00",
      "cost_info": {
        "read_cost": "1.06",
        "eval_cost": "0.80",
        "prefix_cost": "1.86",
        "data_read_per_join": "192"
      },
      "used_columns": [
        "id",
        "f1",
        "f2",
        "c1"
      ],
      "attached_condition": "((`testdb`.`t1`.`id` > 1) and (`testdb`.`t1`.`id` < 10))"
    }
  }
}
1 row in set, 1 warning (0.00 sec)

ERROR:
No query specified

mysql>
mysql> explain format=json select * from t1 where id>1 and id<4000 \G;
*************************** 1. row ***************************
EXPLAIN: {
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "800.97"
    },
    "table": {
      "table_name": "t1",
      "access_type": "range",
      "possible_keys": [
        "PRIMARY"
      ],
      "key": "PRIMARY",
      "used_key_parts": [
        "id"
      ],
      "key_length": "4",
      "rows_examined_per_scan": 3998,
      "rows_produced_per_join": 3998,
      "filtered": "100.00",
      "cost_info": {
        "read_cost": "401.17",
        "eval_cost": "399.80",
        "prefix_cost": "800.97",
        "data_read_per_join": "93K"
      },
      "used_columns": [
        "id",
        "f1",
        "f2",
        "c1"
      ],
      "attached_condition": "((`testdb`.`t1`.`id` > 1) and (`testdb`.`t1`.`id` < 4000))"
    }
  }
}
1 row in set, 1 warning (0.00 sec)

ERROR:
No query specified

mysql> explain format=json select * from t1 where id>1 and id<8000 \G;
*************************** 1. row ***************************
EXPLAIN: {
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "1000.65"
    },
    "table": {
      "table_name": "t1",
      "access_type": "range",
      "possible_keys": [
        "PRIMARY"
      ],
      "key": "PRIMARY",
      "used_key_parts": [
        "id"
      ],
      "key_length": "4",
      "rows_examined_per_scan": 4995,
      "rows_produced_per_join": 4995,
      "filtered": "100.00",
      "cost_info": {
        "read_cost": "501.15",
        "eval_cost": "499.50",
        "prefix_cost": "1000.65",
        "data_read_per_join": "117K"
      },
      "used_columns": [
        "id",
        "f1",
        "f2",
        "c1"
      ],
      "attached_condition": "((`testdb`.`t1`.`id` > 1) and (`testdb`.`t1`.`id` < 8000))"
    }
  }
}
1 row in set, 1 warning (0.00 sec)

ERROR:
No query specified

mysql>
mysql> explain format=json select * from t1 where id>1 and id<10000 \G;
*************************** 1. row ***************************
EXPLAIN: {
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "1000.65"
    },
    "table": {
      "table_name": "t1",
      "access_type": "range",
      "possible_keys": [
        "PRIMARY"
      ],
      "key": "PRIMARY",
      "used_key_parts": [
        "id"
      ],
      "key_length": "4",
      "rows_examined_per_scan": 4995,
      "rows_produced_per_join": 4995,
      "filtered": "100.00",
      "cost_info": {
        "read_cost": "501.15",
        "eval_cost": "499.50",
        "prefix_cost": "1000.65",
        "data_read_per_join": "117K"
      },
      "used_columns": [
        "id",
        "f1",
        "f2",
        "c1"
      ],
      "attached_condition": "((`testdb`.`t1`.`id` > 1) and (`testdb`.`t1`.`id` < 10000))"
    }
  }
}
1 row in set, 1 warning (0.00 sec)

ERROR:
No query specified

mysql>

t1表上只有id这一个主键索引,总共1w条数据。

上面SQL结果可以看到:where 条件从 id>1 and id<10,   id>1 and id<1000,   id>1 and id<4000, id>1 and id<8000,    id>1 and id<10000去观察cost_info的成本变化。无论where条件扫描的数据范围是多少,始终是走access_type=range的主键索引的范围扫描。

 接下来我们将数据行比较的成本常量row_evaluate_cost从0.1改为1,并且刷新内存中的值:

可以看到主键的执行计划很稳定,修改前后成本消耗一样,还是走主键的范围扫描(理论上全部扫描还要走索引,成本是更高的)。

强制不走主键索引,比走主键索引,从成本差不多1000.36=1000.65。

注意事项
在使用 FORCE INDEX /IGNORE INDEX 关键字时,需要注意以下几点:

  1. 强制索引可能会导致性能下降:强制 MySQL 使用一个不是最优的索引可能会导致查询性能下降,因此在使用前需要慎重考虑。
  2. 确保索引的存在:在使用 FORCE INDEX 关键字时,需要确保指定的索引是存在的,否则会导致语法错误。
  3. 谨慎使用:强制不使用索引是一种绕过优化器的方法,一般情况下并不建议使用,除非有明确的理由和测试结果支持。
     

最后,记得将row_evaluate_cost的还原成默认设置并重新连接数据库:

update mysql.server_cost 
set cost_value= null
where cost_name='row_evaluate_cost';

flush optimizer_costs;

备注:不要轻易修改成本常量,因为这样可能导致许多查询计划变得更糟!在大多数生产情况下,推荐通过添加优化器提示(optimizer hint)控制查询计划的选择。

 

4.执行计划改进

MySQL提供了一个系统变量 optimizer_switch,用于控制优化器的优化行为。

科普文:软件架构数据库系列之【MySQL查询优化器中的优化策略optimizer_switch】-CSDN博客

该变量支持全局和会话级别的设置,可以在运行时进行更改。

SET [GLOBAL|SESSION] optimizer_switch='command[,command]...';

其中,command可以是以下形式:

  • default,将所有优化行为设置为默认值。
  • opt_name=default,将指定优化行为设置为默认值。
  • opt_name=off,禁用指定的优化行为。
  • opt_name=on,启用指定的优化行为。

科普文:软件架构数据库系列之【MySQL查询优化器中的优化策略optimizer_switch--ICP索引下推】_查询优化器 optimizer-CSDN博客

科普文:软件架构数据库系列之【MySQL查询优化器中的优化策略optimizer_switch--MRR 优化器】-CSDN博客

科普文:软件架构数据库系列之【MySQL查询优化器中的优化策略optimizer_switch-- Index Merge 索引合并 优化器】-CSDN博客

科普文:软件架构数据库系列之【MySQL查询优化器中的优化策略optimizer_switch--Hash Join和BNL(Block Nested Loop)优化器】-CSDN博客

虽然通过系统变量optimizer_switch可以控制优化器的优化策略,但是一旦改变它的值,后续的查询都会受到影响,除非再次进行设置。

另一种控制优化器策略的方法就是优化器提示(Optimizer Hint)和索引提示(Index Hint),它们只对单个语句有效,而且优先级比optimizer_switch更高。

优化器提示使用 /*+ … */ 注释风格的语法,可以对连接顺序、表访问方式、索引使用方式、子查询、语句执行时间限制、系统变量以及资源组等进行语句级别的设置。

科普文:软件架构数据库系列之【MySQL控制查询优化器Hints】-CSDN博客

优化器的作用

优化器按照一定原则生成它认为的SQL命令在当前情形下最有效的执行路径,其作用就是为了生成SQL的执行计划。

优化器的不足和局限性

MySQL优化器可以很好地处理大部分查询语句,尤其是简单查询。随着MySQL版本的更新,对于复杂查询的实现也更加高效,例如MySQL 8.0支持提供了哈希连接(Hash Join)算法,替代之前基于块的嵌套循环连接(Block Nested-Loop Join),可以极大地提升多表连接的性能。

尽管如此,MySQL优化器目前仍然存在一些局限性,某些情况下的实现并不是最优方案。我们需要了解这些限制,并通过改写查询或者采用其他方法优化性能。

  • 不支持并行执行。MySQL采用单进程多线程模型,不支持多核并行执行特性。可以在应用层拆分查询,实现多个SQL语句的并行查询。

  • UNION的限制。优化器选项derived_condition_pushdown可以将查询条件下推到子查询内部,包括使用了UNION子句的派生表,但不是所有的外部查询子句都可以下推,例如LIMIT子句。

CREATE TABLE t1 (
  id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, 
  c1 INT, 
  KEY i1 (c1)
);

CREATE TABLE t2 (
  id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, 
  c1 INT, 
  KEY i1 (c1)
);

EXPLAIN FORMAT=TREE 
SELECT * FROM 
(  SELECT id, c1 FROM t1
   UNION ALL
   SELECT id, c1 FROM t2) v
WHERE c1 = 12;

-> Table scan on v  (cost=2.16..3.42 rows=2)
    -> Union all materialize  (cost=0.90..0.90 rows=2)
        -> Covering index lookup on t1 using i1 (c1=12)  (cost=0.35 rows=1)
        -> Covering index lookup on t2 using i1 (c1=12)  (cost=0.35 rows=1)


EXPLAIN FORMAT=TREE 
SELECT * FROM 
(  SELECT id, c1 FROM t1
   UNION ALL
   SELECT id, c1 FROM t2) v
WHERE c1 = 12
LIMIT 10;

-> Limit: 10 row(s)  (cost=2.16..3.42 rows=2)
    -> Table scan on v  (cost=2.16..3.42 rows=2)
        -> Union all materialize  (cost=0.90..0.90 rows=2)
            -> Covering index lookup on t1 using i1 (c1=12)  (cost=0.35 rows=1)
            -> Covering index lookup on t2 using i1 (c1=12)  (cost=0.35 rows=1)
  • 不允许更新和查询同一张表。(字面意思就是update的表不能出现在from语句中,原因是mysql对子查询的支持是比较薄弱的 。)例如:
UPDATE t1 AS o
SET c1 = (SELECT count(*) FROM t1 AS i WHERE i.id=o.id);

-- SQL 错误 [1093] [HY000]: You can't specify target table 'o' for update in FROM clause
-- 这个问题可以使用派生表解决:

UPDATE t1 
JOIN (SELECT id, count(*) tot FROM t1 GROUP BY id) AS i
USING (id)
SET t1.c1 = i.tot;

MySQL执行update时的[ERROR 1093]处理方法_mysql update 1093-CSDN博客

优化器分类

传统关系型数据库里面的优化器分为CBO和RBO两种。

RBO--- Rule_Based Potimizer  基于规则的优化器

RBO   RBO所用的判断规则是一组内置的规则,这些规则是硬编码在数据库的编码中的,RBO会根据这些规则去从SQL诸多的路径中来选择一条作为执行计划(比如在RBO里面,有这么一条规则:有索引使用索引。那么所有带有索引的表在任何情况下都会走索引)所以,RBO现在被很多数据库抛弃(oracle默认是CBO,但是仍然保留RBO代码,MySQL只有CBO)

CBO---Cost_Based Potimizer   基于成本的优化器

CBO  RBO最大问题在于硬编码在数据库里面的一系列固定规则,来决定执行计划。并没有考虑目标SQL中所涉及的对象的实际数量,实际数据的分布情况,这样一旦规则不适用于该SQL,那么很可能选出来的执行计划就不是最优执行计划了。

CBO在会从目标诸多的执行路径中选择一个成本最小的执行路径来作为执行计划。这里的成本他实际代表了MySQL根据相关统计信息计算出来目标SQL对应的步骤的IO,CPU等消耗。也就是意味着数据库里的成本实际上就是对于执行目标SQL所需要IO,CPU等资源的一个估计值。而成本值是根据索引,表,行的统计信息计算出来的。(计算过程比较复杂)。

成本优化器:MySQL :: MySQL 8.4 Reference Manual :: 10.9.5 The Optimizer Cost Model

统计信息:MySQL :: MySQL 8.4 Reference Manual :: 10.9.6 Optimizer Statistics 

CBO的一些基本概念

基数Cardinality

基数Cardinality是CBO特有的概念,它是指指定集合包含的记录数,说白了就是指定结果集的行数。Cardinality和成本值的估计息息相关,因为MySQL的指定结果集所消耗的io资源可以近似看做随着该结果集的递增而递增。

可选择率selectivity

可选择率也是CBO特有的概念,它是指施加指定条件后返回结果集的记录数占未施加任何谓词条件的原始结果集的记录数的比率(谓词条件,可以理解为where等限定词进行限定)

selectivity(可选择率) = 施加指定谓词条件后返回结果集的记录数 / 未施加指定谓词条件的结果集的记录数。

我们可以通过可选择率的估计,来确定一个列上是否需要添加索引,实际上,MySQL的CBO也是通过可选择率来确定是否走索引,值越大在做表连接的时候,就越有机会选择这个索引。

基数Cardinality的总结

1. 列值代表的是此列中存储的唯一值的个数(如果此列为primary key 则值为记录的行数)

2. 列值只是个估计值,并不准确。

3. 列值不会自动更新,需要通过Analyze table来更新一张表或者mysqlcheck -Aa来进行更新整个数据库。

4. 列值的大小影响Join时是否选用这个Index的判断。

如何查看MySQL的成本?

执行一次where的成本,可以通过查询当前会话的last_query_cost来计算当前的查询成本

mysql> select * from t1 where id=1;
+----+----+----+------+
| id | f1 | f2 | c1   |
+----+----+----+------+
|  1 |  1 |  1 |    1 |
+----+----+----+------+
1 row in set (0.00 sec)

mysql> show status like '%last_query_cost%';
+-----------------+----------+
| Variable_name   | Value    |
+-----------------+----------+
| Last_query_cost | 1.000000 |
+-----------------+----------+
1 row in set (0.00 sec)

这个结果表示,MySQL大概要做1.000000页的随机查找才能完成查询,这是根据一系列的统计信息得来的。每个表或者索引的页面个数,索引的基数(Cardinality),索引和数据行的长度,索引分布,优化器在估算成本的时候不考虑缓存,他预设每次都需要一次磁盘IO。


mysql> select count(*) from t1;
+----------+
| count(*) |
+----------+
|    10000 |
+----------+
1 row in set (0.00 sec)

mysql> show status like '%last_query_cost%';
+-----------------+-------------+
| Variable_name   | Value       |
+-----------------+-------------+
| Last_query_cost | 1005.349000 |
+-----------------+-------------+
1 row in set (0.00 sec)

mysql> select * from t1 where f1 in (5,100,671);
+-----+-----+-----+------+
| id  | f1  | f2  | c1   |
+-----+-----+-----+------+
|   5 |   5 |   5 |    5 |
| 100 | 100 | 100 |  100 |
| 671 | 671 | 671 |  671 |
+-----+-----+-----+------+
3 rows in set (0.01 sec)

mysql> show status like '%last_query_cost%';
+-----------------+-------------+
| Variable_name   | Value       |
+-----------------+-------------+
| Last_query_cost | 1005.349000 |
+-----------------+-------------+
1 row in set (0.00 sec)

mysql>

MySQL统计信息

MySQL统计信息是指数据库通过采样、统计出来的表、索引的相关信息,例如,表的记录数、聚集索引page个数、字段的Cardinality....。

MySQL在生成执行计划时,需要根据索引的统计信息进行估算,计算出最低代价(或者说是最小开销)的执行计划.MySQL支持有限的索引统计信息,因存储引擎不同而统计信息收集的方式也不同. MySQL官方关于统计信息的概念介绍几乎等同于无,不过对于已经接触过其它类型数据库的同学而言,理解这个概念应该不在话下。

备注:本文以MySQL8.4为准,后面的验证均在MySQL8.4环境中完成。

统计信息概叙

相对于其它数据库而言,MySQL统计信息无法手工删除。MySQL 8.0之前的版本,MySQL是没有直方图的。

1. 优化器使用统计信息为SQL选择执行计划。

2. MySQL8.0之前的版本没有数据直方图也无法手工删除统计信息。MySQL8.0开始支持直方图,直方图是对统计信息的增强。

3. 在服务器层有优化器,却没有保存数据和索引统计信息。统计信息由存储引擎实现,不同的存储引擎会存储不同的统计信息。

4. 统计信息分为索引统计信息,表统计信息;InnoDB统计信息分为持久化统计信息和非持久化统计信息两类。

表统计信息是数据库基于成本的优化器最重要的参考信息;统计信息不准确,优化器可能给出不够优化的执行计划或者是错误的执行计划

如果后期有 SQL 走的执行计划不对,或者不是最优的,那就可以断定相关统计信息太旧了,需要及时更新。比如有时候多表 JOIN 的顺序不对,导致查询效率变差,需要人工介入等等。

持久化统计信息在服务器重启期间持久化,从而实现更大的计划稳定性和更一致的查询性能。持久统计信息还提供了控制和灵活性,还有以下额外好处:

  1. 可以使用innodb_stats_auto_recalc配置选项来控制表发生重大更改后统计信息是否自动更新。
  2. 您可以在CREATE TABLE和ALTER TABLE语句中使用STATS_PERSISTENT、STATS_AUTO_RECALC和STATS_SAMPLE_PAGES子句来配置各个表的优化统计信息。
  3. 您可以在mysql中查询优化器统计数据。mysql.innodb_table_stats和mysql.innodb_index_stats表。
  4. 可以查看last_update在mysql.innodb_table_stats和mysql.Innodb_index_stats表,查看上一次更新统计信息的时间。
  5. 您可以手动修改mysql.innodb_table_stats和mysql.Innodb_index_stats表强制执行一个特定的查询优化计划,或者在不修改数据库的情况下测试替代计划。

默认情况下,持久化优化器统计特性是启用的(innodb_stats_persistent=ON)。

mysql> show variables like '%innodb_stats%';
+--------------------------------------+-------------+
| Variable_name                        | Value       |
+--------------------------------------+-------------+
| innodb_stats_auto_recalc             | ON          |
| innodb_stats_include_delete_marked   | OFF         |
| innodb_stats_method                  | nulls_equal |
| innodb_stats_on_metadata             | OFF         |
| innodb_stats_persistent              | ON          |
| innodb_stats_persistent_sample_pages | 20          |
| innodb_stats_transient_sample_pages  | 8           |
+--------------------------------------+-------------+
7 rows in set, 1 warning (0.01 sec)

非持久性优化器统计信息将在每次服务器重启和一些其他操作之后清除,并在下一次访问表时重新计算。因此,在重新计算统计信息时可能产生不同的估计,从而导致执行计划的不同选择和查询性能的变化。

统计信息语法

除了成本常量之外,MySQL优化器在优化的过程中还会使用数据字典和存储引擎中的统计信息。例如表的数据量、索引、索引的唯一性以及字段是否可以为空都会影响到执行计划的选择,包括数据的访问方法和表的连接顺序等。

MySQL会在日常操作过程中粗略统计表的大小和索引的基数(Cardinality),我们也可以使用ANALYZE TABLE语句手动更新表的统计信息和索引的数据分布。

ANALYZE TABLE tbl_name [, tbl_name] ...;

这些统计信息默认会持久化到数据字典表mysql.innodb_index_stats和mysql.innodb_table_stats中,也可以通过INFORMATION_SCHEMA视图TABLES、STATISTICS以及INNODB_INDEXES进行查看。

MySQL :: MySQL 8.4 Reference Manual :: 15.7.3.1 ANALYZE TABLE Statement

ANALYZE [NO_WRITE_TO_BINLOG | LOCAL]
    TABLE tbl_name [, tbl_name] ...

ANALYZE [NO_WRITE_TO_BINLOG | LOCAL]
    TABLE tbl_name
    UPDATE HISTOGRAM ON col_name [, col_name] ...
        [WITH N BUCKETS]
    [{MANUAL | AUTO} UPDATE]

ANALYZE [NO_WRITE_TO_BINLOG | LOCAL] 
    TABLE tbl_name
    UPDATE HISTOGRAM ON col_name [USING DATA 'json_data']

ANALYZE [NO_WRITE_TO_BINLOG | LOCAL]
    TABLE tbl_name
    DROP HISTOGRAM ON col_name [, col_name] ...

直方图

MySQL :: MySQL 8.4 Reference Manual :: 15.7.3.1 ANALYZE TABLE Statement

ANALYZE TABLE t UPDATE HISTOGRAM ON c1, c2, c3 WITH 10 BUCKETS;
ANALYZE TABLE t UPDATE HISTOGRAM ON c1, c3 WITH 10 BUCKETS;
ANALYZE TABLE t DROP HISTOGRAM ON c2;

从MySQL 8.0开始增加了直方图统计(histogram statistics),也就是字段值的分布情况。用户同样可以通过ANALYZE TABLE语句生成或者删除字段的直方图:

ANALYZE TABLE tbl_name
UPDATE HISTOGRAM ON col_name [, col_name] ...
[WITH N BUCKETS];

ANALYZE TABLE tbl_name
DROP HISTOGRAM ON col_name [, col_name] ...;

其中,WITH N BUCKETS用于指定直方图统计时桶的个数,取值范围从1到1024,默认为100。

直方图统计主要用于没有创建索引的字段,当查询使用这些字段与常量进行比较时,MySQL优化器会使用直方图统计评估过滤之后的行数。

例如,以下语句显示了没有直方图统计时的优化器评估:

explain analyze
select * 
from employee
where salary = 10000;

-> Filter: (employee.salary = 10000.00)  (cost=2.75 rows=3) (actual time=0.612..0.655 rows=1 loops=1)
    -> Table scan on employee  (cost=2.75 rows=25) (actual time=0.455..0.529 rows=25 loops=1)

由于salary字段上既没有索引也没有直方图统计,因此优化器评估返回的行数为3,但实际返回的行数为1。

我们为salary字段创建直方图统计:

analyze table employee update histogram on salary;
Table        |Op       |Msg_type|Msg_text                                         |
-------------|---------|--------|-------------------------------------------------|
hrdb.employee|histogram|status  |Histogram statistics created for column 'salary'.|

然后再次查看执行计划:

explain analyze
select * 
from employee
where salary = 10000;

-> Filter: (employee.salary = 10000.00)  (cost=2.75 rows=1) (actual time=0.265..0.291 rows=1 loops=1)
    -> Table scan on employee  (cost=2.75 rows=25) (actual time=0.206..0.258 rows=25 loops=1)

此时,优化器评估的行数和实际返回的行数一致,都是1。

MySQL使用数据字典表column_statistics存储字段值分布的直方图统计,用户可以通过查询视图INFORMATION_SCHEMA.COLUMN_STATISTICS获得直方图信息:

select * from information_schema.column_statistics;
SCHEMA_NAME|TABLE_NAME|COLUMN_NAME|HISTOGRAM                                            |-----------|----------|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
hrdb       |employee  |salary     |{"buckets": [[4000.00, 0.08], [4100.00, 0.12], [4200.00, 0.16], [4300.00, 0.2], [4700.00, 0.24000000000000002], [4800.00, 0.28], [5800.00, 0.32], [6000.00, 0.4], [6500.00, 0.48000000000000004], [6600.00, 0.52], [6800.00, 0.56], [7000.00, 0.600000000000000|

删除以上直方图统计的命令如下:

analyze table employee drop histogram on salary;

索引和直方图之间的区别在于:

  • 索引需要随着数据的修改而更新;
  • 直方图通过命令手动更新,不会影响数据更新的性能。但是,直方图统计会随着数据修改变得过时。

相对于直方图统计,优化器会优先选择索引范围优化评估返回的数据行。因为对于索引字段而言,范围优化可以获得更加准确的评估。

 

非持久化统计信息

  • 统计信息没有保存在磁盘上,而是频繁的实时计算统计信息;

  • 每次对表的访问都会重新计算其统计信息;

假设针对一张大表的频繁查询,那么每次都要重新计算统计信息,很耗费资源。

持久化统计信息

  • 把一张表在某一时刻的统计信息值保存在磁盘上;

  • 避免每次查询时重新计算;

  • 如果表更新不是很频繁,或者没有达到 MySQL 必须重新计算统计信息的临界值,可直接从磁盘上获取;

  • 即使 MySQL 服务重启,也可以快速的获取统计信息值;

  • 统计信息的持久化可以针对全局设置也可以针对单表设置。

统计信息的收集

Analyze table收集表和索引统计信息,适用于MyISAM和InnoDB

对于INNODB存储引擎,在以下情况下,会重新收集统计信息:

1.    表第一次打开的时候

2.    表修改的行超过1/16 或者新插入20亿行的时候计算索引的统计信息

3.     执行show index或者查询information schema下的表

information_schema.TABLES

information_schema.STATISTICS

information_schema.PARTITIONS

information_schema.KEY_COLUMN_USAGE

information_schema.TABLE_CONSTRAINTS

information_schema.REFERENTIAL_CONSTRAINTS

information_schema.table_constraints

 统计信息参数

MySQL :: MySQL 8.4 Reference Manual :: 17.8.10.1 Configuring Persistent Optimizer Statistics Parameters

#是否持久化统计信息,默认打开

持久性的统计存储在mysql.innodb_table_stats和mysql.innodb_index_stats中

innodb_stats_persistent_sample_pages

#当打开innodb_stats_persistent选项时,这个设置才生效

innodb_stats_transient_sample_pages

#当关闭innodb_stats_persistent选项时生效,采样page数(尤其是后者)不应该设置的太大,否则会产生额外的IO开销,但也不应设置的太小,否则会导致查询计划不准确

innodb_stats_auto_recalc

#用于决定是否在表上存在大量更新时(超过10%的记录更新)重新计算统计信息。默认打开,如果关闭该选项,就需要在每次创建索引或者更改列之后,运行一次ANALYZE TABLE命令来更新统计信息,否则可能选择错误的执行计划。同样的,也可以在CREATE TABLE/ALTER TABLE命令中指定STATS_AUTO_RECALC值

innodb_stats_on_metadata

源码中,对于统计信息的收集,每个表维护一个stat_modified_counter 变量每次DML更新一行,此变量就+1,

当达到更新统计信息的阈值时,此重置为0,数据库层面,innodb_stats_on_metadata参数用来控制是否自动收集元数据信息,设置为false时不更新统计信息,在5.5中此参数默认开启;但是5.6中默认关闭,

innodb_stats_method

这个参数描述了在收集统计信息的时候,对于null的处理,这个参数可以影响到index的统计信息的对于Cardinality的处理。如果使用Nulls_unequal时,而且此时的null数量过多的话,是会影响查询性能的。

Nulls_equal:   所有Null都相等

Nulls_unequal: 所有Null互不相同

Nulls_ignored: 忽略Null      

 

一、持久化统计相关参数:

  • innodb_stats_persistent :是否开启统计信息持久化,默认开启。

  • innodb_stats_auto_recalc :是否自动重新计算持久化统计信息,默认开启。

二、具体的更新策略为:

当一张表数据变化超过 10% 后,MySQL 会针对这张表统计信息的更新时间戳做一个判断,检查最后一次更新的时间是否超过 10 秒;如果不到 10 秒,把这张表加到一个统计信息更新队列中,到时间了再重新计算;如果超过了 10 秒,直接重新计算,并且更新时间戳。目前这个超时时间写死在 MySQL 代码里,暂时不能更改。不过在某些 MySQL 分支版还可以控制这个时间,比如 Percona。

  • innodb_stats_include_delete_marked :更新持久化统计信息时,是否会计算已经标记为删除的行。

    默认是关闭的,会获取未提交的脏数据。开启这个选项,MySQL 计算统计信息时只会考虑已经提交的数据。

  • innodb_stats_persistent_sample_pages :用于更新持久化索引分布或者其他统计信息的随机基数页,默认 20 个。

    页数越多,统计信息也就越准确,也就有助于查询优化器选择最优的查询计划。

什么时候考虑更改这个值呢?

1. 当查询计划不是很准确时。比如对比指定表在系统表 mysql.innodb_index_stats 的数据跟 distinct 查询的结果,如果相差太大,可以考虑增加这个值。

2. 当 analyze table 变的非常慢时,可能是这个值设置的太大了,此时要考虑减小这个值。

三、非持久化统计信息参数

  • innodb_stats_transient_sample_pages:设置非持久化统计信息的采样页数目,默认 8 个。

  • innodb_stats_on_metadata:当统计信息配置为非持久化时生效,默认关闭。

    参数 innodb_stats_persistent 为 0 或者建表时属性 STATS_PERSISTENT=0 才起作用。

当开启后,对以下元数据的访问会自动更新统计信息:

  • show table status

  • show index

  • information_schema.tables

  • information_schema.statistics

所以开启这个选项会额外增加访问表的开销,特别是大表。

还有一些其他的场景会自动更新非持久化统计信息,比如:

1. 表第一次被访问;

2. InnoDB 检测到有十六分之一的表自从上次统计信息计算后被更新了,这时触发自动更新;

3. MySQL 客户端默认选项 --auto-rehash 打开所有 InnoDB 表,导致所有 InnoDB 表被自动更新统计信息;

四、表属性控制

STATS_AUTO_RECALC

用来指定是否要自动计算指定 InnoDB 表的统计信息。三个值:default, 0, 1

  • default:也就是默认值,依赖 server 端参数 innodb_stats_auto_recalc 的设置效果

  • 0:表示禁用统计信息的自动重新计算,也就是永远不重新计算,需要手动执行 analyze table

  • 1:表示当表数据有 10% 的数据变化后,则重新计算持久化统计信息。

STATS_PERSISTENT

用来指定是否要开启指定 InnoDB 表的统计信息持久化。三个值:default, 0, 1

  • default:依赖 server 端参数 innodb_stats_persistent 的设置

  • 0:表示不需要持久化统计信息

  • 1:表示开启持久化统计信息

STATS_SAMPLE_PAGES

用来指定计算统计信息时的采样页数量。

五、手动更新统计信息

analyze table 用来手动更新表统计信息。建议在业务低峰时执行。

六、持久化表统计元数据信息

优化器通过两张元数据表里的数据来决定查询最优执行计划。

表统计信息保存在表 mysql.innodb_table_stats 里

比如表 ytt_sample_persist 的统计信息

重要列说明:

  • n_rows:表的行数
  • clustered_index_size:主键的数据页个数
  • sum_of_other_index_sizes:二级索引的数据页个数

以下例子可以看testdb库的t1表的统计信息

强制收集统计信息

索引统计信息保存在表 mysql.innodb_index_stats 里

比如表t1 索引统计信息

  • Index_name:索引名字
  • stat_name / stat_value:统计名字和对应的值
  • sample_size:采样页个数
  • stat_description:统计名字详细信息描述

如何查看统计信息

索引统计信息

Show index from table或information_schema.statistics表

表统计信息

Show table statuslike或information_schema.tables表

***要注意的是,如果生产环境上,大表大数据上,要谨慎做这些线上的数据统计分析,如果在大表上面查询性能会出现大幅度抖动。

表统计信息的分析

 select * from information_schema.tables where table_schema='test'\G;

*************************** 2. row ***************************

TABLE_CATALOG: def     ---- 数据表登记目录

TABLE_SCHEMA: test      ----所属数据库名字

TABLE_NAME: t2          ----表名称

TABLE_TYPE: BASE TABLE  ----表类型(view|base table)

ENGINE: InnoDB          ----使用的存储引擎类型

VERSION: 10                ------数据文件对应FRM的版本(默认10)

ROW_FORMAT: Compact    -----行格式(compact|dynamic|fixed)

TABLE_ROWS: 7             ---行数

AVG_ROW_LENGTH: 2340 ---------平均行长度

DATA_LENGTH: 16384    ---------数据长度

MAX_DATA_LENGTH: 0   ---------最大数据长度

INDEX_LENGTH: 16384   ---------索引长度

DATA_FREE: 0              -----磁盘已经分配但是未使用

AUTO_INCREMENT: NULL    ------自增

CREATE_TIME: 2016-03-18 15:26:55  ----创建时间

UPDATE_TIME: NULL                ----更新时间

CHECK_TIME: NULL                       ---创建时间

TABLE_COLLATION: utf8_general_ci    ----字符集

CHECKSUM: NULL                      ---校验和

CREATE_OPTIONS:                 

TABLE_COMMENT:                    ---表描述

统计信息中,我们可以判断一个表的碎片的多少,看是否要进行碎片处理

 [(data_length + Index_length) - rows *Avg_row_length] /1024/1024

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

-无-为-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值