MySQL高级_第10章_索引优化与查询优化
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;
set global log_bin_trust_function_creators= 1 ; # 不加 global 只是当前窗口有效。
# 随机产生字符串DELIMITER //CREATE FUNCTION rand_string(n INT ) RETURNS VARCHAR ( 255 )BEGINDECLARE chars_str VARCHAR ( 100 ) DEFAULT'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ' ;DECLARE return_str VARCHAR ( 255 ) DEFAULT '' ;DECLARE i INT DEFAULT 0 ;WHILE i < n DOSET 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;
# 用于随机产生多少到多少的编号DELIMITER //CREATE FUNCTION rand_num (from_num INT ,to_num INT ) RETURNS INT ( 11 )BEGINDECLARE i INT DEFAULT 0 ;SET i = FLOOR(from_num +RAND()*(to_num - from_num+ 1 )) ;RETURN i;END //DELIMITER ;# 假如要删除#drop function rand_num;
# 创建往 stu 表中插入数据的存储过程DELIMITER //CREATE PROCEDURE insert_stu( START INT , max_num INT )BEGINDECLARE 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_numEND REPEAT ;COMMIT ; # 提交事务END //DELIMITER ;# 假如要删除#drop PROCEDURE insert_stu;
# 执行存储过程,往 class 表添加随机数据DELIMITER //CREATE PROCEDURE `insert_class` ( max_num INT )BEGINDECLARE i INT DEFAULT 0 ;SET autocommit = 0 ;REPEATSET i = i + 1 ;INSERT INTO class ( classname,address,monitor ) VALUES(rand_string( 8 ),rand_string( 10 ),rand_num( 1 , 100000 ));UNTIL i = max_numEND REPEAT ;COMMIT ;END //DELIMITER ;# 假如要删除#drop PROCEDURE insert_class;
# 执行存储过程,往 class 表添加 1 万条数据CALL insert_class( 10000 );
# 执行存储过程,往 stu 表添加 50 万条数据CALL insert_stu( 100000 , 500000 );
DELIMITER //CREATE PROCEDURE `proc_drop_index` (dbname VARCHAR ( 200 ),tablename VARCHAR ( 200 ))BEGINDECLARE done INT DEFAULT 0 ;DECLARE ct INT DEFAULT 0 ;DECLARE _index VARCHAR ( 200 ) DEFAULT '' ;DECLARE _cur CURSOR FOR SELECT index_name FROMinformation_schema .STATISTICS WHERE table_schema=dbname AND table_name =tablename ANDseq_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 设为 2OPEN _cur;FETCH _cur INTO _index;WHILE _index<> '' DOSET @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 ;
CALL proc_drop_index( "dbname" , "tablename" );
2. 索引失效案例
2.1 全值匹配我最爱
2.2 最佳左前缀法则
拓展: Alibaba 《 Java 开发手册》索引文件具有 B-Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。
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));
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' ;
CREATE INDEX idx_name ON student(NAME);
mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student .name LIKE 'abc%' ;
mysql> SELECT SQL_NO_CACHE * FROM student WHERE student .name LIKE 'abc%' ;+---------+---------+--------+------+---------+| id | stuno | name | age | classId |+---------+---------+--------+------+---------+| 5301379 | 1233401 | AbCHEa | 164 | 259 || 7170042 | 3102064 | ABcHeB | 199 | 161 || 1901614 | 1833636 | ABcHeC | 226 | 275 || 5195021 | 1127043 | abchEC | 486 | 72 || 4047089 | 3810031 | AbCHFd | 268 | 210 || 4917074 | 849096 | ABcHfD | 264 | 442 || 1540859 | 141979 | abchFF | 119 | 140 || 5121801 | 1053823 | AbCHFg | 412 | 327 || 2441254 | 2373276 | abchFJ | 170 | 362 || 7039146 | 2971168 | ABcHgI | 502 | 465 || 1636826 | 1580286 | ABcHgK | 71 | 262 || 374344 | 474345 | abchHL | 367 | 212 || 1596534 | 169191 | AbCHHl | 102 | 146 |...| 5266837 | 1198859 | abclXe | 292 | 298 || 8126968 | 4058990 | aBClxE | 316 | 150 || 4298305 | 399962 | AbCLXF | 72 | 423 || 5813628 | 1745650 | aBClxF | 356 | 323 || 6980448 | 2912470 | AbCLXF | 107 | 78 || 7881979 | 3814001 | AbCLXF | 89 | 497 || 4955576 | 887598 | ABcLxg | 121 | 385 || 3653460 | 3585482 | AbCLXJ | 130 | 174 || 1231990 | 1283439 | AbCLYH | 189 | 429 || 6110615 | 2042637 | ABcLyh | 157 | 40 |+---------+---------+--------+------+---------+401 rows in set , 1 warning ( 0.01 sec)
mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE LEFT (student .name , 3 ) = 'abc' ;
mysql> SELECT SQL_NO_CACHE * FROM student WHERE LEFT (student .name , 3 ) = 'abc' ;+---------+---------+--------+------+---------+| id | stuno | name | age | classId |+---------+---------+--------+------+---------+| 5301379 | 1233401 | AbCHEa | 164 | 259 || 7170042 | 3102064 | ABcHeB | 199 | 161 || 1901614 | 1833636 | ABcHeC | 226 | 275 || 5195021 | 1127043 | abchEC | 486 | 72 || 4047089 | 3810031 | AbCHFd | 268 | 210 || 4917074 | 849096 | ABcHfD | 264 | 442 || 1540859 | 141979 | abchFF | 119 | 140 || 5121801 | 1053823 | AbCHFg | 412 | 327 || 2441254 | 2373276 | abchFJ | 170 | 362 || 7039146 | 2971168 | ABcHgI | 502 | 465 || 1636826 | 1580286 | ABcHgK | 71 | 262 || 374344 | 474345 | abchHL | 367 | 212 || 1596534 | 169191 | AbCHHl | 102 | 146 |...| 5266837 | 1198859 | abclXe | 292 | 298 || 8126968 | 4058990 | aBClxE | 316 | 150 || 4298305 | 399962 | AbCLXF | 72 | 423 || 5813628 | 1745650 | aBClxF | 356 | 323 || 6980448 | 2912470 | AbCLXF | 107 | 78 || 7881979 | 3814001 | AbCLXF | 89 | 497 || 4955576 | 887598 | ABcLxg | 121 | 385 || 3653460 | 3585482 | AbCLXJ | 130 | 174 || 1231990 | 1283439 | AbCLYH | 189 | 429 || 6110615 | 2042637 | ABcLyh | 157 | 40 |+---------+---------+--------+------+---------+401 rows in set , 1 warning ( 3.62 sec)
- student表的字段stuno上设置有索引
CREATE INDEX idx_sno ON student(stuno);
EXPLAIN SELECT SQL_NO_CACHE id, stuno, NAME FROM student WHERE stuno+ 1 = 900001 ;
- 索引优化生效:
EXPLAIN SELECT SQL_NO_CACHE id, stuno, NAME FROM student WHERE stuno = 900000 ;
- student表的字段name上设置有索引
CREATE INDEX idx_name ON student(NAME);
EXPLAIN SELECT id, stuno, name FROM student WHERE SUBSTRING(name, 1 , 3 )= 'abc' ;
EXPLAIN SELECT id, stuno, NAME FROM student WHERE NAME LIKE 'abc%';
2.5 类型转换导致索引失效
# 未使用到索引EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name= 123 ;
# 使用到索引EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name= '123' ;
- 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 studentWHERE student .age = 30 AND student .classId > 20 AND student .name = 'abc' ;
create index idx_age_name_classid on student(age,name,classid);
- 将范围查询条件放置语句最后:
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student .age = 30 AND student .name ='abc' AND student .classId > 20 ;
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;
2.9 like以通配符%开头索引失效
拓展: Alibaba 《 Java 开发手册》【强制】页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。
2.10 OR 前后存在非索引的列,索引失效
# 未使用到索引EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 10 OR classid = 100 ;
# 使用到索引EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 10 OR name = 'Abel' ;
2.11 数据库和表的字符集统一使用utf8mb4
2.12 练习及一般性建议
练习:假设: index(a,b,c)
Where语句 | 索引是否被使用 |
where a= 3 | Y,使用到a |
where a = 3 and b=5 | Y,使用到a,b |
where a= 3 and b= 5 and c= 4 | Y,使用到a,b,c |
where b= 3或者where b= 3 and c= 4或者where c= 4 | N |
where a= 3 and c=5 | 使用到a,但是c不可以,b中间断了 |
where a = 3 and b> 4 and c= 5 | 使用到a和b,c不能用在范围之后,b断了 |
where a is null and b is not null | is null支持索引但是is not null不支持。所以a可以使用索引,但是b不可以使用 |
where a <> 3 | 不能使用索引 |
where abs(a) =3 | 不能使用索引 |
where a = 3 and b like 'kk%' and c =4 | Y,使用到a,b,c |
where a= 3 and b like '%kk' and c =4 | Y,只用到a |
where a= 3 and b like '%kk%' and c=4 | Y,只用到a |
where a = 3 and b like " k%kks%"and c = 4 | Y,使用到a,b,c |
一般性建议:
- 对于单列索引,尽量选择针对当前query过滤性更好的索引
- 在选择组合索引的时候,当前query中过滤性最好的字段在索引字段顺序中,位置越靠前越好。
- 在选择组合索引的时候,尽量选择能够包含当前query中的where子句中更多字段的索引。
- 在选择组合索引的时候,如果某个字段可能出现范围查询时,尽量把这个字段放在索引次序的最后面。
3. 关联查询优化
3.1 数据准备
3.2 采用左外连接
EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type .card = book .card ;
ALTER TABLE book ADD INDEX Y ( card); # 【被驱动表】,可以避免全表扫描EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type .card = book .card ;
ALTER TABLE `type` ADD INDEX X (card); # 【驱动表】,无法避免全表扫描EXPLAIN SELECT SQL_NO_CACHE * FROM `type` LEFT JOIN book ON type .card = book .card ;
3.3 采用内连接
drop index X on type;drop index Y on book; (如果已经删除了可以不用再执行该操作)
EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type .card =book .card ;
ALTER TABLE book ADD INDEX Y ( card);EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type .card =book .card ;
ALTER TABLE type ADD INDEX X (card);EXPLAIN SELECT SQL_NO_CACHE * FROM type INNER JOIN book ON type .card =book .card ;
DROP INDEX X ON `type` ;EXPLAIN SELECT SQL_NO_CACHE * FROM TYPE INNER JOIN book ON type .card =book .card ;
ALTER TABLE `type` ADD INDEX X (card);EXPLAIN SELECT SQL_NO_CACHE * FROM `type` INNER JOIN book ON type .card =book .card ;
3.4 join语句原理
join方式连接多个表,本质就是各个表之间数据的循环匹配。MySQL5.5版本之前,MySQL只支持一种表间关联方式,就是嵌套循环(Nested Loop Join)。如果关联表的数据量很大,则join关联的执行时间会非常长。在MySQL5.5以后的版本中,MySQL通过引入BNLJ算法来优化嵌套执行。
1.驱动表和被驱动表
驱动表就是主表。被驱动表就是从表、非驱动表。
- 对于内连接来说:
SELECT * FROM A JOIN B ON ...
A一定是驱动表吗?不一定,优化器会根据你查询语句做优化,决定先查哪张表。先查询的那张表就是驱动表,反之就是被驱动表。通过explain关键字可以查看,
- 对于外连接来说:
SELECT * FROM A LEFT JOIN B ON ...
#或
SELECT FROM B RIGHT JOIN A ON...
通常,大家会认为A就是驱动表,B就是被驱动表。但也未必。测试如下:
CREATE TABLE a(f1 INT,f2 INT,INDEX(f1) )ENGINE=INNODB;
CREATE TABLE b(f1 INT,f2 INT)ENGINE=INNODB;
INSERT INT0 a VALUES(1,1),(2,2),(3,3 ),(4,4),(5,5),(6,6);
INSERT INTO b VALUES(3,3),(4,4),(5,5),(6,6),(7,7), (8,8);
SELECT *FROM b ;
#测试1
EXPLAIN SELECT * FROM a LEFT JOIN b ON(a.f1=b.f1 ) WHERE (a . f2=b.f2);
#测试2
EXPLAIN SELECT * FROM a LEFT JOIN b ON(a.f1=b.f1) AND ( a.f2=b.f2);
2.Simple Nested-Loop Join (简单嵌套循环连接)
算法相当简单,从表A中取出一条数据1,遍历表B,将匹配到的数据放到result..以此类推,驱动表A中的每一条记录与被驱动表B的记录进行判断:
可以看到这种方式效率是非常低的,以上述表A数据100条,表B数据1000条计算,则A*B=10万次。开销统计如下:
当然mysql肯定不会这么粗暴的去进行表的连接,所以就出现了后面的两种对Nested-Loop Join优化算法。
3.Index Nested-Loop Join(索引嵌套循环连接)
Index Nested-Loop Join其优化的思路主要是为了减少内层表数据的匹配次数,所以要求被驱动表上必须有索引才行。通过外层表匹配条件直接与内层表索引进行匹配,避免和内层表的每条记录去进行比较,这样极大的减少了对内层表的匹配次数。
驱动表中的每条记录通过被驱动表的索引进行访问,因为索引查询的成本是比较固定的,故mysql优化器都倾向于使用记录数少的表作为驱动表(外表)。
如果被驱动表加索引,效率是非常高的,但如果索引不是主键索引,所以还得进行一次回表查询。相比,被驱动表的索引是主键索引,效率会更高。
EXPLAIN SELECT * FROM t1 STRAIGHT_JOIN t2 ON (t1 .a =t2 .a );
比如: N 扩大 1000 倍的话,扫描行数就会扩大 1000 倍;而 M 扩大 1000 倍,扫描行数扩大不到 10 倍。
两个结论:1. 使用 join 语句,性能比强行拆成多个单表执行 SQL 语句的性能要好;2. 如果使用 join 语句的话,需要让小表做驱动表。
4.Block Nested-Loop Join(块嵌套循环连接)
如果存在索引,那么会使用index的方式进行join,如果join的列没有索引,被驱动表要扫描的次数太多了。每次访问被驱动表,其表中的记录都会被加载到内存中,然后再从驱动表中取一条与其匹配,匹配结束后清除内存,然后再从驱动表中加载一条记录,然后把被驱动表的记录在加载到内存匹配这样周而复始,大大增加了IO的次数。为了减少被驱动表的Io次数,就出现了Block Nested-Loop Join的方式。
不再是逐条获取驱动表的数据,而是一块一块的获取,引入了join buffer缓冲区,将驱动表join相关的部分数据列(大小受join buffer的限制)缓存到join buffer中,然后全表扫描被驱动表。被驱动表的每一条记录一次性和join buffer中的所有驱动表记录进行匹配(内存中操作),将简单嵌套循环中的多次比较合并成一次,降低了被驱动表的访问频率。
注意:
这里缓存的不只是关联表的列, select后面的列也会缓存起来。
在一个有N个join关联的sql中会分配N-1个join buffer。所以查询的时候尽量减少不必要的字段,可以让joinbuffer中可以存放更多的列。
参数设置:
- block_nested_loop
通过 show variables like '%optimizer_switch%’查看block_nested_loop状态。默认是开启的。
- join_buffer_size
驱动表能不能一次加载完,要看join buffer能不能存储所有的数据,默认情况下join_buffer_size=256k 。
mysql> show variables like '%join_buffer% ' ;
十----------------—-十+--—-——--+
l Variable_namel Valuel十-—--------------——+-----—--+l
join_buffer_size | 262144 |
十-——------------——-十-—-
1 row in set (0.0e sec)
5 Join小结
1、整体效率比较:INLJ >BNLJ > SNLJ
2、永远用小结果集驱动大结果集(其本质就是减少外层循环的数据数量)
(小的度量单位指的是表行数*每行大小)
select t1.b, t2.* from t1 straight_join t2 on (t1.b=t2.b) where t2.id<=100;#推荐
select t1.b, t2.* from t2 straight_join t1 on (t1.b=t2.b) where t2.id<=108;#不推荐
3、为被驱动表匹配的条件增加索引(减少内层表的循环匹配次数)
4、增大join buffer size的大小(一次缓存的数据越多,那么内层包的扫表次数就越少)5、减少驱动表不必要的字段查询(字段越少,join buffer所缓存的数据就越多)
- 保证被驱动表的JOIN字段已经创建了索引
- 需要JOIN 的字段,数据类型保持绝对一致。
- LEFT JOIN 时,选择小表作为驱动表, 大表作为被驱动表 。减少外层循环的次数。
- INNER JOIN 时,MySQL会自动将 小结果集的表选为驱动表 。选择相信MySQL优化策略。
- 能够直接多表关联的尽量直接关联,不用子查询。(减少查询的趟数)
- 不建议使用子查询,建议将子查询SQL拆开结合程序多次查询,或使用 JOIN 来代替子查询。
- 衍生表建不了索引
6.Hash Join
从MySQL的8.0.20版本开始将废弃BNLJ,因为从MySQL8.0.18版本开始就加入了hash join默认都会使用hash join
- Nested Loop:
对于被连接的数据子集较小的情况,Nested Loop是个较好的选择。
- Hash Join是做大数据集连接时的常用方式,优化器使用两个表中较小(相对较小)的表利用Join Key在内存中建立散列表,然后扫描较大的表并探测散列表,找出与Hash表匹配的行。
- 这种方式适用于较小的表完全可以放于内存中的情况,这样总成本就是访问两个表的成本之和。
- 在表很大的情况下并不能完全放入内存,这时优化器会将它分割成若干不同的分区,不能放入内存的部分就把该分区写入磁盘的临时段,此时要求有较大的临时段从而尽量提高I/o 的性能。
- 它能够很好的工作于没有索引的大表和并行查询的环境中,并提供最好的性能。大多数人都说它是Join的重型升降机。Hash Join只能应用于等值连接(如WHERE A.COL1 =B.COL2),这是由Hash的特点决定的。
4. 子查询优化
结论:尽量不要使用 NOT IN 或者 NOT EXISTS ,用 LEFT JOIN xxx ON xx WHERE xx IS NULL 替代
5. 排序优化
5.1 排序优化
INDEX a_b_c(a,b,c)order by 能使用索引最左前缀- ORDER BY a- ORDER BY a,b- ORDER BY a,b,c- ORDER BY a DESC ,b DESC ,c DESC如果 WHERE 使用索引的最左前缀定义为常量,则 order by 能使用索引- WHERE a = const ORDER BY b,c- WHERE a = const AND b = const ORDER BY c- WHERE a = const ORDER BY b,c- WHERE a = const AND b > const ORDER BY b,c不能使用索引进行排序- ORDER BY a ASC ,b DESC ,c DESC /* 排序不一致 */- WHERE g = const ORDER BY b,c /* 丢失 a 索引 */- WHERE a = const ORDER BY c /* 丢失 b 索引 */- WHERE a = const ORDER BY a,d /*d 不是索引的一部分 */- WHERE a in (...) ORDER BY b,c /* 对于排序来说,多个相等条件也是范围查询 */
5.3 案例实战
DROP INDEX idx_age ON student;DROP INDEX idx_age_classid_stuno ON student;DROP INDEX idx_age_classid_name ON student;# 或者call proc_drop_index( 'atguigudb2' , 'student' );
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 30 AND stuno < 101000 ORDER BYNAME ;
mysql> SELECT SQL_NO_CACHE * FROM student WHERE age = 30 AND stuno < 101000 ORDER BYNAME ;+---------+--------+--------+------+---------+| id | stuno | name | age | classId |+---------+--------+--------+------+---------+| 922 | 100923 | elTLXD | 30 | 249 || 3723263 | 100412 | hKcjLb | 30 | 59 || 3724152 | 100827 | iHLJmh | 30 | 387 || 3724030 | 100776 | LgxWoD | 30 | 253 || 30 | 100031 | LZMOIa | 30 | 97 || 3722887 | 100237 | QzbJdx | 30 | 440 || 609 | 100610 | vbRimN | 30 | 481 || 139 | 100140 | ZqFbuR | 30 | 351 |+---------+--------+--------+------+---------+8 rows in set , 1 warning ( 3.16 sec)
结论: type 是 ALL ,即最坏的情况。 Extra 里还出现了 Using filesort, 也是最坏的情况。优化是必须的。
# 创建新索引CREATE INDEX idx_age_name ON student(age,NAME);
DROP INDEX idx_age_name ON student;CREATE INDEX idx_age_stuno_name ON student (age,stuno,NAME);EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 30 AND stuno < 101000 ORDER BYNAME ;
mysql> SELECT SQL_NO_CACHE * FROM student-> WHERE age = 30 AND stuno < 101000 ORDER BY NAME ;+-----+--------+--------+------+---------+| id | stuno | name | age | classId |+-----+--------+--------+------+---------+| 167 | 100168 | AClxEF | 30 | 319 || 323 | 100324 | bwbTpQ | 30 | 654 || 651 | 100652 | DRwIac | 30 | 997 || 517 | 100518 | HNSYqJ | 30 | 256 || 344 | 100345 | JuepiX | 30 | 329 || 905 | 100906 | JuWALd | 30 | 892 || 574 | 100575 | kbyqjX | 30 | 260 || 703 | 100704 | KJbprS | 30 | 594 || 723 | 100724 | OTdJkY | 30 | 236 || 656 | 100657 | Pfgqmj | 30 | 600 || 982 | 100983 | qywLqw | 30 | 837 || 468 | 100469 | sLEKQW | 30 | 346 || 988 | 100989 | UBYqJl | 30 | 457 || 173 | 100174 | UltkTN | 30 | 830 || 332 | 100333 | YjWiZw | 30 | 824 |+-----+--------+--------+------+---------+15 rows in set , 1 warning ( 0.00 sec)
1. 两个索引同时存在, mysql 自动选择最优的方案。(对于这个例子, mysql 选择idx_age_stuno_name )。但是, 随着数据量的变化,选择的索引也会随之变化的 。2. 当【范围条件】和【 group by 或者 order by 】的字段出现二选一时,优先观察条件字段的过滤数量,如果过滤的数据足够多,而需要排序的数据并不多时,优先把索引放在范围字段上。反之,亦然。
DROP INDEX idx_age_stuno_name ON student;CREATE INDEX idx_age_stuno ON student(age,stuno);
5.4 filesort算法:双路排序和单路排序
- MySQL 4.1之前是使用双路排序 ,字面意思就是两次扫描磁盘,最终得到数据, 读取行指针和 order by列 ,对他们进行排序,然后扫描已经排序好的列表,按照列表中的值重新从列表中读取对应的数据输出
- 从磁盘取排序字段,在buffer进行排序,再从 磁盘取其他字段 。
- 由于单路是后出的,总体而言好过双路
- 但是用单路有问题
6. GROUP BY优化
- group by 使用索引的原则几乎跟order by一致 ,group by 即使没有过滤条件用到索引,也可以直接使用索引。
- group by 先排序再分组,遵照索引建的最佳左前缀法则
- 当无法使用索引列,增大 max_length_for_sort_data 和 sort_buffer_size 参数的设置
- where效率高于having,能写在where限定的条件就不要写在having中了
- 减少使用order by,和业务沟通能不排序就不排序,或将排序放到程序端去做。Order by、group by、distinct这些语句较为耗费CPU,数据库的CPU资源是极其宝贵的。
- 包含了order by、group by、distinct这些查询的语句,where条件过滤出来的结果集请保持在1000行以内,否则SQL会很慢。
7. 优化分页查询
EXPLAIN SELECT * FROM student t,( SELECT id FROM student ORDER BY id LIMIT 2000000 , 10 )aWHERE t .id = a .id ;
EXPLAIN SELECT * FROM student WHERE id > 2000000 LIMIT 10 ;
8. 优先考虑覆盖索引
8.1 什么是覆盖索引?
8.2 覆盖索引的利弊
9. 如何给字符串添加索引
create table teacher(ID bigint unsigned primary key ,email varchar ( 64 ),...) engine = innodb ;
mysql> select col1, col2 from teacher where email= 'xxx' ;
9.1 前缀索引
mysql> alter table teacher add index index1(email);# 或mysql> alter table teacher add index index2(email( 6 ));
9.2 前缀索引对覆盖索引的影响
结论:使用前缀索引就用不上覆盖索引对查询性能的优化了,这也是你在选择是否使用前缀索引时需要考虑的一个因素。
10. 索引下推
10.1 使用前后的扫描过程
- storage层:
- server 层:
10.2 ICP的使用条件
10.3 ICP使用案例
SELECT * FROM tuserWHERE NAME LIKE ' 张 %'AND age = 10AND ismale = 1 ;
案例2
11. 普通索引 vs 唯一索引
mysql> create table test(id int primary key ,k int not null ,name varchar ( 16 ),index (k)) engine = InnoDB ;
11.1 查询过程
- 对于普通索引来说,查找到满足条件的第一个记录(5,500)后,需要查找下一个记录,直到碰到第一个不满足k=5条件的记录。
- 对于唯一索引来说,由于索引定义了唯一性,查找到第一个满足条件的记录后,就会停止继续检索。
11.2 更新过程
11.3 change buffer的使用场景
- 首先, 业务正确性优先 。我们的前提是“业务代码已经保证不会写入重复数据”的情况下,讨论性能问题。如果业务不能保证,或者业务就是要求数据库来做约束,那么没得选,必须创建唯一索引。 这种情况下,本节的意义在于,如果碰上了大量插入数据慢、内存命中率低的时候,给你多提供一个排查思路。
- 然后,在一些“ 归档库 ”的场景,你是可以考虑使用唯一索引的。比如,线上数据只需要保留半年,然后历史数据保存在归档库。这时候,归档数据已经是确保没有唯一键冲突了。要提高归档效率,可以考虑把表里面的唯一索引改成普通索引。
12. 其它查询优化策略
12.1 EXISTS 和 IN 的区分
12.2 COUNT(*)与COUNT(具体字段)效率
12.3 关于SELECT(*)
12.4 LIMIT 1 对优化的影响
12.5 多使用COMMIT
- 回滚段上用于恢复数据的信息
- 被程序语句获得的锁
- redo / undo log buffer 中的空间
- 管理上述 3 种资源中的内部花费
13. 淘宝数据库,主键如何设计的?
13.1 自增ID的问题
13.2 业务字段做主键
- 选择卡号(cardno)
mysql> CREATE TABLE demo .membermaster-> (-> cardno CHAR ( 8 ) PRIMARY KEY , -- 会员卡号为主键-> membername TEXT ,-> memberphone TEXT ,-> memberpid TEXT ,-> memberaddress TEXT ,-> sex TEXT ,-> birthday DATETIME-> );Query OK, 0 rows affected ( 0.06 sec)
mysql> SELECT b .membername ,c .goodsname ,a .quantity ,a .salesvalue ,a .transdate-> FROM demo .trans AS a-> JOIN demo .membermaster AS b-> JOIN demo .goodsmaster AS c-> ON (a .cardno = b .cardno AND a .itemnumber =c .itemnumber );+------------+-----------+----------+------------+---------------------+| membername | goodsname | quantity | salesvalue | transdate |+------------+-----------+----------+------------+---------------------+| 张三 | 书 | 1.000 | 89.00 | 2020 - 12 - 01 00 :00:00 |+------------+-----------+----------+------------+---------------------+1 row in set ( 0.00 sec)
mysql> SELECT b .membername ,c .goodsname ,a .quantity ,a .salesvalue ,a .transdate-> FROM demo .trans AS a-> JOIN demo .membermaster AS b-> JOIN demo .goodsmaster AS c-> ON (a .cardno = b .cardno AND a .itemnumber =c .itemnumber );+------------+-----------+----------+------------+---------------------+| membername | goodsname | quantity | salesvalue | transdate |+------------+-----------+----------+------------+---------------------+| 王五 | 书 | 1.000 | 89.00 | 2020 - 12 - 01 00 :00:00 |+------------+-----------+----------+------------+---------------------+1 row in set ( 0.01 sec)
- 选择会员电话 或 身份证号
经验:刚开始使用 MySQL 时,很多人都很容易犯的错误是喜欢用业务字段做主键,想当然地认为了解业务需求,但实际情况往往出乎意料,而更改主键设置的成本非常高。
13.3 淘宝的主键设计
1550672064762308113148119584718030811314311561711423081131431146631521308113
订单 ID = 时间 + 去重字段 + 用户 ID 后 6 位尾号
13.4 推荐的主键设计
- 为什么UUID是全局唯一的?
- 为什么UUID占用36个字节?
- 为什么UUID是无序的?
UUID = 时间 +UUID 版本( 16 字节) - 时钟序列( 4 字节) - MAC 地址( 12 字节)
SET @uuid = UUID();SELECT @uuid ,uuid_to_bin( @uuid ),uuid_to_bin( @uuid , TRUE );
在当今的互联网环境中,非常不推荐自增 ID 作为主键的数据库设计。更推荐类似有序 UUID 的全局唯一的实现。另外在真实的业务系统中,主键还可以加入业务和系统属性,如用户的尾号,机房的信息等。这样的主键设计就更为考验架构师的水平了。