MySQL视图与索引详解

一、MySQL 视图简介

MySQL 视图(View)是一种虚拟存在的表,同真实表一样,视图也由列和行构成,但视图并不实际存在于数据库中。行和列的数据来自于定义视图的查询中所使用的表,并且还是在使用视图时动态生成的。

数据库中只存放了视图的定义,并没有存放视图中的数据,这些数据都存放在定义视图查询所引用的真实表中。使用视图查询数据时,数据库会从真实表中取出对应的数据。因此,视图中的数据是依赖于真实表中的数据的。一旦真实表中的数据发生改变,显示在视图中的数据也会发生改变。

视图可以从原有的表上选取对用户有用的信息,那些对用户没用,或者用户没有权限了解的信息,都可以直接屏蔽掉,作用类似于筛选。这样做既使应用简单化,也保证了系统的安全。

例如,下面的数据库中有一张公司部门表 department。表中包括部门号(d_id)、部门名称(d_name)、功能(function)和办公地址(address)。department 表的结构如下:

mysql> DESC department;
+----------+-------------+------+-----+---------+-------+
| Field    | Type        | Null | Key | Default | Extra |
+----------+-------------+------+-----+---------+-------+
| d_id     | int(4)      | NO   | PRI | NULL    |       |
| d_name   | varchar(20) | NO   | UNI    | NULL    |       |
| function | varchar(50) | YES  |     | NULL    |       |
| address  | varchar(50) | YES  |     | NULL    |       |
+----------+-------------+------+-----+---------+-------+
4 rows in set (0.02 sec)

还有一张员工表 worker。表中包含了员工的工作号(num)、部门号(d_id)、姓名(name)、性别(sex)、出生日期(birthday)和家庭住址(homeaddress)。

worker 表的结构如下:

mysql> DESC worker;
+-------------+-------------+------+-----+---------+-------+
| Field       | Type        | Null | Key | Default | Extra |
+-------------+-------------+------+-----+---------+-------+
| num         | int(10)     | NO   | PRI | NULL    |       |
| d_id        | int(4)      | YES  |MUL     | NULL    |       |
| name        | varchar(20) | NO   |     | NULL    |       |
| sex         | varchar(4)  | NO   |     | NULL    |       |
| birthday    | datetime    | YES  |     | NULL    |       |
| homeaddress | varchar(50) | YES  |     | NULL    |       |
+-------------+-------------+------+-----+---------+-------+
6 rows in set (0.01 sec)

由于各部门领导的权力范围不同,因此,各部门的领导只能看到该部门的员工信息;而且,领导可能不关心员工的生日和家庭住址。为了达到这个目的,可以为各部门的领导建立一个视图,通过该视图,领导只能看到本部门员工的指定信息。

例如,为生产部门建立一个名为 product _view 的视图。通过视图 product_ view,生产部门的领导只能看到生产部门员工的工作号、姓名和性别等信息。这些 department 表的信息和 worker 表的信息依然存在于各自的表中,而视图 product_view 中不保存任何数据信息。当 department 表和 worker 表的信息发生改变时,视图 product_view 显示的信息也会发生相应的变化。

技巧:如果经常需要从多个表查询指定字段的数据,可以在这些表上建立一个视图,通过这个视图显示这些字段的数据。

MySQL 的视图不支持输入参数的功能,因此交互性上还有欠缺。但对于变化不是很大的操作,使用视图可以很大程度上简化用户的操作。

视图并不同于数据表,它们的区别在于以下几点:

  • 视图不是数据库中真实的表,而是一张虚拟表,其结构和数据是建立在对数据中真实表的查询基础上的。
  • 存储在数据库中的查询操作 SQL 语句定义了视图的内容,列数据和行数据来自于视图查询所引用的实际表,引用视图时动态生成这些数据。
  • 视图没有实际的物理记录,不是以数据集的形式存储在数据库中的,它所对应的数据实际上是存储在视图所引用的真实表中的。
  • 视图是数据的窗口,而表是内容。表是实际数据的存放单位,而视图只是以不同的显示方式展示数据,其数据来源还是实际表。
  • 视图是查看数据表的一种方法,可以查询数据表中某些字段构成的数据,只是一些 SQL 语句的集合。从安全的角度来看,视图的数据安全性更高,使用视图的用户不接触数据表,不知道表结构。
  • 视图的建立和删除只影响视图本身,不影响对应的基本表。

视图与表在本质上虽然不相同,但视图经过定义以后,结构形式和表一样,可以进行查询、修改、更新和删除等操作。同时,视图具有如下优点:

1)定制用户数据,聚焦特定的数据

在实际的应用过程中,不同的用户可能对不同的数据有不同的要求。

例如,当数据库同时存在时,如学生基本信息表、课程表和教师信息表等多种表同时存在时,可以根据需求让不同的用户使用各自的数据。学生查看修改自己基本信息的视图,安排课程人员查看修改课程表和教师信息的视图,教师查看学生信息和课程信息表的视图。

2)简化数据操作

在使用查询时,很多时候要使用聚合函数,同时还要显示其他字段的信息,可能还需要关联到其他表,语句可能会很长,如果这个动作频繁发生的话,可以创建视图来简化操作。

3)提高数据的安全性

视图是虚拟的,物理上是不存在的。可以只授予用户视图的权限,而不具体指定使用表的权限,来保护基础数据的安全。

4)共享所需数据

通过使用视图,每个用户不必都定义和存储自己所需的数据,可以共享数据库中的数据,同样的数据只需要存储一次。

5)更改数据格式

通过使用视图,可以重新格式化检索出的数据,并组织输出到其他应用程序中。

6)重用 SQL 语句

视图提供的是对查询操作的封装,本身不包含数据,所呈现的数据是根据视图定义从基础表中检索出来的,如果基础表的数据新增或删除,视图呈现的也是更新后的数据。视图定义后,编写完所需的查询,可以方便地重用该视图。
 
要注意区别视图和数据表的本质,即视图是基于真实表的一张虚拟的表,其数据来源均建立在真实表的基础上。
 
使用视图的时候,还应该注意以下几点:

  • 创建视图需要足够的访问权限。
  • 创建视图的数目没有限制。
  • 视图可以嵌套,即从其他视图中检索数据的查询来创建视图。
  • 视图不能索引,也不能有关联的触发器、默认值或规则。
  • 视图可以和表一起使用。
  • 视图不包含数据,所以每次使用视图时,都必须执行查询中所需的任何一个检索操作。如果用多个连接和过滤条件创建了复杂的视图或嵌套了视图,可能会发现系统运行性能下降得十分严重。因此,在部署大量视图应用时,应该进行系统测试。

提示:ORDER BY 子句可以用在视图中,但若该视图检索数据的 SELECT 语句中也含有 ORDER BY 子句,则该视图中的 ORDER BY 子句将被覆盖。

二、MySQL 视图操作

1、创建视图

可以使用 CREATE VIEW 语句来创建视图。

语法格式如下:

CREATE VIEW <视图名> AS <SELECT语句>

语法说明如下:

  • <视图名>:指定视图的名称。该名称在数据库中必须是唯一的,不能与其他表或视图同名。
  • <SELECT语句>:指定创建视图的 SELECT 语句,可用于查询多个基础表或源视图。

对于创建视图中的 SELECT 语句的指定存在以下限制:

  • 用户除了拥有 CREATE VIEW 权限外,还具有操作中涉及的基础表和其他视图的相关权限。
  • SELECT 语句不能引用系统或用户变量。
  • SELECT 语句不能包含 FROM 子句中的子查询。
  • SELECT 语句不能引用预处理语句参数。

视图定义中引用的表或视图必须存在。但是,创建完视图后,可以删除定义引用的表或视图。可使用 CHECK TABLE 语句检查视图定义是否存在这类问题。

视图定义中允许使用 ORDER BY 语句,但是若从特定视图进行选择,而该视图使用了自己的 ORDER BY 语句,则视图定义中的 ORDER BY 将被忽略。

视图定义中不能引用 TEMPORARY 表(临时表),不能创建 TEMPORARY 视图。

WITH CHECK OPTION 的意思是,修改视图时,检查插入的数据是否符合 WHERE 设置的条件。

1. 创建基于单表的视图

MySQL 可以在单个数据表上创建视图。

查看 test_db 数据库中的 tb_students_info 表的数据,如下所示:

mysql> SELECT * FROM tb_students_info;
+----+--------+---------+------+------+--------+------------+
| id | name   | dept_id | age  | sex  | height | login_date |
+----+--------+---------+------+------+--------+------------+
|  1 | Dany   |       1 |   25 | F    |    160 | 2015-09-10 |
|  2 | Green  |       3 |   23 | F    |    158 | 2016-10-22 |
|  3 | Henry  |       2 |   23 | M    |    185 | 2015-05-31 |
|  4 | Jane   |       1 |   22 | F    |    162 | 2016-12-20 |
|  5 | Jim    |       1 |   24 | M    |    175 | 2016-01-15 |
|  6 | John   |       2 |   21 | M    |    172 | 2015-11-11 |
|  7 | Lily   |       6 |   22 | F    |    165 | 2016-02-26 |
|  8 | Susan  |       4 |   23 | F    |    170 | 2015-10-01 |
|  9 | Thomas |       3 |   22 | M    |    178 | 2016-06-07 |
| 10 | Tom    |       4 |   23 | M    |    165 | 2016-08-05 |
+----+--------+---------+------+------+--------+------------+
10 rows in set (0.00 sec)

在 tb_students_info 表上创建一个名为 view_students_info 的视图:

mysql> CREATE VIEW view_students_info
    -> AS SELECT * FROM tb_students_info;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM view_students_info;
+----+--------+---------+------+------+--------+------------+
| id | name   | dept_id | age  | sex  | height | login_date |
+----+--------+---------+------+------+--------+------------+
|  1 | Dany   |       1 |   25 | F    |    160 | 2015-09-10 |
|  2 | Green  |       3 |   23 | F    |    158 | 2016-10-22 |
|  3 | Henry  |       2 |   23 | M    |    185 | 2015-05-31 |
|  4 | Jane   |       1 |   22 | F    |    162 | 2016-12-20 |
|  5 | Jim    |       1 |   24 | M    |    175 | 2016-01-15 |
|  6 | John   |       2 |   21 | M    |    172 | 2015-11-11 |
|  7 | Lily   |       6 |   22 | F    |    165 | 2016-02-26 |
|  8 | Susan  |       4 |   23 | F    |    170 | 2015-10-01 |
|  9 | Thomas |       3 |   22 | M    |    178 | 2016-06-07 |
| 10 | Tom    |       4 |   23 | M    |    165 | 2016-08-05 |
+----+--------+---------+------+------+--------+------------+
10 rows in set (0.04 sec)

默认情况下,创建的视图和基本表的字段是一样的,也可以通过指定视图字段的名称来创建视图。

在 tb_students_info 表上创建一个名为 v_students_info 的视图:

mysql> CREATE VIEW v_students_info
    -> (s_id,s_name,d_id,s_age,s_sex,s_height,s_date)
    -> AS SELECT id,name,dept_id,age,sex,height,login_date
    -> FROM tb_students_info;
Query OK, 0 rows affected (0.06 sec)
mysql> SELECT * FROM v_students_info;
+------+--------+------+-------+-------+----------+------------+
| s_id | s_name | d_id | s_age | s_sex | s_height | s_date     |
+------+--------+------+-------+-------+----------+------------+
|    1 | Dany   |    1 |    24 | F     |      160 | 2015-09-10 |
|    2 | Green  |    3 |    23 | F     |      158 | 2016-10-22 |
|    3 | Henry  |    2 |    23 | M     |      185 | 2015-05-31 |
|    4 | Jane   |    1 |    22 | F     |      162 | 2016-12-20 |
|    5 | Jim    |    1 |    24 | M     |      175 | 2016-01-15 |
|    6 | John   |    2 |    21 | M     |      172 | 2015-11-11 |
|    7 | Lily   |    6 |    22 | F     |      165 | 2016-02-26 |
|    8 | Susan  |    4 |    23 | F     |      170 | 2015-10-01 |
|    9 | Thomas |    3 |    22 | M     |      178 | 2016-06-07 |
|   10 | Tom    |    4 |    23 | M     |      165 | 2016-08-05 |
+------+--------+------+-------+-------+----------+------------+
10 rows in set (0.01 sec)

可以看到,view_students_info 和 v_students_info 两个视图中的字段名称不同,但是数据却相同。因此,在使用视图时,可能用户不需要了解基本表的结构,更接触不到实际表中的数据,从而保证了数据库的安全。 

2. 创建基于多表的视图

MySQL 中也可以在两个以上的表中创建视图,使用 CREATE VIEW 语句创建。

在表 tb_student_info 和表 tb_departments 上创建视图 v_students_info:

mysql> CREATE VIEW v_students_info
    -> (s_id,s_name,d_id,s_age,s_sex,s_height,s_date)
    -> AS SELECT id,name,dept_id,age,sex,height,login_date
    -> FROM tb_students_info;
Query OK, 0 rows affected (0.06 sec)
mysql> SELECT * FROM v_students_info;
+------+--------+------+-------+-------+----------+------------+
| s_id | s_name | d_id | s_age | s_sex | s_height | s_date     |
+------+--------+------+-------+-------+----------+------------+
|    1 | Dany   |    1 |    24 | F     |      160 | 2015-09-10 |
|    2 | Green  |    3 |    23 | F     |      158 | 2016-10-22 |
|    3 | Henry  |    2 |    23 | M     |      185 | 2015-05-31 |
|    4 | Jane   |    1 |    22 | F     |      162 | 2016-12-20 |
|    5 | Jim    |    1 |    24 | M     |      175 | 2016-01-15 |
|    6 | John   |    2 |    21 | M     |      172 | 2015-11-11 |
|    7 | Lily   |    6 |    22 | F     |      165 | 2016-02-26 |
|    8 | Susan  |    4 |    23 | F     |      170 | 2015-10-01 |
|    9 | Thomas |    3 |    22 | M     |      178 | 2016-06-07 |
|   10 | Tom    |    4 |    23 | M     |      165 | 2016-08-05 |
+------+--------+------+-------+-------+----------+------------+
10 rows in set (0.01 sec)

通过这个视图可以很好地保护基本表中的数据。视图中包含 s_id、s_name 和 dept_name,s_id 字段对应 tb_students_info 表中的 id 字段,s_name 字段对应 tb_students_info 表中的 name 字段,dept_name 字段对应 tb_departments 表中的 dept_name 字段。

2、查看视图

视图一经定义之后,就可以如同查询数据表一样,使用 SELECT 语句查询视图中的数据,语法和查询基础表的数据一样。

视图用于查询主要应用在以下几个方面:

  • 使用视图重新格式化检索出的数据。
  • 使用视图简化复杂的表连接。
  • 使用视图过滤数据。

DESCRIBE 可以用来查看视图,语法如下:

DESCRIBE 视图名;

或简写成:

DESC 视图名;

通过 DESCRIBE 语句查看视图 v_students_info 的定义:

mysql> DESCRIBE v_students_info;
+----------+---------------+------+-----+------------+-------+
| Field    | Type          | Null | Key | Default    | Extra |
+----------+---------------+------+-----+------------+-------+
| s_id     | int(11)       | NO   |     | 0          |       |
| s_name   | varchar(45)   | YES  |     | NULL       |       |
| d_id     | int(11)       | YES  |     | NULL       |       |
| s_age    | int(11)       | YES  |     | NULL       |       |
| s_sex    | enum('M','F') | YES  |     | NULL       |       |
| s_height | int(11)       | YES  |     | NULL       |       |
| s_date   | date          | YES  |     | 2016-10-22 |       |
+----------+---------------+------+-----+------------+-------+
7 rows in set (0.04 sec)

注意:DESCRIBE 一般情况下可以简写成 DESC,输入这个命令的执行结果和输入 DESCRIBE 是一样的。 

下面创建学生信息表 studentinfo 的一个视图,用于查询学生姓名和考试分数。

创建学生信息表 studentinfo 的 SQL 语句和运行结果如下:

mysql> CREATE TABLE studentinfo(
    -> ID INT(11) PRIMARY KEY,
    -> NAME VARCHAR(20),
    -> SCORE DECIMAL(4,2),
    -> SUBJECT VARCHAR(20),
    -> TEACHER VARCHAR(20));
Query OK, 0 rows affected (0.10 sec)

创建查询学生姓名和分数的视图语句如下:

mysql> CREATE VIEW v_studentinfo AS SELECT name,score FROM studentinfo;
Query OK, 0 rows affected (0.04 sec)

通过 DESCRIBE 语句查看视图 v_studentsinfo 中的字段信息,SQL 语句和运行结果如下所示。

mysql> DESCRIBE v_studentinfo;
+-------+--------------+------+-----+---------+-------+
| Field | Type         | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| name  | varchar(20)  | YES  |     | NULL    |       |
| score | decimal(4,2) | YES  |     | NULL    |       |
+-------+--------------+------+-----+---------+-------+
2 rows in set (0.01 sec)

注意:使用 DESC 的执行结果和使用 DESCRIBE 是一样的。

由运行结果可以看出,查看视图的字段内容与查看表的字段内容显示的格式是相同的。因此,更能说明视图实际上也是一张数据表了,不同的是,视图中的数据都来自于数据库中已经存在的表。 

在 MySQL 中,SHOW CREATE VIEW 语句可以查看视图的详细定义。

其语法如下所示:

SHOW CREATE VIEW 视图名;

通过上面的语句,还可以查看创建视图的语句。创建视图的语句可以作为修改或者重新创建视图的参考,方便用户操作。

使用 SHOW CREATE VIEW 查看视图,SQL 语句和运行结果如下所示:

mysql>  SHOW CREATE VIEW v_studentinfo \G
*************************** 1. row ***************************
                View: v_studentinfo
         Create View: CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `v_studentinfo` AS select `studentinfo`.`NAME` AS `name`,`studentinfo`.`SCORE` AS `score` from `studentinfo`
character_set_client: gbk
collation_connection: gbk_chinese_ci
1 row in set (0.00 sec)

上述 SQL 语句以\G结尾,这样能使显示结果格式化。如果不使用\G,显示的结果会比较混乱,如下所示:

mysql> DESCRIBE v_studentinfo;
+-------+--------------+------+-----+---------+-------+
| Field | Type         | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| name  | varchar(20)  | YES  |     | NULL    |       |
| score | decimal(4,2) | YES  |     | NULL    |       |
+-------+--------------+------+-----+---------+-------+
2 rows in set (0.01 sec)

mysql>  SHOW CREATE VIEW v_studentinfo;
+---------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------+----------------------+
| View          | Create View                                                                                                                                                                                  | character_set_client | collation_connection |
+---------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------+----------------------+
| v_studentinfo | CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `v_studentinfo` AS select `studentinfo`.`NAME` AS `name`,`studentinfo`.`SCORE` AS `score` from `studentinfo` | gbk                  | gbk_chinese_ci       |
+---------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------+----------------------+
1 row in set (0.01 sec)

所有视图的定义都是存储在 information_schema 数据库下的 views 表中,也可以在这个表中查看所有视图的详细信息,SQL 语句如下: 

SELECT * FROM information_schema.views;

不过,通常情况下都是使用 SHOW CREATE VIEW 语句。 

3、修改视图

修改视图是指修改 MySQL 数据库中存在的视图,当基本表的某些字段发生变化时,可以通过修改视图来保持与基本表的一致性。

可以使用 ALTER VIEW 语句来对已有的视图进行修改。

语法格式如下:

ALTER VIEW <视图名> AS <SELECT语句>

语法说明如下:

  • <视图名>:指定视图的名称。该名称在数据库中必须是唯一的,不能与其他表或视图同名。
  • <SELECT 语句>:指定创建视图的 SELECT 语句,可用于查询多个基础表或源视图。

需要注意的是,对于 ALTER VIEW 语句的使用,需要用户具有针对视图的 CREATE VIEW 和 DROP 权限,以及由 SELECT 语句选择的每一列上的某些权限。

修改视图的定义,除了可以通过 ALTER VIEW 外,也可以使用 DROP VIEW 语句先删除视图,再使用 CREATE VIEW 语句来实现。

视图是一个虚拟表,实际的数据来自于基本表,所以通过插入、修改和删除操作更新视图中的数据,实质上是在更新视图所引用的基本表的数据。

注意:对视图的修改就是对基本表的修改,因此在修改时,要满足基本表的数据定义。

某些视图是可更新的。也就是说,可以使用 UPDATE、DELETE 或 INSERT 等语句更新基本表的内容。对于可更新的视图,视图中的行和基本表的行之间必须具有一对一的关系。

还有一些特定的其他结构,这些结构会使得视图不可更新。更具体地讲,如果视图包含以下结构中的任何一种,它就是不可更新的:

  • 聚合函数 SUM()、MIN()、MAX()、COUNT() 等。
  • DISTINCT 关键字。
  • GROUP BY 子句。
  • HAVING 子句。
  • UNION 或 UNION ALL 运算符。
  • 位于选择列表中的子查询。
  • FROM 子句中的不可更新视图或包含多个表。
  • WHERE 子句中的子查询,引用 FROM 子句中的表。
  • ALGORITHM 选项为 TEMPTABLE(使用临时表总会使视图成为不可更新的)的时候。

使用 ALTER 语句修改视图 view_students_info:

mysql> ALTER VIEW view_students_info
    -> AS SELECT id,name,age
    -> FROM tb_students_info;
Query OK, 0 rows affected (0.07 sec)
mysql> DESC view_students_info;
+-------+-------------+------+-----+---------+-------+
| Field | Type        | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id    | int(11)     | NO   |     | 0       |       |
| name  | varchar(45) | YES  |     | NULL    |       |
| age   | int(11)     | YES  |     | NULL    |       |
+-------+-------------+------+-----+---------+-------+
3 rows in set (0.03 sec)

用户可以通过视图来插入、更新、删除表中的数据,因为视图是一个虚拟的表,没有数据。通过视图更新时转到基本表上进行更新,如果对视图增加或删除记录,实际上是对基本表增加或删除记录。

查看视图 view_students_info 的数据内容,如下所示。

mysql> SELECT * FROM view_students_info;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | Dany   |   24 |
|  2 | Green  |   23 |
|  3 | Henry  |   23 |
|  4 | Jane   |   22 |
|  5 | Jim    |   24 |
|  6 | John   |   21 |
|  7 | Lily   |   22 |
|  8 | Susan  |   23 |
|  9 | Thomas |   22 |
| 10 | Tom    |   23 |
+----+--------+------+
10 rows in set (0.00 sec)

使用 UPDATE 语句更新视图 view_students_info:

mysql> UPDATE view_students_info
    -> SET age=25 WHERE id=1;
Query OK, 0 rows affected (0.24 sec)
Rows matched: 1  Changed: 0  Warnings: 0
mysql> SELECT * FROM view_students_info;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | Dany   |   25 |
|  2 | Green  |   23 |
|  3 | Henry  |   23 |
|  4 | Jane   |   22 |
|  5 | Jim    |   24 |
|  6 | John   |   21 |
|  7 | Lily   |   22 |
|  8 | Susan  |   23 |
|  9 | Thomas |   22 |
| 10 | Tom    |   23 |
+----+--------+------+
10 rows in set (0.00 sec)

查看基本表 tb_students_info 和视图 v_students_info 的内容,如下所示。 

mysql> SELECT * FROM tb_students_info;
+----+--------+---------+------+------+--------+------------+
| id | name   | dept_id | age  | sex  | height | login_date |
+----+--------+---------+------+------+--------+------------+
|  1 | Dany   |       1 |   25 | F    |    160 | 2015-09-10 |
|  2 | Green  |       3 |   23 | F    |    158 | 2016-10-22 |
|  3 | Henry  |       2 |   23 | M    |    185 | 2015-05-31 |
|  4 | Jane   |       1 |   22 | F    |    162 | 2016-12-20 |
|  5 | Jim    |       1 |   24 | M    |    175 | 2016-01-15 |
|  6 | John   |       2 |   21 | M    |    172 | 2015-11-11 |
|  7 | Lily   |       6 |   22 | F    |    165 | 2016-02-26 |
|  8 | Susan  |       4 |   23 | F    |    170 | 2015-10-01 |
|  9 | Thomas |       3 |   22 | M    |    178 | 2016-06-07 |
| 10 | Tom    |       4 |   23 | M    |    165 | 2016-08-05 |
+----+--------+---------+------+------+--------+------------+
10 rows in set (0.00 sec)

mysql> SELECT * FROM v_students_info;
+------+--------+------+-------+-------+----------+------------+
| s_id | s_name | d_id | s_age | s_sex | s_height | s_date     |
+------+--------+------+-------+-------+----------+------------+
|    1 | Dany   |    1 |    25 | F     |      160 | 2015-09-10 |
|    2 | Green  |    3 |    23 | F     |      158 | 2016-10-22 |
|    3 | Henry  |    2 |    23 | M     |      185 | 2015-05-31 |
|    4 | Jane   |    1 |    22 | F     |      162 | 2016-12-20 |
|    5 | Jim    |    1 |    24 | M     |      175 | 2016-01-15 |
|    6 | John   |    2 |    21 | M     |      172 | 2015-11-11 |
|    7 | Lily   |    6 |    22 | F     |      165 | 2016-02-26 |
|    8 | Susan  |    4 |    23 | F     |      170 | 2015-10-01 |
|    9 | Thomas |    3 |    22 | M     |      178 | 2016-06-07 |
|   10 | Tom    |    4 |    23 | M     |      165 | 2016-08-05 |
+------+--------+------+-------+-------+----------+------------+
10 rows in set (0.00 sec)

修改视图的名称可以先将视图删除,然后按照相同的定义语句进行视图的创建,并命名为新的视图名称。 

4、删除视图

删除视图是指删除 MySQL 数据库中已存在的视图。删除视图时,只能删除视图的定义,不会删除数据。

可以使用 DROP VIEW 语句来删除视图。

语法格式如下:

DROP VIEW <视图名1> [ , <视图名2> …]

其中:<视图名>指定要删除的视图名。DROP VIEW 语句可以一次删除多个视图,但是必须在每个视图上拥有 DROP 权限。

删除 v_students_info 视图,输入的 SQL 语句和执行过程如下所示。

mysql> DROP VIEW IF EXISTS v_students_info;
Query OK, 0 rows affected (0.00 sec)
mysql> SHOW CREATE VIEW v_students_info;
ERROR 1146 (42S02): Table 'test_db.v_students_info' doesn't exist

可以看到,v_students_info 视图已不存在,将其成功删除。

三、MySQL 索引简介

索引是一种特殊的数据库结构,由数据表中的一列或多列组合而成,可以用来快速查询数据表中有某一特定值的记录。本节将详细讲解索引的含义、作用和优缺点。

通过索引,查询数据时不用读完记录的所有信息,而只是查询索引列。否则,数据库系统将读取每条记录的所有信息进行匹配。

可以把索引比作新华字典的音序表。例如,要查“库”字,如果不使用音序,就需要从字典的 400 页中逐页来找。但是,如果提取拼音出来,构成音序表,就只需要从 10 多页的音序表中直接查找。这样就可以大大节省时间。

因此,使用索引可以很大程度上提高数据库的查询速度,还有效的提高了数据库系统的性能。

1、为什么要使用索引

索引就是根据表中的一列或若干列按照一定顺序建立的列值与记录行之间的对应关系表,实质上是一张描述索引列的列值与原表中记录行之间一 一对应关系的有序表。

索引是 MySQL 中十分重要的数据库对象,是数据库性能调优技术的基础,常用于实现数据的快速检索。

在 MySQL 中,通常有以下两种方式访问数据库表的行数据:

1. 顺序访问

顺序访问是在表中实行全表扫描,从头到尾逐行遍历,直到在无序的行数据中找到符合条件的目标数据。

顺序访问实现比较简单,但是当表中有大量数据的时候,效率非常低下。例如,在几千万条数据中查找少量的数据时,使用顺序访问方式将会遍历所有的数据,花费大量的时间,显然会影响数据库的处理性能。

2. 索引访问

索引访问是通过遍历索引来直接访问表中记录行的方式。

使用这种方式的前提是对表建立一个索引,在列上创建了索引之后,查找数据时可以直接根据该列上的索引找到对应记录行的位置,从而快捷地查找到数据。索引存储了指定列数据值的指针,根据指定的排序顺序对这些指针排序。

例如,在学生基本信息表 tb_students 中,如果基于 student_id 建立了索引,系统就建立了一张索引列到实际记录的映射表。当用户需要查找 student_id 为 12022 的数据的时候,系统先在 student_id 索引上找到该记录,然后通过映射表直接找到数据行,并且返回该行数据。因为扫描索引的速度一般远远大于扫描实际数据行的速度,所以采用索引的方式可以大大提高数据库的工作效率。

简而言之,不使用索引,MySQL 就必须从第一条记录开始读完整个表,直到找出相关的行。表越大,查询数据所花费的时间就越多。如果表中查询的列有一个索引,MySQL 就能快速到达一个位置去搜索数据文件,而不必查看所有数据,这样将会节省很大一部分时间。

2、索引的优缺点

索引有其明显的优势,也有其不可避免的缺点。

索引的优点如下:

  • 通过创建唯一索引可以保证数据库表中每一行数据的唯一性。
  • 可以给所有的 MySQL 列类型设置索引。
  • 可以大大加快数据的查询速度,这是使用索引最主要的原因。
  • 在实现数据的参考完整性方面可以加速表与表之间的连接。
  • 在使用分组和排序子句进行数据查询时也可以显著减少查询中分组和排序的时间

增加索引也有许多不利的方面,主要如下:

  • 创建和维护索引组要耗费时间,并且随着数据量的增加所耗费的时间也会增加。
  • 索引需要占磁盘空间,除了数据表占数据空间以外,每一个索引还要占一定的物理空间。如果有大量的索引,索引文件可能比数据文件更快达到最大文件尺寸。
  • 当对表中的数据进行增加、删除和修改的时候,索引也要动态维护,这样就降低了数据的维护速度。

使用索引时,需要综合考虑索引的优点和缺点。

索引可以提高查询速度,但是会影响插入记录的速度。因为,向有索引的表中插入记录时,数据库系统会按照索引进行排序,这样就降低了插入记录的速度,插入大量记录时的速度影响会更加明显。这种情况下,最好的办法是先删除表中的索引,然后插入数据,插入完成后,再创建索引。

3、索引的分类

单列索引:一个索引只包含单个列,一个表可以建多个单列索引,但建议不要超过5个。而且用联合索引优于用单列索引。

唯一索引:索引列的值必须唯一,但允许空值。

联合索引:一个索引包含多个列的值。

4、哪些字段适合建索引

  • 一个表必须有主键索引,这里会建索引;
  • 频繁作为查询条件的字段要;
  • 与其他表进行关联的字段或者外键;
  • 查询中要经常排序的字段;
  • 查询中统计和分组字段(因为分组会先进行排序);

5、不适合建索引的情况

  1. 表记录很少(数据量不大全表扫描也很快)
  2. 频繁更新的字段不适合建索引,where条件用不到的字段不建索引
  3. 有很多重复的值的字段或者说离散度很低的字段不适合建索引(你想想看,一个B+树的节点的key中全是相同的值,排不排序都一样,而且还要读取很多很多的非叶子节点到内存,同样进行了很多io操作)

例如下图:

查找where tid = 1 ,由于这棵树的索引全是1,所以所有的树节点都会从磁盘读到内存,io的次数=树中的节点的个数。这个时候比全表扫描好一点点,但是也好不到哪里去。 

四、索引数据结构

索引的本质是一种数据结构,而且是排好序的。

索引作用有2个,一个是排序,一个是快速查找,而快速查找的基础就是排好了序的索引。

那么索引可以有哪些数据结构:

二叉树、红黑树、hash表和B-Tree。

1、二叉树索引

下面我们以二叉树这种数据结构的索引为例,说明索引是如何工作的。

假如现在有一张表,表里面有两个字段 Col1 和 Col2:

现在我查询一个sql:

select * from t where col2=89;

在不使用索引情况下,mysql会从头遍历表t,一条条的往下查询,并比对 col2 字段是否为89。

如果使用了二叉树索引,在对col2字段建立索引的时候,mysql会将col2字段的所有内容以二叉树的数据结构写入到一个索引文件中。

我们知道二叉树其实是由链表变形而来的,是由多个节点和链接指向构成的。在二叉树索引中,每一个节点都存储着key和value,key是从来col2的值(89),value是col2这个字段所在行的磁盘地址(0x07,0x56之类的)。

二叉树的特点就是一个节点的右子节点的key比左子节点的key大,通过二叉树进行查找的时间复杂度是 O(log2n),而不用二叉树通过遍历的方式查找的复杂度是 O(n)。所以在二叉树中可以快速找到 key 为 89 的节点,获取到这个节点的value(存储col2 = 89的行所在的磁盘空间地址),然后再从这个地址中获取col2=89这一行的所有字段的数据。

树上的每一个索引节点都被分配一个磁盘空间地址,也就是说一棵树的所有节点都是存在磁盘上的不同位置。每一个父节点都有两条单向链接指向左右子节点,单向链接存储着子节点的磁盘地址。父节点A可以通过单向链接上记录的磁盘地址找到子节点B的位置。

查找一个索引的时候需要将树的节点中的数据从磁盘加载到内存,这就会发生一次磁盘IO操作。如果这个节点的key不是我要找的索引值,就会根据单向链接中存的磁盘地址找到子节点的磁盘空间,读取到内存,这又是一次IO操作。

因此,从根节点每往下找一层子节点就是一次IO操作。

如果二叉树这种数据结构是按字段值从小到大或者从大到小的顺序来构建的话,就会完全退化为一个单向链表,复杂度就又变回了O(n),和全表扫描一行行遍历没什么区别。

例如,对上图中的col1这个字段建立二叉树索引就会发生这种情况。

结论:二叉树构建索引可能会退化为一个接近单向链表的结构,此时查找和排序的复杂度与全表扫描没有什么区别。因此二叉树不适合作为索引的数据结构

2、红黑树索引

红黑树的本质是一个二叉平衡树。红黑树每添加一个节点(例如是节点A)会检查该节点A左右两边的子分支是否平衡,如果该节点A的右边的层级大于该节点左边的层级超过2层,就会做一个节点的平衡(左旋),使得该节点A节点的左右两边的层数相等。

而红黑树的自动平衡功能可以解决二叉树退化为单向链表的问题。

以上图中col1字段创建红黑树索引为例:

 

红黑树的复杂度也是 O(log2n),它的特点是添加或者删除节点的时候会自动平衡。

但是,用红黑树作为mysql数据表的索引还是存在问题的。

你想想看一个几百万的表用红黑树构建索引,这棵树就会有很多很多层,因为红黑树毕竟是一个二叉树,每个节点只能有两个分叉,所以数据一多,树的层级就多。当查找一个key比较大的数据时,就要从根节点一直找到底层叶节点,效率还是不高。当数据表中的记录数越多,红黑树的层级越高,查询效率就越低。

举个例子,用红黑树构建一个有100多万数据的表的索引,那么这个红黑树大概有20层。假如我查找的数据刚好在叶子节点,意味着我要在红黑树上查20个节点才能找到底层。

每往下层去遍历一个节点就是一次IO操作,就会发生20次磁盘io。

所以红黑树的瓶颈在于层数可能太多。我们希望能够在建立几百万的索引的基础上把树的层级控制在3~4层之内。

结论:红黑树构建索引的瓶颈在于存储大表索引时,树层级太多,导致查找发生的io次数多。

3、B-Tree索引(多叉平衡树)

B-Tree在红黑树的基础上采用了横向扩展的优化。普通二叉树和红黑树的一个节点只能存一个索引(一个节点的磁盘空间只存一个key-value数据),而B-Tree的每个节点可以存储多个索引(给一个节点分配一个大一些的磁盘空间存多个key-value数据)。所以B-Tree的每个节点可以有多个单向链接。

接下来我们看看一个保持在3层的B-Tree的构建过程:

B-Tree的特点:

  1. 所有的叶子节点的层级都是一样的且叶节点从左到右key是排好序的是递增的,例如上面 所有叶子节点都是在第三层。
  2. B树的每个节点虽然有多个链接指向,但是每个索引还是只有2个链接指向,每一个索引构成的子树都满足二叉树的特性(右子节点比左子节点的key大)。
  3. 单个节点内的多个索引是递增的有排序的。当往B树中插入一个索引的时候,索引被插入到一个节点会在这个节点中和其他索引进行排序排列。

结论:B树的横向扩展特性就很好的解决了红黑树层数过高的问题,但mysql还是没有选择B树作为索引的数据结构,原因是B树无法高效的做到范围查找。

4、B+Tree索引

B+ 树符合B Tree的所有特性。但是B+ Tree 的非叶子节点不存储value,只存储key(key就是索引字段的字段值,value是该索引字段对应的行的磁盘地址或者是索引所在行的其他列的实际数据,value存什么是因存储引擎的类型而异的)。这使得每个非叶子节点可以存放更多的索引(因为不存value节省了空间)

B+树的每一个叶子节点之间维护这一个双向链接,这个双向链接存储着相邻叶节点的磁盘空间地址,使得相邻的叶节点可以找到对方的磁盘地址。

叶子节点会包含所有的索引字段的key和value。部分子节点的key会冗余存储一份在父节点中(如下图的44)。

B+树的一个节点在磁盘中表现为一个数据页,在添加或者删除行的时候可能会发生的节点平衡(页分裂或旋转)。

一个节点中的每一个索引的两条链接指针存储着其子节点的磁盘地址。

B+树的结构如下图:

B+ 树的查找也是从根节点开始往下查。以查找上图中的53为例:

B+ Tree 的根节点是直接存在内存中的,所以第一个节点无需从磁盘读到内存。Mysql通过一定的算法(比如二分查找)得出53在50和66这两个索引之间,于是获取50和66之间的单向链接,这个链接存储着根节点的一个子节点的磁盘空间地址。于是根据这个地址从磁盘中读取[52 66]这个子节点的数据到内存。在内存中进行查找运算得到53在52和66之间,于是又获取到链接找到[52 53]这个叶节点,加载到内存,得到53号索引对应的value值。 

B树和B+树通过横向扩展的方式让树保持一个比较低的层级,那么有一个问题:既然树的层级越低,查找索引的IO操作次数越少的话,可不可以将所有索引的key-value都放在一个节点中,这样就只有1层了?

首先要知道,在树中查找一个索引的时候,需要将节点从磁盘加载到内存中。如果几千万个索引都放在一个节点(大概会有几百兆)。一来为了找1个索引而把所有索引的数据加载到内存会很浪费内存;二来几百兆的数据从磁盘写到内存的速度也要很长时间。

所以这样做的效率很低很笨。

对于mysql而言,树的一个节点(无论是叶子节点还是非叶子节点)会被分配一个16K的大小的页来存多个索引,一个节点就是一个数据页。

我们可以查看mysql一页有多大:

mysql> show global status like "Innodb_page_size";
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| Innodb_page_size | 16384 |
+------------------+-------+

我们从磁盘取行数据加载到内存的时候,不可能是从磁盘读一行数据到内存又再回到磁盘再读一行数据到内存,这样频繁的发生io效率会很低。因此实际情况是,每次从磁盘读取数据到内存的最小单位是一个页(B+树中一个节点),所以有时候虽然你只想查一行,但是还是会把一个页给读取到内存。 

那么一棵B+树能存多少个索引(多少条行)呢?

以innodb表的主键索引为例,一个整型id大概占8B,每个索引的链接指针占6B,16K/14B = 1170; 所以一个非叶子节点大概能放1170个索引。而叶子节点保存着value和双向指针,假设value是索引字段所在行的其他字段的实际数据(假设平均是1K,指针大概占12B,可以忽略不计),那么一个叶子节点最多只有16个索引。

那么一棵3层的B+ Tree,大概能存储 1170*1170*16 = 两千多万 条数据。

所以两千多万条数据如果通过B+ Tree建立索引,要查找一行数据也就进行2次磁盘IO即可(因为B+树的根节点一般是直接加载在内存中的),花的时间也就两次IO的时间。

如果数据超过2千多万,那就增加 B+ 树的层数为4层即可。

5、Hash表索引

如果以Hash结构作为索引,mysql会建立一个hash表,这个hash表是这样的:先对索引列的值进行一个hash散列函数的计算得到一个散列值,以这个散列值作为key,以索引所在的行数据的磁盘地址作为value。将key和value存在一个高速的映射表中。这样下次根据索引查找行的时候就可以快速找到行所在的地址。

对于where条件为精确查找(where in/=)来说,hash表的结构比B+树的性能高。但是对于范围查找(>/</between)来说,还是要对mysql表进行逐行遍历,一个个的通过hash函数计算得到散列值,再通过散列值查hash表的value。

B+树是怎么进行范围查找的呢?这全靠B+树叶子节点之间的双向指针和叶子节点是有序的这两个特性。例如做出where id > 30 这样的范围查询,mysql会先通过B+树的查找算法找到30所在的叶子节点A,然后通过叶子节点A的双向链接找到了它右边相邻节点B的地址,然后通过B的链接找到了B的右边相邻节点C的地址,一直这样找下去,就获取到了 id > 30的数据。

考虑到同是兼顾精确查找和范围查找mysql还是使用了B+树而不用hash表。

B+ 树和B树的区别在于两点:

  1. B树的叶子节点之间没有双向指针,不支持范围查找,如果B树要进行范围查找的话需要找到范围的左边界的索引所在的节点并从它开始进行中序遍历;
  2. B树的非叶子节点包含了value,但是B+树的非叶子节点只有key没有value,由于B树的非叶子节点包含value就意味着相同层数的B树和B+树,B树能存储的索引个数远小于B+。同样是3层数,B+数可以存 1170 * 1170 * 16 = 两千多万个索引,而B树只能存 16*16*16 = 4096个索引。如果用B树结构存一张两千多万的大表的索引,就需要大概7层。

不论是二叉树,红黑树,B树还是B+树,他们都是对数据排好序的结构,排好序的索引是高效查找的前提。“排好序”体现在一个节点的key比左节点的key大,比右节点的key小(叶子节点从左到右的key是从小到大的)。

这也应了本文的第一句话:“索引是帮助mysql高效获取数据的排好序的数据结构”。

五、索引类型

我们知道mysql的数据库和表是存放在mysql的data目录中。一个数据库对应一个目录,一个数据表对应一个或多个文件。

1、Myisam引擎的主键索引

Myisam的表对应三个文件:frm、MYD和MYI,分别存储着表结构,表数据和表索引。

下面是以主键为索引构建的myisam表的B+树:

上图中,左上角是一个B+ Tree , 存在MYI文件中。右下角是整个表数据,存在MYD文件中。

当我查询:

select * from t where col1 = 30;

它会先判断col1是否为索引字段,是则先到MYI文件中,按照B+ 树的查找算法找到 30这个索引的vlaue,在myisam中value存的是索引所在行的磁盘地址。

于是mysql会拿着这个地址在MYD文件中找到磁盘地址对应的位置的行,读取这个位置的行数据(这个过程叫做回表)。

对于myisam使用索引查找行数据会跨两个文件。所以myisam的索引是非聚集索引(非聚集索引的概念是数据存储顺序和索引存储顺序不同的索引结构)。

2、Innodb引擎的主键索引

Innodb的表对应frm和idb两个文件,frm是表结构,idb是数据+索引。

下面是以主键为索引构建的innodb表的B+树:

 

innodb表的主键索引和其他字段的数据一起存放在以B+树的数据结构存储起来的。

Idb文件中的B+树的非叶子节点也是存放多个key但不存放data/value。叶子节点的key是多个(主键)索引字段值,而value是(主键)索引字段所在行的其他列的数据内容。

因为innodb的(主键)索引和数据是放到一个文件中,放在一个叶节点中,而且数据保存顺序和索引保存顺序一致,所以这种索引叫做聚集索引。

聚集索引的效率其实比非聚集索引的效率高,因为它只用查找1个文件,少了回表过程(少了一次磁盘IO操作),尤其在范围查询的时候这个差距会更明显。

3、二级索引

上面介绍了myisam和innodb的主键索引的B+树,下面介绍myisam和innodb的非主键索引(二级索引)的B+树。

首先要明白的一件事是:无论是myisam还是innodb,当我们为一个表多创建一个索引的时候,底层就会多构建出一颗B+树。

对于一个表A,我们一般会在建表的时候,给A创建一个主键索引(id)。此时底层就会为这个主键索引构建一棵B+树并存在idb或myi文件中。

但是如果我想给A的另外一个字段构建一个普通索引,或者我想给A的另外几个字段构建一个联合索引,此时底层会多构建出一棵B+树。像这种非主键的索引(普通索引或联合索引或者非主键的唯一索引),我们把它叫做二级索引。

每次新建一个二级索引,都会在构建一棵新的B+树。所以如果索引建的太多,还是挺耗磁盘存储空间的。

那么接下来我要分开介绍innodb和myisam这两种引擎的二级索引的B+树,因为他们是有所不同的。 

Myisam的二级索引的B+树和myisam的主键索引的B+树没有任何区别,二级索引的B+树的叶子节点中的value也是存放着数据行所在的磁盘地址。无论使用普通索引还是主键来查找数据行都需要找到行的磁盘地址进行回表。  

Myisam的二级索引: 

Innodb的二级索引: 

Innodb的二级索引和innodb的主键索引不同,innodb的主键索引的B+树叶子节点的value存放着行数据,但是他的二级索引叶子节点存的不是行数据的地址也不是行数据本身,而是这个普通索引对应的主键值(id)。

如果使用innodb的普通索引或者联合索引查找一条行记录,会先在二级索引的B+树找到这个索引对应的主键值,再根据主键值在主键索引B+树的根节点往下找到叶节点的行数据。

比较myisam和innodb的二级索引:

  1. 如果使用innodb的二级索引(普通索引、非主键唯一索引或联合索引)查询行,需要走两棵树,假如每棵树都是3层,就会发生3+3=6次io操作。如果myisam的话,只需走一棵树,只会发生3次io拿到行地址,但是myisam还要根据行地址到数据表中找到行数据,innodb直接在B+树中就拿到了行数据。
  2. Innodb的二级索引的叶子节点存的是主键值,myisam的二级索引存的是行地址,后者更节省存储空间。

4、稀疏索引和稠密索引   

稀疏索引就是索引列和其他列 的数据不放在一棵B+树中的索引,稠密索引就是索引列和其他列的数据放在同一棵B+树的索引。

Innodb的主键索引(聚集索引)是稠密索引,innodb的二级索引是稀疏索引。

Myisam的主键索引(非聚集索引) 是稀疏索引,myisam的二级索引也是稀疏索引。 

5、InnoDB与MyISAM存储引擎之间的比较

下面我们以下几个方面来比较一下这两个存储引擎的不同。 

1. 事务的支持

InnoDB支持ACID的事务处理,MyISAM并不支持事务

2. 索引与主键处理

InnoDB存储引擎使用的是聚集索引,InnoDB主键的叶子节点是该行的数据,而其他索引则指向主键,而MyISAM存储引擎使用的是非聚集索引,主键与其他索引的叶子节点都存储了指向数据的指针。

另外一个是MyISAM数据表允许没有主键和其他索引,而InnoDB数据表如果没有主键的话,而会生成一个用户不可见6字节的主键。

3. 外键

MyISAM不支持外键,而Innodb则支持建立数据表之间的外键关联。

4. 存储文件的不同

Innodb存储文件有frm、ibd,而MyISAM是frm、MYD、MYI,Innodb存储文件中frm是数据表结构定义文件,ibd是数据文件,MyISAM中frm是数据表结构定义文件,MYD是数据的文件,MYI则是存储索引的文件。

5. select count(*)

使用MyISAM存储引擎的数据表会记录一个数据表的总行数,所以对使用MyISAM存储引擎的数据表进行select count(*),可以很快得到一个数据表的总行数,而对于InnoDB存储引擎的数据表,想要查询总行数需要进行全表扫描才能得到。

6. 锁的级别

InnoDB支持行级锁,而MyISAM只支持表级锁,因此InnoDB更能支持高并发。

6、为什么Innodb表必须有主键

只要你是innodb的表,B+树必须用一个列的值作为节点中每个索引的key。如果不创建主键,innodb也会从数据表中选一列没有重复值的列的值作为节点的key,目的就是为了把B+树这个结构给组织起来。如果表中所有的列都有重复值,mysql会维护一个没有重复值的隐藏列(row_id) 作为这个B+树节点的索引的key,但是这会增加mysql的负担,所以还是自己建一个主键。

为什么推荐用自增整型主键,我们之前说过在用B+树进行查找一个索引A的时候,会逐次把树的节点加载到内存,将这个节点内所有的key与索引A进行比对找到索引A所在的范围。整型肯定比字符串的比对效率高。而且数字占用的存储空间肯定比字符串小,使得一棵层数相同的B+树能存储更多的索引。

为啥要自增的?因为如果是自增的索引,在insert数据的时候,索引会直接添加在B+树最右边的叶子节点中,避免了复杂的页分裂。如果不是自增的索引,而是一个唯一字符串(uuid),插入这样的索引到B+树的叶子节点时可能让B+树做多次平衡(页分裂),而平衡的过程需要进行一系列计算。所以使用自增数字作为主键相比于使用非自增的字符串uuid作为主键相比,索引加入到B+树的这个操作的性能会更高(数据的插入的速度更快)。

所以无论从时间还是空间来说,自增整型作主键都是更好的。

7、范围查找(> < between and)和in查找B+树实现

情况1:> <

比如我想查找id>30的数据(假设这里id是主键索引),那么mysql会先从根节点往下层节点找到30所在的叶子节点,然后通过叶子节点的双向链接找右边相邻的节点,这些右边相邻的叶子节点逐一加载到内存中。

如图所示,它的一个查找轨迹是蓝色线条所示,一共经过了5个指针,发生了5次磁盘IO。

如果 范围查找 的范围条件过大,那么在B+树中的IO操作次数会过多,此时mysql会认为还不如直接全表扫描快,就会放弃使用索引改为用全表扫描。

情况2:in查找

比如我想查 where id in (18, 30, 31,37,51),它的过程如下如所示:

每查找一个id都会从根节点往下去找到这个id对应的叶子节点。所以上面的 where in查询共发生了8次io。

In查找有时候会被mysql认为是范围查找,有时候被认为是多个精确查找。

情况3:

现在假设views字段是一个普通索引而不是一个主键,如果我想查找 views > 100(假设满足>100的views有103,234,177,302这4个数字,这4个数字放在了2个叶节点上,二级索引共3层节点)。那么会先在二级索引通过范围查找找到103,234,177,302这4个数字对应的主键id(假设对应的id分别是57,11,90,33),这个过程发生了3次io操作。然后根据这4个主键id到主键索引的B+树中从根节点往下找这4个id对应的行记录(有4个id就要分4次从根节点往下找)。我们假设57,11,90,33这4个id分别放在了4个叶子节点中,主键索引的树有3层,那么在主键索引共进行 (3-1) * 4 = 8次io,再加上在二级索引发生的3次io,一共是11次io。

所以范围查找和in查找相比于精确查找(=)来说是更费io的。

8、联合索引查找B+树中实现

在实际工作中,如果我们对一个表的3,4个字段建索引的话,我们很少会对每一个字段单独建索引。而是对这3,4个字段建立一个联合索引。

联合索引的底层是怎么实现的(重要,现在网上说的索引优化规则都是基于此)。

以一个innodb表,4个字段,7条记录为例:

Col1	Col2	    Col3	    Col4
10001	Assistant	1998-09-03	2002-06-03
10001	Engineer	1996-08-03	2001-08-03
10001	Staff	    2001-09-03	2006-03-06
10002	Staff	    1996-08-03	
10003	Staff	    1997-08-03	2011-08-07
10003	Staff	    2001-09-03	2009-06-03
10004	Staff	    1996-08-03	

建立 index(col1, col2, col3) 这3个字段的联合索引。

构建B+的排序原则如下:先按照col1排序,如果col1相同,那么再按col2排序,col1和col2都相同则按col3排序。构建出来的一个B+树如下:

按照网上说的最左前缀原则,我们知道 where col1 = 10003  and col2=”staff” 使用到了联合索引,但是where col3=1996-08-03没有用到联合索引。

接下来从底层解释一下,为什么 where col3=1996-08-03没有用到联合索引。

其实原因很简单,当执行 where col3=1996-08-03 的时候,相当于不看 col1和col2,那么我们在B+树中不看col1和col2是这个样子的(划线法):

我们只到索引的高速查找是基于树节点的每一个索引key都是排好序的,可是此时 只看col3它在B+树中(从左到右)就不是一个排好序的样子而是一个乱序的样子,所以此时它会做一个全表扫描而无法用到索引进行查找。

再举一个例子:

一个文章表,我给它的id和view设置了单独索引,现在有两个索引:

Select * from article where id>500 and view < 100;

请问这个sql会不会同时用到id和view这两个索引进行查找?

是不会的,原因很简单,从底层的角度看,建立了两个单独索引意味着创建了两棵B+树,但是mysql不会对一条sql语句去查两棵B+树,而是只会去其中一颗B+树查。所以上面的sql虽然where中写上了id和view的条件,但是真正用到的就只有一个索引。

再举一个例子:

这个例子就是我们网上搜mysql优化的时候提到的一条规则:where用联合索引作为条件时,使用范围查找之后的条件都用不到索引。

数据集如下:

col1	col2	col3
5	13	254
5	24	500
8	18	304
6	22	108
9	33	290
10	24	350
9	22	333
8	30	566
10	40	302
10	17	130
10	36	280

建立联合索引index col1_col2_col3(col1, col2, col3)

Select * from t where col1=10 and col2>20 order by col3

Select * from t where col1=10 and col2>20 and col3 =300

上面的2个sql中,Col1和 col2 都用到了索引,但是col3没有用到索引。这里也要从B+树解释,用一句话说就是 符合col1=10 的叶节点他们的col2字段是排好序的。但是符合 col1=10且col2>10的叶节点他们的col3字段是乱序的,所以order by col3没有用到索引,需要mysql在内存中对col3进行排序,而 col3=300 没有用到索引,因为索引快速查找是依赖于排好序这个特性的。

画个图:

上面是一个构建好的B+ Tree,红框内的索引是满足 col1 = 10 and col2>20的索引。你看看他们的col3字段,也就是黄色框框之内的内容(350,302,280),黄框内的col3不是排好序的,而是乱序的。所以col3是没有用到索引。 

9、Memory 和 Merge 引擎

Memory存储引擎将表的数据存放在内存,每个memory表对应一个frm文件,只存储表结构,而Memory的数据存在内存当中,这是为了快速查询和插入数据。Memory的访问非常快,默认使用Hash索引,但是由于是存在内存中,所以一旦mysql关闭,数据就会消失,也就是不支持持久化。

还有Memory引擎是将数据存在内存,所以表的数据不能太多,否则内存会不足,也就是说不能存大表。

Merge引擎是一组Myisam表的组合,这些Myisam表必须结构完全相同。Merge表本身没有存数据,对Merge类型的表的增删改查本质都是对内部的Myisam表进行的。

对Merge表drop操作是不会删除内部的Myisam,只会删除Merge表。

六、索引操作

1、创建索引

​创建索引是指在某个表的一列或多列上建立一个索引,可以提高对表的访问速度。创建索引对 MySQL 数据库的高效运行来说是很重要的。

​MySQL 提供了三种创建索引的方法:

1. 使用 CREATE INDEX 语句

可以使用专门用于创建索引的 CREATE INDEX 语句在一个已有的表上创建索引,但该语句不能创建主键。

语法格式:

CREATE <索引名> ON <表名> (<列名> [<长度>] [ ASC | DESC])

语法说明如下:

  • <索引名>:指定索引名。一个表可以创建多个索引,但每个索引在该表中的名称是唯一的。
  • <表名>:指定要创建索引的表名。
  • <列名>:指定要创建索引的列名。通常可以考虑将查询语句中在 JOIN 子句和 WHERE 子句里经常出现的列作为索引列。
  • <长度>:可选项。指定使用列前的 length 个字符来创建索引。使用列的一部分创建索引有利于减小索引文件的大小,节省索引列所占的空间。在某些情况下,只能对列的前缀进行索引。索引列的长度有一个最大上限 255 个字节(MyISAM 和 InnoDB 表的最大上限为 1000 个字节),如果索引列的长度超过了这个上限,就只能用列的前缀进行索引。另外,BLOB 或 TEXT 类型的列也必须使用前缀索引。
  • ASC|DESC:可选项。ASC指定索引按照升序来排列,DESC指定索引按照降序来排列,默认为ASC。

2. 使用 CREATE TABLE 语句

索引也可以在创建表(CREATE TABLE)的同时创建。在 CREATE TABLE 语句中添加以下语句。语法格式:

CONSTRAINT PRIMARY KEY [索引类型] (<列名>,…)

在 CREATE TABLE 语句中添加此语句,表示在创建新表的同时创建该表的主键。

语法格式:

KEY | INDEX [<索引名>] [<索引类型>] (<列名>,…)

在 CREATE TABLE 语句中添加此语句,表示在创建新表的同时创建该表的索引。

语法格式:

UNIQUE [ INDEX | KEY] [<索引名>] [<索引类型>] (<列名>,…)

在 CREATE TABLE 语句中添加此语句,表示在创建新表的同时创建该表的唯一性索引。

语法格式:

FOREIGN KEY <索引名> <列名>

在 CREATE TABLE 语句中添加此语句,表示在创建新表的同时创建该表的外键。

在使用 CREATE TABLE 语句定义列选项的时候,可以通过直接在某个列定义后面添加 PRIMARY KEY 的方式创建主键。而当主键是由多个列组成的多列索引时,则不能使用这种方法,只能用在语句的最后加上一个 PRIMARY KRY(<列名>,…) 子句的方式来实现。

3. 使用 ALTER TABLE 语句

CREATE INDEX 语句可以在一个已有的表上创建索引,ALTER TABLE 语句也可以在一个已有的表上创建索引。在使用 ALTER TABLE 语句修改表的同时,可以向已有的表添加索引。

具体的做法是在 ALTER TABLE 语句中添加以下语法成分的某一项或几项。

语法格式:

ADD INDEX [<索引名>] [<索引类型>] (<列名>,…)

在 ALTER TABLE 语句中添加此语法成分,表示在修改表的同时为该表添加索引。

语法格式:

ADD PRIMARY KEY [<索引类型>] (<列名>,…)

在 ALTER TABLE 语句中添加此语法成分,表示在修改表的同时为该表添加主键。

语法格式:

ADD UNIQUE [ INDEX | KEY] [<索引名>] [<索引类型>] (<列名>,…)

在 ALTER TABLE 语句中添加此语法成分,表示在修改表的同时为该表添加唯一性索引。

语法格式:

ADD FOREIGN KEY [<索引名>] (<列名>,…)

在 ALTER TABLE 语句中添加此语法成分,表示在修改表的同时为该表添加外键。

创建普通索引时,通常使用 INDEX 关键字。

创建一个表 tb_stu_info,在该表的 height 字段创建普通索引:

mysql> CREATE TABLE tb_stu_info
    -> (
    -> id INT NOT NULL,
    -> name CHAR(45) DEFAULT NULL,
    -> dept_id INT DEFAULT NULL,
    -> age INT DEFAULT NULL,
    -> height INT DEFAULT NULL,
    -> INDEX(height)
    -> );
Query OK,0 rows affected (0.40 sec)
mysql> SHOW CREATE TABLE tb_stu_info\G
*************************** 1. row ***************************
       Table: tb_stu_info
Create Table: CREATE TABLE `tb_stu_info` (
  `id` int(11) NOT NULL,
  `name` char(45) DEFAULT NULL,
  `dept_id` int(11) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  `height` int(11) DEFAULT NULL,
  KEY `height` (`height`)
) ENGINE=InnoDB DEFAULT CHARSET=gb2312
1 row in set (0.01 sec)

创建唯一索引,通常使用 UNIQUE 参数。

创建一个表 tb_stu_info2,在该表的 id 字段上使用 UNIQUE 关键字创建唯一索引:

mysql> CREATE TABLE tb_stu_info2
    -> (
    -> id INT NOT NULL,
    -> name CHAR(45) DEFAULT NULL,
    -> dept_id INT DEFAULT NULL,
    -> age INT DEFAULT NULL,
    -> height INT DEFAULT NULL,
    -> UNIQUE INDEX(height)
    -> );
Query OK,0 rows affected (0.40 sec)
mysql> SHOW CREATE TABLE tb_stu_info2\G
*************************** 1. row ***************************
       Table: tb_stu_info2
Create Table: CREATE TABLE `tb_stu_info2` (
  `id` int(11) NOT NULL,
  `name` char(45) DEFAULT NULL,
  `dept_id` int(11) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  `height` int(11) DEFAULT NULL,
  UNIQUE KEY `height` (`height`)
) ENGINE=InnoDB DEFAULT CHARSET=gb2312
1 row in set (0.00 sec)

2、查看索引

索引创建完成后,可以利用 SQL 语句查看已经存在的索引。在 MySQL 中,可以使用 SHOW INDEX 语句查看表中创建的索引。

查看索引的语法格式如下:

SHOW INDEX FROM <表名> [ FROM <数据库名>]

语法说明如下:

  • <表名>:指定需要查看索引的数据表名。
  • <数据库名>:指定需要查看索引的数据表所在的数据库,可省略。比如,SHOW INDEX FROM student FROM test; 语句表示查看 test 数据库中 student 数据表的索引。

使用 SHOW INDEX 语句查看 tb_stu_info2 数据表的索引信息:

mysql> SHOW INDEX FROM tb_stu_info2\G
*************************** 1. row ***************************
        Table: tb_stu_info2
   Non_unique: 0
     Key_name: height
 Seq_in_index: 1
  Column_name: height
    Collation: A
  Cardinality: 0
     Sub_part: NULL
       Packed: NULL
         Null: YES
   Index_type: BTREE
      Comment:
Index_comment:
1 row in set (0.03 sec)

其中各主要参数说明如下:

参数 说明
Table 表示创建索引的数据表名,这里是 tb_stu_info2 数据表。
Non_unique 表示该索引是否是唯一索引。若不是唯一索引,则该列的值为 1;若是唯一索引,则该列的值为 0。
Key_name 表示索引的名称。
Seq_in_index 表示该列在索引中的位置,如果索引是单列的,则该列的值为 1;如果索引是组合索引,则该列的值为每列在索引定义中的顺序。
Column_name 表示定义索引的列字段。
Collation 表示列以何种顺序存储在索引中。在 MySQL 中,升序显示值“A”(升序),若显示为 NULL,则表示无分类。
Cardinality 索引中唯一值数目的估计值。基数根据被存储为整数的统计数据计数,所以即使对于小型表,该值也没有必要是精确的。基数越大,当进行联合时,MySQL 使用该索引的机会就越大。
Sub_part 表示列中被编入索引的字符的数量。若列只是部分被编入索引,则该列的值为被编入索引的字符的数目;若整列被编入索引,则该列的值为 NULL。
Packed 指示关键字如何被压缩。若没有被压缩,值为 NULL。
Null 用于显示索引列中是否包含 NULL。若列含有 NULL,该列的值为 YES。若没有,则该列的值为 NO。
Index_type 显示索引使用的类型和方法(BTREE、FULLTEXT、HASH、RTREE)。
Comment 显示评注。

3、修改和删除索引

删除索引是指将表中已经存在的索引删除掉。不用的索引建议进行删除,因为它们会降低表的更新速度,影响数据库的性能。对于这样的索引,应该将其删除。

在 MySQL 中修改索引可以通过删除原索引,再根据需要创建一个同名的索引,从而实现修改索引的操作。

当不再需要索引时,可以使用 DROP INDEX 语句或 ALTER TABLE 语句来对索引进行删除。

1. 使用 DROP INDEX 语句

语法格式:

DROP INDEX <索引名> ON <表名>

语法说明如下:

  • <索引名>:要删除的索引名。
  • <表名>:指定该索引所在的表名。

2. 使用 ALTER TABLE 语句

根据 ALTER TABLE 语句的语法可知,该语句也可以用于删除索引。具体使用方法是将 ALTER TABLE 语句的语法中部分指定为以下子句中的某一项。

  • DROP PRIMARY KEY:表示删除表中的主键。一个表只有一个主键,主键也是一个索引。
  • DROP INDEX index_name:表示删除名称为 index_name 的索引。
  • DROP FOREIGN KEY fk_symbol:表示删除外键。

注意:如果删除的列是索引的组成部分,那么在删除该列时,也会将该列从索引中删除;如果组成索引的所有列都被删除,那么整个索引将被删除。

删除表 tb_stu_info 中的索引:

mysql> DROP INDEX height
    -> ON tb_stu_info;
Query OK, 0 rows affected (0.27 sec)
Records: 0  Duplicates: 0  Warnings: 0
mysql> SHOW CREATE TABLE tb_stu_info\G
*************************** 1. row ***************************
       Table: tb_stu_info
Create Table: CREATE TABLE `tb_stu_info` (
  `id` int(11) NOT NULL,
  `name` char(45) DEFAULT NULL,
  `dept_id` int(11) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  `height` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=gb2312
1 row in set (0.00 sec)

删除表 tb_stu_info2 中名称为 id 的索引:

mysql> ALTER TABLE tb_stu_info2
    -> DROP INDEX height;
Query OK, 0 rows affected (0.13 sec)
Records: 0  Duplicates: 0  Warnings: 0
mysql> SHOW CREATE TABLE tb_stu_info2\G
*************************** 1. row ***************************
       Table: tb_stu_info2
Create Table: CREATE TABLE `tb_stu_info2` (
  `id` int(11) NOT NULL,
  `name` char(45) DEFAULT NULL,
  `dept_id` int(11) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  `height` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=gb2312
1 row in set (0.00 sec)

七、覆盖索引

1、Using index 覆盖索引

概念:一个索引(B+树)中包含所有需要查询的字段的值,称为覆盖索引。覆盖索引的一个特点是无需回表。

覆盖索引不是一种索引类型,不是一个名词,而是一个动词。

下面举一个渐进的例子来描述覆盖索引在底层上是怎么做的。比如,我给 col1 字段设置了一个普通索引,给id设置了主键索引,使用的是innodb的表。

1. Select * from t;

全表扫描,在底层表现为在聚集索引这棵树中把所有叶子节点一个个的读取到内存中,获取每个叶节点内的行数据。

2. Select col1 from t;

Mysql检查到 col1 字段是索引,所以会直接去索引获取col1字段,因为col1索引B+树中就包含col1字段的值,无需拿到叶子节点中存储的主键id值再跳到聚集索引获取col1的数据。

此时直接找到二级索引的B+树将所有叶子节点的col1的值获取到。这个过程只查了col1二级索引的B+树,没有去查主键索引的B+树,因此这个例子就用到了覆盖索引。

3. 假设我现在把col1的索引删了,再去执行 select col1from t;请问会发生什么变化?

我们还是要在脑袋里构建B+树的图:有col1索引的时候,会直接去col1的B+树中去找,并且把所有叶子节点读取到内存中(假设有n个叶子节点,3层,就进行了3-1 + n-1=n+1次io操作),并在内存中找出每个叶子节点的col1值。

没有col1索引的时候,会去主键索引的B+树中去找,并且也把所有的叶子节点读取到内存,也是发生了 n+1 次io操作。

好像没什么区别,都是n+1次io操作,但真的没区别吗?

主键索引的叶子节点存了所有表字段的数据,每一个节点从磁盘读取到内存的时候都会把一行中所有表字段的数据读到内存。然后在内存中,mysql程序再从多个行的多个表字段中获取其中的col1字段。

col1索引的叶子节点只存了col1和id这两个字段的数据。每一个节点从磁盘读取到内存的时候只把col1和id的值加载到内存。然后只要col1,不要id。

也就是说虽然两者之间的io操作次数相同,但是前者每次io操作的速度比后者快,因为二级索引每个节点的数据比主键索引的每个节点的数据少,io读写的数据量不同会影响io的速度的。

为了读取col1字段而把其他全部字段都读取到内存,这就没有用到覆盖索引,还是一个全表扫描。

我有试过在一个100万的文章表中执行 select title from article 查询所有title。加title索引的时候耗时0.03秒,不加title索引的时候耗时56秒。

所以不要小看 覆盖索引 的作用,它在特定情境下可以带来极大的优化。

4. Select id from t;

分别在innodb引擎和myisam引擎下执行这个语句,请问有没有用到覆盖索引?

答案是有,因为id建立了主键索引,所以直接会去主键索引的树中读取所有叶节点的key但是不会去读取value(主键索引中叶节点的key就是id)。两种引擎下都没有回表。

5. Select id from t where col1>100;

假设我给col1加了普通索引,分别在innodb引擎和myisam引擎下执行这个语句,请问有没有用到覆盖索引?

答案是,innodb和myisam都用到了二级索引查col1(type是range),但是innodb引擎用到了覆盖索引,myisam没用到覆盖索引。

首先,加了条件 col1>100后,就会去col1这个二级索引的树中查(而不会直接在主键索引中查)满足 col1>100的叶子节点,innodb和myisam都需要做这一步。

但是innodb的二级索引的叶子节点存储着col1的值和对应的id值。因此只需访问二级索引这一棵B+树就能获取到全部id,无需回表到主键索引去拿id字段。

而myisam的二级索引的叶子节点中,叶子节点只有col1,没有id,所以需要通过叶子节点存储的行地址到MYD文件中找对应的行,再从这些行中提取id字段。

也就是说,innodb在这个过程中没有回表,而myisam发生了回表。

我有试过在一个100万的文章表中执行 select id from article where create_time > 1586421023 。
Create_time加了索引,在innodb的表中查只花了0.7秒,在myisam的表中查花了31.67秒

6. Select id from t where col2>100

现在我添加了联合索引 index col1_col2 (col1,col2)。

请问在innodb引擎下,是否用到了覆盖索引?

答案是用到了联合索引的覆盖索引,只是条件查找没有用到联合索引而已(意思是 Extra中有Using index,但是type中没有出现range而是All)。

底层发生了什么?首先 mysql 会思考说主人想搜索id,id在主键索引和二级索引这两棵树中都有,可是如果在主键索引中搜,为了判断col2>100这个条件,mysql会把所有叶子节点从磁盘读到内存,主键索引的叶子节点是包含很多字段的,这会很慢。如果在联合索引中搜索,由于col2>100不符合联合索引的最左前缀原则,所以mysql也会把全部叶子节点从磁盘读到内存,在内存中筛选 col2>100的节点,并获取id字段的值。

所以这就是为什么用到了覆盖索引,但是范围查询没用到联合索引的原因。

7. Select col1,col2,col3 from t where col1 > 100;

现在我添加了联合索引 index col1_col2 (col1,col2)。

请问在innodb引擎下,是否用到了覆盖索引?

答案是范围查询用到了二级索引(因为where col1>100遵循了最左前缀原则),但是查字段的时候由于col3不在二级索引的叶子节点中,所以需要回表到主键索引的叶子节点中找col3字段。所以没有用到覆盖索引。

不过因为在二级索引中用到了range,所以mysql不会加载所有主键索引的叶子节点,而是加载对应id的叶子节点。

8. Select col1,col2,col3 from t where col2 > 100;

这句完全没用到联合索引,单纯的一个全表扫表,直接在主键索引读取所有的叶节点。Col2的条件判断和col1,col2,col3的字段提取全在内存中计算完成。

总结:无论判断一条sql有没有用到索引,有没有用到覆盖索引,都可以通过画一个B+树的图来分析。知道底层原理,sql优化变得有理有据,不知道底层原理,sql优化就只能凭感觉。

2、Using FileSort 文件排序

在Sql优化中,我们希望尽可能不要出现文件排序,因为出现了文件排序意味着没有使用到索引构建好的排序,而是需要在内存中对字段进行重新排序,排序的过程是计算的过程比较消耗cpu。

多字段排序要尽量遵循最左前缀原则,而且不要对一个字段升序对另一个字段降序,否则也会使用到Using filesort

如果一定会发生 Using filesort,那么我们要了解的文件排序有两种方式:双路排序和单路排序

举个例子,一个表建立的联合索引 index age_salary (age, salary):

Select * from t where id>500 and id<1000 order by  salary, age;

上面的例子中:

双路排序:会在二级索引的B+树取出满足where条件的行(501~999行)的salary和age字段(不会取其他字段),和501~999行的地址指针,然后在sort buffer内存中排序。如果sort buffer不够(要排序的salary和age太多了),此时会创建一个 temporary table 存储结果(临时表的出现意味着更多次的io)。排完序之后再根据行指针(这是的行指针也是排好序的)回表(回到主键索引)取记录(排序的时候只取了要排序的字段,现在回表是要取整行的所有字段)。

单路排序:会取出满足where条件的行(501~999行)的所有字段(不过这样更容易生成临时表,这样的话io反而会比双路排序高),然后再sort buffer中根据salary 和 age字段排序,然后直接输出结果。

由于双路排序发生了回表,所以大大增加了io次数(是单路排序的两倍,如果单路排序不生成临时表的话), 但是单路排序的内存开销更大,更容易在排序过程中生成临时表,从而增加io次数。

Mysql会根据情况选择其中当然一种算法来进行文件排序filesort。但无论是哪种排序我们都可以通过提高 sort_buffer_size 和 max_length_for_sort_data来增大排序缓冲区的大小,减小创建临时表的可能。

结论:在非要出现文件排序不可的情况下,可以通过增大排序缓冲区的大小来优化。

3、Using Temporary  使用临时表

临时表可以有我们用户手动创建,也可能是在执行sql是mysql在内部创建,我们只讨论后者。

MySQL临时表分为“内存临时表”和“磁盘临时表”,其中内存临时表使用MySQL的MEMORY存储引擎,磁盘临时表使用MySQL的MyISAM存储引擎;一般情况下,MySQL会先创建内存临时表,但内存临时表超过配置指定的值后,MySQL会将内存临时表导出到磁盘临时表。

mysql会在什么时候创建内部临时表(一般都是内存不够用的时候)?

  1. 在排序或者分组过程中由于内存不足而导致mysql创建临时表进行额外存储。
  2. 在JOIN查询中,ORDER BY或者GROUP BY使用了不是第一个表的列
  3. 排序或分组时,表包含TEXT或者BLOB列(这样对于单路排序而言sort buffer肯定不足);
  4. GROUP BY 或者 DISTINCT 子句中包含长度大于512字节的列;
  5. 使用UNION或者UNION ALL时,SELECT子句中包含大于512字节的列;

最后3个是直接使用磁盘临时表。

为了避免使用到临时表,我们可以在排序和分组的时候尽量是去对索引的字段来排序分组,而且不能让索引失效。再者拆分长度很长的列,例如将Text或者Blob类型的字段垂直分表到另一张表中。

临时表的危害是大大增加io次数,严重时导致磁盘读写压力过大。

、使用索引技巧 

1、索引失效问题

80%的Sql优化都是通过合理使用索引就能完成的,合理使用索引意味着要建立索引并且不让索引失效。

如何避免索引失效:

  1. 尽量用全值匹配;
  2. 尽量满足最左前缀原则;
  3. 不在索引列上做任何的操作(计算、函数、自动或手动转换类型);
  4. 对索引按范围条件查找的操作尽可能放在最后,因为范围作为条件之后的条件不会用到索引
  5. 尽量使用覆盖索引,少用select *;
  6. 对索引字段使用 != 的时候索引会失效;
  7. Is null,is not null 会索引失效;
  8. Like “%...%”模糊匹配会索引失效(like “xxx%”会用到索引,type访问类型为range);
  9. 字符串不加单引号会索引失效;
  10. 用where ... or... 会索引失效;

具体例子分析:

记住一句话,分析一个sql有没有用到索引,一定要先画图,在脑袋里画一个B+树的图,然后用我在Mysql索引篇的第二篇文章中的划线法去验证。

例子1:

CREATE TABLE `t` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `col1` int(11) DEFAULT NULL,
  `col2` int(11) DEFAULT NULL,
  `col3` int(11) DEFAULT NULL,
  `extra_file` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `ccc` (`col1`,`col2`,`col3`)
) ENGINE=InnoDB
explain select * from t where col1=3 and col2=12 and col3=11; # 3个字段都用到索引
explain select * from t where col2=12 and col3=11; # 没用到索引
explain select * from t where col2=12 and col3=11 and col1=3; # 3个字段都用到索引。只是把col1放到后面,和第一句完全没区别,mysql还不至于笨到把条件的位置换一下就索引失效。(其实是mysql的查询优化器optimizer会对sql语句进行调整把它变成了where col1=3 and col2=12 and col3=11)
explain select * from t where col3=11 and col1=3; # 只有col1用到索引,col3=11只能等到把叶子节点的数据加载到内存后再用查找算法查找了
explain select * from t where col2>11 and col1=3; # col1和col2都用到索引
explain select * from t where col2=11 and col1>3; # col1用到了索引
explain select * from t where  col1=13 and col2>15 and col3=10;   # 只有col3没用到索引
explain select * from t where id<15 and col1>17; # id用到了主键索引,但是col1没用到。因为where以两个索引为条件,mysql不可能对一个查询同时使用两个独立的索引,所以会挑一个效率更高的索引。而id是主键索引,是一个聚集索引,索引和数据绑定在一块,而col1位于一个二级索引,如果用了二级索引还得再去主键索引查。所以当然优先使用id索引。分析这条语句的时候,脑海里应该浮现出一个主键索引的b+树和一个二级索引的b+树。
explain select * from t where  col1<13; # 没有用到索引,全表扫描,因为虽然符合最左前缀原则,但是满足col1<13条件的条数有25条,占了25/45=5/9 占了一半以上,而且还要根据二级索引对应的主键再到主键索引的B+树中找对应的数据。所以mysql认为还不如全表扫描的快,于是直接全表扫描。其实是因为我这个表只有45行,数据量比较少,如果是在一个几千几万行的表中就不会这样。
select id from t where col1<13; # col1用到了ccc索引(type是一个range),而且还用到了覆盖索引(using  index)。
Select * from t where col1 = 10 and col2 like “kk%” and col3 = 14  # col1,col2都用到了索引,col3没用到
Select * from t where col1 = 10 and col2 like “%kk%” and col3 = 14  # col1用到了索引,col2,col3没用到
Select * from t where col1 = 10 and col2 like “k%kk%” and col3 = 14  # col1,col2都用到了索引,col3没用到

例子2:

还是使用例子1中的数据和索引。下面不但要说出有没有使用索引,还要说出那些字段用了索引,用于排序还是查找。

Explain select * from t where col1 = 15 and col3=20 order by col2 # col1用到索引的查找,col2用到了索引的排序,没有出现using filesort,col3没有用到索引(当where和order同时出现的时候,先看where后看order。在col1是15的情况下,col3是乱序的;所以会将满足col1=15的行全读到内存,然后内存再筛选出col3=20的行,由于这些行的col2在col1=15的情况下在树中已经排好序,所以不会再再内存中排序,所以col2用到了索引的排序)
Explain select * from t where col1 = 15  order by col3 # col1 用到了索引的查找,col3没用到排序,会在内存中对col3排序,出现了using filesort
Explain select * from t where col1 = 15  order by col2,col3  # col1 用到了索引的查找,col2和col3用到了索引的排序,没出现 using filesort
Explain select * from t where col1 = 15  order by col3,col2   # col1 用到了索引的查找,col2和col3都没用到索引的排序,出现 using filesort,内存中会对行进行两次排序,一次对col3排序,再对col2排序。
Explain select * from t where col1 = 15  and col2= 20 order by col3,col2 # col1用到了索引的查找,col2也用到了查找,col3也用到了索引的排序(结果集中col2全都是20,对col2还排个鬼的序呀)
Explain select * from t where col1 = 15  group by col2,col3 # col1用到了索引的查找,col2和col3用到了索引的排序
Explain select * from t where col1 = 15  group by col3,col2 # col1用到了索引的查找, c3 和 c2都没用到索引的排序功能

难度升级:

Explain select * from t where col2 > 20 order by col1 # 查询条件违反了最左前缀原则,此时不会走二级索引,而是直接走主键索引进行一个全表扫描。主键索引中的 col1是乱序的,所以order by col1会在内存中进行排序,而无法用到索引B+树的排序,所以会出现 Using filesort。

一个sql是走二级索引还是走全表扫描,主要看where语句,不是看order by语句。上面的where语句中,col2是一个乱序的(col1相同的节点下的col2才是一个有序的)所以col2>20根本用不了二级索引,所以会去走全表扫描。

还是那句话,脑袋里展开一个B+树的草图才能分析。

Explain select col1,col2,col3 from t where col2>20 order by col1  # 虽然where条件每遵循最左前缀,但是select后跟的字段全在二级索引的树中,而且没有其他多余的字段,所以会用到覆盖索引,用到覆盖索引肯定是走二级索引不走聚集索引啦。然后,先看where,col2>20违反了最左前缀原则,所以会扫描所有二级索引的叶子节点到内存中去筛选满足col2>20的行;满足col2>20的行的col1是排好序的,所以不会再在内存中对col1排序。所以,用到了 Using index 覆盖索引,但是没有Using filesort。不过,col2>20这个条件的判断筛选会在内存中发生,所以,type不是range而是index。
Explain select col1,col2,col3 from t order by col1 asc , col2 desc   # col1用到了索引的排序,因为二级索引中col1本身就是排好序的,但是 col1相同的叶节点下的col2是一个升序排序的,如果你希望把他变为降序就只能在内存中重新排了。所以用到了Using filesort

如果 order by col1 desc , col2 desc 那就都用到了索引的排序。

小总结:

一个字段用到了索引的排序功能时,它的好处是避免了mysql在内存中对这个字段排序,减少计算量而提高性能(在内存中排序的过程是一个cpu密集型操作),体现在explain中就是没有Using filesort的出现。

上面的例子2中的语句,如果sql语句中出现order by A,但是A字段没用到索引的排序功能,就会出现 using filesort。如果sql语句中出现group by A,但是A字段没用到索引的排序功能,就会出现 using filesort 和 using temporary。

分组的前提是排序,所以分析group by 的时候只要把它当成order by来分析即可。

例子3:用覆盖索引优化 like “%...%”

现在有一个100w数据的innodb文章表,我要做根据关键词搜索文章的,例如,在搜索框搜索“金融交易”,就能把含有金融关键词的标题的文章按发布时间字段倒序排序查出所有字段,我只看前30篇。

一开始,只有主键id有索引。

版本1:

Select * from arts where title like “%金融交易%” order by create_time desc limit 30;

分析:这是一个全表扫描,mysql会一个一个的把主键索引的叶节点从磁盘读取到内存,并在内存用字符串查找的方式找title是否有“金融交易”这个关键字。Mysql读取够30条满足条件的行时,就会停止读取后面的叶节点。

所以如果包含“金融交易”的文章刚好放在100W个数据的前1000条,那么恭喜你,查找的时间会很短,但是如果这些文章集中在100W个数据的最后1000条,那么基本上你差不多执行100W次io,真正的遍历了整个表。

用这个sql查了我整整一分多钟。

为了使得where title like “%xxx%” 不发生全表扫描,我们可以使用覆盖索引优化。

版本2:

建立一个联合索引 index ct_title (create_time, title)

分两句sql来查:

Select id from arts where title like “%金融交易%” order by create_time desc limit 30;

使用了覆盖索引。而且没有出现 Using filesort ,说明order by create_time排序也用到了索引的排序,没有在内存中进行排序。

底层发生了什么事呢?

首先,mysql把联合索引的所有叶节点从右到左一个个的从磁盘读进内存,并在内存查找title是否包含“金融交易”关键字。直到找到30个的时候,停止读取,但是如果没找到30个,就会一直往下读取下一个叶节点直到把叶子节点全部读完(但是这个过程很快,因为二级索引的叶节点只存着索引值和id值)。

这样就获取到了30个满足条件的文章的id。

Select * from arts where id in (刚刚查到的文章id)。

这个时候,就会往聚集索引的树中去逐一对每个id从根节点往下找到叶子节点,假如树只有3层,这句sql也就一个共发生了 (3-1)*30 = 60次sql。

这两句合起来一共就花了0.3秒。

不要这样写,因为子查询不能用limit。

select * from arts where id in (Select id from arts where title like "%金融交易%" order by create_time desc limit 30);

以上例子中,例子1研究了精确匹配和范围匹配下是否使用了索引,例子2研究了排序和分组是否使用了索引,是否有using filesort 文件排序的出现,例子3研究了using index 覆盖索引。

但是,万变不离其宗,只要不是复杂的关联查询或者子查询,画一个B+树的草图来分析,一切问题迎刃而解。

2、索引使用技巧总结

1. 复合索引按最左前缀的原则筛选

create index idx_a_b_c on test (a,b,c);  #给a,b,c建立的索引
where a=xxx
where a=xxx and b=xxx 
where a=xxx and b=xxx and c=xxx
where a=xxx and c=xxx 
where b=xxx and a=xxx

前三种情况用到了索引,第四种只用到了a的索引,最后一种a和b都没用到索引。如果where条件中同时有精确条件(=,in)和范围条件,那么靠左的列先使用了范围条件则靠右的列则用不到索引,因为mysql索引只支持一个而且是最近的范围索引:

idx_a_b_c_d (a,b,c,d)
where a=? and b=? and c>? and d<?

a,b 都没有使用范围条件,所以c会用到索引,但是c用了范围条件,所以d没用到索引。

where a>? and b=? and c=? and d<?

a用到索引,b,c,d没用到,所以范围查找尽可能放在最后。

优: select * from test where a=10 and b>50

优: select * from test where order by a
差: select * from test where order by b
差: select * from test where order by c

优: select * from test where a=10 order by a
优: select * from test where a=10 order by b
差: select * from test where a=10 order by c

优: select * from test where a>10 order by a
差: select * from test where a>10 order by b
差: select * from test where a>10 order by c

优: select * from test where a=10 and b=10 order by a
优: select * from test where a=10 and b=10 order by b
优: select * from test where a=10 and b=10 order by c

优: select * from test where a=10 and b=10 order by a
优: select * from test where a=10 and b>10 order by b
差: select * from test where a=10 and b>10 order by c

所以在建立复合索引的时候,越常用的字段放越左边,上面常用性是a>b>c所以,定义的时候是(a,b,c)

2. 如果一个 Like 语句的查询条件不以通配符起始则使用索引

如:

%车 或 %车%

不使用索引。

车%              

使用索引。

3. 使用函数

如果没有使用基于函数的索引,那么where子句中对存在索引的列使用函数时,会使优化器忽略掉这些索引。

下面的查询就不会使用索引:

select * from staff where trunc(birthdate) = '01-MAY-82';  

但是把函数应用在条件上,索引是可以生效的,把上面的语句改成下面的语句,就可以通过索引进行查找。

select * from staff where birthdate < (to_date('01-MAY-82') + 0.9999);

4. 比较不匹配的数据类型

比较不匹配的数据类型也是难于发现的性能问题之一。

下面的例子中,dept_id是一个varchar2型的字段,在这个字段上有索引,但是下面的语句会执行全表扫描。

select * from dept where dept_id = 900198;  

这是因为oracle会自动把where子句转换成to_number(dept_id)=900198,就是3所说的情况,这样就限制了索引的使用。

把SQL语句改为如下形式就可以使用索引:

select * from dept where dept_id = '900198';  

5. 索引列的范围查找

如果某列定义了索引,对该列使用 where between and / > / < 也是会使用到索引的,会用到索引范围查找。

但是如果这个范围太大,数据库觉得成本太高,可能会变成全表索引。

6. 索引常识

对经常作为搜索条件(where)、经常排序(order)、经常分组(group by) 的字段,建立索引能提高效率。

如果作为索引的字段有越多相同的值,那么这个索引的效率越低。

7. 关于多表联查时使用到的索引的情况

在多表联查的时候,数据库会指定一个表为驱动表,另一个表为被驱动表。

如下:

select a.col1,b.col2 from a join b on a.id=b.id 

其中id是两个表的主键,如果a表被判定为驱动表,那么数据库可能会全表扫描a表,并用a表的每个id探测b表的索引查找匹配的记录。

那么我们先了解在join连接时哪个表是驱动表,哪个表是被驱动表:

  1. 当使用left join时,左表是驱动表,右表是被驱动表
  2. 当使用right join时,右表时驱动表,左表是驱动表
  3. 当使用join时,mysql会选择数据量比较小的表作为驱动表,大表作为被驱动表,我们知道如果大表做驱动表,会全表扫描驱动表,那么就会效率很低。也就是说join的情况下,数据库会自动做优化。

join查询中,永远是以小表驱动大表。

例如:

A是小表,B是大表。  

使用left join 时,则应该这样写select * from A a left join B b on a.code=b.code

A表时驱动表,B表是被驱动表。

测试:

A表140多条数据,B表20万左右的数据量。

select * from A a left join B b on a.code=b.code

执行时间:7.5s 。

select * from B b left join A a on a.code=b.code

执行时间:19s。

结论:小表驱动大表优于大表驱动小表

join查询在有索引条件下:

  • 驱动表有索引不会使用到索引
  • 被驱动表建立索引会使用到索引

在以小表驱动大表的情况下,再给大表建立索引会大大提高执行速度。

在我做的一个项目中有个查询,这个查询涉及到两张表:分类表和文章表。

分类表 type 有20条数据,文章表 arts 有70万条数据,文章表有一个字段是is_send,用来标记文章是否发送,is_send字段的值只有两个。

我想查每个分类下有多少篇文章:

select t.id,t.name,count(*) as arts_count from arts a join type t on a.tid=t.id group by t.id;

我在arts中对tid也做了索引。

上面使用了join所以,默认以type作为驱动。而且分组的对象t.id是主键,主键肯定也是做了索引的,所以上面的查询效率不会低,只花了1秒。

但是如果加了一个条件 is_send=0:

select t.id,t.name,count(*) as arts_count from arts a join type t on a.tid=t.id where is_send=0 group by t.id;

那么,查询时间变成了12秒。

原因是is_send没有建立索引,所以以他为条件会对arts表全表扫描。

更关键的是is_send只有0和1两个值,所以即使对它建立了索引,效率也只能提高一半。而且还是0和1分布比较均匀的情况下才能提高一半,如果0占百分之90,1占百分之10,那么where is_send=0 提高的效率不到百分之10。

8. 基于主键来取数据是最快的

基于二级索引(即普通的 index)则要进行两次索引查找,先找到二级索引再根据二级索引找到主键,再根据主键找到对应的记录。

9. 避免重复对一个列创建多个索引

这样会浪费空间,而且对一个列创建多个索引不会报错。

10. 使用覆盖索引可以大大提高性能

覆盖索引指所有数据可以从索引中得到,不需要去读取物理记录。

例如 idx_a_b_c:

select a,b from tb1 where a=? and b=? and c=?

这就是覆盖索引,也避免了二次索引查找。

11. 利用索引排序

mysql有两种方式可以产生有序的结果,一种是文件排序(filesort)对记录排序,另一种是扫描有序的索引排序。

文件排序,mysql是将取得的数据在内存中排序,如果对少量数据进行排序会很快,但如果是对大量数据排序就会很慢。

order by create_time //就是文件排序
order by id          //就是索引排序

但是之前做项目,都是对分页数据排序,每一页不超过100条数据,所以用文件排序也不慢.

像复合索引在排序的时候也要遵循前导列和最左前缀原则,否则就不算索引排序。

idx_a_b_c:

order by a,b,c
where a=? and b=? order by c

这两个都复合索引排序,可以通过 explain 的extra查看是否是文件索引,显示filesort就是文件索引。

12. 避免冗余索引

冗余索引就是我定义了a字段为索引,有定义了(a,b)的复合索引。

但是有一种情况是要定义冗余索引:比如原本我对a建立了索引,a是一个整型列,如果我突然想将a索引扩展为a,b索引,而b是一个长度较长的字符串列,那么索引会很大。

此时就不得不添加一个新的复合索引,保留原本的索引。

13. 使用更短的索引

比如 我想对文章的标题建立索引,标题会很长,此时建立的索引会很大(我们知道建立索引会将索引字段单独放到一个表中存储),为此我们可以使用前缀索引,即只对标题的前多少个字符进行索引。

index title (title(6))

就是只对标题的前6个字符进行索引,这样存进索引表的就不是整个标题,而是标题的前6个字符。

但是要确保所选择的前缀的长度的内容大部分值是唯一的。

14. where不以索引字段为条件时会全表扫描

where以索引为条件时,如果索引效率不高时,mysql依旧会全表扫描。

所以不要对不必要的字段建立索引,例如性别。

15. innodb的主键不能太长

以防止二级索引过大,主键一般都是选整型。

九、SQL优化建议使用

1、Insert优化

用一个insert插入多条数据(批量插入)。

按照主键顺序插入。

使用手动提交事务。

2、Order by优化

Order by后面的排序字段尽量按照建联合索引时的字段顺序来放(遵循最左前缀原则),从而避开文件排序(Using filesort)。如果可以的话最好能用上覆盖索引(Using index),不过非要用到覆盖索引的话select 查的字段只能是索引的字段了。

对于多字段排序:遵循最左前缀原则,而且不要对一个字段升序对另一个字段降序,否则也会使用到Using filesort(要么都升序排序,要么都降序排序)。

如果一定会发生 Using filesort,那么可以通过提高 sort_buffer_size 和 max_length_for_sort_data来增大排序缓冲区的大小,减小创建临时表的可能。

我再小结一下order by的优化:

  • 遵循最左前缀原则,避开文件排序,最好能用上覆盖索引;
  • 不要对一个字段升序对另一个字段降序;
  • 如果一定会发生文件排序,可以增大排序缓冲区的大小,减小创建临时表的可能;

3、Group by优化

Group by是会先进行排序后分组的,所以所有能用于order by的优化都可以用于group by。

如果我们只想分组不用排序,就可以使用order by null。

Select age, count(age) from t group by age order by null

4、子查询优化

尽量用多表联查来代替子查询,你可以用explain分析一下用子查询和用join联查的差别。

5、Or 优化

Or的左右两边都必须是索引字段,而且or两边尽量不要是复合索引的两个字段,否则都会导致索引失效变成一个全表扫描(最好是or两边都是同一个字段的条件或者是两个单列索引)。

尽量用union代替or,你可以对比一下:

Select * from t where id=1 or id = 10;
Select * from t where id=1 union select * from emp where id = 10;

前者是一个type为range的查询,后者是2个type为const的查询。

Select * from t where id=1 or age = 20;
Select * from t where id=1 union select * from emp where age = 20;

前者是一个type为 index_merge , 后者是1个type为const和ref的查询。

6、Limit分页查询优化

随着偏移量的增加,limit的查询效率越低,limit 20w,10会扫描200w零10条记录,但只返回200w到200万零10这10条数据。

优化1:可以用到覆盖索引,现在二级索引上得到分页的id,再根据二级索引上的id找到聚集索引查询所需要的的其他列内容

如:

select * from t order by create_time limit 20w, 10;    (create_time)加了索引

优化为:

Select * from t t join (select id from t order by create_time limit 20w, 10) a where t.id=a.id

一个是全表扫描All ,一个是覆盖索引 index。

优化2:如果我的分页是根据id排序的,而且id没有断层,那么可以先获取200w页的最后一个id,然后根据这个id往后查10条。

Select * from t where id>200w limit 10;

7、大批量插入数据的优化(一次性插入100万或1000万的数据)

对于innodb而言,考虑到B+树的结构,在插入数据的时候应该按主键顺序从小到大插入,主键类型选择自增的整型。使用整型是为了减小索引占用的存储空间,减小非叶子节点中每个元素的大小,使得一个非叶子节点能够容纳更多的索引,使得在B+树占用空间大小相同的情况下这棵树的层数更少。而按从小到大的插入可以减少构建B+树过程中页分裂的次数,提高插入效率。

关闭唯一性校验:

set unique_checks = 0; 

插入结束后可以把它设置会1。

手动提交事务:

set autocommit=0; 

每插入1w条数据手动提交1次,插入完成后在设置回1。用一个insert命令插入多条数据。

猜你喜欢

转载自blog.csdn.net/qq_35029061/article/details/128641539