MySQL进阶篇(一)

一.存储引擎

1.1 MySQL体系结构

MySQL进阶篇(一)_第1张图片

1.连接层

最上层是一些客户端和链接服务,包含本地sock 通信和大多数基于客户端/服务端工具实现的类似于 TCP/IP的通信。主要完成一些类似于连接处理、授权认证、及相关的安全方案。在该层上引入了线程 池的概念,为通过认证安全接入的客户端提供线程。同样在该层上可以实现基于SSL的安全链接。服务 器也会为安全接入的每个客户端验证它所具有的操作权限。

2.服务层

第二层架构主要完成大多数的核心服务功能,如SQL接口,并完成缓存的查询,SQL的分析和优化,部分内置函数的执行。所有跨存储引擎的功能也在这一层实现,如 过程、函数等。在该层,服务器会解析查询并创建相应的内部解析树,并对其完成相应的优化如确定表的查询的顺序,是否利用索引等, 最后生成相应的执行操作。如果是select语句,服务器还会查询内部的缓存,如果缓存空间足够大, 这样在解决大量读操作的环境中能够很好的提升系统的性能。 

3.引擎层

存储引擎层, 存储引擎真正的负责了MySQL中数据的存储和提取,服务器通过API和存储引擎进行通 信。不同的存储引擎具有不同的功能,这样我们可以根据自己的需要,来选取合适的存储引擎。数据库 中的索引是在存储引擎层实现的。

4.存储层

数据存储层, 主要是将数据(如: redolog、undolog、数据、索引、二进制日志、错误日志、查询 日志、慢查询日志等)存储在文件系统之上,并完成与存储引擎的交互。 和其他数据库相比,MySQL有点与众不同,它的架构可以在多种不同场景中应用并发挥良好作用。主要 体现在存储引擎上,插件式的存储引擎架构,将查询处理和其他的系统任务以及数据的存储提取分离。 这种架构可以根据业务的需求和实际需要选择合适的存储引擎。

1.2 存储引擎介绍

存储引擎是mysql数据库的核心,我们需要在合适的场景选择合适的存储引擎。

存储引擎就是存储数据、建立索引、更新/查询数据等技术的实现方式 。存储引擎是基于表的,而不是 基于库的,所以存储引擎也可被称为表类型。我们可以在创建表的时候,来指定选择的存储引擎,如果 没有指定将自动选择默认的存储引擎。

1.建表时指定存储引擎

CREATE TABLE 表名(
字段1 字段1类型 [ COMMENT 字段1注释 ] ,
......
字段n 字段n类型 [COMMENT 字段n注释 ]
) ENGINE = INNODB [ COMMENT 表注释 ] ;

2.查询当前数据库支持的存储引擎

show engines;

1.3 存储引擎特点

接下来介绍一下三种存储引擎 InnoDB、MyISAM、Memory的特点。

1.3.1 InnoDB

1.介绍

InnoDB是一种兼顾高可靠性和高性能的通用存储引擎,在 MySQL 5.5 之后,InnoDB是默认的 MySQL 存储引擎。

2.特点

DML操作遵循ACID模型,支持事务; 行级锁,提高并发访问性能; 支持外键FOREIGN KEY约束,保证数据的完整性和正确性;

3.文件

xxx.ibd:xxx代表的是表名,innoDB引擎的每张表都会对应这样一个表空间文件,存储该表的表结 构(frm-早期的 、sdi-新版的)、数据和索引。

可通过以下SQL来查看参数:innodb_file_per_table的值,如果该参数开启,代表对于InnoDB引擎的表,每一张表都对应一个ibd文件。

show variables like 'innodb_file_per_table';

MySQL进阶篇(一)_第2张图片

我们直接打开MySQL的 数据存放目录: C:\ProgramData\MySQL\MySQL Server 8.0\Data , 这个目录下有很多文件 夹,不同的文件夹代表不同的数据库,我们直接打开itcast文件夹,也就是itcast数据库。

MySQL进阶篇(一)_第3张图片

可以看到里面有很多的ibd文件,每一个ibd文件就对应一张表,比如:我们有一张表 account,就 有这样的一个account.ibd文件,而在这个ibd文件中不仅存放表结构、数据,还会存放该表对应的 索引信息。 而该文件是基于二进制存储的,不能直接基于记事本打开,我们可以使用mysql提供的一 个指令 ibd2sdi ,通过该指令就可以从ibd文件中提取sdi信息,而sdi数据字典信息中就包含该表的表结构。

MySQL进阶篇(一)_第4张图片

4.逻辑存储结构

MySQL进阶篇(一)_第5张图片

表空间 : InnoDB存储引擎逻辑结构的最高层,ibd文件其实就是表空间文件,在表空间中可以 包含多个Segment段。

段 : 表空间是由各个段组成的, 常见的段有数据段、索引段、回滚段等。InnoDB中对于段的管 理,都是引擎自身完成,不需要人为对其控制,一个段中包含多个区。

区 : 区是表空间的单元结构,每个区的大小为1M。 默认情况下, InnoDB存储引擎页大小为 16K, 即一个区中一共有64个连续的页。

页 : 页是组成区的最小单元,页也是InnoDB 存储引擎磁盘管理的最小单元,每个页的大小默 认为 16KB。为了保证页的连续性,InnoDB 存储引擎每次从磁盘申请 4-5 个区。

行 : InnoDB 存储引擎是面向行的,也就是说数据是按行进行存放的,在每一行中除了定义表时 所指定的字段以外,还包含两个隐藏字段(后面会详细介绍)。 

1.3.2 MyISAM

1.介绍

MyISAM是MySQL早期的默认存储引擎。

2.特点

不支持事务,不支持外键,支持表锁,不支持行锁 访问速度快

3.文件

xxx.sdi:存储表结构信息

xxx.MYD: 存储数据

xxx.MYI: 存储索引

MySQL进阶篇(一)_第6张图片

1.3.3 Memory

1.介绍

Memory引擎的表数据时存储在内存中的,由于受到硬件问题、或断电问题的影响,只能将这些表作为 临时表或缓存使用。

2.特点

内存存放

hash索引(默认)

3.文件

xxx.sdi:存储表结构信息

1.3.4 区别及特点

MySQL进阶篇(一)_第7张图片

1.4 存储引擎选择

在选择存储引擎时,应该根据应用系统的特点选择合适的存储引擎。对于复杂的应用系统,还可以根据 实际情况选择多种存储引擎进行组合。

1.InnoDB: 是Mysql的默认存储引擎,支持事务、外键。如果应用对事务的完整性有比较高的要 求,在并发条件下要求数据的一致性,数据操作除了插入和查询之外,还包含很多的更新、删除操 作,那么InnoDB存储引擎是比较合适的选择。

2.MyISAM : 如果应用是以读操作和插入操作为主,只有很少的更新和删除操作,并且对事务的完 整性、并发性要求不是很高,那么选择这个存储引擎是非常合适的。

3.MEMORY:将所有数据保存在内存中,访问速度快,通常用于临时表及缓存。MEMORY的缺陷就是 对表的大小有限制,太大的表无法缓存在内存中,而且无法保障数据的安全性。

二.索引

2.1 索引概述

2.1.1 介绍

索引(index)是帮助MySQL高效获取数据的数据结构(有序)。在数据之外,数据库系统还维护着满足 特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据, 这样就可以在这些数据结构 上实现高级查找算法,这种数据结构就是索引。

2.1.2 索引的优缺点

MySQL进阶篇(一)_第8张图片

2.2 索引结构

2.2.1 概述 

MySQL的索引是在存储引擎层实现的,不同的存储引擎有不同的索引结构,主要包含以下几种:

MySQL进阶篇(一)_第9张图片

上述是MySQL中所支持的所有的索引结构,接下来,再来看看不同的存储引擎对于索引结构的支持情况。

MySQL进阶篇(一)_第10张图片

注意: 我们平常所说的索引,如果没有特别指明,都是指B+树结构组织的索引。

2.2.2 B-Tree

B-Tree,B树是一种多叉路平衡查找树,相对于二叉树,B树每个节点可以有多个分支,即多叉。 以一颗最大度数为5的b-tree为例,那这个B树每个节点最多存储4个key,5 个指针:

MySQL进阶篇(一)_第11张图片

我们可以通过一个数据结构可视化的网站来简单演示一下。 https://www.cs.usfca.edu/~gall es/visualization/BTree.html

插入一组数据: 100 65 169 368 900 556 780 35 215 1200 234 888 158 90 1000 88 120 268 250 。然后观察一些数据插入过程中,节点的变化情况。

MySQL进阶篇(一)_第12张图片

特点:

5阶的B树,每一个节点最多存储4个key,对应5个指针。

一旦节点存储的key数量到达5,就会裂变,中间元素向上分裂。

在B树中,非叶子节点和叶子节点都会存放数据。 

2.2.3 B+Tree

B+Tree是B-Tree的变种,我们以一颗最大度数为4的b+tree为例,来看一 下其结构示意图:

MySQL进阶篇(一)_第13张图片

我们可以看到,两部分:

绿色框框起来的部分,是索引部分,仅仅起到索引数据的作用,不存储数据。

红色框框起来的部分,是数据存储部分,在其叶子节点中要存储具体的数据。

我们可以通过一个数据结构可视化的网站来简单演示一下。 https://www.cs.usfca.edu/~gall es/visualization/BPlusTree.html

插入一组数据: 100 65 169 368 900 556 780 35 215 1200 234 888 158 90 1000 88 120 268 250 。然后观察一些数据插入过程中,节点的变化情况。

MySQL进阶篇(一)_第14张图片

最终我们看到,B+Tree 与 B-Tree相比,主要有以下三点区别:

所有的数据都会出现在叶子节点。

叶子节点形成一个单向链表。

非叶子节点仅仅起到索引数据作用,具体的数据都是在叶子节点存放的。 

上述我们所看到的结构是标准的B+Tree的数据结构,MySQL对B+Tree做了一点优化。

MySQL索引数据结构对经典的B+Tree进行了优化。在原B+Tree的基础上,增加一个指向相邻叶子节点 的链表指针,就形成了带有顺序指针的B+Tree,提高区间访问的性能,利于排序。

MySQL进阶篇(一)_第15张图片

2.2.4 Hash

MySQL中除了支持B+Tree索引,还支持一种索引类型---Hash索引。

1.结构

哈希索引就是采用一定的hash算法,将键值换算成新的hash值,映射到对应的槽位上,然后存储在 hash表中。

MySQL进阶篇(一)_第16张图片

如果两个(或多个)键值,映射到一个相同的槽位上,他们就产生了hash冲突(也称为hash碰撞),可以通过链表来解决。 

MySQL进阶篇(一)_第17张图片

2.特点

A.Hash索引只能用于对等比较(=,in),不支持范围查询(between,>,< ,...)

B. 无法利用索引完成排序操作

C. 查询效率高,通常(不存在hash冲突的情况)只需要一次检索就可以了,效率通常要高于B+tree索 引

3.存储引擎支持

在MySQL中,支持hash索引的是Memory存储引擎。 而InnoDB中具有自适应hash功能,hash索引是 InnoDB存储引擎根据B+Tree索引在指定条件下自动构建的。

2.3 索引分类

2.3.1 索引分类

在MySQL数据库,将索引的具体类型主要分为以下几类:主键索引、唯一索引、常规索引、全文索引。

MySQL进阶篇(一)_第18张图片

2.3.2 聚集索引&二级索引 

而在InnoDB存储引擎中,根据索引的存储形式,又可以分为以下两种:

MySQL进阶篇(一)_第19张图片

聚集索引选取规则:

如果存在主键,主键索引就是聚集索引。

如果不存在主键,将使用第一个唯一(UNIQUE)索引作为聚集索引。

如果表没有主键,或没有合适的唯一索引,则InnoDB会自动生成一个rowid作为隐藏的聚集索 引。 

聚集索引和二级索引的具体结构如下:

MySQL进阶篇(一)_第20张图片

聚集索引的叶子节点下挂的是这一行的数据 。

二级索引的叶子节点下挂的是该字段值对应的主键值。 

接下来,我们来分析一下,当我们执行如下的SQL语句时,具体的查找过程是什么样子的。

MySQL进阶篇(一)_第21张图片

具体过程如下:

①. 由于是根据name字段进行查询,所以先根据name='Arm'到name字段的二级索引中进行匹配查 找。但是在二级索引中只能查找到 Arm 对应的主键值 10。

②. 由于查询返回的数据是*,所以此时,还需要根据主键值10,到聚集索引中查找10对应的记录,最 终找到10对应的行row。

③. 最终拿到这一行的数据,直接返回即可。 

回表查询: 这种先到二级索引中查找数据,找到主键值,然后再到聚集索引中根据主键值,获取 数据的方式,就称之为回表查询。

2.4 索引语法

1.创建索引

CREATE [ UNIQUE | FULLTEXT ] INDEX index_name ON table_name (
index_col_name,... ) ;

2.查看索引

SHOW INDEX FROM table_name ;

3.删除索引

DROP INDEX index_name ON table_name ;

2.5 SQL性能分析

2.5.1 SQL执行频率

MySQL 客户端连接成功后,通过 show [session|global] status 命令可以提供服务器状态信 息。通过如下指令,可以查看当前数据库的INSERT、UPDATE、DELETE、SELECT的访问频次:

-- session 是查看当前会话 ;
-- global 是查询全局数据 ;
SHOW GLOBAL STATUS LIKE 'Com_______';

MySQL进阶篇(一)_第22张图片

Com_delete: 删除次数

Com_insert: 插入次数

Com_select: 查询次数

Com_update: 更新次数 

通过上述指令,我们可以查看到当前数据库到底是以查询为主,还是以增删改为主,从而为数据 库优化提供参考依据。 如果是以增删改为主,我们可以考虑不对其进行索引的优化。 如果是以 查询为主,那么就要考虑对数据库的索引进行优化了。

那么通过查询SQL的执行频次,我们就能够知道当前数据库到底是增删改为主,还是查询为主。 假如说是以查询为主,我们又该如何定位针对于哪些查询语句进行优化呢? 次数我们可以借助于慢查询日志。

接下来,我们就来介绍一下MySQL中的慢查询日志。

2.5.2 慢查询日志

慢查询日志记录了所有执行时间超过指定参数(long_query_time,单位:秒,默认10秒)的所有 SQL语句的日志。

MySQL的慢查询日志默认没有开启,我们可以查看一下系统变量 slow_query_log。

如果要开启慢查询日志,需要在MySQL的配置文件(/etc/my.cnf)中配置如下信息:

# 开启MySQL慢日志查询开关
slow_query_log=1
# 设置慢日志的时间为2秒,SQL语句执行时间超过2秒,就会视为慢查询,记录慢查询日志
long_query_time=2

配置完毕之后,通过以下Linux指令重新启动MySQL服务器进行测试,查看慢日志文件中记录的信息

Linux指令:systemctl restart mysqld

慢日志文件中:/var/lib/mysql/localhost-slow.log。

然后,再次查看开关情况,慢查询日志就已经打开了。

2.5.3 profile详情

show profiles 能够在做SQL优化时帮助我们了解时间都耗费到哪里去了。通过have_profiling 参数,能够看到当前MySQL是否支持profile操作:

SELECT @@have_profiling ;

MySQL进阶篇(一)_第23张图片

可以看到,当前MySQL是支持 profile操作的,但是开关是关闭的。可以通过set语句在 session/global级别开启profiling:

SET profiling = 1;

开关已经打开了,接下来,我们所执行的SQL语句,都会被MySQL记录,并记录执行时间消耗到哪儿去了。 我们直接执行如下的SQL语句:

select * from tb_user;
select * from tb_user where id = 1;
select * from tb_user where name = '白起';
select count(*) from tb_sku;

执行一系列的业务SQL的操作,然后通过如下指令查看指令的执行耗时:

-- 查看每一条SQL的耗时基本情况
show profiles;
-- 查看指定query_id的SQL语句各个阶段的耗时情况
show profile for query query_id;
-- 查看指定query_id的SQL语句CPU的使用情况
show profile cpu for query query_id;

查看每一条SQL的耗时情况:

MySQL进阶篇(一)_第24张图片

查看指定SQL各个阶段的耗时情况 :

MySQL进阶篇(一)_第25张图片

2.5.4 explain 

EXPLAIN 或者 DESC命令获取 MySQL 如何执行 SELECT 语句的信息,包括在 SELECT 语句执行过程中表如何连接和连接的顺序。

语法:

-- 直接在select语句之前加上关键字 explain / desc
EXPLAIN SELECT 字段列表 FROM 表名 WHERE 条件 ;

Explain 执行计划中各个字段的含义:

MySQL进阶篇(一)_第26张图片

2.6 索引使用 

2.6.1 最左前缀法则

如果索引了多列(联合索引),要遵守最左前缀法则。最左前缀法则指的是查询从索引的最左列开始, 并且不跳过索引中的列。如果跳跃某一列,索引将会部分失效(后面的字段索引失效)。

注意 : 最左前缀法则中指的最左边的列,是指在查询时,联合索引的最左边的字段(即是 第一个字段)必须存在,与我们编写SQL时,条件编写的先后顺序无关。

2.6.2 范围查询

联合索引中,出现范围查询(>,<),范围查询右侧的列索引失效。

例如:

explain select * from tb_user where profession = '软件工程' and age > 30 and status
= '0';

当范围查询使用> 或 < 时,走联合索引了,但是索引的长度为49,就说明范围查询右边的status字 段是没有走索引的。

explain select * from tb_user where profession = '软件工程' and age >= 30 and
status = '0';

当范围查询使用>= 或 <= 时,走联合索引了,但是索引的长度为54,就说明所有的字段都是走索引 的。

所以,在业务允许的情况下,尽可能的使用类似于 >= 或<=  这类的范围查询,而避免使用 > 或 <。

2.6.4 索引失效情况 

1.不要在索引列上进行运算操作, 索引将失效。

2.字符串类型字段使用时,不加引号,索引将失效。

3.在模糊匹配的情况下,如果仅仅是尾部模糊匹配,索引不会失效。如果是头部模糊匹配,索引失效。

4.用or分割开的条件, 如果or前的条件中的列有索引,而后面的列中没有索引,那么涉及的索引都不会被用到。

5.如果MySQL评估使用索引比全表更慢,则不使用索引。

2.6.5 SQL提示

SQL提示,是优化数据库的一个重要手段,简单来说,就是在SQL语句中加入一些人为的提示来达到优化操作的目的。

1.use index : 建议MySQL使用哪一个索引完成此次查询(仅仅是建议,mysql内部还会再次进 行评估)。

2.ignore index : 忽略指定的索引。

3.force index : 强制使用索引。

2.6.6 覆盖索引

覆盖索引是指查询使用了索引,并且需要返回的列,在该索引中已经全部能够找到 。通过覆盖索引,可以避免回表查询,所以应尽量使用覆盖索引,减少select *。

回表查询:回表查询指的是,通过二级索引(或者叫辅助索引)进行查找时,由于非聚集索引并未包含所有查询所需的列,所以数据库需要返回到聚集索引中去查询所需的其他列数据,即“回表”查找数据,这个过程会增加查询的成本。

例子:

1.执行SQL : select * from tb_user where id = 2;

MySQL进阶篇(一)_第27张图片

根据id查询,直接走聚集索引查询,一次索引扫描,直接返回数据,性能高。

2.执行SQL:selet id,name from tb_user where name = 'Arm';

MySQL进阶篇(一)_第28张图片虽然是根据name字段查询,查询二级索引,但是由于查询返回在字段为 id,name,在name的二级索 引中,这两个值都是可以直接获取到的,因为覆盖索引,所以不需要回表查询,性能高。 

3.执行SQL:selet id,name,gender from tb_user where name = 'Arm';

MySQL进阶篇(一)_第29张图片

由于在name的二级索引中,不包含gender,所以,需要两次索引扫描,也就是需要回表查询,性能相 对较差一点。

2.6.7 前缀索引 

当字段类型为字符串(varchar,text,longtext等)时,有时候需要索引很长的字符串,这会让 索引变得很大,查询时,浪费大量的磁盘IO, 影响查询效率。此时可以只将字符串的一部分前缀,建 立索引,这样可以大大节约索引空间,从而提高索引效率。

1.语法

create index idx_xxxx on table_name(column(n)) ;

示例:

为tb_user表的email字段,建立长度为5的前缀索引。

create index idx_email_5 on tb_user(email(5));

MySQL进阶篇(一)_第30张图片

2.前缀长度

可以根据索引的选择性来决定,而选择性是指不重复的索引值(基数)和数据表的记录总数的比值, 索引选择性越高则查询效率越高, 唯一索引的选择性是1,这是最好的索引选择性,性能也是最好的。

select count(distinct email) / count(*) from tb_user ;
select count(distinct substring(email,1,5)) / count(*) from tb_user ;

3.前缀索引的查询流程

MySQL进阶篇(一)_第31张图片

2.6.8 单列索引与联合索引

单列索引:即一个索引只包含单个列。

联合索引:即一个索引包含了多个列。

首先来看 tb_user 表中目前的索引情况:

MySQL进阶篇(一)_第32张图片

在查询出来的索引中,既有单列索引,又有联合索引。

接下来,执行一条SQL语句,看看其执行计划:

通过上述执行计划我们可以看出来,在and连接的两个字段 phone、name上都是有单列索引的,但是 最终mysql只会选择一个索引,也就是说,只能走一个字段的索引,此时是会回表查询的。

我们再来创建一个phone和name字段的联合索引来查询一下执行计划。

create unique index idx_user_phone_name on tb_user(phone,name);

此时,查询时,就走了联合索引,而在联合索引中包含 phone、name的信息,在叶子节点下挂的是对 应的主键id,所以查询是无需回表查询的。

总结:尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间, 避免回表,提高查询效率。

2.7 索引设计原则

1.针对于数据量较大,且查询比较频繁的表建立索引。

2. 针对于常作为查询条件(where)、排序(order by)、分组(group by)操作的字段建立索 引。

3. 尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的效率越高。

4. 如果是字符串类型的字段,字段的长度较长,可以针对于字段的特点,建立前缀索引。

5. 尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间, 避免回表,提高查询效率。

6. 要控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价也就越大,会影响增 删改的效率。

7. 如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它。当优化器知道每列是否包含 NULL值时,它可以更好地确定哪个索引最有效地用于查询。

三.SQL优化

3.1 insert优化

3.1.1 小批量插入数据

如果我们需要一次性往数据库表中插入多条记录,可以从以下三个方面进行优化。

insert into tb_test values(1,'tom');
insert into tb_test values(2,'cat');
insert into tb_test values(3,'jerry');
.....

1.优化方案一

批量插入数据

Insert into tb_test values(1,'Tom'),(2,'Cat'),(3,'Jerry');

2.优化方案二

每执行一条SQL语句,MySQL默认是自动提交事务的,如果要执行多次insert语句,就会频繁的涉及事物的开启与提交,所以建议手动控制事务,在一次事务中包含多条insert语句。

start transaction;
insert into tb_test values(1,'Tom'),(2,'Cat'),(3,'Jerry');
insert into tb_test values(4,'Tom'),(5,'Cat'),(6,'Jerry');
insert into tb_test values(7,'Tom'),(8,'Cat'),(9,'Jerry');
commit;

3.优化方案三

主键顺序插入,性能要高于乱序插入。

主键乱序插入 : 8 1 9 21 88 2 4 15 89 5 7 3

主键顺序插入 : 1 2 3 4 5 7 8 9 15 21 88 89

3.1.2 大批量插入数据

如果一次性需要插入大批量数据(比如: 几百万的记录),使用insert语句插入性能较低,此时可以使 用MySQL数据库提供的load指令进行插入。操作如下:

可以执行如下指令,将数据脚本文件中的数据加载到表结构中:

-- 客户端连接服务端时,加上参数 -–local-infile
mysql –-local-infile -u root -p
-- 设置全局参数local_infile为1,开启从本地加载文件导入数据的开关
set global local_infile = 1;
-- 执行load指令将准备好的数据,加载到表结构中
load data local infile '/root/sql1.log' into table tb_user fields
terminated by ',' lines terminated by '\n' ;

MySQL进阶篇(一)_第33张图片

 注意:在load时,主键顺序插入性能高于乱序插入

3.2 主键优化

在上一小节,我们提到,主键顺序插入的性能是要高于乱序插入的。 这一小节,就来介绍一下具体的原因,然后再分析一下主键又该如何设计。

3.2.1 表数据组织方式

在InnoDB存储引擎中,表数据都是根据主键顺序组织存放的,这种存储方式的表称为索引组织表。

MySQL进阶篇(一)_第34张图片

行数据,都是存储在聚集索引的叶子节点上的。而我们之前也讲解过InnoDB的逻辑结构图:

MySQL进阶篇(一)_第35张图片在InnoDB引擎中,数据行是记录在逻辑结构 page 页中的,而每一个页的大小是固定的,默认16K。 那也就意味着, 一个页中所存储的行也是有限的,如果插入的数据行row在该页存储不下,将会存储到下一个页中,页与页之间会通过指针连接。

3.2.2 页分裂

页可以为空,也可以填充一半,也可以填充100%。InnoDB引擎规定每个页至少包含2行数据。每个页包含了2-N行数据(如果一行数据过大,会行溢出),根据主键排列。

1.主键顺序插入效果

从磁盘中申请页, 主键顺序插入

MySQL进阶篇(一)_第36张图片

第一个页没有满,继续往第一页插入

MySQL进阶篇(一)_第37张图片 当第一个页写满之后,再写入第二个页,页与页之间会通过指针连接

当第二页写满了,再往第三页写入

2. 主键乱序插入效果

假如1#、2#页都已经写满了,存放了如图所示的数据

此时再插入id为50的记录,我们来看看会发生什么现象

会再次开启一个页,写入新的页中吗?

MySQL进阶篇(一)_第38张图片

不会。因为,索引结构的叶子节点是有顺序的。按照顺序,应该存储在47之后。

MySQL进阶篇(一)_第39张图片

但是47所在的1#页,已经写满了,存储不了50对应的数据了。 那么此时会开辟一个新的页 3#。

但是并不会直接将50存入3#页,而是会将1#页后一半的数据,移动到3#页,然后在3#页,插入50。

MySQL进阶篇(一)_第40张图片

移动数据,并插入id为50的数据之后,那么此时,这三个页之间的数据顺序是有问题的。 1#的下一个 页,应该是3#, 3#的下一个页是2#。 所以,此时,需要重新设置链表指针。

上述的这种现象,称之为 "页分裂",是比较耗费性能的操作。

3.2.3 页合并

目前表中已有数据的索引结构(叶子节点)如下

当我们对已有数据进行删除时,具体的效果如下: 当删除一行记录时,实际上记录并没有被物理删除,只是记录被标记(flaged)为删除并且它的空间 变得允许被其他记录声明使用。

当我们继续删除2#的数据记录 

MySQL进阶篇(一)_第41张图片

当页中删除的记录达到 MERGE_THRESHOLD(默认为页的50%),InnoDB会开始寻找最靠近的页(前 或后)看看是否可以将两个页合并以优化空间使用。

MySQL进阶篇(一)_第42张图片 删除数据,并将页合并之后,再次插入新的数据21,则直接插入3#页

这个里面所发生的合并页的这个现象,就称之为 "页合并"。

注意:合并页的阈值,可以自己设置,在创建表或者创建索引时指定。

3.2.4 主键索引设计原则

满足业务需求的情况下,尽量降低主键的长度。

插入数据时,尽量选择顺序插入,选择使用AUTO_INCREMENT自增主键。

尽量不要使用UUID做主键或者是其他自然主键,如身份证号。

业务操作时,避免对主键的修改。

3.3 order by优化

MySQL的排序,有两种方式:

Using filesort : 通过表的索引或全表扫描,读取满足条件的数据行,然后在排序缓冲区sort buffer中完成排序操作,所有不是通过索引直接返回排序结果的排序都叫 FileSort 排序。

Using index : 通过有序索引顺序扫描直接返回有序数据,这种情况即为 using index,不需要 额外排序,操作效率高。

对于以上的两种排序方式,Using index的性能高,而Using filesort的性能低,我们在优化排序 操作时,尽量要优化为 Using index。

order by优化原则:

1.根据排序字段建立合适的索引,多字段排序时,也遵循最左前缀法则。

2.尽量使用覆盖索引。

3.多字段排序, 一个升序一个降序,此时需要注意联合索引在创建时的规则(ASC/DESC)。

因为创建索引时,如果未指定顺序,默认都是按照升序排序的,而查询时,如果order by后的第一个字段升序,第二个字段降序,此时就会出现Using filesort。为了解决上述的问题,我们可以创建一个联合索引,这个联合索引中 第一个字段升序排序,第二个字段倒序排序。

指定索引字段排序规则如下:

create index idx_user_age_phone_ad on tb_user(age asc ,phone desc);

4.如果不可避免的出现filesort,大数据量排序时,可以适当增大排序缓冲区大小,sort_buffer_size(默认256k)。

3.4 group by优化

我们通过一个例子来看看索引对于分组操作的影响

示例:

首先我们先将 tb_user 表的索引全部删除掉 。通过show index from tb_user语句发现tb_user表中已经不存在索引。

接下来,在没有索引的情况下,执行如下SQL,查询执行计划:

explain select profession , count(*) from tb_user group by profession ;

然后,我们在针对于 profession , age, status 创建一个联合索引。

create index idx_user_pro_age_sta on tb_user(profession , age , status);

紧接着,再执行前面相同的SQL查看执行计划。

explain select profession , count(*) from tb_user group by profession ;

再执行如下的分组查询SQL,查看执行计划:

MySQL进阶篇(一)_第43张图片我们发现,如果仅仅根据age分组,就会出现 Using temporary ;而如果是 根据 profession,age两个字段同时分组,则不会出现 Using temporary。原因是因为对于分组操作, 在联合索引中,也是符合最左前缀法则的。

综上所述,group by的优化原则为:

1.在分组操作时,可以通过索引来提高效率。 

2.分组操作时,索引的使用也是满足最左前缀法则的。

3.5 limit优化

在数据量比较大时,如果进行limit分页查询,在查询时,越往后,分页查询效率越低。

MySQL进阶篇(一)_第44张图片

通过测试我们会看到,越往后,分页查询效率越低,这就是分页查询的问题所在。

因为,当在进行分页查询时,如果执行 limit 2000000,10 ,此时需要MySQL排序前2000010 记 录,仅仅返回 2000000 - 2000010 的记录,其他记录丢弃,查询排序的代价非常大 。

优化思路: 一般分页查询时,通过创建 覆盖索引 能够比较好地提高性能,可以通过覆盖索引加子查 询形式进行优化。

explain select * from tb_sku t , (select id from tb_sku order by id
limit 2000000,10) a where t.id = a.id;

3.6 count优化

1.概述

如果数据量很大,在执行count操作时,是非常耗时的。

        MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回这个 数,效率很高; 但是如果是带条件的count,MyISAM也慢。

        InnoDB 引擎就麻烦了,它执行 count(*) 的时候,需要把数据一行一行地从引擎里面读出 来,然后累积计数。

如果说要大幅度提升InnoDB表的count效率,主要的优化思路:自己计数(可以借助于redis这样的数 据库进行,但是如果是带条件的count又比较麻烦了)。

2.count用法

count() 是一个聚合函数,对于返回的结果集,一行行地判断,如果 count 函数的参数不是 NULL,累计值就加 1,否则不加,最后返回累计值。

用法:count(*)、count(主键)、count(字段)、count(数字)

MySQL进阶篇(一)_第45张图片

按照效率排序的话,count(字段) < count(主键 id) < count(1) ≈ count(*),所以尽 量使用 count(*)。

3.7 update优化

我们主要需要注意一下update语句执行时的注意事项。

update course set name = 'javaEE' where id = 1 ;

当我们在执行删除的SQL语句时,会锁定id为1这一行的数据,然后事务提交之后,行锁释放。

但是当我们在执行如下SQL时。name这个字段没有创建索引,所以开启事务并commit之前执行下面这个语句后,行锁会升级为表锁,导致其它事务无法对表中其余行进行操作。导致该update语句的性能 大大降低。

update course set name = 'SpringBoot' where name = 'PHP' ;

综上所述:InnoDB的行锁是针对索引加的锁,不是针对记录加的锁 ,并且该索引不能失效,否则会从行锁 升级为表锁 。

四.视图、存储过程、函数

4.1 视图

4.1.1 介绍

视图(View)是一种虚拟存在的表。视图中的数据并不在数据库中实际存在,行和列数据来自定义视 图的查询中使用的表,并且是在使用视图时动态生成的。

通俗的讲,视图只保存了查询的SQL逻辑,不保存查询结果。所以我们在创建视图的时候,主要的工作 就落在创建这条SQL查询语句上。

4.1.2 语法

1.创建视图

CREATE [OR REPLACE] VIEW 视图名称[(列名列表)] AS SELECT语句 [ WITH [
CASCADED | LOCAL ] CHECK OPTION ]

2.查询视图

查看创建视图语句:SHOW CREATE VIEW 视图名称;
查看视图数据:SELECT * FROM 视图名称 ...... ;

 3.修改视图

方式一:CREATE [OR REPLACE] VIEW 视图名称[(列名列表)] AS SELECT语句 [ WITH
[ CASCADED | LOCAL ] CHECK OPTION ]
方式二:ALTER VIEW 视图名称[(列名列表)] AS SELECT语句 [ WITH [ CASCADED |
LOCAL ] CHECK OPTION ]

4.删除视图

DROP VIEW [IF EXISTS] 视图名称 [,视图名称] ...

4.1.3 检查选项

当使用WITH CHECK OPTION子句创建视图时,MySQL会通过视图检查正在更改的每个行,例如 插 入,更新,删除,以使其符合视图的定义。 MySQL允许基于另一个视图创建视图,它还会检查依赖视 图中的规则以保持一致性。为了确定检查的范围,mysql提供了两个选项: CASCADED 和 LOCAL ,默认值为 CASCADED 。

1.CASCADED(级联)

比如,v2视图是基于v1视图的,如果在v2视图创建的时候指定了检查选项为 cascaded,但是v1视图 创建时未指定检查选项。 则在执行检查时,不仅会检查v2,还会级联检查v2的关联视图v1。

MySQL进阶篇(一)_第46张图片

2.LOCAL(本地)

比如,v2视图是基于v1视图的,如果在v2视图创建的时候指定了检查选项为 local ,但是v1视图创 建时未指定检查选项。 则在执行检查时,只会检查v2,不会检查v2的关联视图v1。 

MySQL进阶篇(一)_第47张图片

4.1.4 视图的更新

要使视图可更新,视图中的行与基础表中的行之间必须存在一对一的关系。如果视图包含以下任何一 项,则该视图不可更新:

A. 聚合函数或窗口函数(SUM()、 MIN()、 MAX()、 COUNT()等)

B. DISTINCT

C. GROUP BY

D. HAVING

E. UNION 或者 UNION ALL

4.1.5 视图作用

1.简单

视图不仅可以简化用户对数据的理解,也可以简化他们的操作。那些被经常使用的查询可以被定义为视 图,从而使得用户不必为以后的操作每次指定全部的条件。

例如:

查询每个学生所选修的课程(三张表联查),这个功能在很多的业务中都有使用到,为了简化操 作,定义一个视图。

create view tb_stu_course_view as select s.name student_name , s.no student_no ,
c.name course_name from student s, student_course sc , course c where s.id =
sc.studentid and sc.courseid = c.id;

select * from tb_stu_course_view;

2.安全

数据库可以授权,但不能授权到数据库特定行和特定的列上。通过视图用户只能查询和修改他们所能见 到的数据

例如:

create view tb_user_view as select id,name,profession,age,gender,status,createtime
from tb_user;
select * from tb_user_view;

3.数据独立

视图可帮助用户屏蔽真实表结构变化带来的影响。

4.2 存储过程

4.2.1 介绍

存储过程是事先经过编译并存储在数据库中的一段 SQL 语句的集合,调用存储过程可以简化应用开发 人员的很多工作,减少数据在数据库和应用服务器之间的传输,对于提高数据处理的效率是有好处的。

存储过程思想上很简单,就是数据库 SQL 语言层面的代码封装与重用。

MySQL进阶篇(一)_第48张图片

特点:

封装,复用 -----------------------> 可以把某一业务SQL封装在存储过程中,需要用到 的时候直接调用即可。

可以接收参数,也可以返回数据 --------> 再存储过程中,可以传递参数,也可以接收返回 值。

减少网络交互,效率提升 -------------> 如果涉及到多条SQL,每执行一次都是一次网络传 输。 而如果封装在存储过程中,我们只需要网络交互一次可能就可以了。 

4.2.2 基本语法

1.创建存储过程

CREATE PROCEDURE 存储过程名称 ([ 参数列表 ])
BEGIN
-- SQL语句
END ;

2.调用存储过程

CALL 名称 ([ 参数 ]);

3.查看存储过程

SELECT * FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA = 'xxx'; -- 查询指
定数据库的存储过程及状态信息
SHOW CREATE PROCEDURE 存储过程名称 ; -- 查询某个存储过程的定义

4.删除存储过程

DROP PROCEDURE [ IF EXISTS ] 存储过程名称 ;

注意: 在命令行中,执行创建存储过程的SQL时,需要通过关键字 delimiter 指定SQL语句的 结束符来代替原来的结束符";",因为存储过程中含有SQL语句,SQL语句后面会有";",这回导致命令行发现";"后就结束掉SQL语句的执行。

4.2.3 变量

在MySQL中变量分为三种类型: 系统变量、用户定义变量、局部变量。

1.系统变量

系统变量是MySQL服务器提供,不是用户定义的,属于服务器层面。分为全局变量(GLOBAL)、

会话变量(SESSION)。全局变量针对于所有的会话。会话变量针对于单个会话,在另外一个会话窗口就不生效了。

A.查看系统变量

SHOW [ SESSION | GLOBAL ] VARIABLES ; -- 查看所有系统变量
SHOW [ SESSION | GLOBAL ] VARIABLES LIKE '......'; -- 可以通过LIKE模糊匹配方
式查找变量
SELECT @@[SESSION | GLOBAL] 系统变量名; -- 查看指定变量的值

B.设置系统变量

SET [ SESSION | GLOBAL ] 系统变量名 = 值 ;
SET @@[SESSION | GLOBAL]系统变量名 = 值 ;

注意:

如果没有指定SESSION/GLOBAL,默认是SESSION,会话变量。

mysql服务重新启动之后,所设置的全局参数会失效,要想不失效,可以在 /etc/my.cnf 中配置。

2.用户定义变量

用户定义变量是用户根据需要自己定义的变量,用户变量不用提前声明,在用的时候直接用 "@变量名" 使用就可以。其作用域为当前连接。

A.赋值

方式一:

SET @var_name = expr [, @var_name = expr] ... ;
SET @var_name := expr [, @var_name := expr] ... ;

方式二:

SELECT @var_name := expr [, @var_name := expr] ... ;
SELECT 字段名 INTO @var_name FROM 表名;

B.使用

SELECT @var_name ;

注意: 用户定义的变量无需对其进行声明或初始化,只不过获取到的值为NULL。

3.局部变量

局部变量 是根据需要定义的在局部生效的变量,访问之前,需要DECLARE声明。可用作存储过程内的 局部变量和输入参数,局部变量的范围是在其内声明的BEGIN ... END块。

将局部变量用做存储过程的输入参数的例子如下所示:

CREATE PROCEDURE myProcedure(IN param1 INT)
BEGIN
   DECLARE var1 INT;
   SET var1 = param1 * 2;
   ...
END;

A.声明

变量类型就是数据库字段类型:INT、BIGINT、CHAR、VARCHAR、DATE、TIME等。

DECLARE 变量名 变量类型 [DEFAULT ... ] ;

B.赋值

SET 变量名 = 值 ;
SET 变量名 := 值 ;
SELECT 字段名 INTO 变量名 FROM 表名 ... ;

4.2.4 if

if 用于做条件判断,具体的语法结构为:

IF 条件1 THEN
.....
ELSEIF 条件2 THEN -- 可选
.....
ELSE -- 可选
.....
END IF;

在if条件判断的结构中,ELSE IF 结构可以有多个,也可以没有。 ELSE结构可以有,也可以没有。

4.2.5 参数

参数的类型,主要分为以下三种:IN、OUT、INOUT。 具体的含义如下:

MySQL进阶篇(一)_第49张图片

 用法:

CREATE PROCEDURE 存储过程名称 ([ IN/OUT/INOUT 参数名 参数类型 ])
BEGIN
-- SQL语句
END ;

4.2.6 case

case结构及作用,和我们在基础篇中所讲解的流程控制函数很类似。有两种语法格式:

语法1:

-- 含义: 当case_value的值为 when_value1时,执行statement_list1,当值为 when_value2时,
执行statement_list2, 否则就执行 statement_list
CASE case_value
WHEN when_value1 THEN statement_list1
[ WHEN when_value2 THEN statement_list2] ...
[ ELSE statement_list ]
END CASE;

语法2:

-- 含义: 当条件search_condition1成立时,执行statement_list1,当条件search_condition2成
立时,执行statement_list2, 否则就执行 statement_list
CASE
WHEN search_condition1 THEN statement_list1
[WHEN search_condition2 THEN statement_list2] ...
[ELSE statement_list]
END CASE;

注意:如果判定条件有多个,多个条件之间,可以使用 and 或 or 进行连接。

4.2.7 while

while 循环是有条件的循环控制语句。满足条件后,再执行循环体中的SQL语句。具体语法为:

MySQL进阶篇(一)_第50张图片

4.2.8 repeat 

repeat是有条件的循环控制语句, 当满足until声明的条件的时候,则退出循环 。具体语法为:

-- 先执行一次逻辑,然后判定UNTIL条件是否满足,如果满足,则退出。如果不满足,则继续下一次循环
REPEAT
SQL逻辑...
UNTIL 条件
END REPEAT;

4.2.9 loop

LOOP 实现简单的循环,如果不在SQL逻辑中增加退出循环的条件,可以用其来实现简单的死循环。 LOOP可以配合一下两个语句使用:

LEAVE :配合循环使用,退出循环。

ITERATE:必须用在循环中,作用是跳过当前循环剩下的语句,直接进入下一次循环。

MySQL进阶篇(一)_第51张图片

上述语法中出现的 begin_label,end_label,label 指的都是我们所自定义的标记。

4.2.10 游标

游标(CURSOR)是用来存储查询结果集的数据类型 , 在存储过程和函数中可以使用游标对结果集进 行循环的处理。游标的使用包括游标的声明、OPEN、FETCH 和 CLOSE,其语法分别如下。

1.声明游标

DECLARE 游标名称 CURSOR FOR 查询语句 ;

2.打开游标

OPEN 游标名称 ;

3.获取游标记录

FETCH 游标名称 INTO 变量 [, 变量 ] 

4.关闭游标

CLOSE 游标名称 ;

案例:

根据传入的参数uage,来查询用户表tb_user中,所有的用户年龄小于等于uage的用户姓名 name和专业profession,并将用户的姓名和专业插入到所创建的一张新表 (id,name,profession)中。

-- 逻辑:
-- A. 声明游标, 存储查询结果集
-- B. 准备: 创建表结构
-- C. 开启游标
-- D. 获取游标中的记录
-- E. 插入数据到新表中
-- F. 关闭游标
create procedure p11(in uage int)
begin
    declare uname varchar(100);
    declare upro varchar(100);
    declare u_cursor cursor for select name,profession from tb_user where age <=uage;
    drop table if exists tb_user_pro;
    create table if not exists tb_user_pro(
        id int primary key auto_increment,
        name varchar(100),
        profession varchar(100)
    );
    open u_cursor;
    while true do
        fetch u_cursor into uname,upro;
        insert into tb_user_pro values (null, uname, upro);
    end while;
    close u_cursor;
end;
call p11(30);

上述的存储过程,最终我们在调用的过程中,会报错,之所以报错是因为上面的while循环中,并没有 退出条件。当游标的数据集获取完毕之后,再次获取数据,就会报错,从而终止了程序的执行。

MySQL进阶篇(一)_第52张图片

但是此时,tb_user_pro表结构及其数据都已经插入成功了,我们可以直接刷新表结构,检查表结构中的数据。

上述的功能,虽然我们实现了,但是逻辑并不完善,而且程序执行完毕,获取不到数据,数据库还报错。 接下来,需要完善这个存储过程,可以通过MySQL中提供的条件处理程序 Handler 来进行完善。

4.2.11 条件处理程序

条件处理程序(Handler)可以用来定义在流程控制结构执行过程中遇到问题时相应的处理步骤。

语法:

DECLARE handler_action HANDLER FOR condition_value [, condition_value]
... statement ;
handler_action 的取值:
CONTINUE: 继续执行当前程序
EXIT: 终止执行当前程序
condition_value 的取值:
SQLSTATE sqlstate_value: 状态码,如 02000
SQLWARNING: 所有以01开头的SQLSTATE代码的简写
NOT FOUND: 所有以02开头的SQLSTATE代码的简写
SQLEXCEPTION: 所有没有被SQLWARNING 或 NOT FOUND捕获的SQLSTATE代码的简写

案例:

继续来完成在上一小节提出的这个需求,并解决其中的问题。

-- 逻辑:
-- A. 声明游标, 存储查询结果集
-- B. 准备: 创建表结构
-- C. 开启游标
-- D. 获取游标中的记录
-- E. 插入数据到新表中
-- F. 关闭游标
create procedure p11(in uage int)
begin
    declare uname varchar(100);
    declare upro varchar(100);
    declare u_cursor cursor for select name,profession from tb_user where age <=uage;
    -- 声明条件处理程序 : 当SQL语句执行抛出的状态码为02000时,将关闭游标u_cursor,并退出
    declare exit handler for SQLSTATE '02000' close u_cursor;
    drop table if exists tb_user_pro;
    create table if not exists tb_user_pro(
        id int primary key auto_increment,
        name varchar(100),
        profession varchar(100)
    );
    open u_cursor;
    while true do
        fetch u_cursor into uname,upro;
        insert into tb_user_pro values (null, uname, upro);
    end while;
    close u_cursor;
end;
call p11(30);

4.3 存储函数

存储函数是有返回值的存储过程,存储函数的参数只能是IN类型的。具体语法如下:

CREATE FUNCTION 存储函数名称 ([ 参数列表 ])
RETURNS type [characteristic ...]
BEGIN
-- SQL语句
RETURN ...;
END ;

characteristic说明:

        DETERMINISTIC:相同的输入参数总是产生相同的结果

        NO SQL :不包含 SQL 语句。

        READS SQL DATA:包含读取数据的语句,但不包含写入数据的语句。

案例:

计算从1累加到n的值,n为传入的参数值。

create function fun1(n int)
returns int deterministic
begin
    declare total int default 0;
    while n>0 do
        set total := total + n;
        set n := n - 1;
    end while;
    return total;
end;
select fun1(50);

注意:

在mysql8.0版本中binlog默认是开启的,一旦开启了,mysql就要求在定义存储过程时,需要指定 characteristic特性,否则就会报如下错误:

4.4 触发器

4.4.1 介绍

        触发器是与表有关的数据库对象,指在insert/update/delete之前(BEFORE)或之后(AFTER),触 发并执行触发器中定义的SQL语句集合。触发器的这种特性可以协助应用在数据库端确保数据的完整性 , 日志记录 , 数据校验等操作 。

        使用别名OLD和NEW来引用触发器中发生变化的记录内容,这与其他的数据库是相似的。现在触发器还 只支持行级触发,不支持语句级触发。

MySQL进阶篇(一)_第53张图片

4.4.2 语法

1.创建触发器

CREATE TRIGGER trigger_name
BEFORE/AFTER INSERT/UPDATE/DELETE
ON tbl_name FOR EACH ROW -- 行级触发器
BEGIN
    trigger_stmt ;
END;

2.查看触发器

SHOW TRIGGERS ;

3.删除触发器

DROP TRIGGER [schema_name.]trigger_name ; -- 如果没有指定 schema_name,默认为当前数
据库 。

你可能感兴趣的:(#,MySQL,java,服务器,linux)