MySQL优化---单表索引失效原因及优化策略

一、数据准备

往表里插50W数据以测试我们的SQL

建表语句

 CREATE TABLE `dept` (
 `id` INT(11) NOT NULL AUTO_INCREMENT,
 `deptName` VARCHAR(30) DEFAULT NULL,
 `address` VARCHAR(40) DEFAULT NULL,
 ceo INT NULL ,
 PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
 
 
CREATE TABLE `emp` (
 `id` INT(11) NOT NULL AUTO_INCREMENT,
 `empno` INT NOT NULL ,
 `name` VARCHAR(20) DEFAULT NULL,
 `age` INT(3) DEFAULT NULL,
 `deptId` INT(11) DEFAULT NULL,
 PRIMARY KEY (`id`)
 #CONSTRAINT `fk_dept_id` FOREIGN KEY (`deptId`) REFERENCES `t_dept` (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

创建函数,保证每条数据都不同。

 
 
DELIMITER $$
CREATE FUNCTION rand_string(n INT) RETURNS VARCHAR(255)
BEGIN    
DECLARE chars_str VARCHAR(100) DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';
 DECLARE return_str VARCHAR(255) DEFAULT '';
 DECLARE i INT DEFAULT 0;
 WHILE i < n DO  
 SET return_str =CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*52),1));  
 SET i = i + 1;
 END WHILE;
 RETURN return_str;
END $$
 
 
#假如要删除
#drop function rand_string;
 
#用于随机产生多少到多少的编号
DELIMITER $$
CREATE FUNCTION  rand_num (from_num INT ,to_num INT) RETURNS INT(11)
BEGIN   
 DECLARE i INT DEFAULT 0;  
 SET i = FLOOR(from_num +RAND()*(to_num -from_num+1))   ;
RETURN i;  
 END$$ 
 
#假如要删除
#drop function rand_num;

创建插入数据的存储过程

 
 
DELIMITER $$
CREATE PROCEDURE  insert_emp(  START INT ,  max_num INT )
BEGIN  
DECLARE i INT DEFAULT 0;   
#set autocommit =0 把autocommit设置成0  
 SET autocommit = 0;    
 REPEAT  
 SET i = i + 1;  
 INSERT INTO emp (empno, NAME ,age ,deptid ) VALUES ((START+i) ,rand_string(6)   , rand_num(30,50),rand_num(1,10000));  
 UNTIL i = max_num  
 END REPEAT;  
 COMMIT;  
 END$$ 
 
#删除
# DELIMITER ;
# drop PROCEDURE insert_emp;
 
 
#执行存储过程,往dept表添加随机数据
DELIMITER $$
CREATE PROCEDURE `insert_dept`(  max_num INT )
BEGIN  
DECLARE i INT DEFAULT 0;   
 SET autocommit = 0;    
 REPEAT  
 SET i = i + 1;  
 INSERT INTO dept ( deptname,address,ceo ) VALUES (rand_string(8),rand_string(10),rand_num(1,500000));  
 UNTIL i = max_num  
 END REPEAT;  
 COMMIT;  
 END$$
 
#删除
# DELIMITER ;
# drop PROCEDURE insert_dept;

执行存储过程,插入数据

 
 #执行存储过程,往dept表添加1万条数据
DELIMITER ;
CALL insert_dept(10000); 

#执行存储过程,往emp表添加50万条数据
DELIMITER ;
CALL insert_emp(100000,500000); 

写个批量删除表上的某个索引的存储过程

DELIMITER $$
CREATE  PROCEDURE `proc_drop_index`(dbname VARCHAR(200),tablename VARCHAR(200))
BEGIN
       DECLARE done INT DEFAULT 0;
       DECLARE ct INT DEFAULT 0;
       DECLARE _index VARCHAR(200) DEFAULT '';
       DECLARE _cur CURSOR FOR  SELECT   index_name   FROM information_schema.STATISTICS   WHERE table_schema=dbname AND table_name=tablename AND seq_in_index=1 AND    index_name <>'PRIMARY'  ;
       DECLARE  CONTINUE HANDLER FOR NOT FOUND set done=2 ;      
        OPEN _cur;
        FETCH   _cur INTO _index;
        WHILE  _index<>'' DO 
               SET @str = CONCAT("drop index ",_index," on ",tablename ); 
               PREPARE sql_str FROM @str ;
               EXECUTE  sql_str;
               DEALLOCATE PREPARE sql_str;
               SET _index=''; 
               FETCH   _cur INTO _index; 
        END WHILE;
   CLOSE _cur;
   END$$

如果我们想删除某个表的索引,只需要调用此存储过程即可

CALL proc_drop_index("dbname","tablename");

二、导致索引失效的几个情况

1.全值匹配我最爱:系统经常出现的sql语句为

EXPLAIN SELECT SQL_NO_CACHE * FROM emp WHERE emp.age=30 and deptid=4 AND emp.name = 'abcd'  

如何建立索引比较好?所用到的查询条件都应建立为一个复合索引

CREATE INDEX idx_age_deptid_name ON emp(age,deptid,NAME)

建立索引前

 
索引后

2.最佳左前缀法则:如果建立了一个复合索引,在查询时最好遵循最左前缀法则。即查询时从复合索引的第一个索引开始并且查询条件中不跳过索引的列。

假如经常出现的sql为

 EXPLAIN SELECT SQL_NO_CACHE * FROM emp WHERE emp.age=30   AND emp.name = 'abcd'   
或者
EXPLAIN SELECT SQL_NO_CACHE * FROM emp WHERE emp.deptid=1   AND emp.name = 'abcd'   

那原来的索引idx_age_deptid_name还会生效吗?

只用到了一部分,可以通过key_len看出,只用到了deptid这个字段的索引,后面的索引没有用到。

这个查询则完全没有用到索引。

这与索引建立的原理有关,索引是一个排好序的快速查找的数据结构,在建立一个复合索引的过程中,索引会从第一个字段开始排序,再从这基础上进行第二,第三个索引字段的排序,类似盖楼的顺序,先从一楼开始盖,再从一楼的基础上盖二楼、三楼。所以我们查询语句中的过滤条件要想使用到索引,就必须按照索引建立的顺序,依次满足,一旦跳过某个字段,则该字段对应的索引之后的索引都无法使用,这也就是为什么第一条sql只用到了deptid的索引,而第二个sql根本没有用到索引的原因。

3.不要在索引列上做任何操作(计算、函数、类型转换),这也会导致索引失效而转向全表扫描。

这两条sql哪种写法更好
EXPLAIN  SELECT SQL_NO_CACHE * FROM emp WHERE   emp.name  LIKE 'abc%' 
 
EXPLAIN   SELECT SQL_NO_CACHE * FROM emp WHERE   LEFT(emp.name,3)  = 'abc'

不论是哪一种,既然用到了name字段,我们就要依照“全值匹配我最爱”,在name列建立索引。

create index idx_name on emp(name)

第一个SQL 用到了索引 且type理想,没有什么问题。

第二个SQL,索引失效了,type变为了全表扫描。

所以在索引列上不要做任何操作,否则会索引失效

4.复合索引中范围查询右边的列会失效

执行这个SQL,发现用到了索引,但根据key_len可得知只用到了age和deptid这两个索引,name的索引失效了

当在索引列上有范围查询时,范围查询对应的索引字段右边的索引会失效,这里指的是建立索引的顺序,不是where后查询的顺序,因为MySQL的optimizer查询优化器会将我们的where查询自动按索引顺序进行优化。

如果这样的SQL较多,我们可以在建立索引的时候将范围查询的字段建立到最后。

create index idx_age_name_deptid on emp(age,name,deptid)

这样再进行查询,发现索引正常。

5.在使用不等于(!= 或者 <>)的时候索引会失效导致全表扫描

 CREATE INDEX idx_name ON emp(NAME)
  
  EXPLAIN SELECT SQL_NO_CACHE * FROM emp WHERE   emp.name <>  'abc' 
 

6.使用is not null 会导致索引失效

is null 会正常使用索引,但is not null会导致索引失效

7.like模糊查询的条件以%开头会导致索引失效

8.字符串不加单引号导致索引失效

三、总结案例

四、总结优化建议

1.对于单键索引,尽量选择针对当前query过滤性更好的索引

不要用性别这种过滤性特别差的字段建立索引,尽量建立在比较唯一定位一条记录的字段,如手机号,身份证号等。

2.在选择组合索引的时候,当前Query中过滤性最好的字段在索引字段顺序中,位置越靠前越好。

根据复合索引的层级排序结构,在第一层就比较精准的定位一个或几个数据有利于查询优化。

3.在选择组合索引的时候,尽量选择可以能够包含当前query中的where字句中更多字段的索引(全值匹配我最爱)

4.在选择组合索引的时候,如果某个字段可能出现范围查询时,尽量把这个字段放在索引次序的最后面

5.尽量避免造成索引失效的情况

发布了227 篇原创文章 · 获赞 77 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/m2606707610/article/details/103686630