MySQL查询优化看一篇就够了

关联查询优化

数据准备

#分类
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次
#向分类表中添加20条记录
INSERT INTO type (card) VALUES (FLOOR(1 +(RAND() * 20)));
#向图书表中添加20条记录
INSERT INTO book(card) VALUES (FLOOR(1 +(RAND() * 20)) );

采用左外连接

explain select * from `type` left join book on type.card = book.card;

image.png
可以看出没有使用索引,在left join中默认右边的表是被驱动表(但不绝对),即book表是被驱动表。给book表添加索引,可以避免全表扫描

alter table book add index idx_bcard(card);
explain select * from `type` left join book on type.card = book.card;

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

如果只能添加一边的索引,那就给被驱动表添加上索引。

此时给驱动表添加索引,看看效果如何?

alter table `type` add index idx_tcard(card);
explain select * from `type` left join book on type.card = book.card;

image.png
type的type是index,速度比ref慢;并且rows还是20行,依旧要全表遍历。

采用内连接

把我们之前的索引都删除掉,方便后续演示

drop index idx_tcard on `type`;
drop index idx_bcard on book;

使用inner join此时谁是驱动表谁是被驱动表,由优化器决定,哪个数据量小,谁是驱动表

explain select * from type left join book on type.card = book.card;

image.png
添加索引优化

create index idx_bcard on book(card);
explain select * from type join book on type.card = book.card;

image.png
被添加索引的表,自动变成被驱动表。
此时给type也添加索引

alter table type add index idx_tcard(card);

image.png
此时两者关系还没变,如果此时给type表添加数据。
MySQL查询优化看一篇就够了_第1张图片
你猜怎么着?type变被驱动表了,因为优化器判断type表数据比较多,就作为被驱动表了。
综上我想告诉大家的是:

  • 内连接: 驱动表还是被驱动表是由优化器决定的。优化器认为哪个成本比较小,就采用哪种作为驱动表。
  • 如果两张表只有一个有索引,那有索引的表作为被驱动表。
    • 原因:驱动表要全查出来。有没有索引你都得全查出来。
  • 两个索引都存在的情况下, 数据量大的 作为被驱动表(小表驱动大表)
    • 原因:驱动表要全部查出来,而大表可以通过索引加快查找

join语句原理

join方式连接多个表,本质就是各个表之间数据的循环匹配。MySQL5.5版本之前,MySQL只文持一种表间关联方式,就是嵌套循环(Nested Loop Join)。如果关联表的数据量很大,则join关联的执行时间会非常长。在MySQL5.5以后的版本中,MySQL通过引入BNLJ算法(Block Nested-Loop Join)来优化嵌套执行.

驱动表与被驱动表
  1. 对于内连接来说
select * from A join B on....

A一定是驱动表吗?不一定,优化器会根据你查询语句做优化,决定先查哪张表。先查询的那张表就是驱动表,反之就是被驱动表。可以通过explain关键字查看。

  1. 对于外连接来说
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 INTO 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);

进行第一次测试

EXPLAIN SELECT* FROM a LEFT JOIN b ON (a.f1=b.f1)WHERE (a.f2=b.f2);

image.png
我们发现b表竟然是被驱动表,出乎意料呀!用show warnings看看mysql底层怎么执行的
image.png
什么?竟然被优化成inner join了!
进行第二次测试

explain select * from a left join b on (a.f1=b.f1) and (a.f2=b.f2);

image.png
此时b表又变回了被驱动表了,继续使用show warnings看看
image.png
此时依旧是left join

Simple Nested-Loop join(简单嵌套循环连接)

算法相当简单,从表A中取出一条数据1,遍历表B,将匹配到的数据放到result…以此类推,驱动表A中的每一条记录与被驱动表B的记录进行判断:
MySQL查询优化看一篇就够了_第2张图片
这个例子是在没有索引的情况,做了全表扫描。
可以看到这种方式效率是非常低的,以上述表A数据100条,表B数据1000条计算,则A*B=10万次。开销统计如下:
MySQL查询优化看一篇就够了_第3张图片

外表A加载到内存中一次,A表的每一条记录都要去B表匹配。
驱动表是外表,被驱动表是内表。

当然mysql肯定不会这么粗暴的去进行表的连接,所以就出现了后面的两种对Nested-Loop Join优化算法

Index Nested-Loop join(索引嵌套循环连接)

Index Nested-Loop Join其优化的思路主要是为了减少内层表数据的匹配次数,所以要求被驱动表上必须有索引才行。通过外层表匹配条件直接与内层表索引进行匹配,避免和内层表的每条记录去进行比较,这样极大的减少了对内层表的匹配次数。
MySQL查询优化看一篇就够了_第4张图片
驱动表中的每条记录通过被驱动表的索引进行访问,因为索引查询的成本是比较固定的,故mysql优化器都倾向于使用记录数少的表作为驱动表(外表)。
MySQL查询优化看一篇就够了_第5张图片
如果被驱动表加索引,效率是非常高的,但如果索引不是主键索引,所以还得进行一次回表查询。相比,被驱动表的索引是主键索引,效率会更高。

Block Nested-Loop join(块嵌套循环连接)

如果存在索引,那么会使用index的方式进行join,如果join的列没有索引,被驱动表要扫描的次数太多了。每次访问被驱动表,其表中的记录都会被加载到内存中,然后再从驱动表中取一条与其匹配,匹配结束后清除内存,然后再从驱动表中加载一条记录,然后把被驱动表的记录在加载到内存匹配这样周而复始,大大增加了I0的次 数。为了减少被驱动表的Io次数,就出现了Block Nested-Loop Join的方式。
不再是逐条获取驱动表的数据,而是一块一块的获取,引入了join buffer缓冲区,将驱动表join相关的部分数据列(大小受join buffer的限制)缓存到join buffer中,然后全表扫描被驱动表,被驱动表的每—条记录—次性和join buffer中的所有驱动表记录进行匹配(内存中操作),将简单嵌套循环中的多次比较合并成一次,降低了被驱动 表的访问频率。

注意:
这里缓存的不只是关联表的列, select后面的列也会缓存起来。(存的是驱动表)
在一个有N个join关联的sql中会分配N-1个join buffer。所以查询的时候尽量减少不必要的字段,可以让joinbuffer中可以存放更多的列。

MySQL查询优化看一篇就够了_第6张图片
MySQL查询优化看一篇就够了_第7张图片

join总结

1、整体效率比较:INLJ > BNLJ > SNLJ
2、永远用小结果集驱动大结果集(其本质就是减少外层循环的数据数量)(小的度量单位指的是表行数*每行大小)

# straight_join 规定谁是驱动表谁是被驱动表,  驱动表 straight_join 被驱动表
# 这个例子是说t2 的列比较多,相同的join buffer 加的会比较少。所以不适合用t2作为驱动表!!!
select t1.b,t2.* from t1 straight_join t2 on (t1.b=t2.b) where t2.id<=180;#推荐

select t1.b,t2.* from t2 straight_join t1 on (t1.b=t2.b) where t2.id<=100;#不推荐

3、为被驱动表匹配的条件增加索引(减少内层表的循环匹配次数)
4、增大join buffer size的大小(一次缓存的数据越多,那么内层包的扫表次数就越少)
5、减少驱动表不必要的字段查询(字段越少,join buffer 所缓存的数据就越多)
6、在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与join的各个字段的总数据量,数据量小的那个表,就是“小表”,应该作为驱动表。

子查询优化

使用子查询可以进行SELECT语句的嵌套查询,即一个SELECT查询的结果作为另一个SELECT语句的条件。子查询是MySQL的一项重要的功能,可以帮助我们通过一个SQL语句实现比较复杂的查询。但是,子查询的执行效率不高。原因如下:
①执行子查询时MySQL需要为内层查询语句的查询结果建立一个临时表,然后外层查询语句从临时表中查询记录。查询完毕后,再撤销这些临时表。这样会消耗过多的CPU和IO资源,产生大量的慢查询。
②子查询的结果集存储的临时表,不论是内存临时表还是磁盘临时表都不会存在索引,所以查询性能会受到一定的影响。
③对于返回结果集比较大的子查询,其对查询性能的影响也就越大。

在MySQL中,可以使用连接(JOIN)查询来替代子查询。连接查询不需要建立临时表,其速度比子查询要快,如果查询中使用索引的话,性能就会更好。

前言:后面使用到的表(class, student),在《索引失效》章节已经建立

需求1:查询学生表中是班长的学生信息

  • 使用子查询
CREATE INDEX idx_monitor ON class(monitor);
EXPLAIN SELECT * FROM student WHERE student.stuno IN (
	SELECT monitor FROM class WHERE monitor IS NOT NULL); # 0.069s
  • 使用多表查询(推荐)
EXPLAIN SELECT * FROM student JOIN class ON student.stuno = class.monitor WHERE monitor IS NOT NULL; # 0.033s

需求2:查询所有不为班长的同学

  • 子查询(不推荐)
EXPLAIN SELECT * FROM student WHERE student.stuno NOT IN(
	SELECT monitor FROM class WHERE monitor IS NOT NULL); # 0.056s

image.png

  • 多表查询
EXPLAIN SELECT * FROM student LEFT JOIN class ON student.stuno = class.monitor
WHERE class.monitor IS NULL; # 0.011s

image.png

结论: 尽量不要使用NOT IN或者NOT EXISTS,用LEFT JOIN Xxx ON xx WHERE xx IS NULL替代。
并且这里使用not in正好查找的列是索引列,才能使用索引覆盖index,否则就是全表查询all

排序优化

排序优化

问题: 在WHERE 条件字段上加索引但是为什么在ORDER BY字段上还要加索引呢?
回答:
在MySQL中,支持两种排序方式,分别是FileSort和Index排序。

  • Index排序中,索引可以保证数据的有序性,不需要再进行排序,效率更高。
  • FileSort排序则一般在内存中进行排序,占用CPU较多。如果待排结果较大,会产生临时文件I/O到磁盘进行排序的情况,效率较低。

优化建议:

  1. SQL中,可以在WHERE子句和ORDER BY子句中使用索引,目的是在WHERE子句中避免全表扫描,在ORDER BY子句避免使用FileSort排序。当然,某些情况下全表扫描,或者FileSort排序不一定比索引慢。但总的来说,我们还是要避免,以提高查询效率。
  2. 尽量使用Index完成ORDER BY排序。如果WHERE和ORDER BY后面是相同的列就使用单索引列;如果不同 就使用联合索引。
  3. 无法使用Index时,需要对FileSort方式进行调优。

测试

前言:删除class表和student表中建立的索引

在无索引情况下:
MySQL查询优化看一篇就够了_第8张图片
添加索引,再次执行
order by不使用limit,导致索引失效

create index idx_age_classid_name on student(age,classid,name);
explain select * from student order by age, classid;
explain select * from student order by age, classid limit 10;

MySQL查询优化看一篇就够了_第9张图片添加索引以后第一种查询方式依旧没用使用索引,因为优化器觉得,即使使用了索引,还需要回表,会更加的费时。而第二种查询方式,由于查询的列少,回表消耗也不大,因此使用索引。
使用覆盖索引
MySQL查询优化看一篇就够了_第10张图片
此时第二种查询方式使用到了索引,因为查询字段全在二级索引上,不需要回表,效率高。
order by时顺序错误/方向不一致,导致索引失效

create index idx_age_classid_stuno on student(age,classid,stuno);
# 不符合最左前缀原则
explain select * from student order by classid limit 10;
# 不符合最左前缀原则
explain select * from student order by classid, name limit 10;
# 顺序正确
explain select * from student order by age,classid limit 10;
# 方向反了,索引失效
explain select * from student order by age desc, classid asc limit 10;
# 方向反了,不走索引
# age asc没问题,但是calssid desc降序,优化器认为,文件排序更快
explain select * from student order by age asc, classid desc limit 10;
# 顺序正确,方向一致,走索引
explain select * from student order by age desc, classid desc limit 10;

无过滤,不索引
MySQL查询优化看一篇就够了_第11张图片

# 不符合最左前缀原则
explain select * from student where classid = 45 order by name;
# 却可以使用索引???
explain select * from student where classid = 45 order by age limit 10;

MySQL查询优化看一篇就够了_第12张图片
虽然age不在第一个字段,但是order by中有age 字段,如果建立联合索引,直接根据age字段筛选出前10条记录中查找classid=45的记录,再回表的效率最高。

实战

order by子句,尽量使用index方式排序,避免使用fileSort方式排序

清除student表上的索引,只留主键索引

场景:查询年龄30岁,且学生编号小于10000的学生,按照用户名排序
image.png

type是all,即最差的查询方式,还是使用filesort排序,也是最差的情况,必须进行优化

优化思路
  1. 建立索引,去掉filesort
create index idx_age_name on student(age,name);
explain select * from student where age = 30 and stuno < 10000 order by name;

image.png
idx_age_name索引中只使用到age字段,因为优化器认为,通过age索引已经可以确定记录了,不需要进行排序了

  1. 尽量让where的过滤条件和排序字段都使用上索引
create index idx_age_stuno_name on student(age,stuno,name);
explain select * from student where age = 30 and stuno < 10000 order by name;

image.png
这个方案虽然使用了 Using filesort 但是速度反而更快了。
原因:
所有的排序都是在条件过滤之后才执行的。所以,如果条件过滤掉大部分数据的话,剩下几百几千条数据进行排序其实并不是很消耗性能,即使索引优化了排序,但实际提升性能很有限。相对的stuno<101000这个条件,如果没有用到索引的话,要对几万条的数据进行扫描,这是非常消耗性能的,所以索引放在这个字段上性价比最高,是最优选择。

结论: 1.两个索引同时存在,mysql自动选择最优的方案。(对于这个例子mysql选择idx_age_stuno_name)。但是,随着数据量的变化,选择的索引也会随之变化的。
2.当【范围条件】和【group by或者order by】的字段出现二选一时,优先观察条件字段的过滤数量,如 果过滤的数据足够多,而需要排序的数据并不多时,优先把索引放在范围字段上。反之,亦然。

filesort算法

排序的字段若如果不在索引列上,则filesort会有两种算法: 双路排序单路排序
双路排序(慢)

  • MySQL 4.1之前是使用双路排序,字面意思就是两次扫描磁盘,最终得到数据,读取行指针和order by列,对他们进行排序,然后扫描已经排序好的列表,按照列表中的值重新从列表中读取对应的数据输出
  • 从磁盘取排序字段,在buffer进行排序,再从磁盘取其他字段。

取一批数据,要对磁盘进行两次扫描,众所周知,lo是很耗时的,所以在mysql4.1之后,出现了第二种改进的算法,就是单路排序。
单路排序(快)
从磁盘读取查询需要的所有列,按照order by列在buffer对数据进行排序但是它会使用更多的空间,因为它把每一效率更快一些,避免了第二次读取数据。并且把随机Io变成了顺序IO,行都保存在内存中了。

  • 单路排序缺陷
    • 在sort_buffer中,单路比多路要多占用更多空间,因为单路是把所有字段都取出,所以有可能取出的数据的总大小超出了sort_buffer的容量,导致每次只能取sort_buffer容量大小的数据,进行排序(创建tmp文件,多路合并),排完再取sort_buffer容量大小,再排…从而多次I/O。
    • 单路本来想省一次I/o操作,反而导致了大量的I/0操作,反而得不偿失。

优化策略
1.尝试提高**sort_buffer_size**

  • 不管用哪种算法,提高这个参数都会提高效率,要根据系统的能力去提高,因为这个参数是针对每个进程 (connection)的1M-8M之间调整。MySQL5.7,InnoDB存储引擎默认值是1048576字节,1MB。

MySQL查询优化看一篇就够了_第13张图片
2尝试提高max_length_for_sort_data

  • 提高这个参数,会增加用改进算法的概率。

MySQL查询优化看一篇就够了_第14张图片

  • 但是如果设的太高,数据总容量超出sort_buffer_size的概率就增大,明显症状是高的磁盘I/o活动和低的处理器使用率。如果需要返回的列的总长度大于max_length_for_sort_data使用双路算法,否则使用单路算法。1024-8192字节之间调整

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会很慢。

分页查询优化

在分页查询中一个常见又非常头疼的问题就是limit 2000000,10,此时需要MySQL排序前2000010记录,仅仅返回2000000 - 2000010的记录,其他记录丢弃,查询排序的代价非常大。

explain select * from student limit 2000000 10;

**优化思路一:**在索引上完成排序分页操作,最后根据主键关联会原表查询所需要的其他列的内容。

explain select * from student,(select id from student order by id limit 2000000,10) tmp 
where student.id = tmp.id;

image.png
优化思路二(基本没法用)**:**该方案适用于主键自增的表,可以把Limit查询转换成某个位置的查询。

explain select * from student where id > 2000000 limit 10;

image.png

不靠谱,生产中id可能会删除,查询的条件也不可能这么简单。

优先考虑覆盖索引

什么是覆盖索引

一句话说明白:select语句要查询的字段,正好是索引列,主键。

使用覆盖索引

删除掉之前的索引,建立关于(age,name)的索引

image.png

explain select * from student where age <> 20; # <>不会使用到索引

image.png

# 使用覆盖索引
explain select id,age,name from student where age <> 20;

image.png
按理来说使用<>会导致索引失效,但是由于查询字段和二级索引idx_age_name匹配,不需要进行回表。优化器认为这样的查询效率更高,因此使用上索引。

explain select id,age,name,classid from student where age <> 20;

image.png
由于查询字段中的classid并不在二级索引中,需要进行回表,又使用<>导致索引失效。

# 左模糊查询会导致索引失效,但是在覆盖索引下并不会
explain select id,age,name from student where name like '%abc';

image.png
此处没有索引失效,是因为数据都在索引上。直接遍历索引就可以返回数据,肯定比遍历全表数据数据量少,减少IO。

覆盖索引的利弊

好处
  1. 避免回表,进行二次查询

Innodb引擎中数据是存储在聚簇索引上,二级索引在叶子节点中所保存的是行的主键信息,如果是用二级索引查询数据,在查找到相应的键值后,还需通过主键进行二次查询才能获取我们真实所需要的数据。在覆盖索引中,二级索引的键值中可以获取所要的数据,避免了对主键的二次查询,减少了IO操作,提升了查询效率。

  1. 可以把随机IO变成顺序IO加快查询效率

由于覆盖索引是按键值的顺序存储的,对于I0密集型的范围查找来说,对比随机从磁盘读取每一行的数据IO要少的多,因此利用覆盖索引在访问时也可以把磁盘的随机读取的IO转变成索引查找的顺序IO。

  1. 数据在索引里面的数据量更少

非聚簇索引中的数据量比聚簇索引中少,因此一个数据页中可以存储更多的覆盖索引,减少IO

缺点

索引字段的维护需要付出代价,因此在建立冗余索引来支持覆盖索引就需要权衡考虑

前缀索引

create table teacher(
	id bigint unsigned primary key,
	email varchar(64)
	...
)engine = innodb;

需求:教师通过邮箱登入
select col1,col2 from teacher where email='xxx';

email字段创建索引,如果你创建索引的语句不指定前缀长度,那么索引就会包含整个字符串。

alter table teacher add index index1(email);
# 前缀索引
alter table teacher add index index2(email(6));

这两种索引的示意图如下:
MySQL查询优化看一篇就够了_第15张图片
如果使用的是index1(即email整个字符串的索引结构),执行顺序是这样的:

  1. 从index1索引树找到满足索引值是’ [email protected] ’的这条记录,取得ID2的值;
  2. 到主键上查到主键值是ID2的行,判断email的值是正确的,将这行记录加入结果集;
  3. 取index1索引树上刚刚查到的位置的下一条记录,发现已经不满足email=’ [email protected] ’的 条件了,循环结束。

这个过程中,只需要回主键索引取一次数据,所以系统认为只扫描了一行。
如果使用的是index2(即email(6)索引结构),执行顺序是这样的:

  1. 从index2索引树找到满足索引值是’zhangs’的记录,找到的第一个是ID1;
  2. 到主键上查到主键值是ID1的行,判断出email的值不是’ [email protected] ’,这行记录丢弃;
  3. 取index2上刚刚查到的位置的下一条记录,发现仍然是’zhangs’,取出ID2,再到ID索引上取整行然 后判断,这次值对了,将这行记录加入结果集;
  4. 重复上一步,直到在idxe2上取到的值不是’zhangs’时,循环结束。

前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本。区分度越高,重复的键值越少

前缀索引对覆盖索引的影响

使用前缀索引就用不上覆盖索引,这也是是否选择前缀索引的考虑因素

索引下推(ICP)

什么是索引下推

MySQL查询优化看一篇就够了_第16张图片
在如上结构的表s1中

explain select * from s1 where key1 > 'z' and key1 like '%abc';

image.png

  • 无索引下推的情况下:通过key1索引,找到符合条件的记录,假设100条,然后回表查询,这100条中符合第二个条件的记录
  • 引入索引下推后:原本key1 like '%abc'是索引失效的,索引下推到第二个条件中,让他这100条记录中进行筛选符合'%abc'的记录,假设筛选出10条,然后再回表,找到这10条记录。
    :::success
    使用索引下推后
  1. 减少了回表的次数
  2. 减少了随机IO的次数
    :::

ICP开启/关闭

默认情况下启用索引条件下推。可以通过设置系统变量optimizer_switch控制:index_condition_pushdown

#打开索引下推
SET optimizer_switch = 'index_condition_pushdown=off ' ;
#关闭索引下推
SET optimizer_switch = 'index_condition_pushdown=on ' ;

ICP使用前后的扫描过程

不使用IPC的扫描过程

storage层:只将满足index key条件的索引记录对应的整行记录取出,返回给server层
server 层:对返回的数据,使用后面的where条件过滤,直至返回最后一行。
MySQL查询优化看一篇就够了_第17张图片

使用ICP扫描的过程

使用ICP扫描的过程: storage层: 首先将index key条件满足的索引记录区间确定,然后在索引上使用index filter进行过滤。将满足的index filter条件的索引记录才去回表取出整行记录返回server层。不满足index filter条件的索引记录丢弃,不回 表、也不会返回server层。 server 层: 对返回的数据,使用table filter条件做最后的过滤。
MySQL查询优化看一篇就够了_第18张图片

ICP的使用条件

  1. 如果表访问的类型为range、ref、eq_ref和ref_or_null可以使用ICP
  2. ICP可以用于InnoDB和MyISAM表,包括分区表InnoDB和MyISAM表
  3. 对于InnoDB表,ICP仅用于二级索引。ICP的目标是减少全行读取次数,从而减少I/o操作。
  4. 当SQL使用覆盖索引时,不支持ICP。因为这种情况下使用ICP不会减少I/O。索引覆盖不能使用,一个原因是,索引覆盖,不需要回表。。ICP作用是减小回表,ICP需要回表
  5. 相关子查询的条件不能使用

你可能感兴趣的:(MySql,mysql,数据库,索引,查询优化)