前言
对于非数据库开发人员而言,难以对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。
不使用索引对行进行查找的查询,需要在启用log_queries_not_using_indexes系统变量后,才会在写入慢查询日志时被包含进去。该系统默认为关闭。2.1.2. 慢查询日志内容格式
为了通过慢查询日志记录查询语句的准确耗时,需要将慢查询日志保存至文件,log_output系统参数需要设置为FILE。慢查询日志文件中,每条语句生成的记录都有一行以#开头的数据,并包含以下字段(所有字段在一行中显示):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
设置“slow_query_log = 1”,以开启慢查询日志。设置“long_query_time = 0”,将每次的查询语句都记录至慢查询日志文件中。设置“min_examined_row_limit = 1”,可以使explain语句不出现在慢查询日志中,因为explain语句的Rows_examined为0。slow_query_log = 1
long_query_time = 0
慢查询日志文件默认生成在安装目录的data目录中。使用可视化数据库管理工具连接MySQL时,可能会定时发送请求,也会生成在慢查询日志文件中。2.1.4. 慢查询日志内容示例
# Time: 200311 19:25:39
# User@Host: test[test] @ localhost [127.0.0.1]
# Thread_id: 3 Schema: testdb QC_hit: No
Query_time: 0.015360 Lock_time: 0.001452 Rows_sent: 5222 Rows_examined: 5222
use testdb;
SET timestamp=1583925939;
select id,cust_info from test_table_log where cust_info='1881';
经测试,以上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 [GLOBAL | SESSION] STATUS [LIKE 'pattern' | WHERE expr]
SHOW STATUS语句接受可选的作用域修饰符,可为GLOBAL或SESSION:- 当指定作用域为GLOBAL时,SHOW STATUS显示全局的状态值。全局状态变量可以表示服务器本身某些方面的状态,或者表示MySQL所有连接的汇总状态。如果某个变量没有全局值,则显示会话值;
- 当指定作用域为SESSION时,SHOW STATUS显示当前连接的状态变量值。如果某个变量没有会话值,则显示全局值。LOCAL是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”开头,如下所示:索引中第一个条目的读取索引次数。如果该数值很高,则表明服务器正在执行很多全索引扫描(例如,SELECT col1 FROM foo,假设col1列存在索引);基于索引键读取行的请求数(即通过索引读取行的数量)。如果该数值很高,则表明表的索引很合适查询;读取索引中最后一个键的请求数。使用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)请求;按索引键顺序读取下一行的请求数。当对索引列使用范围约束进行查询,或进行索引扫描时,该数值会增加;按索引键顺序读取上一行的请求数。这种读取方法主要用于优化ORDER BY … DESC;基于固定位置读取行的请求数。当执行很多需要对结果进行排序的查询时,该数值会很高。可能有很多查询需要MySQL扫描整个表,或者联接未正确使用索引键;读取数据文件下一行的请求数(即通过全表扫描读取行的数量)。当进行大量全表扫描时,该数值会变高。通常这表明表未正确建立索引,或者编写的查询未利用索引。2.2.3. 使用SHOW STATUS获取SQL语句读取索引次数
当需要获取当前会话的SQL语句读取索引次数,可以执行SHOW STATUS查看以“HANDLER_READ”开头的服务器状态变量(以下执行命令中的HANDLER_READ大小写不限):SHOW STATUS LIKE 'HANDLER_READ%';
若需要使以上数值重置,可以重新连接MySQL服务器(创建新的会话),或重启MySQL服务器程序。2.2.4. SQL语句读取索引次数示例
在以下示例中,test_table_log表的create_time字段存在单列索引,id列为主键。执行以下语句时,需要读取索引中第一个记录,Handler_read_first值加1。select * from test_table_log order by create_time limit 1;
执行以下语句时,需要读取索引中的最后一个记录,Handler_read_last值加1。select * from test_table_log order by create_time desc limit 1;
执行以下语句时,需要基于索引读取行,Handler_read_key值加1。select * from test_table_log where create_time = '2020/3/10 12:23:38' limit 1;
执行以下语句时,需要基于索引读取行,Handler_read_key值加1;并需要根据索引读取后续9行,Handler_read_next值增加9。select * from test_table_log where create_time = '2020/3/10 12:23:38' limit 10;
执行以下语句时,共查询到4条记录需要基于索引读取行,Handler_read_key值加1;并需要根据索引读取后续行,Handler_read_next值增加4。select * from test_table_log where id >= 'testtime1583814037num1' and id <= 'testtime1583814037num1000'
执行以下语句时,需要读取索引中第一个记录,Handler_read_first值加1;并需要根据索引读取后续9行,Handler_read_next值增加9。select * from test_table_log order by create_time limit 10;
执行以下语句时,需要读取索引中的最后一个记录,Handler_read_last值加1;并需要根据索引读取之前的9行,Handler_read_prev值增加9。select * from test_table_log order by create_time desc limit 10;
执行以下语句时,需要通过全表扫描读取数据文件中的行,数量为110,Handler_read_rnd_next值增加110。select * from test_table_log limit 100,10;
执行以下语句时,需要通过全表扫描读取数据文件中的行,Handler_read_rnd_next值增加超过110;并需要对查询到的10条记录进行排序,Handler_read_rnd值增加10。select a.* from
(
select * from test_table_log limit 100,10
) as a order by a.create_time;
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”命令显示对应的信息。查询示例如下:select * from INFORMATION_SCHEMA.SESSION_STATUS
where VARIABLE_NAME like 'HANDLER_READ%' and VARIABLE_VALUE <> '0'
order by cast(VARIABLE_VALUE as UNSIGNED INTEGER) desc;
当从上述表查询数据获取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表中包含以下关注的字段:页的类型。例如“INDEX”代表B+树节点,“BLOB”代表未压缩的BLOB页等。页所属的表名。该列仅适用于PAGE_TYPE值为INDEX的页。页所属的索引名。可以是聚簇索引或二级索引的名称。该列仅适用于PAGE_TYPE值为INDEX的页。2.3.3. 从INNODB_BUFFER_PAGE表获取InnoDB缓冲池中索引页与记录数
从INNODB_BUFFER_PAGE表中可获取InnoDB缓冲池中索引页与记录数,table_name字段的形式为“`[数据库名]`.`[表名]`”。查询的示例语句如下,page_num为指定表的指定索引在InnoDB缓冲池中的页的总数,record_num为指定表的指定索引在InnoDB缓冲池中记录的总数:select TABLE_NAME,INDEX_NAME,count(*) as page_num,sum(NUMBER_RECORDS) as record_num
from INFORMATION_SCHEMA.INNODB_BUFFER_PAGE
where table_name = '`testdb`.`test_table_log`'
group by TABLE_NAME,INDEX_NAME
order by page_num desc;
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表操作,从InnoDB缓冲池读取的页数。使用SHOW STATUS获取InnoDB索引页操作次数时,可执行以下语句:SHOW STATUS LIKE 'Innodb_pages_%';
SHOW STATUS LIKE 'Innodb_buffer_pool_pages_data';
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”输出相同的缓冲池信息。InnoDB缓冲池中包含数据的页数。该数量包含脏页与非脏页。与“SHOW STATUS”输出中的Innodb_buffer_pool_pages_data相同。与“SHOW STATUS”输出中的Innodb_pages_read相同。与“SHOW STATUS”输出中的Innodb_pages_created相同。与“SHOW STATUS”输出中的Innodb_pages_written相同。从上述表获取InnoDB索引页操作次数示例语句如下(查询操作不会导致以上参数变化):select DATABASE_PAGES, NUMBER_PAGES_READ, NUMBER_PAGES_CREATED, NUMBER_PAGES_WRITTEN from INFORMATION_SCHEMA.INNODB_BUFFER_POOL_STATS;
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表包含以下关注的列:enabled代表计数器在执行,disabled代表已停止;在查询INFORMATION_SCHEMA.INNODB_METRICS表时,需要有PROCESS权限。在INFORMATION_SCHEMA.INNODB_METRICS表中,存在NAME为“index_page_splits”,COMMENT为“Number of index page splits”的记录,即索引页分裂次数对应的计数器,COUNT列为索引页分裂次数。select count from INFORMATION_SCHEMA.INNODB_METRICS where name='index_page_splits';
2.5.2. 启用索引页分裂次数计数器
参考 https://dev.mysql.com/doc/refman/5.6/en/innodb-information-schema-metrics-table.html 。全局变量innodb_monitor_enable、innodb_monitor_disable可分别用于启用、禁用记数器。命令如下所示:SET GLOBAL innodb_monitor_enable = index_page_splits;
SET GLOBAL innodb_monitor_disable = index_page_splits;
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输出列
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是const连接类型的特例。查询的开头读取的表,最多只有一行匹配。因为只有一行,优化器的其余部分可以将此行中对应列的值视为常量。const连接类型对应的表查询非常快,因为它们只读取一次。将主键或唯一索引的所有部分与常量值进行比较时会使用const连接类型。在以下示例中,tbl_name表可以作为const连接类型使用:SELECT * FROM tbl_name WHERE primary_key=1;
SELECT * FROM tbl_name WHERE primary_key_part1=1 AND primary_key_part2=2;
对于先前的表的每种行组合,从eq_ref连接类型对应的表中读取一行。除了system和const连接类型外,eq_ref是最好的联接类型。当连接使用索引的所有部分并且索引是主键或非空唯一索引时,将使用eq_ref连接类型。eq_ref可用于使用=运算符进行比较的索引列。比较值可以是常量,也可以是使用在此表之前读取的表中列的表达式。在以下示例中,MySQL可以使用eq_ref连接类型处理ref_table表:SELECT * FROM ref_table,other_table WHERE ref_table.key_column=other_table.column;
SELECT * FROM ref_table,other_table WHERE ref_table.key_column_part1=other_table.column AND ref_table.key_column_part2=1;
对于先前的表的每种行组合,从ref连接类型对应的表读取所有具有匹配索引值的行。如果连接仅使用索引键的最左前缀或者索引键不是主键或唯一索引(即连接不能基于索引键值选择单行),则使用ref连接类型。如果使用的键只匹配少量几行,则ref是一个好的连接类型。ref连接类型可用于使用=或<=>运算符进行比较的索引列。在以下示例中,MySQL可以使用ref连接类型处理ref_table表:SELECT * FROM ref_table WHERE key_column=expr;
SELECT * FROM ref_table,other_table WHERE ref_table.key_column=other_table.column;
SELECT * FROM ref_table,other_table WHERE ref_table.key_column_part1=other_table.column AND ref_table.key_column_part2=1;
ref_or_null与ref类似,区别在于使用ref_or_null时,MySQL对包含NULL值的行进行了额外搜索。ref_or_null连接类型最常用于解决子查询。在以下示例中,MySQL可以使用ref_or_null连接类型处理ref_table表:SELECT * FROM ref_table WHERE key_column=expr OR key_column IS NULL;
index_merge连接类型表示使用了索引合并优化。在这种情况下,输出行中的key列包含使用的索引列表,key_len列包含所用索引的最长键部分列表。unique_subquery连接类型在某些使用IN的子查询的情况下替代eq_ref,如下所示:value IN (SELECT primary_key FROM single_table WHERE some_expr)
unique_subquery只是一个索引查找函数,可以完全替代子查询以提高效率。index_subquery连接类型与unique_subquery类似,它替代了IN子查询,支持子查询中的非唯一索引,如下所示:value IN (SELECT key_column FROM single_table WHERE some_expr)
range连接类型表示仅检索给定范围内的行,使用索引选择行。EXPLAIN输出行中的key列表示使用哪个索引。key_len列包含使用的最长的索引键部分。对于此连接类型,ref列为NULL。使用=,<>,>,> =,<,<=,IS NULL,<=>,BETWEEN,LIKE或IN()运算符中的任何一个将索引键列与常量进行比较时,可以使用range类型。index连接类型与ALL类型类似,区别在于index连接类型有对索引树进行扫描。有两种方式:a. 如果查询的索引是覆盖索引,并且可满足所需表的所有数据,则仅扫描索引树。在这种情况下,Extra列显示“Using index”。仅扫描索引通常比ALL连接类型快,因为索引的大小通常小于表数据。b. 对指定索引读取数据,进行全表扫描,以按索引顺序查找数据行。Extra列不会显示“Using index”。当查询仅使用属于单个索引一部分的列时,MySQL可以使用此连接类型。如果当前表是第一个没有标记为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列的部分可能的值及说明:出现该提示说明MySQL在进行排序时需要使用filesort,未使用索引进行排序。从表中检索列信息时,仅使用索引树的信息,不需要进行额外的查找以读取实际行。对于具有用户定义的聚簇索引的InnoDB表,即使Extra列中没有出现“Using index”,对应的索引也可能被使用。当type列为index且key列为主键时,对应以上情况。出现该提示说明MySQL在查询时使用了索引条件下推。- Using sort_union(…), Using union(…), Using intersect(…)
以上显示了对应index_merge连接类型,即使用索引合并时,索引扫描合并时使用的特定算法。为了解决该查询,MySQL需要创建一个临时表来保存结果。如果查询包含GROUP BY和ORDER BY子句,且列出了不同的列时,通常会发生这种情况。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/
推荐阅读