正文
如何查看一个表的索引
如何查看一个表中有哪些索引呢?可以通过如下sql查看
SHOW INDEX FROM tb_user; #tb_user是表名
各个字段表示含义如下
table列表示表名;
Non_unique-表示是否是唯一索引,0代表是唯一索引,1代表不是唯一索引;
key_name表示索引名称;
seq_in_index序号;
column_name 表示在哪列上创建索引;
collation表示列以什么方式存储在索引中,可以是A或者null,B+Tree的索引总是A,即排序的,如果使用hash索引的话,就是null;
Cardinality表示索引中不重复数量的预估值,也可以理解数据等值的数量比较少,一般这个数据值越大越比较适合创建索引,如果特别小那就不适合建索引,如性别(gender)字段。
sub_part是否是列的部分索引,如上面在address前15个字符创建了索引。
packed 关键字如何被压缩,如果没有被压缩则为null;
null 表示是否索引的列含有空值(null);
Index_type 表示索引类型,因为表是Innodb存储引擎,所以是Btree;
Comment表示注释;
Cardinality
对这个字段说明一下,Cardinality表示索引中不重复记录数量的预估值。Cardinality是一个预估值而不是一个准确的值。mysql优化器会根据这个值来判断是否使用索引,但是在某些情况下Cardinality可能为null,在这种情况下可能导致明明创建了索引,而索引却没生效,可以尝试执行如下sql尝试。
ANALYZE TABLE tb_user;
Cardinality在更新通常发生在insert和update。但是不能每次执行insert和update都更新这个值,这样很明显不合理。在Innodb引擎中更新Cardinality值的策略为
表中1/16的数据已经发生过变化。
stat_modified_counter>20亿。考虑到如果对同一条数据进行修改,那么在Innodb存储内部会有一个计数器stat_modified_counter,如果该数值超过了20亿就会更新Cardinality。
Innodb存储引擎通过采样的方法来统计这个数值。
默认情况下Innodb通过采样的方式来统计该数值,采取8个叶子结点的数值。采样过程如下
采取B+Tree中叶子节点的数量 记为A。
随机获取8个叶子结点。统计每个页不同的记录数记为P1,P2.......
计算出预估值Cardinality(P1+P2+....P8)*A/8。
由于Cardinality是通过采样计算出的数值,所以每次执行SHOW INDEX FROM tb_user;查询出的结果值可能不同。
索引优化
本文主要对联合索引讨论。
慢查询日志
如何查看慢查询日志呢?可以通过如下sql
SHOW VARIABLES LIKE '%query%' ;
默认情况下慢查询是关闭状态(图上是我已经开启过的),默认超过10s(long_query_time)定义为慢查询,slow_query_log_file慢查询日志的存放目录。可以通过如下sql开启慢查询
set global slow_query_log =1; #开启慢查询或者set global slow_query_log ='on'; set global slow_query_log =0; #关闭慢查询或者set global slow_query_log ='off'; set global long_query_time = 1 ;#修改慢查询时间1s show variables like '%dir%'; #查看日志存放目录
注意设置完之后,需要关闭navicat连接重新连接才会生效,这只是临时开启慢查询,如果想要永久开启需要修改配置文件
编辑mysql配置文件/etc/my.cnf [mysqld] slow_query_log = 1 #是否开启慢查询日志,1表示开启,0表示关闭,也可以使用off和on long_query_time = 1 #慢查询时间 log-slow-queries=/var/log/slowquery.log #mysql5.6以下版本 slow-query-log-file=/var/log/slowquery.log #mysql5.6及以上版本
慢查询会记录查询缓慢的sql记录,以及索引使用情况。
优化规则
全值匹配
全值匹配是指在查询条件中尽量条件都是索引中的字段,而且索引都使用。另外尽量创建联合索引,因为每一个索引都是一颗B+Tree,在增删改的时候,需要维护这颗B+Tree;
EXPLAIN SELECT * FROM tb_user WHERE `name`='行言孙'; EXPLAIN SELECT * FROM tb_user WHERE `name`='行言孙' AND age=22 ; EXPLAIN SELECT * FROM tb_user WHERE `name`='行言孙' AND age=22 AND position='武汉信息经理' ;
最左前缀匹配
顾名思义就是最左优先,查找会按照索引的顺序依次查找,索引的最左前列开始并且不跳过索引中的列。
EXPLAIN SELECT * FROM tb_user WHERE `name`='行言孙' AND age=22 AND position='武汉信息经理' ; EXPLAIN SELECT * FROM tb_user WHERE age=22 AND position='武汉信息经理' ;
上面sql的执行计划如下图
第一个查询使用了索引,第二个索引中没有使用索引而使用了全表扫描,可以判断出,如果索引中第一个列没使用上,那么索引就不能使用,而会全表扫描。
EXPLAIN SELECT * FROM tb_user WHERE `name`='行言孙' AND age>22 AND position='武汉信息经理' ;
实际上这三个索引的key_len应该是221,而实际上是68,证明position这个索引没有用上。可以判断出,如果索引在遇到范围(> 、<、Like 不包括>=、<= )查询时后面的索引就会失效。
如果等值查询中索引顺序变了,索引会生效吗?
EXPLAIN SELECT * FROM tb_user WHERE `name`='行言孙' AND position='武汉信息经理' AND age=22; EXPLAIN SELECT * FROM tb_user WHERE age=22 AND position='武汉信息经理' AND name='行言孙'; EXPLAIN SELECT * FROM tb_user WHERE position='武汉信息经理' AND age=22 AND name='行言孙';
这三个sql的执行计划是一模一样的,可知在等值查询中如果索引顺序跟索引的顺序不一致依然会使用索引,原因可能是Mysql优化器会将我们的sql进行优化而使用索引。
like
EXPLAIN SELECT * FROM tb_user WHERE `name`LIKE '%行言孙' AND age=22 AND position='武汉信息经理' ; EXPLAIN SELECT * FROM tb_user WHERE `name`LIKE '行言孙%' AND age=22 AND position='武汉信息经理' ;
通过执行计划可知%或者_(%匹配一个或者多个字符;_匹配一个字符)放在左边的时候是不会使用索引的,如果在右边则可以使用到索引。如果非要把%放在左边则可以使用索引覆盖,这样同样会使用索引。
EXPLAIN SELECT name,age,position FROM tb_user WHERE `name`LIKE '%行言孙' AND age=22 AND position='武汉信息经理' ;
可以看到同样使用到了索引
不在索引列做计算、函数、类型转换
EXPLAIN SELECT * FROM tb_user WHERE LTRIM(name)= '行言孙' AND age=21 AND position='武汉信息经理' ;
可知如果在索引列上执行函数户或者计算不会使用到索引。
索引覆盖
从辅助索引中就可以得到查询记录,而不需要查询聚集索引中的记录(回表),辅助索引不包含整行数据,其大小远小于聚集索引,可以减少大量IO操作。尽量使用覆盖索引(只访问索引的查询(索引列包含查询列)),减少select *语句。
EXPLAIN SELECT name,age,position FROM tb_user WHERE name= '行言孙' AND age=21 AND position='武汉信息经理' ;
<>、!=不等号
EXPLAIN SELECT * FROM tb_user WHERE name!='行言孙' AND age=21 AND position='武汉信息经理' ; EXPLAIN SELECT * FROM tb_user WHERE name='行言孙' AND age<>21 AND position='武汉信息经理' ;
当不等号在索引的开头使用时,不会使用索引,而改为全表扫描。不在开头时索引是会使用到索引的,但是<>之后的字段的索引不会使用(同范围查询一样)。
null&is not null
EXPLAIN SELECT * FROM tb_user WHERE name IS NOT NULL AND age<>21 AND position='武汉信息经理' ; EXPLAIN SELECT * FROM tb_user WHERE name IS NULL AND age<>21 AND position='武汉信息经理' ;
可以看出在使用is not null时,并没有使用索引,而在is null时使用了索引。那么为什么呢?通过执行计划可知is not null 的数据大概有1282633条,Mysql优化器认为如果使用索引然后再回表查询 不如直接全表扫描快,而is null 时才有大概6614条数据。由可以得出结论MySQL中决定使不使用某个索引执行查询的依据很简单:就是成本够不够小。而不是是否在WHERE子句中用了 is null 或者 is not null 。
你可能不信 那么我证明给你看。
可以使用optimizer_trace 分析sql是否选择使用索引。
执行以下sql可以开启trace,默认是关闭的,不建议开启,会消耗mysql的性能,使用完之后记得关闭。
#开启 SET optimizer_trace='enabled=on',end_markers_in_json=on; #关闭 SET session optimizer_trace="enabled=off";
注意两条语句要全选中同时执行。
SELECT * FROM tb_user WHERE name IS NOT NULL AND age<>21 AND position='武汉信息经理' ; SELECT * FROM information_schema.OPTIMIZER_TRACE;
{ "steps": [ { "join_preparation": { /*sql准备阶段*/ "select#": 1, "steps": [ { "expanded_query": "/* select#1 */ select `tb_user`.`id` AS `id`,`tb_user`.`name` AS `name`,`tb_user`.`age` AS `age`,`tb_user`.`position` AS `position`,`tb_user`.`address` AS `address`,`tb_user`.`create_time` AS `create_time`,`tb_user`.`update_time` AS `update_time`,`tb_user`.`delete_flag` AS `delete_flag` from `tb_user` where ((`tb_user`.`name` is not null) and (`tb_user`.`age` <> 21) and (`tb_user`.`position` = '武汉信息经理'))" } ] /* steps */ } /* join_preparation */ }, { "join_optimization": {/*sql优化*/ "select#": 1, "steps": [ { "condition_processing": { /*sql条件*/ "condition": "WHERE", "original_condition": "((`tb_user`.`name` is not null) and (`tb_user`.`age` <> 21) and (`tb_user`.`position` = '武汉信息经理'))", "steps": [ { "transformation": "equality_propagation", "resulting_condition": "((`tb_user`.`name` is not null) and (`tb_user`.`age` <> 21) and (`tb_user`.`position` = '武汉信息经理'))" }, { "transformation": "constant_propagation", "resulting_condition": "((`tb_user`.`name` is not null) and (`tb_user`.`age` <> 21) and (`tb_user`.`position` = '武汉信息经理'))" }, { "transformation": "trivial_condition_removal", "resulting_condition": "((`tb_user`.`name` is not null) and (`tb_user`.`age` <> 21) and (`tb_user`.`position` = '武汉信息经理'))" } ] /* steps */ } /* condition_processing */ }, { "substitute_generated_columns": { } /* substitute_generated_columns */ }, { "table_dependencies": [ /*表依赖*/ { "table": "`tb_user`", "row_may_be_null": false, "map_bit": 0, "depends_on_map_bits": [ ] /* depends_on_map_bits */ } ] /* table_dependencies */ }, { "ref_optimizer_key_uses": [ ] /* ref_optimizer_key_uses */ }, { "rows_estimation": [/*预估访问成本*/ { "table": "`tb_user`", "range_analysis": { /*全表扫描*/ "table_scan": { "rows": 1282633,/*扫描行数*/ "cost": 142326/*花费时间*/ } /* table_scan */, "potential_range_indexes": [ /*查询可能使用的索引*/ { "index": "PRIMARY",/*主键索引*/ "usable": false,/*没用主键索引*/ "cause": "not_applicable"/*不适用*/ }, { "index": "idx_name_age_position",/*辅助索引*/ "usable": true, "key_parts": [ "name", "age", "position", "id" ] /* key_parts */ }, { "index": "address", /*address索引*/ "usable": false, "cause": "not_applicable" } ] /* potential_range_indexes */, "setup_range_conditions": [ ] /* setup_range_conditions */, "group_index_range": { "chosen": false, "cause": "not_group_by_or_distinct" } /* group_index_range */, "skip_scan_range": { "potential_skip_scan_indexes": [ { "index": "idx_name_age_position", "usable": false, "cause": "query_references_nonkey_column" } ] /* potential_skip_scan_indexes */ } /* skip_scan_range */, "analyzing_range_alternatives": {/*分析使用索引的成本*/ "range_scan_alternatives": [ { "index": "idx_name_age_position", "ranges": [ "NULL < name" ] /* ranges */, "index_dives_for_eq_ranges": true, "rowid_ordered": false, "using_mrr": true, "index_only": false, "in_memory": 0, "rows": 641316,/*索引扫描的行数*/ "cost": 476980, /*使用idx_name_age_position索引的成本*/ "chosen": false,/*没有使用该索引*/ "cause": "cost" } ] /* range_scan_alternatives */, "analyzing_roworder_intersect": { "usable": false, "cause": "too_few_roworder_scans" } /* analyzing_roworder_intersect */ } /* analyzing_range_alternatives */ } /* range_analysis */ } ] /* rows_estimation */ }, { "considered_execution_plans": [ /*考虑的执行计划*/ { "plan_prefix": [ ] /* plan_prefix */, "table": "`tb_user`", /*全表扫描*/ "best_access_path": { "considered_access_paths": [ { "rows_to_scan": 1282633, "access_type": "scan", "resulting_rows": 1.28263e+06, "cost": 142324, /*执行成本*/ "chosen": true/*选择这个*/ } ] /* considered_access_paths */ } /* best_access_path */, "condition_filtering_pct": 100, "rows_for_plan": 1.28263e+06, "cost_for_plan": 142324, "chosen": true } ] /* considered_execution_plans */ }, { "attaching_conditions_to_tables": { "original_condition": "((`tb_user`.`name` is not null) and (`tb_user`.`age` <> 21) and (`tb_user`.`position` = '武汉信息经理'))", "attached_conditions_computation": [ ] /* attached_conditions_computation */, "attached_conditions_summary": [ { "table": "`tb_user`", "attached": "((`tb_user`.`name` is not null) and (`tb_user`.`age` <> 21) and (`tb_user`.`position` = '武汉信息经理'))" } ] /* attached_conditions_summary */ } /* attaching_conditions_to_tables */ }, { "finalizing_table_conditions": [ { "table": "`tb_user`", "original_table_condition": "((`tb_user`.`name` is not null) and (`tb_user`.`age` <> 21) and (`tb_user`.`position` = '武汉信息经理'))", "final_table_condition ": "((`tb_user`.`name` is not null) and (`tb_user`.`age` <> 21) and (`tb_user`.`position` = '武汉信息经理'))" } ] /* finalizing_table_conditions */ }, { "refine_plan": [ { "table": "`tb_user`" } ] /* refine_plan */ } ] /* steps */ } /* join_optimization */ }, { "join_execution": { /*sql执行*/ "select#": 1, "steps": [ ] /* steps */ } /* join_execution */ } ] /* steps */ }
可以看出使用索引消耗的成本要远比全表扫描要高,所以选择了全表扫描。如果感觉索引没有执行的话,都可以使用这个工具来具体分析一下。
in&exsits&or
少用in、exsits、or用它查询时,mysql不一定使用索引,mysql内部优化器会根据检索比例、表大小等多个因素整体评估是否使用索引。
字符串不加引号
字符串上未加引号时是不会使用索引的,但是在mysql8.0.27如果字符串不加引号是会报错的。
索引下推
Index Condition PushDown(ICP)从Mysql5.6开始。使用ICP时,当进行索引查询时,Mysql在取出索引的同时会进行where条件的过滤,当然where可以过滤的条件是该索引可以覆盖到的范围。这样过滤完之后可以减少大量的回表操作,提高查询效率。默认是开启ICP优化的。
EXPLAIN SELECT * FROM tb_user WHERE name='行言孙' OR name='德进药' OR name='比张国' AND age=21 AND position='武汉信息经理' ;
Using index condition表示使用了ICP优化。
MRR
从Mysql5.6开始支持Multi_Range_Read(多范围读),MRR的是为了磁盘的随机访问次数。我们通过辅助索引能够使用到B+树的有序性,但是查出来的主键id未必是有序的。此时通过无序的id主键进行回表扫描的话,此时的IO是随机IO。磁盘IO主要分为两大类:一个随机IO,一个顺序IO。顺序IO的效率大概是随机IO的两个数量级。因此MySQL提出了一个名为Disk-Sweep Multi-Range Read (MRR,多范围 读取)的优化措施,即先读取一部分二级索引记录,将它们的主键值排好序之后再统一执行回表操作。因为磁盘读取数据通过扇区为单位进行读取的。一颗B+树它是有序的。比如说id 1到50 它是第一个扇区的。51到100是第二个扇区的以此类推。假设回表的id是这样的96、23、105、12、88等这些。那么磁盘读取的时候先读取第二个扇区,拿到96的数据,发现没有23的数据,在读取第一个扇区,拿到23的数据,发现没有105的数据,在读取第三个扇区。这样的IO就是随机IO。如果说通过MRR机制排好序了:12、23、88、96、105。读取第一个扇区的时候就能拿到12、23的数据,读取第二个扇区的时候就能拿到88、96的数据,减少了回表的次数。
总结
MRR可适用于range、ref、eq_ref类型的查询,使用MRR的好处
MRR使数据访问变得较为有顺序,在查找辅助索引时,首先根据得到的结果按照主键排序,并按照主键的顺序进行回表查找。
批量处理对键值的操作。MRR还可以将某些范围查询,拆分为键值对,以此来进行批量的数据查询。这样做的好处是可以在拆分过程中,直接过滤一些不符合查询条件的数据。
减少缓冲池中页被替换的次数。数据读取之后是首先存放在一个缓冲池中的,如果缓冲池不够大,此时频繁的离散读取导致缓冲池中的数据被替换出缓冲池,若是按照主键排序进行访问则可以将这种重复行为降到最低。
那么如何开启MRR模式呢?可以通过参数optimizer_switch中的flag来控制。当MRR为on时,表示启用MRR优化。mrr_cost_based表示是否通过costbased的方式来选择是否启用mrr。若设置mrr=on,mrr_cost_based=off,则总是启用MRR优化。
SET @@OPTIMIZER_switch='mrr=on,mrr_cost_based=off';
EXPLAIN SELECT * FROM tb_user WHERE name='行言孙' OR name='德进药' OR name='比张国' AND age=21 AND position='武汉信息经理' ;
可以看到已经使用了MRR优化。
缓存区的大小默认是256K(262144/1024)当超过该值时则执行器对已经缓存的数据进行排序,排序之后获取数据。
长字段索引
如果字段过长,整个字段建立索引就会很浪费空间了,所以可以考虑对字段的前面几个字段建立索引。
ALTER TABLE tb_user ADD INDEX address (address(15));
分页优化
如果要查询LMIT 1000000,10,虽然只取了10条数据,但是mysql并不能跳跃取值,而是要取出1000010条数据,然后从这里面取出10条。
优化方式一使用主键id
SELECT * FROM tb_user LIMIT 1000000,10; SELECT * FROM tb_user WHERE id >1000000 LIMIT 10;
优化方式二非主键索引的方式分页优化
EXPLAIN select * from tb_user u inner join (select id from tb_user limit 1000000,10) uid on u.id = uid.id;
优化方式三 分库分表
如果一个标的数据量特别大,可以考虑分表分库,阿里开发手册中推荐,如果单表行数超过500万行或者单表容量超过2G,推荐使用分表分库。
排序优化
1、
EXPLAIN SELECT * FROM tb_user WHERE name='下里国' AND position='天津信息经理' ORDER BY age;
可以看到用到了索引,age索引列用在排序过程中,因为Extra字段里没有using filesort。根据最左前缀原则,position索引没有用上。
2、
EXPLAIN SELECT * FROM tb_user WHERE NAME = '下里国' ORDER BY position;
由于索引跳过了age所以排序索引没有使用上而显示Using filesort。可知在排序中同样需要满足最左前缀原则,如果索引中断,那么将不会使用后面的索引。
3、
EXPLAIN SELECT * FROM tb_user WHERE name='下里国' ORDER BY age,position; EXPLAIN SELECT * FROM tb_user WHERE name='下里国' ORDER BY position,age; EXPLAIN SELECT * FROM tb_user WHERE name='下里国' ORDER BY age DESC ,position DESC; EXPLAIN SELECT * FROM tb_user WHERE name='下里国' ORDER BY age ASC,position DESC;
第一个sql很显然用到了name列上的索引,而extra是null,null意味着用到了索引,但是部分字段未被索引覆盖,必须通过“回表”来实现,不是纯粹地用到了索引,也不是完全没用到索引。
第二个sql由于不符合最左前缀原则(sql优化器没有像等值查询时优化),所以没有使用到索引。
第三个sql Backward index scan是mysql8.0之后才出现的,也是一种mysql的优化方式,叫降序索引。
第四个sql 由于age使用正序,position使用逆序,与创建索引时的顺序不同,所以也没有使用到索引排序。
EXPLAIN SELECT name,age,position FROM tb_user WHERE name='下里国' ORDER BY age,position;
当使用覆盖索引时,排序字段用到了索引。
Using filesort排序原理
Using filesort分为单路排序和双路排序
单路排序:是一次性取出满足条件行的所有字段,然后在sort buffer中进行排序。
双路排序(又叫回表排序模式):是首先根据相应的条件取出相应的排序字段和可以直接定位行 数据的行 ID,然后在 sort buffer 中进行排序,排序完后根据主键id获取其他字段的数据-- 相当于要查询两次。
通过trace工具可以看到sort_mode
可以通过改变 max_length_for_sort_data变量的值来影响mysql选择的算法。因为单路排序为将要排序的每一行创建了固定的缓冲区。如果查询字段总的长度大小比设定的max_length_for_sort_data 要小,则使用单路排序方式;如果查询字段总的长度大小比设定的max_length_for_sort_data 要大,则使用多路排序方式 。注意从 MySQL 8.0.20 开始不推荐使用此变量(max_length_for_sort_data),因为优化器更改使其过时且无效。在MySQL 8.0.20之后该值默认为4096而不是1024。通过以下sql可以查看。
SHOW VARIABLES LIKE '%max_length_for_sort_data%'; SET max_length_for_sort_data = 1024; #max_length_for_sort_data默认值是4096,最小值是4,最大值是8388608
小结
1、order by语句使用索引最左前缀原则。
2、使用where子句与order by子句条件列组合满足索引最左前缀原则。
3、尽量在索引列上完成排序,遵循索引建立(索引创建的顺序)时的最左前缀原则。
4、如果order by的条件不在索引列上,就会产生Using filesort。
5、能用覆盖索引尽量用覆盖索引。
6、如果和where 条件查询冲突,那优先where条件上的索引。
count优化
-- 临时关闭缓存 set global query_cache_size=0; set global query_cache_type=0; EXPLAIN select count(1) from tb_user; EXPLAIN select count(id) from tb_user; EXPLAIN select count(name) from tb_user; EXPLAIN select count(*) from tb_user;
上面的执行计划1、2、4,没有使用主键的聚簇索引,而使用了address列上的索引。原因是Innodb会优先考虑使用索引树较小的索引来查询,因为address比主键索引和idx_name_age_address都小,所以选择了address索引,如果不存在辅助索引,则使用聚簇索引(一般是主键索引)来查询。
sql3使用了辅助索引
其实上面的执行效率基本是相同的,阿里开发手册中规定不要使用count(列名)或者count(1)来替代count(*)。count(*)会统计null的行,count(列名)不会统计此列为null的值。count(distinct col )计算该列除Null之外不重复的数量,count(distinct col1,col2)只要有一列全为null,则结果为0。
优化方案
1、为了更快地计数,创建一个计数器表并让您的应用程序根据它所做的插入和删除来更新。
2、SHOW TABLE STATUS
3、查询mysql自己维护的总行数 对于myisam存储引擎的表做不带where条件的count查询性能是很高的,因为myisam存储引擎的表的总行数会被 mysql存储在磁盘上,查询不需要计算。
参考:
https://blog.csdn.net/weixin_34462016/article/details/113654725
https://blog.csdn.net/admin522043032/article/details/121919494
https://blog.csdn.net/admin522043032/article/details/121281402