前言
对于非数据库开发人员而言,难以对MySQL源码进行分析或调试,接近一个黑盒,但MySQL提供了一些命令及系统状态变量,可对索引及其他内容进行分析。掌握这些方法后,可以尽量深入地了解MySQL的一些实现细节。
MySQL索引分析除了EXPLAIN还有什么方法
通过以下方法,可以获得MySQL索引相关的数据,便于分析与理解索引相关的问题。
2.1. 通过慢查询日志记录SQL语句执行时间
参考 https://dev.mysql.com/doc/refman/5.6/en/slow-query-log.html 。
慢查询日志中包含的SQL语句,其执行时间超过long_query_time(系统参数)秒,并且至少检查min_examined_row_limit(系统参数)行。
获取初始锁的时间不包含在执行时间中。mysqld(MySQL服务器)在SQL语句执行且释放所有锁之后,将一条语句写入慢查询日志中,因此日志顺序可能与执行顺序不同。
2.1.1. 慢查询日志的系统参数
long_query_time系统参数的最小值为0,默认值为10(秒)。将慢查询日志记录到文件时,写入的时间包括微秒部分。将慢查询日志记录到表,仅写入整数时间,微秒部分将被忽略。
默认情况下,慢查询日志是禁用的。slow_query_log系统参数可以设置慢查询日志是否启用,参数值为0或OFF表示禁用,1或ON表示启用。
slow_query_log_file系统参数用于设置日志文件名称,默认值为“[主机名]-slow.log”。MySQL服务器会在data目录中创建文件,除非通过绝对路径名指定了其他目录。
log_output系统参数用于设置通用查询日志与慢查询日志的输出位置。其值为一个或多个逗号分隔的单词组成的列表,可选值为TABLE,FILE及NONE。
- 当指定TABLE时,表示将日志记录到名为mysql的系统数据库的general_log与slow_log表中;
- 当指定FILE时,表示将日志记录到文件中;
- NONE表示禁用日志。
不使用索引对行进行查找的查询,需要在启用log_queries_not_using_indexes系统变量后,才会在写入慢查询日志时被包含进去。该系统默认为关闭。
查询缓存处理的查询不会被MySQL服务器记录。
2.1.2. 慢查询日志内容格式
为了通过慢查询日志记录查询语句的准确耗时,需要将慢查询日志保存至文件,log_output系统参数需要设置为FILE。慢查询日志文件中,每条语句生成的记录都有一行以#开头的数据,并包含以下字段(所有字段在一行中显示):
- Query_time
语句执行时间,单位为秒;
- Lock_time
获取锁的时间,单位为秒;
- Rows_sent
发送给客户端的行数;
- Rows_examined
MySQL服务器层检查的行数(不包含存储引擎内部的处理)。
慢查询日志文件中的每个语句之前都有一个SET语句,该SET语句包括一个时间戳,代表该慢查询语句的记录时间。
2.1.3. 开启慢查询日志的参数配置
为了开启慢查询日志,需要修改MySQL服务器使用的配置文件的“[mysqld]”节点对应的系统参数,修改配置文件并重启MySQL服务器程序后生效。(通过“set GLOBAL 系统变量=xxx”命令修改慢查询日志相关参数时,未生效。)系统参数设置如下所示:
设置“slow_query_log = 1”,以开启慢查询日志。
设置“long_query_time = 0”,将每次的查询语句都记录至慢查询日志文件中。
设置“min_examined_row_limit = 1”,可以使explain语句不出现在慢查询日志中,因为explain语句的Rows_examined为0。
慢查询日志文件默认生成在安装目录的data目录中。
使用可视化数据库管理工具连接MySQL时,可能会定时发送请求,也会生成在慢查询日志文件中。
2.1.4. 慢查询日志内容示例
慢查询日志内容示例如下:
经测试,以上Query_time时间仅包含MySQL服务器执行查询操作的耗时,不包含客户端展示数据的耗时。
2.2. 获取SQL语句读取索引次数
通过以下所述的方法,可以获取SQL语句执行时,读取的索引次数及方法,但不会显示读取了哪个索引。
2.2.1. SHOW STATUS语句用法
参考 https://dev.mysql.com/doc/refman/5.6/en/show-status.html 。
SHOW STATUS语句提供服务器状态信息。该语句不需要任何特权,只要能连接到服务器即可。
SHOW STATUS语句用法如下所示:
SHOW STATUS语句接受可选的作用域修饰符,可为GLOBAL或SESSION:
- 当指定作用域为GLOBAL时,SHOW STATUS显示全局的状态值。全局状态变量可以表示服务器本身某些方面的状态,或者表示MySQL所有连接的汇总状态。如果某个变量没有全局值,则显示会话值;
- 当指定作用域为SESSION时,SHOW STATUS显示当前连接的状态变量值。如果某个变量没有会话值,则显示全局值。LOCAL是SESSION的代名词;
- 如果没有指定作用域,默认使用SESSION。
每次调用SHOW STATUS语句都会使用一个内部临时表,并增加全局Created_tmp_tables值。
SHOW STATUS语句支持LIKE子句,仅显示变量名称与指定模式匹配的行。
2.2.2. 与读取索引次数相关的服务器状态变量
参考 https://dev.mysql.com/doc/refman/5.6/en/server-status-variables.html 。
与读取索引次数相关的服务器状态变量以“HANDLER_READ”开头,如下所示:
- Handler_read_first
索引中第一个条目的读取索引次数。如果该数值很高,则表明服务器正在执行很多全索引扫描(例如,SELECT col1 FROM foo,假设col1列存在索引);
- Handler_read_key
基于索引键读取行的请求数(即通过索引读取行的数量)。如果该数值很高,则表明表的索引很合适查询;
- Handler_read_last
读取索引中最后一个键的请求数。使用ORDER BY时,服务器将发出一个first-key请求(对应Handler_read_first),然后发出多个next-key(对应Handler_read_next)请求;使用ORDER BY DESC时,服务器将发出一个last-key(对应Handler_read_last)请求,然后发出多个previous-key(对应Handler_read_prev)请求;
- Handler_read_next
按索引键顺序读取下一行的请求数。当对索引列使用范围约束进行查询,或进行索引扫描时,该数值会增加;
- Handler_read_prev
按索引键顺序读取上一行的请求数。这种读取方法主要用于优化ORDER BY … DESC;
- Handler_read_rnd
基于固定位置读取行的请求数。当执行很多需要对结果进行排序的查询时,该数值会很高。可能有很多查询需要MySQL扫描整个表,或者联接未正确使用索引键;
- Handler_read_rnd_next
读取数据文件下一行的请求数(即通过全表扫描读取行的数量)。当进行大量全表扫描时,该数值会变高。通常这表明表未正确建立索引,或者编写的查询未利用索引。
2.2.3. 使用SHOW STATUS获取SQL语句读取索引次数
当需要获取当前会话的SQL语句读取索引次数,可以执行SHOW STATUS查看以“HANDLER_READ”开头的服务器状态变量(以下执行命令中的HANDLER_READ大小写不限):
若需要使以上数值重置,可以重新连接MySQL服务器(创建新的会话),或重启MySQL服务器程序。
2.2.4. SQL语句读取索引次数示例
在以下示例中,test_table_log表的create_time字段存在单列索引,id列为主键。执行以下语句时,需要读取索引中第一个记录,Handler_read_first值加1。
执行以下语句时,需要读取索引中的最后一个记录,Handler_read_last值加1。
执行以下语句时,需要基于索引读取行,Handler_read_key值加1。
执行以下语句时,需要基于索引读取行,Handler_read_key值加1;并需要根据索引读取后续9行,Handler_read_next值增加9。
执行以下语句时,共查询到4条记录需要基于索引读取行,Handler_read_key值加1;并需要根据索引读取后续行,Handler_read_next值增加4。
执行以下语句时,需要读取索引中第一个记录,Handler_read_first值加1;并需要根据索引读取后续9行,Handler_read_next值增加9。
执行以下语句时,需要读取索引中的最后一个记录,Handler_read_last值加1;并需要根据索引读取之前的9行,Handler_read_prev值增加9。
执行以下语句时,需要通过全表扫描读取数据文件中的行,数量为110,Handler_read_rnd_next值增加110。
执行以下语句时,需要通过全表扫描读取数据文件中的行,Handler_read_rnd_next值增加超过110;并需要对查询到的10条记录进行排序,Handler_read_rnd值增加10。
2.2.5. 查询数据库表获取SQL语句读取索引次数(不使用)
参考 https://dev.mysql.com/doc/refman/5.6/en/status-table.html 。
INFORMATION_SCHEMA.GLOBAL_STATUS与INFORMATION_SCHEMA.SESSION_STATUS表与“SHOW GLOBAL STATUS”及“SHOW SESSION STATUS”命令显示对应的信息。查询示例如下:
当从上述表查询数据获取SQL语句读取索引次数时,会导致Handler_read_rnd_next变量值增大;当使用排序时,还会导致Handler_read_rnd变量值增大。因此需要获取SQL语句读取索引次数时,不通过上述数据库表查询。
2.3. 获取InnoDB缓冲池中索引页与记录数
InnoDB缓冲池中索引页与记录数,可以反映MySQL在执行SQL语句时,将指定表的索引读取到缓冲池中的索引页及记录数量。
若需要重置InnoDB缓冲池相关数据,需要重启MySQL服务器程序(未找到其他方法清空InnoDB缓冲池)。
2.3.1. INNODB_BUFFER_PAGE表
参考 https://dev.mysql.com/doc/refman/5.6/en/innodb-information-schema-buffer-pool-tables.html 。
InnoDB INFORMATION_SCHEMA缓冲池表提供了缓冲池状态信息,以及有关InnoDB缓冲池页面的元数据。
InnoDB INFORMATION_SCHEMA缓冲池表中包含INNODB_BUFFER_PAGE,该表保存InnoDB缓冲池中每个页的信息。查询INNODB_BUFFER_PAGE或INNODB_BUFFER_PAGE_LRU表可能会影响性能,应避免在生产环境查询。
2.3.2. INNODB_BUFFER_PAGE表字段
参考 https://dev.mysql.com/doc/refman/5.6/en/innodb-buffer-page-table.html 。INNODB_BUFFER_PAGE表中包含以下关注的字段:
- POOL_ID
缓冲池ID。是用于区分多个缓冲池实例的标识符。
- SPACE
表空间ID。
- PAGE_NUMBER
页编号(page number)。
- PAGE_TYPE
页的类型。例如“INDEX”代表B+树节点,“BLOB”代表未压缩的BLOB页等。
- TABLE_NAME
页所属的表名。该列仅适用于PAGE_TYPE值为INDEX的页。
- INDEX_NAME
页所属的索引名。可以是聚簇索引或二级索引的名称。该列仅适用于PAGE_TYPE值为INDEX的页。
- NUMBER_RECORDS
页的记录的数量。
2.3.3. 从INNODB_BUFFER_PAGE表获取InnoDB缓冲池中索引页与记录数
从INNODB_BUFFER_PAGE表中可获取InnoDB缓冲池中索引页与记录数,table_name字段的形式为“`[数据库名]`.`[表名]`”。
查询的示例语句如下,page_num为指定表的指定索引在InnoDB缓冲池中的页的总数,record_num为指定表的指定索引在InnoDB缓冲池中记录的总数:
查询结果如下所示:
2.4. 获取InnoDB索引页操作次数
InnoDB对索引页的操作包括读取、创建及写入等。
若需要重置上述数据,需要重启MySQL服务器程序。
2.4.1. 使用SHOW STATUS获取InnoDB索引页操作次数
参考 https://dev.mysql.com/doc/refman/5.6/en/server-status-variables.html 。
MySQL服务器状态中,包含以下与InnoDB索引页操作次数相关的状态:
- Innodb_buffer_pool_pages_data
InnoDB缓冲池中包含数据的页数。该数量包含脏页与非脏页。
- Innodb_pages_created
对InnoDB表操作创建的页数。
- Innodb_pages_read
对InnoDB表操作,从InnoDB缓冲池读取的页数。
- Innodb_pages_written
对InnoDB表操作导致被写的页数。
使用SHOW STATUS获取InnoDB索引页操作次数时,可执行以下语句:
2.4.2. 查询数据库表获取InnoDB索引页操作次数
参考 https://dev.mysql.com/doc/refman/5.6/en/innodb-buffer-pool-stats-table.html 。
INFORMATION_SCHEMA.INNODB_BUFFER_POOL_STATS表提供了许多与“SHOW ENGINE INNODB STATUS”输出相同的缓冲池信息。
上述表包含以下关注的列:
- DATABASE_PAGES
InnoDB缓冲池中包含数据的页数。该数量包含脏页与非脏页。
与“SHOW STATUS”输出中的Innodb_buffer_pool_pages_data相同。
- NUMBER_PAGES_READ
读取的页数。
与“SHOW STATUS”输出中的Innodb_pages_read相同。
- NUMBER_PAGES_CREATED
创建的页数。
与“SHOW STATUS”输出中的Innodb_pages_created相同。
- NUMBER_PAGES_WRITTEN
写入的页数。
与“SHOW STATUS”输出中的Innodb_pages_written相同。
从上述表获取InnoDB索引页操作次数示例语句如下(查询操作不会导致以上参数变化):
2.4.3. 使用SHOW ENGINE STATUS获取InnoDB索引页操作次数(不使用)
参考 https://dev.mysql.com/doc/refman/5.6/en/show-engine.html 。
“SHOW ENGINE”显示指定存储引擎的操作信息。执行时需要“PROCESS”权限。
“SHOW ENGINE INNODB STATUS”显示来自标准InnoDB监控器的有关InnoDB存储引擎状态的大量信息。
以上输出信息全部包含在Status字段中,无法进行过滤,不方便单独查看Pages read, created, written等参数。
2.5. 获取索引页分裂次数
参考 https://dev.mysql.com/doc/refman/5.6/en/index-page-merge-threshold.html ,频繁发生页分裂可能对性能产生影响。
2.5.1. INFORMATION_SCHEMA.INNODB_METRICS表
参考 https://dev.mysql.com/doc/refman/5.6/en/innodb-metrics-table.html 。
INFORMATION_SCHEMA.INNODB_METRICS表提供了InnoDB的各种性能信息。
每个监控器代表InnoDB源代码中用于收集计数器(counter)信息的位置。每个计数器可被启动、停止及重置。
默认情况下,收集的数据比较少。为了启动、停止及重置计算器,需要对系统变量innodb_monitor_enable、innodb_monitor_disable、innodb_monitor_reset或innodb_monitor_reset_all进行设置。
INFORMATION_SCHEMA.INNODB_METRICS表包含以下关注的列:
- NAME
计数器的唯一名称;
- COUNT
计数器启用后的计数值;
- STATUS
enabled代表计数器在执行,disabled代表已停止;
- COMMENT
计数器描述。
在查询INFORMATION_SCHEMA.INNODB_METRICS表时,需要有PROCESS权限。
在INFORMATION_SCHEMA.INNODB_METRICS表中,存在NAME为“index_page_splits”,COMMENT为“Number of index page splits”的记录,即索引页分裂次数对应的计数器,COUNT列为索引页分裂次数。
2.5.2. 启用索引页分裂次数计数器
参考 https://dev.mysql.com/doc/refman/5.6/en/innodb-information-schema-metrics-table.html 。
全局变量innodb_monitor_enable、innodb_monitor_disable可分别用于启用、禁用记数器。命令如下所示:
innodb_monitor_enable变量还可以MySQL配置文件的[mysqld]节点进行配置,可使用半角逗号对多项进行分隔。innodb_monitor_disable不支持在MySQL配置文件配置。
经验证,对以上系统变量进行设置时,需要有SUPER权限(root用户)。
2.6. 获取执行计划
参考 https://dev.mysql.com/doc/refman/5.6/en/explain-output.html 。
EXPLAIN语句提供有关MySQL如何执行语句的信息。EXPLAIN适用于SELECT,DELETE,INSERT,REPLACE和UPDATE语句。
EXPLAIN为SELECT语句中使用的每个表返回一行信息。输出中的表的顺序,与MySQL在处理语句时读取表的顺序一致。
2.6.1. EXPLAIN输出列
EXPLAIN输出的列及含义如下所示:
2.6.2. id
id列为SELECT的标识符,其值为查询中SELECT的顺序号。
即id列反映了SELECT在执行时的顺序(从小到大)。
2.6.3. select_type
select_type列为SELECT的类型,select_type值及含义如下所示:
DEPENDENT通常表示使用了相关子查询(correlated subquery)。
2.6.4. type
type列为连接类型,描述了表的连接方式。不同的连接类型如下所示:
- system
表只有一行(系统表)。system是const连接类型的特例。
- const
查询的开头读取的表,最多只有一行匹配。因为只有一行,优化器的其余部分可以将此行中对应列的值视为常量。
const连接类型对应的表查询非常快,因为它们只读取一次。
将主键或唯一索引的所有部分与常量值进行比较时会使用const连接类型。
在以下示例中,tbl_name表可以作为const连接类型使用:
- eq_ref
对于先前的表的每种行组合,从eq_ref连接类型对应的表中读取一行。除了system和const连接类型外,eq_ref是最好的联接类型。当连接使用索引的所有部分并且索引是主键或非空唯一索引时,将使用eq_ref连接类型。
eq_ref可用于使用=运算符进行比较的索引列。比较值可以是常量,也可以是使用在此表之前读取的表中列的表达式。
在以下示例中,MySQL可以使用eq_ref连接类型处理ref_table表:
- ref
对于先前的表的每种行组合,从ref连接类型对应的表读取所有具有匹配索引值的行。
如果连接仅使用索引键的最左前缀或者索引键不是主键或唯一索引(即连接不能基于索引键值选择单行),则使用ref连接类型。
如果使用的键只匹配少量几行,则ref是一个好的连接类型。
ref连接类型可用于使用=或<=>运算符进行比较的索引列。
在以下示例中,MySQL可以使用ref连接类型处理ref_table表:
- fulltext
使用全文索引执行连接。
- ref_or_null
ref_or_null与ref类似,区别在于使用ref_or_null时,MySQL对包含NULL值的行进行了额外搜索。
ref_or_null连接类型最常用于解决子查询。
在以下示例中,MySQL可以使用ref_or_null连接类型处理ref_table表:
- index_merge
index_merge连接类型表示使用了索引合并优化。在这种情况下,输出行中的key列包含使用的索引列表,key_len列包含所用索引的最长键部分列表。
- unique_subquery
unique_subquery连接类型在某些使用IN的子查询的情况下替代eq_ref,如下所示:
unique_subquery只是一个索引查找函数,可以完全替代子查询以提高效率。
- index_subquery
index_subquery连接类型与unique_subquery类似,它替代了IN子查询,支持子查询中的非唯一索引,如下所示:
- range
range连接类型表示仅检索给定范围内的行,使用索引选择行。
EXPLAIN输出行中的key列表示使用哪个索引。key_len列包含使用的最长的索引键部分。对于此连接类型,ref列为NULL。
使用=,<>,>,> =,<,<=,IS NULL,<=>,BETWEEN,LIKE或IN()运算符中的任何一个将索引键列与常量进行比较时,可以使用range类型。
- index
index连接类型与ALL类型类似,区别在于index连接类型有对索引树进行扫描。有两种方式:a. 如果查询的索引是覆盖索引,并且可满足所需表的所有数据,则仅扫描索引树。在这种情况下,Extra列显示“Using index”。仅扫描索引通常比ALL连接类型快,因为索引的大小通常小于表数据。b. 对指定索引读取数据,进行全表扫描,以按索引顺序查找数据行。Extra列不会显示“Using index”。
当查询仅使用属于单个索引一部分的列时,MySQL可以使用此连接类型。
- ALL
对之前的表中的每个行组合进行全表扫描。
如果当前表是第一个没有标记为const的表,这通常是不好的,并且通常在所有其他情况下非常糟糕。
通常,可以通过添加索引来避免ALL连接类型,这些索引基于常量值或先前的表的列值,启用从该表的行检索。
2.6.5. possible_keys
possible_keys列表示MySQL可以选择用于查找此表的行的索引。请注意,此列于EXPLAIN输出中显示的表的顺序完全独立。
假如possible_keys列为NULL,说明没有相关的索引。
2.6.6. key
key列表示MySQL实际决定使用的索引键。如果MySQL决定使用其中一个possible_keys索引来查找行,那么该索引将被显示在key列中。
如果key列为NULL,说明MySQL未找到更有效执行查询的索引。
要强制MySQL使用或忽略possible_keys列中列出的索引,请在查询中使用FORCE INDEX,USE INDEX或IGNORE INDEX。
2.6.7. key_len
key_len列表示MySQL决定使用的索引键的长度。根据key_len的值可以确定MySQL实际使用了联合索引的多少部分。
由于索引键的存储格式,可以为NULL的列比不允许为NULL的列的索引键长度大1。
2.6.8. ref
ref列显示了在key列中列出的,用于从表中查询行的索引,与哪些列或常量进行了比较。
如果ref列的值为func,说明使用的值是某些函数的结果。
2.6.9. rows
rows列表示MySQL认为执行查询时必须检查的行数。
对于InnoDB表,该数字是估计值,可能并不总是准确的。
2.6.10. Extra
Extra列包含MySQL如何解决查询的额外信息。以下为Extra列的部分可能的值及说明:
- Using filesort
出现该提示说明MySQL在进行排序时需要使用filesort,未使用索引进行排序。
- Using index
从表中检索列信息时,仅使用索引树的信息,不需要进行额外的查找以读取实际行。
当仅查询属于单个索引的部分列时,可以使用此策略。
对于具有用户定义的聚簇索引的InnoDB表,即使Extra列中没有出现“Using index”,对应的索引也可能被使用。当type列为index且key列为主键时,对应以上情况。
- Using index condition
出现该提示说明MySQL在查询时使用了索引条件下推。
- Using sort_union(…), Using union(…), Using intersect(…)
以上显示了对应index_merge连接类型,即使用索引合并时,索引扫描合并时使用的特定算法。
- Using temporary
为了解决该查询,MySQL需要创建一个临时表来保存结果。如果查询包含GROUP BY和ORDER BY子句,且列出了不同的列时,通常会发生这种情况。
- Using where
WHERE子句用于限制与下一个表匹配的行,或发送到客户端的行。假如Extra列的值不是using where,且表的连接类型为ALL或index时,则说明查询语句存在一些问题,除非是特意为了获取或检查表中所有的行。
3. InnoDB读取索引页时机
InnoDB读取索引页的时机,包括但不限于MySQL启动、执行EXPLAIN语句、执行查询/插入/更新操作等情况。
为了获取InnoDB读取索引页的次数,可参考前文,查询以“Innodb_pages_”开头的系统状态,或从INFORMATION_SCHEMA.INNODB_BUFFER_POOL_STATS表查询。
3.1. MySQL启动时读取索引页
当MySQL启动时,会读取对部分表的索引。此时还未执行任何语句,查看InnoDB读取索引页的次数大于0,查看InnoDB缓冲池中索引页与记录数大于0。
3.2. 执行EXPLAIN语句时读取索引页
在执行EXPLAIN语句时,可能会读取相关表对应的索引页,若所需的索引页数据已被读取至缓冲池中,可能不会读取。对于上述情况,可查看InnoDB读取索引页的次数,执行EXPLAIN语句后次数会增加。
3.3. 执行插入/查询/更新/删除语句时读取索引页
在执行插入/查询/更新/删除语句时,可能会读取相关表对应的索引页,若所需的索引页数据已被读取至缓冲池中,可能不会读取。
4. 参考资料
以上参考的资料如下:https://dev.mysql.com/doc/refman/5.6/en/
原作者:adrninistrat0r