• Mysql高级——索引优化和查询优化(1)


    索引优化

    1. 数据准备

    学员表插50万条, 班级表插1万条。

    建表

    CREATE TABLE `class` (
    	`id` INT ( 11 ) NOT NULL AUTO_INCREMENT,
    	`className` VARCHAR ( 30 ) DEFAULT NULL,
    	`address` VARCHAR ( 40 ) DEFAULT NULL,
    	`monitor` INT NULL,
    	PRIMARY KEY ( `id` ) 
    ) ENGINE = INNODB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;
    CREATE TABLE `student` (
    	`id` INT ( 11 ) NOT NULL AUTO_INCREMENT,
    	`stuno` INT NOT NULL,
    	`name` VARCHAR ( 20 ) DEFAULT NULL,
    	`age` INT ( 3 ) DEFAULT NULL,
    	`classId` INT ( 11 ) DEFAULT NULL,
    	PRIMARY KEY ( `id` ) #CONSTRAINT `fk_class_id` FOREIGN KEY (`classId`) REFERENCES `t_class` (`id`)
    
    ) ENGINE = INNODB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    设置参数

    • 命令开启:允许创建函数设置:
    set global log_bin_trust_function_creators=1; # 不加global只是当前窗口有效。
    
    • 1

    创建函数

    保证每条数据都不同。

    #随机产生字符串
    
    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 // 
    DELIMITER;#假如要删除
    #drop function rand_string;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    随机产生班级编号

    #用于随机产生多少到多少的编号
    
    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 // 
    DELIMITER;#假如要删除
    #drop function rand_num;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    创建存储过程

    #创建往stu表中插入数据的存储过程
    
    DELIMITER //
    CREATE PROCEDURE insert_stu ( START INT, max_num INT ) BEGIN
    	DECLARE
    		i INT DEFAULT 0;
    	
    	SET autocommit = 0;#设置手动提交事务
    	REPEAT#循环
    		
    		SET i = i + 1;#赋值
    		INSERT INTO student ( stuno, NAME, age, classId )
    		VALUES
    			((
    					START + i 
    					),
    				rand_string ( 6 ),
    				rand_num ( 1, 50 ),
    			rand_num ( 1, 1000 ));
    		UNTIL i = max_num 
    	END REPEAT;
    	COMMIT;#提交事务
    	
    END // 
    DELIMITER;#假如要删除
    #drop PROCEDURE insert_stu;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    创建往class表中插入数据的存储过程

    #执行存储过程,往class表添加随机数据
    
    DELIMITER //
    CREATE PROCEDURE `insert_class` ( max_num INT ) BEGIN
    	DECLARE
    		i INT DEFAULT 0;
    	
    	SET autocommit = 0;
    	REPEAT
    			
    			SET i = i + 1;
    		INSERT INTO class ( classname, address, monitor )
    		VALUES
    			(
    				rand_string ( 8 ),
    				rand_string ( 10 ),
    			rand_num ( 1, 100000 ));
    		UNTIL i = max_num 
    	END REPEAT;
    	COMMIT;
    	
    END // 
    DELIMITER;#假如要删除
    #drop PROCEDURE insert_class;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    调用存储过程

    class

    #执行存储过程,往class表添加1万条数据
    CALL insert_class(10000);
    
    • 1
    • 2

    stu

    #执行存储过程,往stu表添加50万条数据
    CALL insert_stu(100000,500000);
    
    • 1
    • 2

    删除某表上的索引

    创建存储过程

    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=1来控制游标的结束
    	DECLARE
    		CONTINUE HANDLER FOR NOT FOUND 
    		SET done = 2;#若没有数据返回,程序继续,并将变量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 // 
    DELIMITER;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    执行存储过程

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

    2. 索引失效案例

    2.1 全值匹配我最爱

    2.2 最佳左前缀法则

    拓展:Alibaba《Java开发手册》
    索引文件具有 B-Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。

    2.3 主键插入顺序

    在这里插入图片描述

    如果此时再插入一条主键值为9 的记录,那它插入的位置就如下图:

    在这里插入图片描述

    可这个数据页已经满了,再插进来咋办呢?我们需要把当前页面分裂成两个页面,把本页中的一些记录移动到新创建的这个页中。页面分裂和记录移位意味着什么?意味着: 性能损耗!所以如果我们想尽量避免这样无谓的性能损耗,最好让插入的记录的主键值依次递增,这样就不会发生这样的性能损耗了。所以我们建议:让主键具有AUTO_INCREMENT ,让存储引擎自己为表生成主键,而不是我们手动插入 ,比如: person_info 表:

    CREATE TABLE person_info (
    	id INT UNSIGNED NOT NULL AUTO_INCREMENT,
    	NAME VARCHAR ( 100 ) NOT NULL,
    	birthday DATE NOT NULL,
    	phone_number CHAR ( 11 ) NOT NULL,
    	country VARCHAR ( 100 ) NOT NULL,
    	PRIMARY KEY ( id ),
    KEY idx_name_birthday_phone_number ( NAME ( 10 ), birthday, phone_number ) 
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    我们自定义的主键列id 拥有AUTO_INCREMENT 属性,在插入记录时存储引擎会自动为我们填入自增的主键值。这样的主键占用空间小,顺序写入,减少页分裂。

    2.4 计算、函数、类型转换(自动或手动)导致索引失效

    EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name LIKE 'abc%';
    
    EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE LEFT(student.name,3) = 'abc';
    
    • 1
    • 2
    • 3

    创建索引

    CREATE INDEX idx_name ON student(NAME);
    
    • 1

    第一种:索引优化生效

    mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name LIKE 'abc%';
    
    • 1

    第二种:索引优化失效

    mysql>  EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE LEFT(student.name,3) = 'abc';
    +----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+-------------+
    | id | select_type | table   | partitions | type | possible_keys | key  | key_len | ref  | rows   | filtered | Extra       |
    +----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+-------------+
    |  1 | SIMPLE      | student | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 498917 |   100.00 | Using where |
    +----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+-------------+
    1 row in set, 2 warnings (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    mysql> SELECT SQL_NO_CACHE * FROM student WHERE LEFT(student.name,3) = 'abc';
    +--------+--------+--------+------+---------+
    | id     | stuno  | name   | age  | classId |
    +--------+--------+--------+------+---------+
    |    399 | 100399 | ABcKtL |   24 |     198 |
    |  16470 | 116470 | ABcJlg |   47 |     251 |
    |  27952 | 127952 | ABcJmj |   10 |     397 |
    |  54809 | 154809 | aBClvu |   37 |     495 |
    |  61540 | 161540 | abclUS |   30 |     374 |
    |  83160 | 183160 | aBCjpV |   34 |     593 |
    |  89664 | 189664 | aBCjmJ |   34 |     350 |
    | 240498 | 340498 | aBCksj |   41 |     491 |
    | 245214 | 345214 | abciJU |   23 |     568 |
    | 258459 | 358459 | aBClxC |   23 |     566 |
    | 300169 | 400169 | aBClxC |   21 |     412 |
    | 300328 | 400328 | ABcJnn |   27 |     870 |
    | 324684 | 424684 | aBCkrg |   30 |     566 |
    | 416907 | 516907 | ABcHgI |   46 |     607 |
    | 424459 | 524459 | abclVU |   39 |     192 |
    | 445547 | 545547 | ABcJpw |   16 |     180 |
    | 454772 | 554772 | AbCHFf |   37 |     313 |
    | 466466 | 566466 | abckRF |   26 |     725 |
    | 475708 | 575708 | abclWY |    4 |     415 |
    | 486611 | 586611 | ABcLwb |   41 |     948 |
    | 490152 | 590152 | ABcHfC |   24 |     717 |
    +--------+--------+--------+------+---------+
    21 rows in set, 1 warning (0.12 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    type为“ALL”,表示没有使用到索引

    2.5 类型转换导致索引失效

    下列哪个sql语句可以用到索引。(假设name字段上设置有索引)

    # 未使用到索引
    mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name=123;
    +----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+-------------+
    | id | select_type | table   | partitions | type | possible_keys | key  | key_len | ref  | rows   | filtered | Extra       |
    +----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+-------------+
    |  1 | SIMPLE      | student | NULL       | ALL  | idx_name      | NULL | NULL    | NULL | 498917 |    10.00 | Using where |
    +----+-------------+---------+------------+------+---------------+------+---------+------+--------+----------+-------------+
    1 row in set, 4 warnings (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    # 使用到索引
    mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name='123';
    +----+-------------+---------+------------+------+---------------+----------+---------+-------+------+----------+-------+
    | id | select_type | table   | partitions | type | possible_keys | key      | key_len | ref   | rows | filtered | Extra |
    +----+-------------+---------+------------+------+---------------+----------+---------+-------+------+----------+-------+
    |  1 | SIMPLE      | student | NULL       | ref  | idx_name      | idx_name | 63      | const |    1 |   100.00 | NULL  |
    +----+-------------+---------+------------+------+---------------+----------+---------+-------+------+----------+-------+
    1 row in set, 2 warnings (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • name=123发生类型转换,索引失效。

    2.6 范围条件右边的列索引失效

    ALTER TABLE student DROP INDEX idx_name;
    ALTER TABLE student DROP INDEX idx_age;
    ALTER TABLE student DROP INDEX idx_age_classid;
    EXPLAIN SELECT SQL_NO_CACHE * FROM student
    WHERE student.age=30 AND student.classId>20 AND student.name = 'abc' ;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    create index idx_age_name_classid on student(age,name,classid);
    
    • 1
    • 将范围查询条件放置语句最后:
    mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age=30 AND student.name =
        -> 'abc' AND student.classId>20 ;
    +----+-------------+---------+------------+-------+----------------------+----------------------+---------+------+------+----------+-----------------------+
    | id | select_type | table   | partitions | type  | possible_keys        | key                  | key_len | ref  | rows | filtered | Extra                 |
    +----+-------------+---------+------------+-------+----------------------+----------------------+---------+------+------+----------+-----------------------+
    |  1 | SIMPLE      | student | NULL       | range | idx_age_name_classid | idx_age_name_classid | 73      | NULL |    1 |   100.00 | Using index condition |
    +----+-------------+---------+------------+-------+----------------------+----------------------+---------+------+------+----------+-----------------------+
    1 row in set, 2 warnings (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    2.7 不等于(!= 或者<>)索引失效

    2.8 is null可以使用索引,is not null无法使用索引

    EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age IS NULL;
    
    EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age IS NOT NULL;
    
    • 1
    • 2
    • 3

    2.9 like以通配符%开头索引失效

    拓展:Alibaba《Java开发手册》
    【强制】页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。

    2.10 OR 前后存在非索引的列,索引失效

    # 未使用到索引
    mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 10 OR classid = 100;
    +----+-------------+---------+------------+------+----------------------+------+---------+------+--------+----------+-------------+
    | id | select_type | table   | partitions | type | possible_keys        | key  | key_len | ref  | rows   | filtered | Extra       |
    +----+-------------+---------+------------+------+----------------------+------+---------+------+--------+----------+-------------+
    |  1 | SIMPLE      | student | NULL       | ALL  | idx_age_name_classid | NULL | NULL    | NULL | 498917 |    11.88 | Using where |
    +----+-------------+---------+------------+------+----------------------+------+---------+------+--------+----------+-------------+
    1 row in set, 2 warnings (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    # 使用到索引
    mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 10 OR name = 'Abel';
    +----+-------------+---------+------------+------+----------------------+------+---------+------+--------+----------+-------------+
    | id | select_type | table   | partitions | type | possible_keys        | key  | key_len | ref  | rows   | filtered | Extra       |
    +----+-------------+---------+------------+------+----------------------+------+---------+------+--------+----------+-------------+
    |  1 | SIMPLE      | student | NULL       | ALL  | idx_age_name_classid | NULL | NULL    | NULL | 498917 |    11.88 | Using where |
    +----+-------------+---------+------------+------+----------------------+------+---------+------+--------+----------+-------------+
    1 row in set, 2 warnings (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    2.11 数据库和表的字符集统一使用utf8mb4

    统一使用utf8mb4( 5.5.3版本以上支持)兼容性更好,统一字符集可以避免由于字符集转换产生的乱码。不同的字符集进行比较前需要进行转换会造成索引失效。

    3. 关联查询优化

    3.1 数据准备

    CREATE TABLE IF NOT EXISTS `type` (
    `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
    `card` INT(10) UNSIGNED NOT NULL,
    PRIMARY KEY (`id`)
    );
    #图书
    CREATE TABLE IF NOT EXISTS `book` (
    `bookid` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
    `card` INT(10) UNSIGNED NOT NULL,
    PRIMARY KEY (`bookid`)
    );
    
    #向分类表中添加20条记录
    INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO TYPE(card) VALUES(FLOOR(1 + (RAND() * 20)));
    
    #向图书表中添加20条记录
    INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
    INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    3.2 采用左外连接

    mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type.card = book.card;
    +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
    | id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra                                              |
    +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
    |  1 | SIMPLE      | type  | NULL       | ALL  | NULL          | NULL | NULL    | NULL |   20 |   100.00 | NULL                                               |
    |  1 | SIMPLE      | book  | NULL       | ALL  | NULL          | NULL | NULL    | NULL |   20 |   100.00 | Using where; Using join buffer (Block Nested Loop) |
    +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
    2 rows in set, 2 warnings (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    结论:type 有All

    添加索引优化

    ALTER TABLE book ADD INDEX Y ( card); #【被驱动表】,可以避免全表扫描
    
    mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type.card = book.card;
    +----+-------------+-------+------------+------+---------------+------+---------+----------------------+------+----------+-------------+
    | id | select_type | table | partitions | type | possible_keys | key  | key_len | ref                  | rows | filtered | Extra       |
    +----+-------------+-------+------------+------+---------------+------+---------+----------------------+------+----------+-------------+
    |  1 | SIMPLE      | type  | NULL       | ALL  | NULL          | NULL | NULL    | NULL                 |   20 |   100.00 | NULL        |
    |  1 | SIMPLE      | book  | NULL       | ref  | Y             | Y    | 4       | atguigudb2.type.card |    1 |   100.00 | Using index |
    +----+-------------+-------+------------+------+---------------+------+---------+----------------------+------+----------+-------------+
    2 rows in set, 2 warnings (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    可以看到第二行的 type 变为了 ref,rows 也变成了优化比较明显。这是由左连接特性决定的。LEFT JOIN
    条件用于确定如何从右表搜索行,左边一定都有,所以右边是我们的关键点,一定需要建立索引。

    ALTER TABLE `type` ADD INDEX X (card); #【驱动表】,无法避免全表扫描
    
    mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type.card = book.card;
    +----+-------------+-------+------------+-------+---------------+------+---------+----------------------+------+----------+-------------+
    | id | select_type | table | partitions | type  | possible_keys | key  | key_len | ref                  | rows | filtered | Extra       |
    +----+-------------+-------+------------+-------+---------------+------+---------+----------------------+------+----------+-------------+
    |  1 | SIMPLE      | type  | NULL       | index | NULL          | X    | 4       | NULL                 |   20 |   100.00 | Using index |
    |  1 | SIMPLE      | book  | NULL       | ref   | Y             | Y    | 4       | atguigudb2.type.card |    1 |   100.00 | Using index |
    +----+-------------+-------+------------+-------+---------------+------+---------+----------------------+------+----------+-------------+
    2 rows in set, 2 warnings (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    DROP INDEX Y ON book;
    
    mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type.card = book.card;
    +----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+----------------------------------------------------+
    | id | select_type | table | partitions | type  | possible_keys | key  | key_len | ref  | rows | filtered | Extra                                              |
    +----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+----------------------------------------------------+
    |  1 | SIMPLE      | type  | NULL       | index | NULL          | X    | 4       | NULL |   20 |   100.00 | Using index                                        |
    |  1 | SIMPLE      | book  | NULL       | ALL   | NULL          | NULL | NULL    | NULL |   20 |   100.00 | Using where; Using join buffer (Block Nested Loop) |
    +----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+----------------------------------------------------+
    2 rows in set, 2 warnings (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    3.3 采用内连接

    drop index X on type;
    drop index Y on book;
    
    • 1
    • 2

    换成 inner join(MySQL自动选择驱动表)

    mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type.card=book.card;
    +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
    | id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra                                              |
    +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
    |  1 | SIMPLE      | type  | NULL       | ALL  | NULL          | NULL | NULL    | NULL |   20 |   100.00 | NULL                                               |
    |  1 | SIMPLE      | book  | NULL       | ALL  | NULL          | NULL | NULL    | NULL |   20 |    10.00 | Using where; Using join buffer (Block Nested Loop) |
    +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
    2 rows in set, 2 warnings (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    添加索引优化

    ALTER TABLE book ADD INDEX Y ( card);
    
    mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type.card=book.card;
    +----+-------------+-------+------------+------+---------------+------+---------+----------------------+------+----------+-------------+
    | id | select_type | table | partitions | type | possible_keys | key  | key_len | ref                  | rows | filtered | Extra       |
    +----+-------------+-------+------------+------+---------------+------+---------+----------------------+------+----------+-------------+
    |  1 | SIMPLE      | type  | NULL       | ALL  | NULL          | NULL | NULL    | NULL                 |   20 |   100.00 | NULL        |
    |  1 | SIMPLE      | book  | NULL       | ref  | Y             | Y    | 4       | atguigudb2.type.card |    1 |   100.00 | Using index |
    +----+-------------+-------+------------+------+---------------+------+---------+----------------------+------+----------+-------------+
    2 rows in set, 2 warnings (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    ALTER TABLE type ADD INDEX X (card);
    
    mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type.card=book.card;
    +----+-------------+-------+------------+-------+---------------+------+---------+----------------------+------+----------+-------------+
    | id | select_type | table | partitions | type  | possible_keys | key  | key_len | ref                  | rows | filtered | Extra       |
    +----+-------------+-------+------------+-------+---------------+------+---------+----------------------+------+----------+-------------+
    |  1 | SIMPLE      | type  | NULL       | index | X             | X    | 4       | NULL                 |   20 |   100.00 | Using index |
    |  1 | SIMPLE      | book  | NULL       | ref   | Y             | Y    | 4       | atguigudb2.type.card |    1 |   100.00 | Using index |
    +----+-------------+-------+------------+-------+---------------+------+---------+----------------------+------+----------+-------------+
    2 rows in set, 2 warnings (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    接着:

    DROP INDEX X ON `type`;
    
    mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM TYPE INNER JOIN book ON type.card=book.card;
    +----+-------------+-------+------------+------+---------------+------+---------+----------------------+------+----------+-------------+
    | id | select_type | table | partitions | type | possible_keys | key  | key_len | ref                  | rows | filtered | Extra       |
    +----+-------------+-------+------------+------+---------------+------+---------+----------------------+------+----------+-------------+
    |  1 | SIMPLE      | TYPE  | NULL       | ALL  | NULL          | NULL | NULL    | NULL                 |   20 |   100.00 | NULL        |
    |  1 | SIMPLE      | book  | NULL       | ref  | Y             | Y    | 4       | atguigudb2.TYPE.card |    1 |   100.00 | Using index |
    +----+-------------+-------+------------+------+---------------+------+---------+----------------------+------+----------+-------------+
    2 rows in set, 2 warnings (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    接着:

    ALTER TABLE `type` ADD INDEX X (card);
    
    mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM `type` INNER JOIN book ON type.card=book.card;
    +----+-------------+-------+------------+-------+---------------+------+---------+----------------------+------+----------+-------------+
    | id | select_type | table | partitions | type  | possible_keys | key  | key_len | ref                  | rows | filtered | Extra       |
    +----+-------------+-------+------------+-------+---------------+------+---------+----------------------+------+----------+-------------+
    |  1 | SIMPLE      | type  | NULL       | index | X             | X    | 4       | NULL                 |   20 |   100.00 | Using index |
    |  1 | SIMPLE      | book  | NULL       | ref   | Y             | Y    | 4       | atguigudb2.type.card |    1 |   100.00 | Using index |
    +----+-------------+-------+------------+-------+---------------+------+---------+----------------------+------+----------+-------------+
    2 rows in set, 2 warnings (0.00 sec)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    3.4 join语句原理

    Index Nested-Loop Join

    EXPLAIN SELECT * FROM t1 STRAIGHT_JOIN t2 ON (t1.a=t2.a);
    
    • 1

    如果直接使用join语句,MySQL优化器可能会选择表t1或t2作为驱动表,这样会影响我们分析SQL语句的执行过程。所以,为了便于分析执行过程中的性能问题,我改用straight_join 让MySQL使用固定的连接方式执行查询,这样优化器只会按照我们指定的方式去join。在这个语句里,t1 是驱动表,t2是被驱动表。

    可以看到,在这条语句里,被驱动表t2的字段a上有索引,join过程用上了这个索引,因此这个语句的执行流程是这样的:

    1. 从表t1中读入一行数据 R;
    2. 从数据行R中,取出a字段到表t2里去查找;
    3. 取出表t2中满足条件的行,跟R组成一行,作为结果集的一部分;
    4. 重复执行步骤1到3,直到表t1的末尾循环结束。

    这个过程是先遍历表t1,然后根据从表t1中取出的每行数据中的a值,去表t2中查找满足条件的记录。在形式上,这个过程就跟我们写程序时的嵌套查询类似,并且可以用上被驱动表的索引,所以我们称之为“Index Nested-Loop Join”,简称NLJ。

    它对应的流程图如下所示:

    在这里插入图片描述

    在这个流程里:

    1. 对驱动表t1做了全表扫描,这个过程需要扫描100行;
    2. 而对于每一行R,根据a字段去表t2查找,走的是树搜索过程。由于我们构造的数据都是一一对应的,因此每次的搜索过程都只扫描一行,也是总共扫描100行;
    3. 所以,整个执行流程,总扫描行数是200。

    两个结论:

    1. 使用join语句,性能比强行拆成多个单表执行SQL语句的性能要好;
    2. 如果使用join语句的话,需要让小表做驱动表。

    Simple Nested-Loop Join
    Block Nested-Loop Join

    在这里插入图片描述

    执行流程图也就变成这样:

    在这里插入图片描述

    在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与join的各个字段的总数据量,数据量小的那个表,就是“小表”,应该作为驱动表。

    总结

    • 保证被驱动表的JOIN字段已经创建了索引
    • 需要JOIN 的字段,数据类型保持绝对一致。
    • LEFT JOIN 时,选择小表作为驱动表, 大表作为被驱动表。减少外层循环的次数。
    • INNER JOIN 时,MySQL会自动将小结果集的表选为驱动表。选择相信MySQL优化策略。
    • 能够直接多表关联的尽量直接关联,不用子查询。(减少查询的趟数)
    • 不建议使用子查询,建议将子查询SQL拆开结合程序多次查询,或使用 JOIN 来代替子查询。
    • 衍生表建不了索引

    4. 子查询优化

    MySQL从4.1版本开始支持子查询,使用子查询可以进行SELECT语句的嵌套查询,即一个SELECT查询的结果作为另一个SELECT语句的条件。子查询可以一次性完成很多逻辑上需要多个步骤才能完成的SQL操作

    子查询是 MySQL 的一项重要的功能,可以帮助我们通过一个 SQL 语句实现比较复杂的查询。但是,子查询的执行效率不高

    ① 执行子查询时,MySQL需要为内层查询语句的查询结果建立一个临时表,然后外层查询语句从临时表中查询记录。查询完毕后,再撤销这些临时表。这样会消耗过多的CPU和IO资源,产生大量的慢查询。

    ② 子查询的结果集存储的临时表,不论是内存临时表还是磁盘临时表都不会存在索引,所以查询性能会受到一定的影响。

    ③ 对于返回结果集比较大的子查询,其对查询性能的影响也就越大。在MySQL中,可以使用连接(JOIN)查询来替代子查询。连接查询不需要建立临时表,其速度比子查询要快,如果查询中使用索引的话,性能就会更好。

    结论:尽量不要使用NOT IN 或者 NOT EXISTS,用LEFT JOIN xxx ON xx WHERE xx IS NULL替代

  • 相关阅读:
    基于springboot实现校园疫情防控系统项目【项目源码+论文说明】计算机毕业设计
    PHP调用接口京东API封装的例子( 获得JD商品详情,按关键字搜索商品, 按图搜索京东商品(拍立淘),获得店铺)
    抖音同城热搜榜上榜技巧有哪些
    C++学习笔记:类与对象1
    Onnxruntime之tensorrt加速
    【数据结构】24王道考研笔记——线性表
    java基于SpringBoot+Vue的家具销售商城系统 element 前后端分离
    Arya碎碎念 | 我的创作纪念日——写在成为创作者的第1095天
    scrapy个人循序渐进
    关于使用mysql_fdw无法查询长度超过64K字段的原因分析
  • 原文地址:https://blog.csdn.net/qq_51495235/article/details/133087565