1000字范文,内容丰富有趣,学习的好帮手!
1000字范文 > MySql进阶篇---006:存储引擎 索引 SQL优化 视图 存储过程 变量 流程控制 游标

MySql进阶篇---006:存储引擎 索引 SQL优化 视图 存储过程 变量 流程控制 游标

时间:2019-01-28 13:58:05

相关推荐

MySql进阶篇---006:存储引擎 索引 SQL优化 视图 存储过程 变量 流程控制 游标

1. 存储引擎

1.1 MySQL体系结构

1).连接层

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

2).服务层

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

3).引擎层

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

4).存储层

数据存储层,主要是将数据(如: redolog、undolog、数据、索引、二进制日志、错误日志、查询日志、慢查询日志等)存储在文件系统之上(磁盘中)并完成与存储引擎的交互

和其他数据库相比,MySQL有点与众不同,它的架构可以在多种不同场景中应用并发挥良好作用。主要体现在存储引擎上,插件式的存储引擎架构,将查询处理和其他的系统任务以及数据的存储提取分离。 这种架构可以根据业务的需求和实际需要选择合适的存储引擎。

1.2 存储引擎介绍

大家可能没有听说过存储引擎,但是一定听过引擎这个词,引擎就是发动机,是一个机器的核心组件。 比如,对于舰载机、直升机、火箭来说,他们都有各自的引擎,是他们最为核心的组件。而我们在选择 引擎的时候,需要在合适的场景,选择合适的存储引擎,就像在直升机上,我们不能选择舰载机的引擎 一样。

而对于存储引擎,也是一样,他是mysql数据库的核心,我们也需要在合适的场景选择合适的存储引 擎。接下来就来介绍一下存储引擎。

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

1).建表时指定存储引擎

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

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

show engines;

示例演示:

A. 查询建表语句 ---- 默认存储引擎: InnoDB

show create table account;

AUTO_INCREMENT:id是自增的,那当我们插入下一条数据时,申请的id是多少。CHARSET:指定当前表的字符集COLLATE:排序方式COMMENT :注释信息

我们可以看到,创建表时,即使我们没有指定存储疫情,数据库也会自动选择默认的存储引擎。

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

show engines ;

Engine:有哪些存储引擎Support:是否支持Comment:注释Transactions:是否支持事务XA:是否支持XA协议Savepoints:是否支持保存点

C. 创建表 my_myisam , 并指定MyISAM存储引擎

create table my_myisam(id int,name varchar(10)) engine = MyISAM ;

D.创建表 my_memory , 指定Memory存储引擎

create table my_memory(id int,name varchar(10)) engine = Memory ;

1.3 存储引擎特点

上面我们介绍了什么是存储引擎,以及如何在建表时如何指定存储引擎,接下来我们就来介绍下来上面 重点提到的三种存储引擎InnoDB、MyISAM、Memory的特点。

1.3.1 InnoDB

1).介绍

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

2).特点

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

3).文件

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

参数:innodb_file_per_table

这个参数决定到底是多张表共用一个共享表空间,还是每一个表都对应一个表空间文件。mysql8.0,默认是on打开的代表每一张表都对应一个表空间文件。

#查看系统的变量:通过以下指令进行模糊匹配,根据系统命令名查看系统变量值show variables like 'innodb_file_per_table';

如果该参数开启,代表对于InnoDB引擎的表,每一张表都对应一个ibd文件。 我们直接打开MySQL的数据存放目录:C:\ProgramData\MySQL\MySQL Server 8.0\Data, 这个目录下有很多文件夹,不同的文件夹代表不同的数据库,我们直接打开itcast文件夹。

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

#account.ibd:想要打开的ibd文件名bd2sdi account.ibd

4).逻辑存储结构

表空间: 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:存储表结构信息 (文本文件可以直接打开,里面是json类型的数据)

xxx.MYD: 存储数据

xxx.MYI: 存储索引

1.3.3 Memory

1).介绍

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

2).特点

内存存放

hash索引(默认)

3).文件

xxx.sdi:存储表结构信息

1.3.4 区别及特点

面试题:

InnoDB引擎与MyISAM引擎的区别 ?

①. InnoDB引擎, 支持事务, 而MyISAM不支持。

②. InnoDB引擎, 支持行锁和表锁, 而MyISAM仅支持表锁, 不支持行锁。

③. InnoDB引擎, 支持外键, 而MyISAM是不支持的。

主要是上述三点区别,当然也可以从索引结构、存储限制等方面,更加深入的回答,具体参考如下官方文档:

/doc/refman/8.0/en/innodb-introduction.html

/doc/refman/8.0/en/myisam-storage-engine.html

1.4 存储引擎选择

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

InnoDB: 是Mysql的默认存储引擎,支持事务、外键。如果应用对事务的完整性有比较高的要求,在并发条件下要求数据的一致性,数据操作除了插入和查询之外,还包含很多的更新、删除操 作,那么InnoDB存储引擎是比较合适的选择。 常用 MyISAM : 如果应用是以读操作和插入操作为主,只有很少的更新和删除操作,并且对事务的完整性、并发性要求不是很高,那么选择这个存储引擎是非常合适的。 eg:业务系统中的日志相关数据、电商中的评论相关数据。很少用,在业务中一般使用NoSql中的mongodb 代替 MEMORY:将所有数据保存在内存中,访问速度快,通常用于临时表及缓存。MEMORY的缺陷就是 对表的大小有限制,太大的表无法缓存在内存中,而且无法保障数据的安全性。 很少用,在业务中一般使用NoSql中的redis代替

1.5 总结

2.索引(重要)

安装MySql(linux版本)

说明

企业中日常的生产环境,测试环境,开发环境在使用的时候,绝大部分使用的都是linux系统。所以呢我们要去模拟一下真实的企业开发当中mysql的使用。linux安装、卸载mysql8.0详情查看:MySQL-Linux安装、卸载

2.1 索引概述

2.1.1 介绍

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

一提到数据结构,大家都会有所担心,担心自己不能理解,跟不上节奏。不过在这里大家完全不用担 心,我们后面在讲解时,会详细介绍。

2.1.2 演示

表结构及其数据如下:

假如我们要执行的SQL语句为 : select * from user where age = 45;

1). 无索引情况

在无索引情况下,就需要从第一行开始扫描,一直扫描到最后一行,我们称之为 全表扫描,性能很低。

2). 有索引情况

如果我们针对于这张表建立了索引,假设索引结构就是二叉树,那么也就意味着,会对age这个字段建立一个二叉树的索引结构。

此时我们在进行查询时,只需要扫描三次就可以找到数据了,极大的提高的查询的效率。

备注: 这里我们只是假设索引的结构是二叉树,介绍一下索引的大概原理,只是一个示意图,并不是索引的真实结构,索引的真实结构,后面会详细介绍。

2.1.3 特点

优点:提高查询效率,提高排序效率缺点:占用磁盘空间,提高查询效率降低增删改效率。 这2个缺点可以忽略,现在磁盘很便宜,一个正常的业务逻辑增删改的比例较小,大部分是查询的业务。

2.2 索引结构

2.2.1 概述

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

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

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

2.2.2 二叉树

假如说MySQL的索引结构采用二叉树的数据结构,比较理想的结构如下:

特点:

每个结点最多有两颗子树,所以二叉树中不存在度大于2的结点。

左子树和右子树是有顺序的,次序不能任意颠倒。

即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。譬如:

如果主键是顺序插入的,则会形成一个单向链表,结构如下:

所以,如果选择二叉树作为索引结构,会存在以下缺点:

顺序插入时,会形成一个链表,查询性能大大降低大数据量情况下,层级较深,检索速度慢

此时大家可能会想到,我们可以选择红黑树,红黑树是一颗自平衡二叉树,那这样即使是顺序插入数据,最终形成的数据结构也是一颗平衡的二叉树,结构如下:

使用红黑树解决第一个问题

但是,即使如此,由于红黑树也是一颗二叉树,所以也会存在一个缺点:

大数据量情况下,层级较深,检索速度慢

所以,在MySQL的索引结构中,并没有选择二叉树或者红黑树,而选择的是B+Tree,那么什么是B+Tree呢?在详解B+Tree之前,先来介绍一个B-Tree

2.2.3 B-Tree

B-Tree,B树是一种多路平衡查找树,相对于二叉树,B树每个节点可以有多个分支,即多叉。

多路:指的是一个节点下面可以包含多个子节点。

以一颗最大度数(max-degree)为5(5阶)b-tree为例,那这个B树每个节点最多存储4个key,5 个指针:

知识小贴士:树的度数指的是一个节点的子节点个数

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

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

特点:

5阶的B树,每一个节点最多存储4个key,对应5个指针。一旦节点存储的key数量到达5,就会裂变,中间元素向上分裂。在B树中,非叶子节点和叶子节点都会存放数据

2.2.4 B+Tree

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

我们可以看到,两部分:

绿色框框起来的部分,是索引部分,仅仅起到索引数据的作用,不存储数据。红色框框起来的部分,是数据存储部分,在其叶子节点中要存储具体的数据。

我们可以通过一个数据结构可视化的网站来简单演示一下。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。然后观察一些数据插入过程中,节点的变化情况。

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

所有的数据都会出现在叶子节点。叶子节点形成一个单向链表。非叶子节点仅仅起到索引数据作用,具体的数据都是在叶子节点存放的。

上述我们所看到的结构是标准的B+Tree的数据结构,接下来,我们再来看看MySQL中优化之后的B+Tree。

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

2.2.5 Hash

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

1). 结构

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

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

2).特点

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

因为:我们在进行等值匹配的时候,只需要计算这个key对应的hash值,根据hash值在链表中查找对应的元素即可,他在存储的时候是没有顺序的,所以没有办法进行范围查询。

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

因为hash运算出来的结果是无序的。

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

3).存储引擎支持

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

思考题:为什么InnoDB存储引擎选择使用B+tree索引结构?

A. 相对于二叉树,层级更少,搜索效率高;

B. 对于B-tree,无论是叶子节点还是非叶子节点,都会保存数据,这样导致一页中存储的键值减少,指针跟着减少,要同样保存大量数据,只能增加树的高度,导致性能降低;

C. 相对Hash索引,B+tree支持范围匹配及排序操作;

2.3 索引分类

2.3.1 索引分类

主键索引: 如果一张表中指定了主键,那么在建表的时候会针对主键自动的创建一个索引。

MySQL的索引包括普通索引、唯一性索引、全文索引、单列索引、多列索引和空间索引等。

功能逻辑上说,索引主要有 4 种,分别是普通索引、唯一索引、主键索引、全文索引。按照物理实现方式,索引可以分为 2 种:聚簇索引和非聚簇索引。按照作用字段个数进行划分,分成单列索引和联合索引。

1 . 普通索引

在创建普通索引时,不附加任何限制条件,只是用于提高查询效率。这类索引可以创建在任何数据类型中,其值是否唯一和非空,要由字段本身的完整性约束条件决定。建立索引以后,可以通过索引进行查询。例如,在表student的字段name上建立一个普通索引,查询记录时就可以根据该索引进行查询。

2 . 唯一性索引

使用UNIQUE参数可以设置索引为唯一性索引,在创建唯一性索引时,限制该索引的值必须是唯一的,但允许有空值。在一张数据表里可以有多个唯一索引。

例如,在表student的字段email中创建唯一性索引,那么字段email的值就必须是唯一的。通过唯一性索引,可以更快速地确定某条记录。

3 . 主键索引

主键索引就是一种特殊的唯一性索引,在唯一索引的基础上增加了不为空的约束,也就是NOTNULL+UNIQUE,一张表里最多只有一个主键索引。

why?这是由主键索引的物理实现方式决定的,因为数据存储在文件中只能按照一种顺序进行存储。

4 . 单列索引

在表中的单个字段上创建索引。单列索引只根据该字段进行索引。单列索引可以是普通索引,也可以是唯一性索引,还可以是全文索引。只要保证该索引只对应一个字段即可。一个表可以有多个单列索引。

5 . 多列(组合、联合)索引

多列索引是在表的多个字段组合上创建一个索引。该索引指向创建时对应的多个字段,可以通过这几个字段进行查询,但是只有查询条件中使用了这些字段中的第一个字段时才会被使用。例如,在表中的字段id、name和gender上建立一个多列索引idx_id_name_gender,只有在查询条件中使用了字段id时该索引才会被使用。使用组合索引时遵循最左前缀集合

6 . 全文索引

全文索引(也称全文检索)是目前搜索引擎使用的一种关键技术。它能够利用【分词技术】等多种算法智能分析出文本文字中关键词的频率和重要性,然后按照一定的算法规则智能地筛选出我们想要的搜索结果。全文索引非常适合大型数据集,对于小的数据集,它的用处比较小。

使用参数FULLTEXT可以设置索引为全文索引。在定义索引的列上支持值的全文查找,允许在这些索引列中插入重复值和空值。全文索引只能创建在CHAR、VARCHARTEXT类型及其系列类型的字段上,查询数据量较大的字符串类型的字段时,使用全文索引可以提高查询速度。例如,表student的字段informationTEXT类型,该字段包含了很多文字信息。在字段information上建立全文索引后,可以提高查询字段information的速度。

全文索引典型的有两种类型:自然语言的全文索引和布尔全文索引。

自然语言搜索引擎将计算每一个文档对象和查询的相关度。这里,相关度是基于匹配的关键词的个数,以及关键词在文档中出现的次数。在整个索引中出现次数越少的词语,匹配时的相关度就越高。相反,非常常见的单词将不会被搜索,如果一个词语的在超过50%的记录中都出现了,那么自然语言的搜索将不会搜索这类词语。

MysQL数据库从3.23.23版开始支持全文索引,但MysQL5.6.4以前只有Myisam支持,5.6.4版本以后innodb才支持,但是官方版本不支持中文分词,需要第三方分词插件。在5.7.6版本,MysQL内置了ngram全文解析器,用来支持亚洲语种的分词。测试或使用全文索引时,要先看一下自己的MysQL版本、存储引擎和数据类型是否支持全文索引。

随着大数据时代的到来,关系型数据库应对全文索引的需求已力不从心,逐渐被solr、ElasticSearch等专门的搜索引擎所替代。

7 . 补充:空间索引

使用参数SPATIAL可以设置索引为空间索引。空间索引只能建立在空间数据类型上,这样可以提高系统获取空间数据的效率。MySQL中的空间数据类型包括GEOMETRY、POINT、LINESTRINGPOLYGON等。目前只有MyISAM存储引擎支持空间检索,而且索引的字段不能为空值。对于初学者来说,这类索引很少会用到。

2.3.2 聚集索引&二级索引

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

聚集索引选取规则:

如果存在主键,主键索引就是聚集索引。如果不存在主键,将使用第一个唯一(UNIQUE)索引作为聚集索引。如果表没有主键,或没有合适的唯一索引,则InnoDB会自动生成一个rowid作为隐藏的聚集索引。

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

聚集索引的叶子节点下挂的是这一行的数据。二级索引的叶子节点下挂的是该字段值对应的主键id值

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

具体过程如下:

①. 由于是根据name字段进行查询,所以先根据name='Arm’到name字段的二级索引中进行匹配查找。但是在二级索引中只能查找到 Arm 对应的主键值 10。②. 由于查询返回的数据是*,所以此时,还需要根据主键值10,到聚集索引中查找10对应的记录,最终找到10对应的行row。③. 最终拿到这一行的数据,直接返回即可。

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

思考题:

以下两条SQL语句,那个执行效率高? 为什么?A. select * from user where id = 10 ;B. select * from user where name = ‘Arm’ ;备注: id为主键,name字段创建的有索引;

解答:

A 语句的执行性能要高于B 语句。因为A语句直接走聚集索引,直接返回数据。 而B语句需要先查询name字段的二级索引,然后再查询聚集索引,也就是需要进行回表查询。

思考题:

InnoDB主键索引的B+tree高度为多高呢?

假设:

一行数据大小为1k,一页中可以存储16行这样的数据。InnoDB的指针占用6个字节的空

间,主键即使为bigint,占用字节数为8。

高度为2:

n * 8 + (n + 1) * 6 = 16*1024 , 算出n约为 1170 n:当前这个节点存储k的数量。8:主键占用的字节数n+1:指针的数量6:指针占用的字节空间16*1024:1k等于1024个字节,16k等于16384个字节。 1171* 16 = 18736 以上可以算出这个节点下面最多有1171个指针,每个指针指向下面的一个子节点一个子节点下面最多可以存储16行数据 也就是说,如果树的高度为2,则可以存储 18000 多条记录。

高度为3:

1171 * 1171 * 16 = 21939856以上算出每个根节点最多有1171个子节点,每个子节点下面又有1171个子节点,所以直接再乘以1171即可。也就是说,如果树的高度为3,则可以存储 2200w 左右的记录。

2.4 索引语法

2.4.1 创建索引

MySQL支持多种方法在单个或多个列上创建索引:在创建表的定义语句CREATE TABLE中指定索引列,使用ALTER TABLE语句在存在的表上创建索引,或者使用CREATE INDEX语句在已存在的表上添加索引。

1)创建表的时候创建索引

使用CREATE TABLE创建表时,除了可以定义列的数据类型外,还可以定义主键约束、外键约束或者唯一性约束,而不论创建哪种约束,在定义约束的同时相当于在指定列上创建了一个索引。

隐式的方式创建索引:

#隐式的方式创建索引。在声明有主键约束、唯一性约束、外键约束的字段上,会自动的添加相关的索引CREATE DATABASE dbtest2;USE dbtest2;CREATE TABLE dept(dept_id INT PRIMARY KEY AUTO_INCREMENT,dept_name VARCHAR(20));CREATE TABLE emp(emp_id INT PRIMARY KEY AUTO_INCREMENT,emp_name VARCHAR(20) UNIQUE,dept_id INT,CONSTRAINT emp_dept_id_fk FOREIGN KEY(dept_id) REFERENCES dept(dept_id));

显式的方式创建索引:

语法格式:

CREATE TABLE 表名 [字段名 字段类型][索引的类型] INDEX [索引的名字] (作用的字段 [字符串类型需要指明长度]) [索引在存储的时候是升序还是降序]CREATE TABLE table_name [col_name data_type][UNIQUE | FULLTEXT | SPATIAL] [INDEX | KEY] [index_name] (col_name [length]) [ASC | DESC]

UNIQUE 、 FULLTEXTSPATIAL为可选参数,分别表示唯一索引、全文索引和空间索引; 注意主键索引只能是通过添加约束的这种隐式方式来创建INDEXKEY为同义词,两者的作用相同,用来指定创建索引,推荐使用index;index_name指定索引的名称,为可选参数,如果不指定索引名,那么MySQL默认col_name字段为索引名;col_name为需要创建索引的字段列(如果是联合索引中间用逗号隔开),该列必须从数据表中定义的多个列中选择;length为可选参数,表示索引的长度,只有字符串类型的字段才能指定索引长度(eg:如果长度为20表示只取前20个作为索引的创建);ASCDESC指定升序或者降序的索引值存储。

1 . 创建普通索引

在book表中的year_publication字段上建立普通索引,SQL语句如下:

CREATE TABLE book(book_id INT ,book_name VARCHAR(100),AUTHORS VARCHAR(100),info VARCHAR(100) ,COMMENT VARCHAR(100),year_publication YEAR,#声明索引INDEX idx_bname(book_name));#通过命令查看索引 (也可以通过sqlyog在表下面查看索引)#方式1:结果是使用一行展示,可以加\G分行展示(在命令行中支持,在SqlYog中报错)SHOW CREATE TABLE book;#方式2:SHOW INDEX FROM book;#性能分析工具:EXPLAIN 查看执行这条sql时是否用到了索引,用到了查询速度就快。EXPLAIN SELECT * FROM book WHERE book_name = 'mysql高级';

2 . 创建唯一索引

举例:

# 声明有唯一索引的字段,在添加数据时,要保证唯一性,但是可以添加null,并且null值可以多次添加# 原因:创建了一个唯一性约束就会有一个唯一性索引,反之创建了一个唯一性索引也会自动有个唯一性约束CREATE TABLE book1(book_id INT ,book_name VARCHAR(100),AUTHORS VARCHAR(100),info VARCHAR(100) ,COMMENT VARCHAR(100),year_publication YEAR,#声明索引UNIQUE INDEX uk_idx_cmt(COMMENT));SHOW INDEX FROM book1;#查看索引#第一次执行成功,第二次执行失败,因为COMMENT字段添加唯一索引就相当于添加了唯一约束。INSERT INTO book1(book_id,book_name,COMMENT)VALUES(1,'Mysql高级','适合有数据库开发经验的人员学习');#插入的COMMENT字段为null,执行几次都可以成功。INSERT INTO book1(book_id,book_name,COMMENT)VALUES(2,'Mysql高级',NULL);SELECT * FROM book1;

3 . 主键索引

设定为主键后数据库会自动建立索引,innodb为聚簇索引,语法:

随表一起建索引:

#通过定义主键约束的方式定义主键索引(只能是通过添加约束的这种隐式方式创建)CREATE TABLE book2(book_id INT PRIMARY KEY ,book_name VARCHAR(100),AUTHORS VARCHAR(100),info VARCHAR(100) ,COMMENT VARCHAR(100),year_publication YEAR);SHOW INDEX FROM book2;

删除主键索引:

#通过删除主键约束的方式删除主键索引ALTER TABLE book2DROP PRIMARY KEY;

修改主键索引:必须先删除掉(drop)原索引,再新建(add)索引

4 . 创建单列索引

举例:

CREATE TABLE book3(book_id INT ,book_name VARCHAR(100),AUTHORS VARCHAR(100),info VARCHAR(100) ,COMMENT VARCHAR(100),year_publication YEAR,#声明索引 (为一个字段创建的索引就是单列索引)INDEX idx_bname(book_name));SHOW INDEX FROM book3; #查看索引

5 . 创建组合索引:能否使用要看是否满足最左前缀法则

举例:创建表test3,在表中的id、name和age字段上建立组合索引,SQL语句如下:

CREATE TABLE book4(book_id INT ,book_name VARCHAR(100),AUTHORS VARCHAR(100),info VARCHAR(100) ,COMMENT VARCHAR(100),year_publication YEAR,#声明索引 (为多个字段创建索引就是多列索引) 注意声明索引的顺序和是否可以使用索引有关联INDEX mul_bid_bname_info(book_id,book_name,info));SHOW INDEX FROM book4;#分析#可以使用索引,满足最左前缀法则EXPLAIN SELECT * FROM book4 WHERE book_id = 1001 AND book_name = 'mysql';#不可以使用索引,因为不满足最左前缀法则。EXPLAIN SELECT * FROM book4 WHERE book_name = 'mysql';

6 . 创建全文索引

FULLTEXT全文索引可以用于全文搜索,并且只为CHARVARCHARTEXT列创建索引。索引总是对整个列进行,不支持局部(前缀)索引。

举例1:创建表test4,在表中的info字段上建立全文索引,SQL语句如下:

CREATE TABLE test4(id INT NOT NULL,name CHAR(30) NOT NULL,age INT NOT NULL,info VARCHAR(255),#为info字段的前50个字符创建全文索引FULLTEXT INDEX futxt_idx_info(info(50))) ENGINE=MyISAM;

在MySQL5.7及之后版本中可以不指定最后的ENGINE了,因为在此版本中InnoDB支持全文索引。

语句执行完之后查看索引

SHOW INDEX FROM test4;

由结果可以看到,info字段上已经成功建立了一个名为futxt_idx_info的FULLTEXT索引。

举例2:

CREATE TABLE articles (id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,title VARCHAR (200),body TEXT,FULLTEXT index (title, body)) ENGINE = INNODB ;

创建了一个给title和body字段添加全文索引的表。

举例3:

CREATE TABLE `papers` (`id` int(10) unsigned NOT NULL AUTO_INCREMENT,`title` varchar(200) DEFAULT NULL,`content` text,PRIMARY KEY (`id`),FULLTEXT KEY `title` (`title`,`content`)) ENGINE=MyISAM DEFAULT CHARSET=utf8;

不同于like方式的的查询:

SELECT * FROM papers WHERE content LIKE ‘%查询字符串%’;

全文索引用match+against方式查询:

SELECT * FROM papers WHERE MATCH(title,content) AGAINST (‘查询字符串’);

明显的提高查询效率。

注意点

使用全文索引前,搞清楚版本支持情况;全文索引比 like + % 快 N 倍,但是可能存在精度问题;如果需要全文索引的是大量数据,建议先添加数据,再创建索引。 因为你不断添加数据的时候,索引也跟着变,反而导致添加数据变慢。

7 . 创建空间索引

空间索引创建中,要求空间类型的字段必须为非空

举例:创建表test5,在空间类型为GEOMETRY的字段上创建空间索引,SQL语句如下:

CREATE TABLE test5(geo GEOMETRY NOT NULL,SPATIAL INDEX spa_idx_geo(geo)) ENGINE=MyISAM;

语句执行完之后查看索引

SHOW INDEX FROM test4;

可以看到,test5表的geo字段上创建了名称为spa_idx_geo的空间索引。注意创建时指定空间类型字段值的非空约束,并且表的存储引擎为MylSAM。

2)在已经存在的表上创建索引

方式一:使用ALTER TABLE ADD语句创建索引,ALTER TABLE语句创建索引的基本语法如下:

ALTER TABLE 表名 ADD [索引的类型] INDEX [索引的名字] (作用的字段[字符类型需要指明长度],...) [索引在存储的时候是升序还是降序]ALTER TABLE table_name ADD [UNIQUE | FULLTEXT | SPATIAL] [INDEX | KEY][index_name] (col_name[length],...) [ASC | DESC]

举例:

#先创建一个表CREATE TABLE book5(book_id INT ,book_name VARCHAR(100),AUTHORS VARCHAR(100),info VARCHAR(100) ,COMMENT VARCHAR(100),year_publication YEAR);#查看这个表的索引情况SHOW INDEX FROM book5;#创建普通索引ALTER TABLE book5 ADD INDEX idx_cmt(COMMENT);#创建唯一索引ALTER TABLE book5 ADD UNIQUE uk_idx_bname(book_name);#创建联合索引ALTER TABLE book5 ADD INDEX mul_bid_bname_info(book_id,book_name,info);

方式二:使用CREATE INDEX ON创建索引,CREATE INDEX语句可以在已经存在的表上添加索引,在MySQL中,CREATE INDEX被映射到一个ALTER TABLE语句上,基本语法结构为:

关键字:CREATE INDEX ON索引类型 UNIQUE :创建的是一个唯一索引,要求该字段不能重复出现数据。FULLTEXT :创建的是一个全文索引SPATIAL:创建的是一个空间索引 如果没有写索引类型,代表创建的是一个常规索引。...:表示一个索引可以关联多个字段如果一个索引只关联一个字段,那么称这个索引为单列索引如果一个索引关联了多个字段,那么称这个索引为联合索引或者组合索引

CREATE [索引类型] INDEX 索引的名字ON 表名 (作用的字段[字符类型需要指明长度],...) [索引在存储的时候是升序还是降序]CREATE [UNIQUE | FULLTEXT | SPATIAL] INDEX index_nameON table_name (col_name[length],...) [ASC | DESC]

举例:

#创建表CREATE TABLE book6(book_id INT ,book_name VARCHAR(100),AUTHORS VARCHAR(100),info VARCHAR(100) ,COMMENT VARCHAR(100),year_publication YEAR);#查看索引SHOW INDEX FROM book6;#创建普通索引CREATE INDEX idx_cmt ON book6(COMMENT);#创建唯一索引CREATE UNIQUE INDEX uk_idx_bname ON book6(book_name);#创建组合索引CREATE INDEX mul_bid_bname_info ON book6(book_id,book_name,info);

2.4.2 查看索引

#方式1:查看创建表的语法,针对的是在建表时候创建的索引SHOW CREATE TABLE table_name ;#方式2:查看指定表中的所有索引(啥时候创建的索引都可以查看)SHOW INDEX FROM table_name ;

其中各个主要参数的含义为

Table表示创建索引的表。Non_unique表示索引非唯—,1代表非唯一索引,0代表唯一索引。Key_name表示索引的名称。Seq_in_index表示该字段在索引中的位置,单列索引该值为1,组合索引为每个字段在索引定义中的顺序。Column_name表示定义索引的列字段。Sub_part表示索引的长度。Null表示该字段是否能为空值。Index_type表示索引类型。

由结果可以看到,id字段上已经成功建立了一个名为mul_bid_bname_info的联合索引。

2.4.3 删除索引

1 . 使用ALTER TABLE删除索引ALTER TABLE删除索引的基本语法格式如下:

#对应的是使用ALTER TABLE创建索引的方式ALTER TABLE table_name DROP INDEX index_name;

2 . 使用DROP INDEX语句删除索引DROP INDEX删除索引的基本语法格式如下:

#对应的是使用CREATE INDEX创建索引的方式#删除哪一张表中的哪一个索引DROP INDEX index_name ON table_name;

提示

添加AUTO_INCREMENT约束字段的唯一索引不能被删除。原因:有唯一索引就会自动添加唯一约束,而自增作用的字段要么有主键约束要么有唯一约束,删除唯一索引也就没有唯一约束了,此时会报错,所以不能删除。

测试:

表book5没有删除之前的索引情况:普通索引,唯一索引,联合索引

# 02-索引的删除SHOW INDEX FROM book5; #查看索引#方式1:ALTER TABLE .... DROP INDEX ....ALTER TABLE book5 DROP INDEX idx_cmt;#方式2:DROP INDEX ... ON ...DROP INDEX uk_idx_bname ON book5;#测试:删除联合索引中的相关字段,索引的变化#效果:3个字段组成的联合索引,如果直接删除其中的一个字段,那么这个字段对应的索引也会被删除。#删除表中的字段book_nameALTER TABLE book5DROP COLUMN book_name;#删除表的字段book_idALTER TABLE book5DROP COLUMN book_id;#删除表的字段infoALTER TABLE book5DROP COLUMN info;

提示

删除表中的列时,如果要删除的列为联合索引的组成部分,则该列也会从索引中删除。如果组成

索引的所有列都被删除,则整个联合索引将被删除。

2.4.4 案例演示:

创建数据库 itcast:

先来创建一张表 tb_user,并且查询测试数据。

create table tb_user(id int primary key auto_increment comment '主键',name varchar(50) not null comment '用户名',phone varchar(11) not null comment '手机号',email varchar(100) comment '邮箱',profession varchar(11) comment '专业',age tinyint unsigned comment '年龄',gender char(1) comment '性别 , 1: 男, 2: 女',status char(1) comment '状态',createtime datetime comment '创建时间') comment '系统用户表';INSERT INTO tb_user (name, phone, email, profession, age, gender, status,createtime) VALUES ('吕布', '17799990000', 'lvbu666@', '软件工程', 23, '1','6', '2001-02-02 00:00:00');INSERT INTO tb_user (name, phone, email, profession, age, gender, status,createtime) VALUES ('曹操', '17799990001', 'caocao666@', '通讯工程', 33,'1', '0', '2001-03-05 00:00:00');INSERT INTO tb_user (name, phone, email, profession, age, gender, status,createtime) VALUES ('赵云', '17799990002', '17799990@', '英语', 34, '1','2', '2002-03-02 00:00:00');INSERT INTO tb_user (name, phone, email, profession, age, gender, status,createtime) VALUES ('孙悟空', '17799990003', '17799990@', '工程造价', 54,'1', '0', '2001-07-02 00:00:00');INSERT INTO tb_user (name, phone, email, profession, age, gender, status,createtime) VALUES ('花木兰', '17799990004', '19980729@', '软件工程', 23,'2', '1', '2001-04-22 00:00:00');INSERT INTO tb_user (name, phone, email, profession, age, gender, status,createtime) VALUES ('大乔', '17799990005', 'daqiao666@', '舞蹈', 22, '2','0', '2001-02-07 00:00:00');INSERT INTO tb_user (name, phone, email, profession, age, gender, status,createtime) VALUES ('露娜', '17799990006', 'luna_love@', '应用数学', 24,'2', '0', '2001-02-08 00:00:00');INSERT INTO tb_user (name, phone, email, profession, age, gender, status,createtime) VALUES ('程咬金', '17799990007', 'chengyaojin@', '化工', 38,'1', '5', '2001-05-23 00:00:00');INSERT INTO tb_user (name, phone, email, profession, age, gender, status,createtime) VALUES ('项羽', '17799990008', 'xiaoyu666@', '金属材料', 43,'1', '0', '2001-09-18 00:00:00');INSERT INTO tb_user (name, phone, email, profession, age, gender, status,createtime) VALUES ('白起', '17799990009', 'baiqi666@', '机械工程及其自动化', 27, '1', '2', '2001-08-16 00:00:00');INSERT INTO tb_user (name, phone, email, profession, age, gender, status,createtime) VALUES ('韩信', '17799990010', 'hanxin520@', '无机非金属材料工程', 27, '1', '0', '2001-06-12 00:00:00');INSERT INTO tb_user (name, phone, email, profession, age, gender, status,createtime) VALUES ('荆轲', '17799990011', 'jingke123@', '会计', 29, '1','0', '2001-05-11 00:00:00');INSERT INTO tb_user (name, phone, email, profession, age, gender, status,createtime) VALUES ('兰陵王', '17799990012', 'lanlinwang666@', '工程造价',44, '1', '1', '2001-04-09 00:00:00');INSERT INTO tb_user (name, phone, email, profession, age, gender, status,createtime) VALUES ('狂铁', '17799990013', 'kuangtie@', '应用数学', 43,'1', '2', '2001-04-10 00:00:00');INSERT INTO tb_user (name, phone, email, profession, age, gender, status,createtime) VALUES ('貂蝉', '17799990014', '84958948374@', '软件工程', 40,'2', '3', '2001-02-12 00:00:00');INSERT INTO tb_user (name, phone, email, profession, age, gender, status,createtime) VALUES ('妲己', '17799990015', '2783238293@', '软件工程', 31,'2', '0', '2001-01-30 00:00:00');INSERT INTO tb_user (name, phone, email, profession, age, gender, status,createtime) VALUES ('芈月', '17799990016', 'xiaomin2001@', '工业经济', 35,'2', '0', '2000-05-03 00:00:00');INSERT INTO tb_user (name, phone, email, profession, age, gender, status,createtime) VALUES ('嬴政', '17799990017', '8839434342@', '化工', 38, '1','1', '2001-08-08 00:00:00');INSERT INTO tb_user (name, phone, email, profession, age, gender, status,createtime) VALUES ('狄仁杰', '17799990018', 'jujiamlm8166@', '国际贸易',30, '1', '0', '-03-12 00:00:00');INSERT INTO tb_user (name, phone, email, profession, age, gender, status,createtime) VALUES ('安琪拉', '17799990019', 'jdodm1h@', '城市规划', 51,'2', '0', '2001-08-15 00:00:00');INSERT INTO tb_user (name, phone, email, profession, age, gender, status,createtime) VALUES ('典韦', '17799990020', 'ycaunanjian@', '城市规划', 52,'1', '2', '2000-04-12 00:00:00');INSERT INTO tb_user (name, phone, email, profession, age, gender, status,createtime) VALUES ('廉颇', '17799990021', 'lianpo321@', '土木工程', 19,'1', '3', '2002-07-18 00:00:00');INSERT INTO tb_user (name, phone, email, profession, age, gender, status,createtime) VALUES ('后羿', '17799990022', 'altycj2000@', '城市园林', 20,'1', '0', '2002-03-10 00:00:00');INSERT INTO tb_user (name, phone, email, profession, age, gender, status,createtime) VALUES ('姜子牙', '17799990023', '37483844@', '工程造价', 29,'1', '4', '-05-26 00:00:00');

表结构中插入的数据如下:

查看目前表中有哪些索引:只有主键索引

SHOW INDEX FROM tb_user;

数据准备好了之后,接下来,我们就来完成如下需求:

A.name字段为姓名字段,该字段的值可能会重复,为该字段创建索引。

说明:字段可以重复说明创建的索引不可能是唯一索引,只能是常规索引

CREATE INDEX idx_user_name ON tb_user(name);

再次查看目前表中有哪些索引:

B.phone手机号字段的值,是非空,且唯一的,为该字段创建唯一索引

CREATE UNIQUE INDEX idx_user_phone ON tb_user(phone);

再次查看目前表中有哪些索引:

C.为profession、age、status创建联合索引

#这个顺序是有讲究的,在索引的使用中讲解。CREATE INDEX idx_user_pro_age_sta ON tb_user(profession,age,status);

再次查看目前表中有哪些索引:

D.为email建立合适的索引来提升查询效率。

说明:邮件,创建常规索引即可。

CREATE INDEX idx_email ON tb_user(email);

再次查看目前表中有哪些索引:

show index from tb_user;

2.5 SQL性能分析

为什么要学习SQL性能分析的工具?

因为要做sql优化,想要做sql优化首先要定位出对哪一类的sql进行优化,这个时候就需要知道每一个sql的执行性能是什么样的。

sql优化主要是针对的查询语句。

优化查询语句的时候,索引这一块的优化占据主导地位。

2.5.1 SQL执行频率

为什么要知道sql的执行频率?假如一个数据库表中的数据量很大,如果这张表只做插入不做查询,那么就没有必要做sql优化了,所以要通过sql的执行频率来判定当前数据库到底是以查询为主,还是以插入为主,还是以修改为主,还是以删除为主。

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

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

Com_delete: 删除次数

Com_insert: 插入次数

Com_select: 查询次数

Com_update: 更新次数

我们可以在当前数据库再执行几次查询操作,然后再次查看执行频次,看看 Com_select 参数会不会变化。

执行2次查询,再次查看SQL执行频率,发现查询的频率由30变为36。

执行一次查询加3,可能和可视化工具有关。

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

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

即:当前通过sql的查询频次只知道select语句的权重比较高,那么我们到底要针对那些select语句进行优化呢????此时就需要借助数据库中的慢查询日志,来定位那些sql语句执行效率比较低,从而对这类的sql语句进行优化。

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

2.5.2 慢查询日志

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

即:只要我们执行了某一条sql,它的执行耗时超过10秒,那么Mysql就认为这一类的sql就是慢查询。此时就会记录在慢查询日志中。

MySQL的慢查询日志默认没有开启,我们可以查看一下开关的开启情况。

SHOW VARIABLES LIKE 'slow_query_log'

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

vim /etc/f

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

配置完毕之后,通过以下指令重新启动MySQL服务器进行测试,之后会生成一个日志文件。

# 重启Mysql服务器systemctl restart mysqld

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

查看慢日志文件中记录的信息:

# 进入到此目录,发现会有一个后缀是-slow.log的日志文件cd /var/lib/mysql/# 查看次日志文件:只记录了一些基本的信息,比如数据库的版本,端口。cat mysql8-slow.log

测试:

A.执行如下SQL语句 :

select * from tb_user; -- 这条SQL执行效率比较高, 执行耗时 0.00sec#tb_sku表存放了1000万条记录,电脑太卡,这里不在演示,只显示最终结果select count(*) from tb_sku; -- 由于tb_sku表中, 预先存入了1000w的记录, count一次,耗时13.35sec

B.检查慢查询日志 :

最终我们发现,在慢查询日志中,只会记录执行时间超多我们预设时间(2s)的SQL,执行较快的SQL 是不会记录的。

#查看慢日志文件尾部实时输出的内容,这要这个文件有新的内容追加上来,在这一块马上就可以刷新出来tail -f mysql8-slow.log

记录通过哪一个用户在哪一个主机上连接的耗时时长 锁了多少行 返回了多少记录用的哪一个数据库当前操作的时间执行的sql语句

那这样,通过慢查询日志,就可以定位出执行效率比较低的SQL,从而有针对性的进行优化。

2.5.3 profile详情

通过慢查询日志记录的是,sql执行耗时超过了我们预设的指定时间后才会记录。比如我们预设的指定时间是2秒,那么超过2秒他才会记录在慢查询日志当中,假如一些sql的执行耗时为1.9秒,它是不会记录在慢查询日志当中的,假如说业务系统当中有一些SQL语句它的业务很简单,但是它的执行达到了1.9几秒,那么这类的sql实际上也相对来说性能是比较低的。那么我们也需要对这类sql进行优化,那么我们如何定位到这类sql呢???慢查询日志满足不了,可以通过profile详情进行定位。

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

SELECT @@have_profiling ;

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

#查看这个默认的开关是否打开:0代表没有开启,1代表开启SELECT @@profiling ;#如果没有打开,可以设置为1打开开关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的耗时情况:

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

查看指定query_id的SQL语句CPU的使用情况

2.5.4 explain执行计划

以上3种sql性能分析的工具,sql语句的执行频次、慢查询日志、以及通过profile详情查看sql语句耗时以及时间耗费在哪了,这些都是通过时间的层面来评判一条sql语句的性能,执行时间段说明sql的性能比较高,这种判定只是粗略的判断,并不能真正的评判一条sql语句的性能,要想真正的查看一条sql语句的性能还要借助第4种手段explain,来查看sql的执行计划。

EXPLAIN 或者 DESC命令获取 MySQL 如何执行 SELECT 语句的信息,包括在 SELECT 语句它的执行过程中是否用到了索引,表的连接情况,表的连接顺序

语法:

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

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

type

NULL:业务系统中一般不太可能优化为null,一般是查询的时候不访问任何表才会出现

system:访问系统表

const:主键、唯一索引

eq_ref

ref:非唯一索引

range

index:用了索引,但是他也会对索引进行扫描,遍历整个索引数,比all快但性能也比较低。

all:全表扫描,性能比较低

2.6 索引使用

2.6.1 验证索引效率

在讲解索引的使用原则之前,先通过一个简单的案例,来验证一下索引,看看是否能够通过索引来提升数据查询性能。在演示的时候,我们还是使用之前准备的一张表 tb_sku , 在这张表中准备了1000w 的记录。

执行耗时:11.03秒

这张表中id为主键,有主键索引,而其他字段是没有建立索引的。 我们先来查询其中的一条记录,看看里面的字段情况,执行如下SQL:

#\G:表中的数据比较多,显示的时候变形了,加上\G会把每一列转化为一行进行展示。# 在命令行中使用,sqlyog中使用报错。select * from tb_sku where id = 1\G;

执行耗时:0.00秒

可以看到即使有1000w的数据,根据id进行数据查询,性能依然很快,因为主键id是有索引的。 那么接下来,我们再来根据 sn 字段进行查询,执行如下SQL:

SELECT * FROM tb_sku WHERE sn = '100000003145001';

执行耗时: 20.78秒

我们可以看到根据sn字段进行查询,查询返回了一条数据,结果耗时 20.78sec,就是因为sn没有索引,而造成查询效率很低。

那么我们可以针对于sn字段,建立一个索引,建立了索引之后,我们再次根据sn进行查询,再来看一 下查询耗时情况。

创建索引:它的执行耗时也比较长

创建索引就是要去构建一种数据结构,为这1000万条数据构建b+tree这种数据结构,所以耗时。

create index idx_sku_sn on tb_sku(sn) ;

然后再次执行相同的SQL语句,再次查看SQL的耗时。

SELECT * FROM tb_sku WHERE sn = '100000003145001'\G;

执行耗时: 0.01秒

我们明显会看到,sn字段建立了索引之后,查询性能大大提升。建立索引前后,查询耗时都不是一个数量级的。

2.6.2 最左前缀法则

主要针对联合索引

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

以 tb_user 表为例,我们先来查看一下之前 tb_user 表所创建的索引。

#查看表中的所有索引SHOW INDEX FROM tb_user ;

在 tb_user 表中,有一个联合索引,这个联合索引涉及到三个字段,顺序分别为:profession,age,status。

对于最左前缀法则指的是,查询时,最左变的列,也就是profession必须存在,否则索引全部失效。 而且中间不能跳过某一列,否则该列后面的字段索引将失效。 接下来,我们来演示几组案例,看一下具体的执行计划:

#满足左前缀法则,走索引explain select * from tb_user where profession = '软件工程' and age = 31 and status = '0';

#满足左前缀法则,走索引explain select * from tb_user where profession = '软件工程' and age = 31;

#满足左前缀法则,走索引explain select * from tb_user where profession = '软件工程';

以上的这三组测试中,我们发现只要联合索引最左边的字段 profession存在,索引就会生效,只不过索引的长度不同。 而且由以上三组测试,我们也可以推测出profession字段索引长度为47、age字段索引长度为2、status字段索引长度为5

#不满足左前缀法则,不走索引,走全表扫描。explain select * from tb_user where age = 31 and status = '0';

#不满足左前缀法则,不走索引,走全表扫描。explain select * from tb_user where status = '0';

而通过上面的这两组测试,我们也可以看到索引并未生效,原因是因为不满足最左前缀法则,联合索引最左边的列profession不存在。

#从最左边开始会走索引,由于跳过了age,所以后面的索引失效。explain select * from tb_user where profession = '软件工程' and status = '0';

上述的SQL查询时,存在profession字段,最左边的列是存在的,索引满足最左前缀法则的基本条件。但是查询时,跳过了age这个列,所以后面的列索引是不会使用的,也就是索引部分生效,所以索引的长度就是47。

思考题:

这个联合索引涉及到三个字段,顺序分别为:profession,age,status当执行SQL语句: explain select * from tb_user where age = 31 and

status = ‘0’ and profession = ‘软件工程’; 时,是否满足最左前缀法则,走不走上述的联合索引,索引长度?

可以看到,是完全满足最左前缀法则的,索引长度54,联合索引是生效的。注意 : 最左前缀法则中指的最左边的列,是指在查询时,联合索引的最左边的字段(即是第一个字段)必须存在,与我们编写SQL时,条件编写的先后顺序无关

2.6.3 范围查询

主要针对联合索引

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

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 andstatus = '0';

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

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

2.6.4 索引失效情况

2.6.4.1 索引列运算

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

2.4.4案例演示,在tb_user表中,除了前面介绍的联合索引之外,还创建了一个索引,是phone字段的唯一索引索引(单列索引)

#查看索引索引SHOW INDEX FROM tb_user;

A.当根据phone字段进行等值匹配查询时, 索引生效。

explain select * from tb_user where phone = '17799990015';

B.当根据phone字段进行函数运算操作之后,索引失效。

explain select * from tb_user where substring(phone,10,2) = '15';

2.6.4.2 字符串不加引号

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

接下来,我们通过两组示例,来看看对于字符串类型的字段,加单引号与不加单引号的区别:

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

满足最左前缀法则,走联合索引,长度发生变化,说明status没有走索引

explain select * from tb_user where phone = '17799990015';explain select * from tb_user where phone = 17799990015;

不走唯一索引。

经过上面两组示例,我们会明显的发现,如果字符串不加单引号,对于查询结果,没什么影响,但是数据库存在隐式类型转换,索引将失效。

2.6.4.3 模糊查询

如果仅仅是尾部模糊匹配,索引不会失效。如果是头部模糊匹配,索引失效。

接下来,我们来看一下这三条SQL语句的执行效果,查看一下其执行计划:

由于下面查询语句中,都是根据profession字段查询,符合最左前缀法则,联合索引是可以生效的, 我们主要看一下,模糊查询时,%加在关键字之前,和加在关键字之后的影响。

#索引生效explain select * from tb_user where profession like '软件%';#索引失效explain select * from tb_user where profession like '%工程';#索引失效explain select * from tb_user where profession like '%工%';

经过上述的测试,我们发现,在like模糊查询中,在关键字后面加%,索引可以生效。而如果在关键字 前面加了%,索引将会失效。

2.6.4.4 or连接条件

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

即:or 2侧都有索引,才会生效。

#id有主键索引,age没有索引:复合索引你单单只查询age所以不会用到复合索引explain select * from tb_user where id = 10 or age = 23;#phone有索引,age没索引explain select * from tb_user where phone = '17799990017' or age = 23;

由于age没有索引,所以即使id、phone有索引,索引也会失效。所以需要针对于age也要建立索引。

然后,我们可以对age字段建立索引。

create index idx_user_age on tb_user(age);

建立了索引之后,我们再次执行上述的SQL语句,看看前后执行计划的变化。

最终,我们发现,当or连接的条件,左右两侧字段都有索引时,索引才会生效。

2.6.4.5 数据分布影响

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

select * from tb_user where phone >= '17799990005';select * from tb_user where phone >= '17799990015';

经过测试我们发现,相同的SQL语句,只是传入的字段值不同,最终的执行计划也完全不一样,这是为什么呢?

就是因为MySQL在查询时,会评估使用索引的效率与走全表扫描的效率,如果走全表扫描更快,则放弃索引,走全表扫描。 因为索引是用来索引少量数据的,如果通过索引查询返回大批量的数据,则还不如走全表扫描来的快,此时索引就会失效。

接下来,我们再来看看 is null 与 is not null 操作是否走索引。

执行如下两条语句 :

#走索引:所有的profession都是有值的,is null过滤出极少部分数据,所以使用索引速度更快。explain select * from tb_user where profession is null;#不走索引:所有的profession都是有值的,is not null返回大部分数据,此时mysql会评估走索引还不如全表扫描快,所以会放弃走索引。explain select * from tb_user where profession is not null;

接下来,我们做一个操作将profession字段值全部更新为null。

然后,再次执行上述的两条SQL,查看SQL语句的执行计划。

#不走索引:profession都为null,此时is null返回大部分数据,所以走全表扫描explain select * from tb_user where profession is null;#走索引:profession都为null,此时is not null返回少部分数据,所以走索引explain select * from tb_user where profession is not null;

最终我们看到,一模一样的SQL语句,先后执行了两次,结果查询计划是不一样的,为什么会出现这种现象,这是和数据库的数据分布有关系。查询时MySQL会评估,走索引快,还是全表扫描快,如果全表扫描更快,则放弃索引走全表扫描。 因此,is null 、is not null是否走索引,得具体情况具体分析,并不是固定的。

2.6.5 SQL提示

目前tb_user表的数据情况如下:恢复更新的数据

索引情况如下:

把上述的 idx_user_age, idx_email 这两个之前测试使用过的索引直接删除。

drop index idx_user_age on tb_user;drop index idx_email on tb_user;

A.执行SQL :explain select * from tb_user where profession = '软件工程';

符合最左前缀法则,查询走了联合索引。

B.执行SQL,创建profession的单列索引:create index idx_user_pro on tb_user(profession);

C.创建单列索引后,再次执行A中的SQL语句,查看执行计划,此时既有复合索引又有单列索引,看看到底走哪个索引。

测试结果,我们可以看到,possible_keys中 idx_user_pro_age_sta,idx_user_pro 这两个索引都可能用到,最终MySQL选择了idx_user_pro_age_sta联合索引。这是MySQL自动选择的结果。

那么,我们能不能在查询的时候,自己来指定使用哪个索引呢? 答案是肯定的,此时就可以借助于MySQL的SQL提示来完成。 接下来,介绍一下SQL提示。

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

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

explain select * from tb_user use index(idx_user_pro) where profession = '软件工程';

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

explain select * from tb_user ignore index(idx_user_pro) where profession = '软件工程';

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

explain select * from tb_user force index(idx_user_pro) where profession = '软件工程';

示例演示:

A.use index

explain select * from tb_user use index(idx_user_pro) where profession = '软件工程';

B.ignore index

explain select * from tb_user ignore index(idx_user_pro) where profession = '软件工程';

C.force index

explain select * from tb_user force index(idx_user_pro_age_sta) where profession ='软件工程';

2.6.6 覆盖索引(Extra)

尽量使用覆盖索引,减少select *。 那么什么是覆盖索引呢? 覆盖索引是指查询使用了索引,并且需要返回的列,在该索引中已经全部能够找到

接下来,我们来看一组SQL的执行计划,看看执行计划的差别,然后再来具体做一个解析。

explain select id, profession from tb_user where profession = '软件工程' and age =31 and status = '0' ;explain select id,profession,age, status from tb_user where profession = '软件工程'and age = 31 and status = '0' ;explain select id,profession,age, status, name from tb_user where profession = '软件工程' and age = 31 and status = '0' ;explain select * from tb_user where profession = '软件工程' and age = 31 and status= '0';

上述这几条SQL的执行结果为:

从上述的执行计划我们可以看到,这四条SQL语句的执行计划前面所有的指标都是一样的,看不出来差异。但是此时,我们主要关注的是后面的Extra,前面两天SQL的结果为Using where; Using Index; 而后面两条SQL的结果为:Using index condition

这个Extra输出的值和mysql的版本有关,不同的版本输出的结果不同。

因为,在tb_user表中有一个联合索引idx_user_pro_age_sta,该索引关联了三个字段profession、age、status,而这个索引也是一个二级索引,所以叶子节点下面挂的是这一行的主键id。 所以当我们查询返回的数据在 id、profession、age、status 之中,则直接走二级索引直接返回数据了。 如果超出这个范围,就需要拿到主键id,再去扫描聚集索引,再获取额外的数据了,这个过程就是回表。 而我们如果一直使用select * 查询返回所有字段值,很容易就会造成回表查询(除非是根据主键查询,此时只会扫描聚集索引)。

为了大家更清楚的理解,什么是覆盖索引,什么是回表查询,我们一起再来看下面的这组SQL的执行过 程。

A.表结构及索引示意图:

id是主键,是一个聚集索引。 name字段建立了普通索引,是一个二级索引(辅助索引)。

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

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

C.执行SQL:selet id,name from tb_user where name = ‘Arm’;

虽然是根据name字段查询,查询二级索引,但是由于查询返回在字段为 id,name,在name的二级索引中,这两个值都是可以直接获取到的,因为覆盖索引,所以不需要回表查询,性能高。

D.执行SQL:selet id,name,gender from tb_user where name = ‘Arm’;

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

思考题:

一张表, 有四个字段(id, username, password, status), 由于数据量大, 需要对以下SQL语句进行优化, 该如何进行才是最优方案:select id,username,password from tb_user where username = 'itcast';答案: 针对于 username, password建立联合索引, sql为: create index

idx_user_name_pass on tb_user(username,password);这样可以避免上述的SQL语句,在查询的过程中,出现回表查询。思路:首先想要提高查询效率肯定需要建立索引,重点是建立什么样的索引才能是提高sql性能的最优方案 情况1:针对username 字段建立单列索引,可以提高性能,但是查询返回的字段不仅仅包含id、username ,还包含password 所以会进行回表查询。情况2:针对于 username, password建立联合索引,这样直接在二级索引中就可以查询到想要的数据了,直接覆盖索引避免回表查询,性能比较好。

2.6.7 前缀索引

当字段类型为字符串(varchar,text,longtext等)时,有时候需要索引很长的字符串(eg:存储一篇文章),这会让索引变得很大(如果根据文章的内容进行查询,那我们直接对文章的内容去建立一个索引),查询时,浪费大量的磁盘IO, 影响查询效率。此时可以只将字符串的一部分前缀,建立索引,这样可以大大节约索引空间,从而提高索引效率。

1).语法

#和之前创建索引的语法基本一致,只需要在对应的写一个字段后面(n),表示我要提取这个字符串的前面几个字符来建立索引。create index idx_xxxx on table_name(column(n)) ;

示例:

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

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

2).前缀长度

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

需求:想要知道tb_user表中email字段的选择性。 首先查询当前表中的总记录数: count(*) ,24条其次查询表中email字段不重复的数量:count(distinct email),24条选择性:24/24=1,选择性为1的性能最好。

#选择性为1,性能最好select count(distinct email) / count(*) from tb_user ;#表示从email的第一个字符开始截取,截取5个。select count(distinct substring(email,1,5)) / count(*) from tb_user ;截取前10个,选择性为1,也就是说截取email的前10个字符数据也是不重复的。截取前9个,选择性为0.9583,出现重复。截取前8个,选择性为0.9583,出现重复。截取前7个,选择性为0.9583,出现重复。截取前6个,选择性为0.9583,出现重复。截取前5个,选择性为0.9583,出现重复。截取前4个,选择性为0.9167,出现重复。总结:这个就带看我们业务系统中想要多大的选择性,如果选择性尽可能高那么就截取10个前缀,如果想要平衡选择性和索引的体积(截取的长度),那么就可以考虑取前5个前缀。

3).前缀索引的查询流程

针对主键构建一个聚集索引针对email的前5个字符构建一个前缀索引/二级索引执行:select * from tb_user where email = lvbu666@的过程: 首先到二级索引中进行匹配,注意不是整个字段匹配而是截取它的前5个前缀进行匹配,拿到二级索引中的id。之后在聚集索引中进行回表查询,根据id查询对应的一行数据,注意这里不是拿到这一行数据直接返回。因为刚才在对比的时候我只是对比了前缀,拿到这一行数据我要从这一行数据中拿出email的值,然后再去看这一行的数据email的值是不是我说传递进来的email,如果是那么此时我就要将这一行的数据来查询到并且返回,然后在去查询当前lvbu6在往下一个节点走,因为它是一个链表,我再去找下一个元素是否为lvbu6,如果不是直接返回这一行数据,如果是那我接下来还需要再去查询下一行的数据,再把下一行的数据也拿到最终组装数据并返回。

2.6.8 单列索引与联合索引的选择

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

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

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

id;主键索引phone:单列索引、唯一索引name:单列索引profession、age、status:联合索引email:前缀索引

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

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

explain select id,phone,name from tb_user where phone = '17799990010' and name = '韩信';

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

多条件联合查询时,MySQL优化器会评估哪个字段的索引效率更高,会选择该索引完成本次查询。

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

#phone已经是一个唯一索引了,再加上name字段那么这个联合索引也是一个唯一的,所以# 可以在前面加上一个关键字unique创建一个唯一索引。create unique index idx_user_phone_name on tb_user(phone,name);

此时再次执行上面的查询语句,可能用到的索引有3个 phone的单列索引、name的单列索引、phone和name的组合索引。实际上使用的是phone的单列索引,这是mysql自己选择的结果。

Extra:显示Null,代表使用回表查询。

使用sql提示指定使用联合索引:

explain select id,phone,name from tb_user use index(idx_user_phone_name) where phone = '17799990010' and name = '韩信';

Extra:显示Using index,代表使用的是覆盖索引,不需要使用回表查询。

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

在业务场景中,如果存在多个查询条件,考虑针对于查询字段建立索引时,建议建立联合索引, 而非单列索引。因为:使用联合索引性能相对比较高,而且联合索引使用得当是可以避免使用回表查询,而使用单列索引很容易出现回表查询的,造成性能降低。

如果查询使用的是联合索引,具体的结构示意图如下:

每一个节点所存储的键值就是phone和name的组合情况,它会先按照phone手机号进行排序,如果手机号一致在按照name字段排序,最终构建成一个b+tree。当然所构建出的这个联合索引属于二级索引,叶子节点挂的是这一页行记录对应的主键。

为什么不需要回表查询???

因为我们查询返回的只是id、phone、name,走这个联合索引查询返回的恰好有id、phone、name,走这个二级索引已经获取到了想要的数据(覆盖索引),所以不需要进行回表查询。

在创建联合索引的时候,对于哪一个字段放在前面哪一个字段放在后面对于查询的结果有没有影响???

有影响,根据之前学习的最左前缀法则,每一次查询想要使用联合索引那么最左边的列必须存在。如果是(phone,name)表示最左边的phone必须存在,如果是(name,phone)表示最左边的name必须存在,所以在创建联合索引的时候我们需要考虑字段的顺序。

2.7 索引设计原则

为了使索引的使用效率更高,在创建索引时,必须考虑在哪些字段上创建索引和创建什么类型的索引。索引设计不合理或者缺少索引都会对数据库和应用程序的性能造成障碍。高效的索引对于获得良好的性能非常重要。设计索引时,应该考虑相应准则。

2.7.1 数据准备

第1步:创建数据库、创建表

CREATE DATABASE atguigudb1;USE atguigudb1;#1.创建学生表和课程表CREATE TABLE `student_info` (`id` INT(11) AUTO_INCREMENT,`student_id` INT NOT NULL ,`name` VARCHAR(20) DEFAULT NULL,`course_id` INT NOT NULL ,`class_id` INT(11) DEFAULT NULL,`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,PRIMARY KEY (`id`)) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;CREATE TABLE `course` (`id` INT(11) NOT NULL AUTO_INCREMENT,`course_id` INT NOT NULL ,`course_name` VARCHAR(40) DEFAULT NULL,PRIMARY KEY (`id`)#还可以设置自增的初始值) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

第2步:创建模拟数据必需的存储函数

#函数1:创建随机产生字符串函数(直接创建会报错,需要设置系统变量为1)#报错原因:主从复制主机写操作记录在bin-log日志中,从机读取bin-log日志这样来保证#主从数据的一致,编写sql的时候默认情况下定义一些函数,就有可能函数记录到#bin-log日志中,在从机读的时候有可能回达到数据不一致的情况出现,所以默认#mysql服务器不相信这个创建函数,所以要想创建需要打开这个开关设置为1.DELIMITER //CREATE FUNCTION rand_string(n INT) RETURNS VARCHAR(255) #该函数会返回一个字符串BEGIN DECLARE chars_str VARCHAR(100) DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';DECLARE return_str VARCHAR(255) DEFAULT '';DECLARE i INT DEFAULT 0;WHILE i < n DO SET return_str =CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*52),1));SET i = i + 1;END WHILE;RETURN return_str;END //DELIMITER ;#解决:#查看此系统变量:默认是0SELECT @@log_bin_trust_function_creators;#设置为1,再次执行上面的函数1就可以创建成功了SET GLOBAL log_bin_trust_function_creators = 1;

#函数2:创建随机数函数DELIMITER //CREATE FUNCTION rand_num (from_num INT ,to_num INT) RETURNS INT(11)BEGIN DECLARE i INT DEFAULT 0; SET i = FLOOR(from_num +RAND()*(to_num - from_num+1)) ;RETURN i; END //DELIMITER ;

创建函数,假如报错:

This function has none of DETERMINISTIC......

由于开启过慢查询日志bin-log, 我们就必须为我们的function指定一个参数。

主从复制,主机会将写操作记录在bin-log日志中。从机读取bin-log日志,执行语句来同步数据。如果使用函数来操作数据,会导致从机和主键操作时间不一致。所以,默认情况下,mysql不开启创建函数设置。

查看mysql是否允许创建函数:

show variables like 'log_bin_trust_function_creators';

命令开启:允许创建函数设置:

#global :全局set global log_bin_trust_function_creators=1; # 不加global只是当前窗口有效。

mysqld重启,上述参数又会消失。永久方法: windows下:my.ini[mysqld]加上:log_bin_trust_function_creators=1linux下:/etc/f下f[mysqld]加上:log_bin_trust_function_creators=1

第3步:创建插入模拟数据的存储过程

# 存储过程1:创建插入课程表存储过程DELIMITER //CREATE PROCEDURE insert_course( max_num INT )BEGIN DECLARE i INT DEFAULT 0; SET autocommit = 0; #设置手动提交事务REPEAT #循环SET i = i + 1; #赋值INSERT INTO course (course_id, course_name ) VALUES (rand_num(10000,10100),rand_string(6)); UNTIL i = max_num END REPEAT; COMMIT; #提交事务END //DELIMITER ;

# 存储过程2:创建插入学生信息表存储过程DELIMITER //CREATE PROCEDURE insert_stu( max_num INT )BEGIN DECLARE i INT DEFAULT 0; SET autocommit = 0; #设置手动提交事务REPEAT #循环SET i = i + 1; #赋值INSERT INTO student_info (course_id, class_id ,student_id ,NAME ) VALUES (rand_num(10000,10100),rand_num(10000,10200),rand_num(1,200000),rand_string(6)); UNTIL i = max_num END REPEAT; COMMIT; #提交事务END //DELIMITER ;

第4步:调用存储过程

#课程表:100门课CALL insert_course(100);SELECT COUNT(*) FROM course;#学生表:100万条记录CALL insert_stu(1000000); #耗时49秒SELECT COUNT(*) FROM student_info;

2.7.2 哪些情况适合创建索引(11种)

1)字段的数值有唯一性的限制

索引本身可以起到约束的作用,比如唯一索引、主键索引都是可以起到唯一性约束的,因此在我们的数据表中,如果某个字段是唯一性的,就可以直接创建唯一性索引,或者主键索引。这样可以更快速地通过该索引来确定某条记录。

解释:

如果这个字段上已经添加了唯一性约束,那么就不需要在创建索引了,因为添加了唯一约束会自动创建索引。 添加唯一性索引会自动添加唯一性约束,反过来添加唯一性约束也会自动创建唯一性索引。 这里指的是这个字段明明是唯一的,但是他既没有唯一性约束有没有唯一性索引,这个时候就可以为这个字段添加唯一索引来提高查询的效率。

例如,学生表中学号是具有唯一性的字段,为该字段建立唯一性索引可以很快确定某个学生的信息,如果使用姓名的话,可能存在同名现象,从而降低查询速度。

业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。(来源:Alibaba)说明:不要以为唯一索引影响了 insert 速度(因为添加数据的时候会维护这个唯一的索引,也要进行修改),这个速度损耗可以忽略,但提高查找速度是明显的。
2)频繁作为 WHERE 查询条件的字段

某个字段在SELECT语句的 WHERE 条件中经常被使用到,那么就需要给这个字段创建索引了。尤其是在数据量大的情况下,创建普通索引就可以大幅提升数据查询的效率。

比如student_info数据表(含100万条数据),假设我们想要查询 student_id=123110 的用户信息。

#查看当前stduent_info表中的索引 SHOW INDEX FROM student_info;#目前只有主键id索引,student_id字段上没有索引的:SELECT course_id, class_id, NAME, create_time, student_id FROM student_infoWHERE student_id = 123110; #耗时276ms#给student_id字段添加索引ALTER TABLE student_infoADD INDEX idx_sid(student_id);#student_id字段上有索引的:SELECT course_id, class_id, NAME, create_time, student_id FROM student_infoWHERE student_id = 123110; #43ms

3)经常 GROUP BY 和 ORDER BY 的列

索引就是让数据按照某种顺序进行存储或检索,因此当我们使用 GROUP BY 对数据进行分组查询,或者使用 ORDER BY 对数据进行排序的时候,就需要对分组或者排序的字段进行索引。如果待排序的列有多个,那么可以在这些列上建立组合索引

比如student_info数据表(含100万条数据),假设我们想要查询 student_id=123110 的用户信息。

测试1:GROUP BY

#student_id字段上有索引的:SELECT student_id, COUNT(*) AS num FROM student_info GROUP BY student_id LIMIT 100; #41ms#删除idx_sid索引DROP INDEX idx_sid ON student_info;#student_id字段上没有索引的:SELECT student_id, COUNT(*) AS num FROM student_info GROUP BY student_id LIMIT 100; #866ms

测试2:ORDER BY

效果同上

测试3:GROUP BY + ORDER BY

如果同时有GROUP BY和ORDER BY的情况:比如我们按照student_id进行分组,同时按照创建时间(create_time)降序的方式进行排序,这时我们就需要同时进行GROUP BY和ORDER BY,那么是不是需要单独创建student_id的索引和create_time的索引呢? 还是创建它们2个的联合索引???

现在student_info这张表只有主键索引

情况1:对student_id和create_time分别创建索引

#添加单列索引ALTER TABLE student_infoADD INDEX idx_sid(student_id);ALTER TABLE student_infoADD INDEX idx_cre_time(create_time);SELECT student_id, COUNT(*) AS num FROM student_info GROUP BY student_id ORDER BY create_time DESC LIMIT 100; #5.212s

情况2:对student_id和create_time创建联合索引

student_id在前,create_time在后

#添加联合索引#mysql8.0才支持索引的降序排序,5.7不支持ALTER TABLE student_infoADD INDEX idx_sid_cre_time(student_id,create_time DESC);SELECT student_id, COUNT(*) AS num FROM student_info GROUP BY student_id ORDER BY create_time DESC LIMIT 100; #0.257s

情况3:对student_id和create_time创建联合索引

create_time在前,student_id在后

#此时有2个联合索引:情况2的和现在情况3创建的联合索引,他们的区别是2个索引的字段顺序相反ALTER TABLE student_infoADD INDEX idx_cre_time_sid(create_time DESC,student_id);#把情况2创建的联合索引删除,因为查询的sql是先执行分组在执行排序,# 此时仍然使用的是原来情况2的联合索引,无法测试查看效果。DROP INDEX idx_sid_cre_time ON student_info;#现在还剩下主键索引,情况1创建的2个单独的索引,本情况3创建的联合索引SELECT student_id, COUNT(*) AS num FROM student_info GROUP BY student_id ORDER BY create_time DESC LIMIT 100; #3.790s

效果:发现使用的是student_id字段的单独索引,而没有使用此联合索引。

原因:sql执行顺序问题,先执行分组在执行排序,在执行分组的时候已经找到了索引所以此时优先使用分组的单独索引。

4)UPDATE、DELETE 的 WHERE 条件列

当我们对某条数据进行UPDATE或者DELETE操作的时候,是否也需要对WHERE的条件列创建索引呢?

#name字段没有索引UPDATE student_info SET student_id = 10002 WHERE NAME = '462eed7ac6e791292a79'; #0.633s#添加索引ALTER TABLE student_infoADD INDEX idx_name(NAME);UPDATE student_info SET student_id = 10001 WHERE NAME = '462eed7ac6e791292a79'; #0.001s

对数据按照某个条件进行查询后再进行 UPDATE 或 DELETE 的操作,如果对 WHERE 字段创建了索引,就能大幅提升效率。原理是因为我们需要先根据 WHERE 条件列检索出来这条记录,然后再对它进行更新或删除。如果进行更新的时候,更新的字段是非索引字段,提升的效率会更明显,这是因为非索引字段更新不需要对索引进行维护。

5)DISTINCT 字段需要创建索引

有时候我们需要对某个字段进行去重,使用 DISTINCT,那么对这个字段创建索引,也会提升查询效率。

比如,我们想要查询课程表中不同的 student_id 都有哪些,如果我们没有对 student_id 创建索引,执行SQL 语句:

SELECT DISTINCT(student_id) FROM `student_info`;

运行结果(600637 条记录,运行时间0.683s):

如果我们对 student_id 创建索引,再执行 SQL 语句:

#因为使用索引后,相同字段挨在一起自然查询的速度就变快了。SELECT DISTINCT(student_id) FROM `student_info`;

运行结果(600637 条记录,运行时间0.010s):

你能看到 SQL 查询效率有了提升,同时显示出来的 student_id 还是按照递增的顺序进行展示的。这是因为索引会对数据按照某种顺序进行排序,所以在去重的时候也会快很多。

6)多表 JOIN 连接操作时,创建索引注意事项

首先,连接表的数量尽量不要超过 3 张,因为每增加一张表就相当于增加了一次嵌套的循环,数量级增长会非常快,严重影响查询的效率。

其次,对 WHERE 条件创建索引,因为 WHERE 才是对数据条件的过滤。如果在数据量非常大的情况下,没有 WHERE 条件过滤是非常可怕的。

最后,对用于连接的字段创建索引,并且该字段在多张表中的类型必须一致。比如 course_id 在student_info 表和 course 表中都为 int(11) 类型,而不能一个为 int 另一个为 varchar 类型。

#NAME 有索引SELECT s.course_id, NAME, s.student_id, c.course_name FROM student_info s JOIN course cON s.course_id = c.course_idWHERE NAME = '462eed7ac6e791292a79'; #0.001s#删除索引DROP INDEX idx_name ON student_info;#NAME 没有索引SELECT s.course_id, NAME, s.student_id, c.course_name FROM student_info s JOIN course cON s.course_id = c.course_idWHERE NAME = '462eed7ac6e791292a79'; #0.227s

7)使用列的类型小的创建索引

我们这里所说的类型大小指的就是该类型表示的数据范围的大小。

我们在定义表结构的时候要显式的指定列的类型,以整数类型为例,有TINYINT、MEDIUMINT、INT、BIGINT等,它们占用的存储空间依次递增,能表示的整数范围当然也是依次递增。如果我们想要对某个整数列建立索引的话,在表示的整数范围允许的情况下,尽量让索引列使用较小的类型,比如我们能使用INT就不要使用BIGINT,能使用MEDIUMINT就不要使用INT。这是因为:

数据类型越小,在查询时进行的比较操作越快数据类型越小,索引占用的存储空间就越少,在一个数据页内就可以放下更多的记录,从而减少磁盘I/0带来的性能损耗,也就意味着可以把更多的数据页缓存在内存中,从而加快读写效率。

这个建议对于表的主键来说更加适用,因为不仅是聚簇索引中会存储主键值,其他所有的二级索引的节点处都会存储一份记录的主键值,如果主键使用更小的数据类型,也就意味着节省更多的存储空间和更高效的/0。

8)使用字符串前缀创建索引

假设我们的字符串很长,那存储一个字符串就需要占用很大的存储空间。在我们需要为这个字符串列建立索引时,那就意味着在对应的B+树中有这么两个问题:

B+树索引中的记录需要把该列的完整字符串存储起来,更费时。而且字符串越长,在索引中占用的存储空间越大。如果B+树索引中索引列存储的字符串很长,那在做字符串比较时会占用更多的时间。

我们可以通过截取字段的前面一部分内容建立索引,这个就叫前缀索引。这样在查找记录时虽然不能精确的定位到记录的位置,但是能定位到相应前缀所在的位置,然后根据前缀相同的记录的主键值回表查询完整的字符串值。既节约空间,又减少了字符串的比较时间,还大体能解决排序的问题。

例如,TEXT和BLOG类型的字段,进行全文检索会很浪费时间,如果只检索字段前面的若干字符,这样可以提高检索速度。

创建一张商户表,因为地址字段比较长,在地址字段上建立前缀索引

create table shop(address varchar(120) not null);alter table shop add index(address(12));

问题是,截取多少呢?截取得多了,达不到节省索引存储空间的目的;截取得少了,重复内容太多,字段的散列度(选择性)会降低。怎么计算不同的长度的选择性呢?

先看一下字段在全部数据中的选择度:

select count(distinct address) / count(*) from shop;

通过不同长度去计算,与全表的选择性对比:

公式:

count(distinct left(列名, 索引长度))/count(*)

例如:

select count(distinct left(address,10)) / count(*) as sub10, -- 截取前10个字符的选择度count(distinct left(address,15)) / count(*) as sub11, -- 截取前15个字符的选择度count(distinct left(address,20)) / count(*) as sub12, -- 截取前20个字符的选择度count(distinct left(address,25)) / count(*) as sub13 -- 截取前25个字符的选择度from shop;

引申另一个问题:索引列前缀对排序的影响

如果使用了索引列前缀,比方说前边只把address列的前12个字符放到了二级索引中,下边这个查询可能就有点儿尴尬了:

select * from shoporder by addresslimit 12;

因为二级索引中不包含完整的address列信息,所以无法对前12个字符相同,后边的字符不同的记录进行排序,也就是使用索引列前缀的方式无法支持使用索引排序,只能使用文件排序。

拓展:Alibaba《Java开发手册》

强制】在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本

区分度决定索引长度。

说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会高达 90% 以上,可以使用 count(distinct left(列名, 索引长度))/count(*)的区分度来确定。

9)区分度高(散列性高)的列适合作为索引

列的基数指的是某一列中不重复数据的个数,比方说某个列包含值2,5,8,2,5,8,2,5,8,虽然有9条记录,但该列的基数却是3。也就是说,在记录行数一定的情况下,列的基数越大,该列中的值越分散;列的基数越小,该列中的值越集中。这个列的基数指标非常重要,直接影响我们是否能有效的利用索引。最好为列的基数大的列建立索引,为基数太小列的建立索引效果可能不好。

可以使用公式select count(distinct a)/count(*) from t1计算区分度,越接近1越好,一般超过33%就算是比较高效的索引了。

拓展:联合索引把区分度高(散列性高)的列放在前面。

10)使用最频繁的列放到联合索引的左侧

这样也可以较少的建立一些索引。同时,由于"最左前缀原则",可以增加联合索引的使用率。

SELECT *FROM student_infoWHERE student_id = 10013 AND course_id = 100;# 为上面的这个查询语句创建联合索引的时候,把student_id放在左边 course_id 放在右边。# 索引在使用的时候优先考虑使用频繁的字段作为索引,如果是把course_id 放在前面这个字段使用的不频繁# 在有别的索引情况下可能使用了别的索引,而没有用到联合索引。

11)在多个字段都要创建索引的情况下,联合索引优于单值索引

2.7.3 限制索引的数目

在实际工作中,我们也需要注意平衡,索引的数目不是越多越好。我们需要限制每张表上的索引数量,建议单张表索引数量不超过6个。原因:

每个索引都需要占用磁盘空间,索引越多,需要的磁盘空间就越大。索引会影响INSERT、DELETE、UPDATE等语句的性能,因为表中的数据更改的同时,索引也会进行调整和更新,会造成负担。优化器在选择如何优化查询时,会根据统一信息,对每一个可以用到的索引来进行评估,以生成出一个最好的执行计划,如果同时有很多个索引都可以用于查询,会增加MysQL优化器生成执行计划时间,降低查询性能。

2.7.4 哪些情况不适合创建索引(7种)

1)在where中使用不到的字段,不要设置索引

WHERE条件(包括GROUP BY、ORDER BY)里用不到的字段不需要创建索引,索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的。举个例子:

SELECT course_id, student_id, create_time FROM student_infoWHERE student_id = 41251;

因为我们是按照student_id 来进行检索的,所以不需要对其他字段创建索引,即使这些字段出现在SELECT字段中。

2)数据量小的表最好不要使用索引

如果表记录太少,比如少于1000个,那么是不需要创建索引的。表记录太少,是否创建索引对查询效率的影响并不大。甚至说,查询花费的时间可能比遍历索引的时间还要短,索引可能不会产生优化效果。

举例:创建表1:

CREATE TABLE t_without_index(a INT PRIMARY KEY AUTO_INCREMENT,b INT);

提供存储过程1:添加900条数据,b字段没有添加索引

#创建存储过程DELIMITER //CREATE PROCEDURE t_wout_insert()BEGINDECLARE i INT DEFAULT 1;WHILE i <= 900DOINSERT INTO t_without_index(b) SELECT RAND()*10000;SET i = i + 1;END WHILE;COMMIT;END //DELIMITER ;#调用CALL t_wout_insert();

创建表2:

CREATE TABLE t_with_index(a INT PRIMARY KEY AUTO_INCREMENT,b INT,INDEX idx_b(b));

创建存储过程2:添加900条数据,b字段添加了索引

#创建存储过程DELIMITER //CREATE PROCEDURE t_with_insert()BEGINDECLARE i INT DEFAULT 1;WHILE i <= 900DOINSERT INTO t_with_index(b) SELECT RAND()*10000;SET i = i + 1;END WHILE;COMMIT;END //DELIMITER ;#调用CALL t_with_insert();

查询对比:

mysql> select * from t_without_index where b = 9879;+------+------+| a | b |+------+------+| 1242 | 9879 |+------+------+1 row in set (0.00 sec)mysql> select * from t_with_index where b = 9879;+-----+------+| a | b |+-----+------+| 112 | 9879 |+-----+------+1 row in set (0.00 sec)

你能看到运行结果相同,但是在数据量不大的情况下,索引就发挥不出作用了。

结论:在数据表中的数据行数比较少的情况下,比如不到 1000 行,是不需要创建索引的。

3)有大量重复数据的列上不要建立索引

在条件表达式中经常用到的不同值较多的列上建立索引,但字段中如果有大量重复数据,也不用创建索引。比如在学生表的“性别""字段上只有“男"与“女"两个不同值,因此无须建立索引。如果建立索引,不但不会提高查询效率,反而会严重降低数据更新速度

举例1:要在 100 万行数据中查找其中的 50 万行(比如性别为男的数据),一旦创建了索引,你需要先访问 50 万次索引,然后再访问 50 万次数据表,这样加起来的开销比不使用索引可能还要大。

举例2:假设有一个学生表,学生总数为 100 万人,男性只有 10 个人,也就是占总人口的 10 万分之 1。

学生表 student_gender 结构如下。其中数据表中的 student_gender 字段取值为 0 或 1,0 代表女性,1 代

表男性。

CREATE TABLE student_gender(student_id INT(11) NOT NULL,student_name VARCHAR(50) NOT NULL,student_gender TINYINT(1) NOT NULL,PRIMARY KEY(student_id))ENGINE = INNODB;

如果我们要筛选出这个学生表中的男性,可以使用:

SELECT * FROM student_gender WHERE student_gender = 1

运行结果(10 条数据,运行时间0.696s):

你能看到在未创建索引的情况下,运行的效率并不高。如果针对student_gender’字段创建索引呢?

SELECT * FROM student_gender WHERE student_gender = 1

同样是10条数据,运行结果相同,时间却缩短到了0.052s,大幅提升了查询的效率。

其实通过这两个实验你也能看出来,索引的价值是帮你快速定位。如果想要定位的数据有很多,那么索引就失去了它的使用价值,比如通常情况下的性别字段。

结论:当数据重复度大,比如高于10%的时候,也不需要对这个字段使用索引。

4)避免对经常更新的表创建过多的索引

第一层含义︰频繁更新的字段不一定要创建索引。因为更新数据的时候,也需要更新索引,如果索引太多,在更新索引的时候也会造成负担,从而影响效率。

第二层含义: 避免对经常更新的表创建过多的索引,并且索引中的列尽可能少。此时,虽然提高了查询速度,同时却会降低更新表的速度。|

5)不建议用无序的值作为索引

例如身份证、UUID(在索引比较时需要转为ASCII,并且插入时可能造成页分裂)、MD5、HASH、无序长字符串等。

6)删除不再使用或者很少使用的索引

表中的数据被大量更新,或者数据的使用方式被改变后,原有的一些索引可能不再需要。数据库管理员应当定期找出这些索引,将它们删除,从而减少索引对更新操作的影响。

7)不要定义冗余或重复的索引

① 冗余索引

有时候有意或者无意的就对同一个列创建了多个索引,比如: index(a,b,c)相当于index(a)、index(a,b)、index(a,b,c)。

举例:建表语句如下

CREATE TABLE person_info(id INT UNSIGNED NOT NULL AUTO_INCREMENT,name VARCHAR(100) NOT NULL,birthday DATE NOT NULL,phone_number CHAR(11) NOT NULL,country varchar(100) NOT NULL,PRIMARY KEY (id),KEY idx_name_birthday_phone_number (name(10), birthday, phone_number),KEY idx_name (name(10)));

我们知道,通过idx_name_birthday_phone_number索引就可以对name列进行快速搜索,再创建一个专门针对name列的索引就算是一个冗余索引,维护这个索引只会增加维护的成本,并不会对搜索有什么好处。

② 重复索引

另一种情况,我们可能会对某个列重复建立索引,比方说这样:

CREATE TABLE repeat_index_demo (col1 INT PRIMARY KEY,col2 INT,UNIQUE uk_idx_c1 (col1),INDEX idx_c1 (col1));

我们看到,col1 既是主键、又给它定义为一个唯一索引,还给它定义了一个普通索引,可是主键本身就会生成聚簇索引,所以定义的唯一索引和普通索引是重复的,这种情况要避免。

2.8 总结

索引是一把双刃剑,可提高查询效率,但也会降低插入和更新的速度并占用磁盘空间。

选择索引的最终目的是为了使查询的速度变快,上面给出的原则是最基本的准则,但不能拘泥于上面的准则,大家要在以后的学习和工作中进行不断的实践,根据应用的实际情况进行分析和判断,选择最合适的索引方式。

3.SQL优化

都有哪些维度可以进行数据库调优? 简言之:

索引失效、没有充分利用到索引----索引建立关联查询太多JOIN (设计缺陷或不得已的需求)----SQL优化服务器调优及各个参数设置(缓冲、线程数等)----调整f数据过多----分库分表

关于数据库调优的知识点非常分散。不同的DBMS,不同的公司,不同的职位,不同的项目遇到的问题都不尽相同。这里我们分为三个章节进行细致讲解。

虽然SQL查询优化的技术有很多,但是大方向上完全可以分成物理查询优化逻辑查询优化两大块。

物理查询优化是通过索引表连接方式等技术来进行优化,这里重点需要掌握索引的使用。逻辑查询优化就是通过sQL等价变换提升查询效率,直白一点就是说,换一种查询写法执行效率可能更高。

3.1 准备数据

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

步骤1:建表

CREATE DATABASE atguigudb2;USE atguigudb2;CREATE TABLE `class` (`id` INT(11) NOT NULL AUTO_INCREMENT,`className` VARCHAR(30) DEFAULT NULL,`address` VARCHAR(40) DEFAULT NULL,`monitor` INT NULL ,PRIMARY KEY (`id`)) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;CREATE TABLE `student` (`id` INT(11) NOT NULL AUTO_INCREMENT,`stuno` INT NOT NULL ,`name` VARCHAR(20) DEFAULT NULL,`age` INT(3) DEFAULT NULL,`classId` INT(11) DEFAULT NULL,PRIMARY KEY (`id`)#CONSTRAINT `fk_class_id` FOREIGN KEY (`classId`) REFERENCES `t_class` (`id`)) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

步骤2:设置参数

命令开启:允许创建函数设置:

set global log_bin_trust_function_creators=1; # 不加global只是当前窗口有效。

步骤3:创建函数

保证每条数据都不同。

#随机产生字符串DELIMITER //CREATE FUNCTION rand_string(n INT) RETURNS VARCHAR(255)BEGINDECLARE chars_str VARCHAR(100) DEFAULT'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ';DECLARE return_str VARCHAR(255) DEFAULT '';DECLARE i INT DEFAULT 0;WHILE i < n DOSET return_str =CONCAT(return_str,SUBSTRING(chars_str,FLOOR(1+RAND()*52),1));SET i = i + 1;END WHILE;RETURN return_str;END //DELIMITER ;#假如要删除#drop function rand_string;

随机产生班级编号

#用于随机产生多少到多少的编号DELIMITER //CREATE FUNCTION rand_num (from_num INT ,to_num INT) RETURNS INT(11)BEGINDECLARE i INT DEFAULT 0;SET i = FLOOR(from_num +RAND()*(to_num - from_num+1)) ;RETURN i;END //DELIMITER ;#假如要删除#drop function rand_num;

步骤4:创建存储过程

#创建往stu表中插入数据的存储过程DELIMITER //CREATE PROCEDURE insert_stu( START INT , max_num INT )BEGINDECLARE i INT DEFAULT 0;SET autocommit = 0; #设置手动提交事务REPEAT #循环SET i = i + 1; #赋值INSERT INTO student (stuno, name ,age ,classId ) VALUES((START+i),rand_string(6),rand_num(1,50),rand_num(1,1000));UNTIL i = max_numEND REPEAT;COMMIT; #提交事务END //DELIMITER ;#假如要删除#drop PROCEDURE insert_stu;

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

#执行存储过程,往class表添加随机数据DELIMITER //CREATE PROCEDURE `insert_class`( max_num INT )BEGINDECLARE i INT DEFAULT 0;SET autocommit = 0;REPEATSET i = i + 1;INSERT INTO class ( classname,address,monitor ) VALUES(rand_string(8),rand_string(10),rand_num(1,100000));UNTIL i = max_numEND REPEAT;COMMIT;END //DELIMITER ;#假如要删除#drop PROCEDURE insert_class;

步骤5:调用存储过程

class

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

stu

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

步骤6:删除某表上的索引

目的:下面演示过程中可能会创建很多索引,避免测试影响需要删除索引,如果一个个的的删除太慢了,可以调用此存储过程删除表中的所有索引(除了主键索引)

创建存储过程

DELIMITER //CREATE PROCEDURE `proc_drop_index`(dbname VARCHAR(200),tablename VARCHAR(200))BEGINDECLARE done INT DEFAULT 0;DECLARE ct INT DEFAULT 0;DECLARE _index VARCHAR(200) DEFAULT '';DECLARE _cur CURSOR FOR SELECT index_name FROM information_schema.STATISTICS WHERE table_schema=dbname AND table_name=tablename AND seq_in_index=1 AND index_name <>'PRIMARY' ;#每个游标必须使用不同的declare continue handler for not found set done=1来控制游标的结束DECLARE CONTINUE HANDLER FOR NOT FOUND SET done=2 ;#若没有数据返回,程序继续,并将变量done设为2OPEN _cur;FETCH _cur INTO _index;WHILE _index<>'' DO SET @str = CONCAT("drop index " , _index , " on " , tablename ); PREPARE sql_str FROM @str ;EXECUTE sql_str;DEALLOCATE PREPARE sql_str;SET _index=''; FETCH _cur INTO _index; END WHILE;CLOSE _cur;END //DELIMITER ;

执行存储过程

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

3.1 插入数据

3.1.1 insert

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

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

1).优化方案一:批量插入数据

如果是一条一条的insert插入数据,每次insert都要与数据库建立连接进行网络传输,这个性能是比较低的,所以我们建议批量插入。即:如果一次性你要插入多条数据可以通过一条sql语句来完成。如果批量插入,一次性插入的数据也不建议超过1000条,500~1000比较合适。如果要插入几万条数据可以将其分割为多条insert语句进行插入。

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

2).优化方案二:手动控制事务

mysql中的事务提交方式默认是自动提交,那也就意味着当你执行完一条insert语句之后,它就自动提交了。再次执行一条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');#在多条insert语句执行完之后,统一提交事务commit;

3).优化方案三:主键顺序插入,性能要高于乱序插入。

这个取决于mysql的数据组织结构,在sql的主键优化中讲解。

主键乱序插入 : 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指令进行插入。操作如下:

注意:这个数据脚本文件中的数据格式,本地文件中写的并不是sql语句而是一个符合一定规则的文件。 每一个字段使用逗号隔开,这个规则里面不一定是逗号还可以是冒号,可以是任意其他的符号。

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

#客户端连接服务端时,加上参数 --local-infile#表示当前客户端连接服务端的时候,需要去加载本地的文件。mysql --local-infile -u root -p#设置全局参数local_infile为1,开启从本地加载文件导入数据的开关#开启这个开关之后,我们才可以在本地去加载文件到数据库的表结构当中。set global local_infile = 1;#执行load指令将准备好的数据,加载到表结构中#要加载本地的哪个磁盘文件(/root/sql1.log),往哪一张表中(tb_user )加载数据,# 每一个字段之间使用什么分隔(逗号),每一行数据使用什么分隔(换行)load data local infile '/root/sql1.log' into table tb_user fieldsterminated by ',' lines terminated by '\n' ;

主键顺序插入性能高于乱序插入

示例演示:

A:连接数据库、显示所有的数据库、创建一个新的数据库

-- 客户端连接服务端时,加上参数 --local-infilemysql --local-infile -uroot -p1234#显示所有的数据库show databases;#创建数据库create database itheima;

B:查看默认开关是否打开,如果没有打开进行开启

#查看开关状态select @@local_infile;-- 设置全局参数local_infile为1,开启从本地加载文件导入数据的开关set global local_infile = 1;

C:创建表结构

#切换数据库use itheima;CREATE TABLE `tb_user` (`id` INT(11) NOT NULL AUTO_INCREMENT,`username` VARCHAR(50) NOT NULL,`password` VARCHAR(50) NOT NULL,`name` VARCHAR(20) NOT NULL,`birthday` DATE DEFAULT NULL,`sex` CHAR(1) DEFAULT NULL,PRIMARY KEY (`id`),UNIQUE KEY `unique_user_username` (`username`)) ENGINE=INNODB DEFAULT CHARSET=utf8 ;show tables;

D:把准备好的数据脚本上传到服务器中

E:load加载数据

load data local infile '/root/load_user_100w_sort.sql' into table tb_userfields terminated by ',' lines terminated by '\n' ;select count(*) from tb_user;

我们看到,插入100w的记录,14.25s就完成了,性能很好。之前测试的如果是insert语句插入需要十几分钟,所以这个性能提升很明显。

在使用load指令时,我们也要按照主键顺序插入,主键顺序插入性能高于乱序插入。

3.2 主键优化

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

3.2.1 数据组织方式

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

表数据都是根据主键顺序组织存放的:InnoDB存储引擎中按照存储方式分为聚集索引和二级索引,聚集索引的叶子节点下面挂的是这一行的行数据。一张表默认主键索引就是聚集索引,所以最终我们表中的数据在存放的时候,他就是根据主键进行顺序存放的。比如:6下面存放的就是6这个主键对应的这一行数据,12下面存放的就是12这个主键对应的数据。所以在InnoDB存储引擎中表数据都是根据主键顺序存放的。

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

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

3.2.2 页分裂

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

在InnoDB引擎中规定每一个页当中至少包含2行数据,如果只包含一行就相当于一个链表了。

A.主键顺序插入效果

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

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

③. 当第一个也写满之后,再写入第二个页,页与页之间会通过指针连接

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

B.主键乱序插入效果

①. 加入1#,2#页都已经写满了,存放了如图所示的数据

②. 此时再插入id为50的记录,我们来看看会发生什么现象会再次开启一个页,写入新的页中吗?

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

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

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

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

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

3.2.3 页合并

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

当我们对已有数据进行删除时,具体的效果如下:

当删除一行记录时,实际上记录并没有被物理删除(并不会直接从磁盘当中,将数据页当中的这一行数据干掉),只是记录被标记(flaged)为删除并且它的空间变得允许被其他记录声明使用。

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

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

删除数据,并将页合并之后,再次插入新的数据21,则直接插入3#页

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

知识小贴士:

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

3.2.4 主键索引设计原则

满足业务需求的情况下,尽量降低主键的长度。 一张表的聚集索引只有一个,二级索引可以有多个,二级索引叶子下面挂的就是主键,所以如果主键长度比较长二级索引比较多,那么将会占用大量的磁盘空间,在搜索的时候占用大量的磁盘io。 插入数据时,尽量选择顺序插入,选择使用AUTO_INCREMENT自增主键。 主键顺序插入性能较高,主键乱序插入有可能出现页分裂现象,导致性能降低。 尽量不要使用UUID做主键或者是其他自然主键,如身份证号。 uuid生成的主键值是无序的,这样插入数据时就是乱序插入,就可能出现页分裂现象。并且uuid生成的数据长度比较长,在检索的时候也会耗费大量的磁盘io。 业务操作时,避免对主键的修改。 修改主键,还要动对应的数据结构,这个代价还是比较大的。

3.3 order by优化(创建索引时可以指定排序)

MySQL的排序,有两种方式:

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

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

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

接下来,我们来做一个测试:

A.数据准备

把之前测试时,为tb_user表所建立的部分索引直接删除掉

#切换数据库use itcast;show tables;#查看索引SHOW INDEX FROM tb_user; #删除索引drop index idx_user_phone on tb_user;drop index idx_user_name on tb_user;

剩下一个主键索引,一个联合索引,一个前缀索引

B.执行排序SQL

#根据年龄进行升序排序的执行计划explain select id,age,phone from tb_user order by age ;

#先按照年龄进行升序排序,如果年龄相同在按照手机号进行升序排序的执行计划explain select id,age,phone from tb_user order by age, phone ;

由于 age, phone 都没有索引,所以此时再排序时,出现Using filesort, 排序性能较低。

C.创建索引

-- 创建索引create index idx_user_age_phone_aa on tb_user(age,phone);

D.创建索引后,根据age, phone进行升序排序

explain select id,age,phone from tb_user order by age;

explain select id,age,phone from tb_user order by age , phone;

建立索引之后,再次进行排序查询,就由原来的Using filesort, 变为了 Using index,性能就是比较高的了。

E.创建索引后,根据age, phone进行降序排序

explain select id,age,phone from tb_user order by age desc , phone desc ;

也出现 Using index, 但是此时Extra中出现了Backward index scan,这个代表反向扫描索引,因为在MySQL中我们创建的索引,默认索引的叶子节点是从小到大排序的,而此时我们查询排序 时,是从大到小,所以,在扫描时,就是反向扫描,就会出现 Backward index scan。 在MySQL8版本中,支持降序索引,我们也可以创建降序索引。

F.根据phone,age进行升序排序,phone在前,age在后。

explain select id,age,phone from tb_user order by phone , age;

此时既出现了index也出现了filesort,排序时,也需要满足最左前缀法则,否则也会出现 filesort。因为在创建索引的时候, age是第一个字段,phone是第二个字段,所以排序时,也就该按照这个顺序来,否则就会出现 Using filesort。

G.根据age, phone进行降序一个升序,一个降序

explain select id,age,phone from tb_user order by age asc , phone desc ;

因为创建索引时,如果未指定顺序,默认都是按照升序排序的,而查询时,一个升序,一个降序,此时就会出现Using filesort额外的排序。

#查看索引的排序方式,这个创建的联合索引的排序方式为 A:asc升序SHOW INDEX FROM tb_user;

为了解决上述的问题,我们可以创建一个新的索引,这个联合索引中 age 升序排序,phone 倒序排序。

H.创建联合索引(age 升序排序,phone 倒序排序)

create index idx_user_age_phone_ad on tb_user(age asc ,phone desc);#再次查看索引的排序方式,这个创建的联合索引的排序方式为 A:asc升序,D:desc降序SHOW INDEX FROM tb_user;

I.然后再次执行如下SQL,不会再出现filesort额外排序了

explain select id,age,phone from tb_user order by age asc , phone desc ;

J.创建索引后,根据age, phone进行升序排,也不会出现filesort

因为他用的之前的老索引idx_user_age_phone_aa,他默认按照是age升序,phone升序排序的。

explain select id,age,phone from tb_user order by age;

升序/降序联合索引结构图示:

由上述的测试,我们得出order by优化原则:

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

B.尽量使用覆盖索引。

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

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

# 查看排序缓冲区大小show variables like 'sort_buffer_size';

如果说在排序的时候这个缓冲区256k占满了,他会在磁盘文件中进行排序,性能比较低,所以如果实在大数据量排序的时候出现了filesort,可以适量的把这个参数往上调,从而提高排序效率。

3.4 group by优化

分组操作,我们主要来看看索引对于分组操作的影响。

首先我们先将 tb_user 表的索引全部删除掉 。

#查看当前表有哪些索引SHOW INDEX FROM tb_user; #删除索引drop index idx_user_pro_age_sta on tb_user;drop index idx_email on tb_user;drop index idx_user_age_phone_aa on tb_user;drop index idx_user_age_phone_ad on tb_user;

只剩下一个主键索引。

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

#根据专业分组并统计每个专业人员的数量select profession,count(*) from tb_user group by profession ;#查看执行计划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,查看执行计划:

#修改为根据年龄进行分组,每组的年龄人数是多少,查看执行计划explain select age, count(*) from tb_user group by age;

效果:出现 Using temporary临时表,性能不高。不满足最左前缀法则,不走索引所以出现了临时表。

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

#修改为根据专业、年龄2个字段进行分组,查看执行计划explain select profession,age, count(*) from tb_user group by profession,age;

效果:没有出现Using temporary临时表,因为排序的字段和创建索引的字段,满足最左前缀法则,所以直接走索引。

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

#修改为根据年龄字段进行分组,但是在分组之前先要对profession进行一个过滤,查看执行计划#查询专业为软件工程的数据,并根据年龄进行分组。explain select age, count(*) from tb_user where profession = "软件工程" group by age;

效果:没有出现Using temporary临时表,因为有第一个字段profession ,满足最左前缀法则,所以直接走索引。

我们发现,如果仅仅根据age分组,就会出现 Using temporary临时表,性能不高;而如果是根据profession,age两个字段同时分组,则不会出现 Using temporary。原因是因为对于分组操作, 在联合索引中,也是符合最左前缀法则的。

所以,在分组操作中,我们需要通过以下两点进行优化,以提升性能:

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

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

3.5 limit优化

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

我们一起来看看执行limit分页查询耗时对比:

tb_sku表,有1000万条数据。

# 查询第一页的数据,每页显示10条数据,耗时0.00秒select * from tb_sku limit 0,10;# 查询100万行记录开始之后的10条记录,耗时1.66秒select * from tb_sku limit 1000000,10;# 查询500万行记录开始之后的10条记录,耗时10.79秒select * from tb_sku limit 5000000,10;# 查询900万行记录开始之后的10条记录,耗时19.39秒select * from tb_sku limit 9000000,10;

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

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

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

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

测试:

原始查询方式:耗时19.39秒

# 查询900万行记录开始之后的10条记录,耗时19.39秒select * from tb_sku limit 9000000,10;

覆盖索引优化:把*改为id,查询*肯定会用到回表查询,查询id直接可以从二级索引查询到,所以性能更高

#可以拿到要查询的id,耗时11.47秒select id from tb_sku order by id limit 9000000,10;

使用子查询拿到id对应的数据:

#多行子查询方式:报错,当前mysql的版本不支持这种语法 在in之后使用limit关键字select * from tb_sku where id in(select id from tb_sku order by id limit 9000000,10;)

解决:把这个select id from tb_sku order by id limit 9000000,10;返回id的查询结果看成是一张表,通过多表联查来实现。

#耗时11.46秒select s.* from tb_sku s ,(select id from tb_sku order by id limit 9000000,10) awhere s.id = a.id;

3.6 count优化

3.6.1 概述

select count(*) from tb_user ;

在之前的测试中,我们发现,如果数据量很大,在执行count操作时,是非常耗时的,它是由存储引擎决定的。

MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count(*) 的时候会直接返回这个数,效率很高; 但是如果是带条件的count,MyISAM也慢。InnoDB 引擎就麻烦了,它执行 count(*) 的时候,需要把数据一行一行地从引擎里面读出来,然后累积计数。

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

自己计数:比如我们可以借助一些像key value形式的内存级别的数据库像Redis,当我们执行插入数据时直接把某一个计数加1,当我们往某一张表中去删除一条数据时把这个计数减1,自己去维护这个计数,比较繁琐。

3.6.2 count用法

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

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

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

3.7 update优化

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

3.7.1 准备数据

create table course(id int auto_increment comment '主键ID' primary key,name varchar(10) null comment '课程名称')comment '课程表';INSERT INTO course (name) VALUES ('Java');INSERT INTO course (name) VALUES ('PHP');INSERT INTO course (name) VALUES ('MySQL');INSERT INTO course (name) VALUES ('Hadoop');select * from course;

3.7.2 正常情况

A:开启事务模拟一个事务操作,执行一条更新语句

回忆 InnoDB引擎的3大特性:事务、外键、行级锁。也就是说在InnoDB引擎中那么我们当前默认的这种事务隔离级别里面,它默认使用的是行锁。

#开启事务模拟一个事务操作。begin;update course set name = 'javaEE' where id = 1 ;

对于InnoDB引擎当前默认的事务隔离级别,我们在进行操作的时候执行一条update语句,那么当前他会把id为1的这一行数据会锁住。只要你的事务没有提交,这一行的行锁就不会释放。

B:开启一个新的会话窗口,开启事务模拟事务操作,执行一条更新语句。

mysql -uroot -p1234use itcast;begin;update course set name = 'Kafka' where id = 4;

第一个窗口使用行级锁锁的是id为1的这行数据,第二个窗口要操作的是id为4的这行数据,如果是行锁可以执行成功。

C:2个会话窗口进行事务提交:都成功没有任何问题。

3.7.3 异常情况

A:当前表的数据情况

B:第一个窗口模拟事务操作,执行更新语句。

#开启事务模拟一个事务操作。begin;update course set name = 'Springboot' where name= "PHP";

当我们执行这条sql语句时,它锁住的是第二行记录

C:第二个窗口模拟事务操作,执行更新语句。

#开启事务模拟一个事务操作。begin;update course set name = 'Kafka2' where id = 4;

第一个窗口锁住的是第二行数据,第二个窗口操作的是第4行数据,那为什么没有更新呢????

答:此时你在执行updata语句时,name这个字段没有索引,此时加的不再是行锁了而是表锁,所以他会把整张表都锁住了,第二个窗口在执行时就阻塞住了。

D:第一个窗口提交事务,此时表锁释放,第二个窗口在才可以更新成功。

这就是我们在执行update语句时需要规避的问题,在更新数据时一定要根据索引字段进行更新。

第2个窗口再次执行更新操作,更新成功,之后提交事务。

E:给name字段建立索引,再次执行相同的sql效果:更新成功

create index idx_course_name on course(name);

表数据效果:

第一个窗口:

begin;update course set name = 'spring' where name= "Springboot";

锁的是第4行记录

第二个窗口:

begin;update course set name = 'cloud' where name= "Kafka2";

操作的是第2行记录,由于第一个窗口使用的是行级锁,所以更新成功。

2个窗口commit提交事务。

3.7.4 更新优化说明

以上演示就是update语句在执行的时候需要规避的问题:在执行update语句的时候,我们一定要根据索引字段进行更新,否则就会出现行锁升级为表锁,锁住整张表,这样会降低并发性能。InnoDB引擎的行锁是针对索引加的锁,不是针对记录加的锁 ,所以在进行更新的时候更新的条件一定要有索引,如果没有索引就会出现行锁升级为表锁。并且这个索引不能失效,否则行锁也会升级为表锁 ,一旦升级为表锁那么并发性能就会降低。

3.8 总结

4.视图/存储过程/存储函数/触发器

4.1 视图

4.1.1介绍

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

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

4.1.2 语法:创建 查询 删除 修改

1).创建

[ ]中括号的内容可以省略。关键字CREATE VIEWOR REPLACE:如果我们要替换某一个视图可以加上OR REPLACE视图名称:自己定义AS SELECT语句:指定视图所封装的数据视图是一张虚拟表,最终的数据是来源于后面的select语句,select语句当中我们要指定要从哪一张表中去查询数据。select语句当中所查询的表就是这个视图所关联的基表,也叫基础表。[ WITH [CASCADED | LOCAL ] CHECK OPTION ]:检查选项

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

2).查询

查看视图数据:视图可以当成一张表去查,表怎么查视图就怎么查询。后面有条件写条件。因为视图是一张虚拟存在的表,所以我们可以像操作表一样操作视图。

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

3).修改

方式一:和创建视图的语法基本一致,只不过创建视图的OR REPLACE替换关键字可以不加,但是修改视图一定要加上OR REPLACE关键字,表示把对应的视图内容替换掉。

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

4).删除

可选项IF EXISTS:如果视图存在,在执行删除操作。

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

4.1.3 演示:创建 查询 删除 修改视图

准备数据:

create table student(id int auto_increment comment '主键ID' primary key,name varchar(10) null comment '姓名',no varchar(10) null comment '学号')comment '学生表';INSERT INTO student (name, no) VALUES ('黛绮丝', '2000100101');INSERT INTO student (name, no) VALUES ('谢逊', '2000100102');INSERT INTO student (name, no) VALUES ('殷天正', '2000100103');INSERT INTO student (name, no) VALUES ('韦一笑', '2000100104');select * from student;

创建视图:

-- 创建视图#创建或者替换视图 指定视图名称 数据来自于(展示student表中的2个字段id name)create or replace view stu_v_1 as select id,name from student where id <= 10;

查询视图:

#查看创建视图语句show create view stu_v_1;#查看视图数据select * from stu_v_1;select * from stu_v_1 where id < 3;

显示的创建视图所执行的sql语句和我们自己编写的有些差异,这个地方所查询出来的sql语句包含了在创建视图的时候我们所没有指定的一些默认参数,在这一块并没有指定但是在查询的时候是查询到了。

修改视图:

#方式一 原先查询2个字段,现在查询3个字段create or replace view stu_v_1 as select id,name,no from student where id <= 10;#查看视图发现已经修改,由显示的2个变为3个select * from stu_v_1;#方式二alter view stu_v_1 as select id,name from student where id <= 10;#查看视图发现已经修改,由显示的3个变为2个select * from stu_v_1;

删除视图:

drop view if exists stu_v_1;

上述我们演示了,视图应该如何创建、查询、修改、删除,那么我们能不能通过视图来插入、更新数据呢? 接下来,做一个测试。

#创建视图create or replace view stu_v_1 as select id,name from student where id <= 10 ;#查询视图表中的数据select * from stu_v_1;#往视图中插入数据insert into stu_v_1 values(6,'Tom');insert into stu_v_1 values(17,'Tom22');

执行没有报错,2条数据都可以插入成功,视图当中并不存储数据,具体的数据都是在基表当中存在的,也就是说现在我们插入的数据是在这个视图对应的基表 student 表当中。

执行上述的SQL,我们会发现,id为6和17的数据都是可以成功插入的。但是我们执行查询视图,查询出来的数据,却没有id为17的记录。

因为:我们在创建视图的时候,指定的条件为 id<=10, id为17的数据,是不符合条件的,所以没有查询出来,但是这条数据确实是已经成功的插入到了基表中

如果我们定义视图时,如果指定了条件,然后我们在插入、修改、删除数据时,是否可以做到必须满足条件才能操作,否则不能够操作呢? 答案是可以的,这就需要借助于视图的检查选项了

给创建视图的sql添加检查选项的语句,之后再次执行不符合条件的sql,如果你插入的这条数据和我们创建视图时的条件是相违背的,这个 CHECK OPTION选项就会阻止我们插入。

create or replace view stu_v_1 as select id,name from student where id <= 10 WITH CASCADED CHECK OPTION;insert into stu_v_1 values(19,'Tom36');

4.1.4 检查选项

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

1).CASCADED

级联

比如,v2视图是基于v1视图的,如果在v2视图创建的时候指定了检查选项为 cascaded,但是v1视图创建时未指定检查选项。 则在执行检查时,不仅会检查是否满足v2的条件,还会级联检查是否满足v1的条件,相当于在v1里面也加上了WITH CASCADED CHECK OPTION

总结:基于v1视图创建v2,v1没有写检查选项,v2写了检查选项。此时在往v2中插入数据的时候不仅要检查v2还要检查v1。相当于v1也写了检查选项。

测试:

恢复student表中的数据:

# CASCADED#创建v1视图,不指定检查选项create or replace view stu_v_1 as select id,name from student where id <= 20;#插入成功:id为5<20insert into stu_v_1 values(5,'Tom');#插入成功:虽然id为25不满足v1视图的查询条件,但是没有指定检查选项,所以可以插入。INSERT INTO stu_v_1 VALUES(25,'Tom');#基于v1视图创建v2视图,指定检查选项CREATE OR REPLACE VIEW stu_v_2 AS SELECT id,NAME FROM stu_v_1 WHERE id >= 10 WITH CASCADED CHECK OPTION;#插入失败:由于v2视图添加了检查选项,7不满足v2视图的条件INSERT INTO stu_v_2 VALUES(7,'Tom');#插入失败:因为v2视图使用的是CASCADED关键字,它会检查当前视图所依赖的底层所有视图,v2视图的条件满足,v1视图的条件不满足,所以插入失败。INSERT INTO stu_v_2 VALUES(26,'Tom');#插入成功:id=15满足v2的条件,也满足v1视图的条件。INSERT INTO stu_v_2 VALUES(15,'Tom');#基于v2视图创建v3视图,没有检查选项CREATE OR REPLACE VIEW stu_v_3 AS SELECT id,NAME FROM stu_v_2 WHERE id <= 15 ;#插入成功:id满足v3 v2 v1视图的查询条件,所以插入成功。INSERT INTO stu_v_3 VALUES(11,'Tom');#插入成功:id=17虽然不满足v3视图的条件,但是v3视图没有写检查选项。之后17又满足v2 v3视图的条件,所以插入成功。INSERT INTO stu_v_3 VALUES(17,'Tom');#插入失败:id=28不满足v3视图,但是由于v3视图没有写检查选项所以即使不满足他也不会进行检查。之后检查v2视图满足,检查v3视图不满足,所以插入失败。INSERT INTO stu_v_3 VALUES(28,'Tom');

2).LOCAL

本地

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

总结:对于local视图检查选项,当我们在操作视图的时候它会递归的去找当前视图所依赖的视图,如果当前视图以及所依赖的视图都定义了WITH LOCAL CHECK OPTION选项,将会去判定我们所操作的数据是否满足这个视图的条件,如果在递归的过程当中找到某一个视图在定义的时候,没有增加这样的检查选项,此时我们在操作这块数据的时候将不对这个条件做检查。

CASCADED和LOCAL的区别:

CASCADED:基于v1视图创建v2视图,v1没有检查选项v2有,在往v2插入数据时会检查v2和v1,v1即使没有写检查选项也相当于写了。 如果v2有检查选项,级联的v1一定有检查选项,所以一定会检查。 LOCAL:基于v1视图创建v2视图,v2有检查选项,在往v2插入数据时会检查v2和v1,此时如果v1有检查选项则会检查,如果v1没有检查选项则不会检查v1有没有检查选项看是不是自己写了,有就检查没有就不检查。而上面那个级联是即使不写也相当于写了检查选项,一定会检查

测试:

恢复student表数据

# local#创建v4视图,不指定检查选项CREATE OR REPLACE VIEW stu_v_4 AS SELECT id,NAME FROM student WHERE id <= 20;#插入成功:id为5满足v4条件INSERT INTO stu_v_4 VALUES(5,'Tom');#插入成功:虽然id不满足v4视图的查询条件,但是v4没有写检查选项,所以不会进行检查。INSERT INTO stu_v_4 VALUES(16,'Tom');#基于v4视图创建v5视图,指定检查选项CREATE OR REPLACE VIEW stu_v_5 AS SELECT id,NAME FROM stu_v_4 WHERE id >= 10 WITH LOCAL CHECK OPTION;#插入成功:13满足v5视图,之后它还会递归的去找所依赖的视图v4,如果v4视图没有写检查选项,则不检查。所以插入成功。INSERT INTO stu_v_5 VALUES(13,'Tom');#插入成功:13满足v5视图,之后它还会递归的去找所依赖的视图v4,如果v4视图没有写检查选项,则不检查。所以插入成功。INSERT INTO stu_v_5 VALUES(17,'Tom');#基于v5视图创建v6视图,没有检查选项CREATE OR REPLACE VIEW stu_v_6 AS SELECT id,NAME FROM stu_v_5 WHERE id <= 15 ;#插入成功:v6没有定义检查选项所以不做检查,之后递归到v5 v5定义的检查选项所以会进行检查 14满足条件,之后递归到v4 v4没有检查选项 所以不做检查。INSERT INTO stu_v_6 VALUES(14,'Tom');

4.1.5 视图的更新

mysql中的视图并不是任何的视图都可以进行增删改。所以我们要了解一下视图的更新需要什么条件。

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

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

DISTINCT

GROUP BY

HAVING

UNION 或者 UNION ALL

示例演示:

#创建视图时使用了聚合函数create view stu_v_count as select count(*) from student;

上述的视图中由于使用了聚合函数,导致视图和基础表不能一 一对应,如果我们对这个视图进行更新或插入的,将会报错。

insert into stu_v_count values(10);

4.1.6 视图作用

1).简单

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

即:如果不使用视图多个插入数据每次都要写条件,如果使用视图只需要在视图中写条件,之后在写插入数据的时候就不需要写条件了。

2).安全

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

即:数据库中的操作我们可以进行用户授权,通过授权来决定每个用户登录mysql后,能够看到操作那些数据库,能够看到操作那些表。但是对于mysql中的数据库授权只能操作到表,不能控制到表中的字段,而视图就可以。

eg:只想要某一个用户只看到学生表当中的id name2个字段,不想让他看到学号,此时就可以创建视图,这个视图在查询时只包含id 和name,这样这个用户在操作视图的时候只能看到id name2个字段并不能看到学号,保证了一些敏感数据的安全性。

3).数据独立

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

即:基础表的name字段变为 student_name,此时我们只需要对视图进行一个更新操作就可以了。把创建视图的student_name字段起个别名为name,这样就屏蔽了基础表的变化对业务的影响。

#基础表原始数据 nameCREATE OR REPLACE VIEW stu_v_4 AS SELECT id,NAME FROM student WHERE id <= 20;#基础表数据name字段变为student_nameCREATE OR REPLACE VIEW stu_v_4 AS SELECT id,student_name FROM student WHERE id <= 20;#使用别名屏蔽基础表的差异 student_name as nameCREATE OR REPLACE VIEW stu_v_4 AS SELECT id,student_name as name FROM student WHERE id <= 20;

4.1.7 案例

1).为了保证数据库表的安全性,开发人员在操作tb_user表时,只能看到的用户的基本字段,屏蔽 手机号和邮箱两个字段。

#创建视图的时候返回查询的数据不包含这2个字段create view tb_user_view as select id,name,profession,age,gender,status,createtimefrom tb_user;#查询视图就看不到这2个字段了select * from tb_user_view;

2).查询每个学生所选修的课程(三张表联查),这个功能在很多的业务中都有使用到,为了简化操作不必每次都把3表联查的sql都写出来,可以把这个三表联查的sql封装到视图当中,在后续的业务课程当中要想查询每个学生所选修的课程,只需要查询该视图就可以了。

学生表、课程表、由于是多对多关系所以还会有一个中间表。

create view tb_stu_course_view asselect 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;

4.2 存储过程

4.2.1 介绍

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

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

重用:业务1想要用到这个p1的sql集合直接去调用这个存储过程就行了,下一次在遇到相同的业务的时候只需要调用对应的存储过程即可。

场景:

问题:一个业务当中它可能需要操作多次数据库,eg 第一次先要去查询数据库表当中的数据,之后根据查询的结果要不要更新,如 更新第一张表的数据之后更新第二张表的数据。一个逻辑当中需要操作多次数据库,因意味着多次网络请求。优化:在数据库层面将多条sq语句l封装到一个集合当中,之后只需要调用这个p1这个sql集合就可以了。

特点:

封装,复用 -----------------------> 可以把某一业务SQL封装在存储过程中,需要用到的时候直接调用即可。可以接收参数,也可以返回数据 --------> 在存储过程中,可以传递参数,也可以接收返回值。减少网络交互,效率提升 -------------> 如果涉及到多条SQL,每执行一次都是一次网络传输。 而如果封装在存储过程中,我们只需要网络交互一次可能就可以了。

4.2.2 基本语法

1).创建

([ 参数列表 ]):存储过程在定义的时候可以指定它输入的参数,以及返回的参数信息的。当然参数是可有可无的。BEGIN,END是固定格式,里面写的是这个存储过程封装的sql语句,可以是一条也可以是是多条。所有存储过程的逻辑都是在BEGIN和END之间定义的。

# 这种创建方式有问题,失败CREATE PROCEDURE 存储过程名称([ 参数列表 ])BEGIN-- SQL语句END ;-----------------------------------------------#这种才可以常创建成功#表示现在sql语句在结束的时候,是以2个$为结束符号的。delimiter $$CREATE PROCEDURE 存储过程名称([ 参数列表 ])BEGIN-- SQL语句END $$-------------------------之后不想要在命令行操作时可以改回来 分号形式

问题:如果在命令行中定义存储过程可能会报错。

原因:在命令行当中执行sql语句的时候,一旦见到分号它就认为当前的sql语句已经结束了,而实际上创建存储过程的语法是到end之后才会结束。解决:在命令行中,执行创建存储过程的SQL时,需要通过关键字 delimiter 指定SQL语句的结束符。默认mysql语句的结束符是分号(;),走到封装的select语句时因为有分号所以就会结束,所以我们需要重新定义它的结束符号,此时走到select是就不会结束了。

2).调用

CALL 名称([ 参数 ]);#此时只能使用$$作为结束符了。

3).查看

#形式1:数据库自带的ROUTINES这张表中去查询,当前数据库服务器里面所有的存储过程的信息 参数:数据库的名字SELECT * FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA = 'xxx'; -- 查询指定数据库的存储过程及状态信息#形式2:查看指定的存储过程在创建的时候,它的sql语句是什么样的。SHOW CREATE PROCEDURE 存储过程名称 ; -- 查询某个存储过程的定义

4).删除

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

演示示例:

-- 存储过程基本语法-- 创建:暂时定义无参的,参数具体的格式类型在后面中学习。create procedure p1()beginselect count(*) from student;end;-- 调用:会执行存储过程中所封装的逻辑call p1();-- 查看# 形式1select * from information_schema.ROUTINES where ROUTINE_SCHEMA = 'itcast';# 形式2show create procedure p1;-- 删除drop procedure if exists p1;

sql会多了个DEFINER=root@%,创建的用户是那个(root用户),创建的时候没写默认会加上这个参数

4.2.3 变量

说明:上面只是演示的是存储过程中定义一个简单的select语句,并没有任何的逻辑,在一些复杂的业务逻辑当中想要去编写一个复杂的存储过程,里面就会涉及到很多的语法结构。所以接下来呢我们就需要针对于存储过程当中,所涉及到的语法结构来进行详细的讲解首先要学习的第一块就是变量

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

4.2.3.1 系统变量

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

全局变量:对所有的会话有效

eg:创建多个查询控制台,一个查询窗口就是一个会话

会话变量:在当前会话中有效

1).查看系统变量

如果写了SESSION :表示查看所有的会话变量如果写了GLOBAL :表示查看所有的全局系统变量SESSION | GLOBAL不指定默认是SESSION 会话级别。

SHOW [ SESSION | GLOBAL ] VARIABLES ; -- 查看所有系统变量#如果知道查看系统变量的某几个字符,就可以查询部分系统变量SHOW [ SESSION | GLOBAL ] VARIABLES LIKE '......'; -- 可以通过LIKE模糊匹配方式查找变量#如果准确的知道了某一个系统变量名,查找具体的系统变量SELECT @@[SESSION | GLOBAL] 系统变量名; -- 查看指定变量的值

2).设置系统变量

#方式一SET [ SESSION | GLOBAL ] 系统变量名 = 值 ;#方式二SET @@[SESSION | GLOBAL] 系统变量名 = 值 ;

注意:

如果没有指定SESSION/GLOBAL,默认是SESSION,会话变量。mysql服务重新启动之后,所设置的全局参数会失效,要想不失效,可以在 /etc/f 中配置。这里就不在演示了。全局变量(GLOBAL):全局变量针对于所有的会话。会话变量(SESSION):会话变量针对于单个会话,在另外一个会话窗口就不生效了。

演示示例:

-- 查看系统变量show session variables ;-- 模糊show session variables like 'auto%';show global variables like 'auto%';-- 具体哪个系统变量select @@global.autocommit;select @@session.autocommit;

-- 设置系统变量#设置事务自动提交开关,关闭状态set session autocommit = 0;#查看是否修改成功select @@session.autocommit;#插入一条数据:执行成功insert into course(id, name) VALUES (6, 'ES');#查询course表发现没有插入数据,原因是事务自动提交关闭了,意味着要手动事务提交#注意如果是直接刷新表是看不到数据的,但如果是select还是能够看到数据的。select * from course;#手动提交事务,再次查询发现commit;select * from course;#开启自动提交set session autocommit = 1;#再次查询系统变量select @@session.autocommit;

4.2.3.2 用户定义变量

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

@@:2个表示系统变量@:1个表示用户自定义变量

1).赋值

方式一:

#形式一SET @var_name = expr [, @var_name = expr] ... ;#形式二SET @var_name := expr [, @var_name := expr] ... ;

赋值时,可以使用 = ,也可以使用 := 。

推荐使用 := ,因为在mysql当中的比较运算符也是=,它是没有==,所以mysql中=既可以作为赋值运算符又可以作为比较运算符,为了区分推荐在赋值的时候使用:=

方式二:

#形式一SELECT @var_name := expr [, @var_name := expr] ... ;#形式二:把查询的某个结果赋值给某个变量SELECT 字段名 INTO @var_name FROM 表名;

2).查询:查看变量是否赋值成功

SELECT @var_name ;

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

演示示例:

-- 赋值set @myname = 'itcast';set @myage := 10;#可以一次性为多个用户变量赋值set @mygender := '男',@myhobby := 'java';select @mycolor := 'red';select count(*) into @mycount from tb_user;-- 使用select @myname,@myage,@mygender,@myhobby;select @mycolor , @mycount;select @abc;

4.2.3.3 局部变量

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

1).声明

[DEFAULT … ]:可选的参数DEFAULT ,如果这个变量有默认值,可以通过default这个关键字给它指定默认值。

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

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

2).赋值

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

演示示例:

-- 定义存储过程create procedure p2()begin#定义局部变量declare stu_count int default 0;#赋值select count(*) into stu_count from student;#查询这个变量select stu_count;end;#调用存储过程call p2();

4.2.4 if

1).介绍

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

如果条件1成立执行then之后的sql逻辑,否则就要去判断条件2的逻辑是否成立,如果成立则执行then之后的sql逻辑,如果还有ELSEIF则继续判断…ELSE :默认的逻辑最后通过END IF语法结束。

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

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

2).案例

根据定义的分数score变量,判定当前分数对应的分数等级。

score >= 85分,等级为优秀。score >= 60分 且 score < 85分,等级为及格。score < 60分,等级为不及格。

# 创建存储过程create procedure p3()begin#定义局部变量declare score int default 58;declare result varchar(10);if score >= 85 thenset result := '优秀';elseif score >= 60 thenset result := '及格';elseset result := '不及格';end if;#查看局部变量select result;end;#调用存储过程call p3();

问题:上述的需求我们虽然已经实现了,但是也存在一些问题,比如:score 分数我们是在存储过程中定义死的,而且最终计算出来的分数等级,我们也仅仅是最终查询展示出来而已

解决:那么我们能不能,把score分数动态的传递进来,计算出来的分数等级是否可以作为返回值返回呢? 答案是肯定的,我们可以通过接下来所讲解的参数来解决上述的问题。

4.2.5 参数

1).介绍

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

用法:

#在创建存储过程的时候声明参数CREATE PROCEDURE 存储过程名称 ([ IN/OUT/INOUT 参数名 参数类型 ])BEGIN-- SQL语句END ;

2).案例一

根据传入(in)参数score,判定当前分数对应的分数等级,并返回(out)

score >= 85分,等级为优秀。score >= 60分 且 score < 85分,等级为及格。score < 60分,等级为不及格。

#定义存储过程create procedure p4(in score int, out result varchar(10))beginif score >= 85 thenset result := '优秀';elseif score >= 60 thenset result := '及格';elseset result := '不及格';end if;end;#查询:存储过程#第一个参数是传入的数据,第二个参数是将执行的返回值返回,通过用户自定义变量进行接收#定义用户变量 @result来接收返回的数据, 用户变量可以不用声明call p4(18, @result);#查看这个用户自定义变量select @result;

3).案例二

传入的200分制的分数,进行换算,换算成百分制,然后返回分数。

分数既是传入参数又是传出参数------INOUT

#创建存储过程create procedure p5(inout score double)beginset score := score * 0.5;end;#调用存储过程:如果直接传递198就没办法拿到返回值了,所以需要定义一个用户自定义变量@score# 这个变量没有值所以要先进行赋值,然后在去调用这个存储过程,调用完毕之后他会将计算完毕后的结果,# 再次赋值给这个用户自定义变量set @score = 198;call p5(@score);#查询这个用户自定义变量封装的返回结果select @score;

4.2.6 case

1).介绍

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

语法1:

-- 含义: 当case_value的值为 when_value1时,执行statement_list1,当值为 when_value2时,执行statement_list2, 否则就执行 statement_listCASE case_valueWHEN 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_listCASEWHEN search_condition1 THEN statement_list1[WHEN search_condition2 THEN statement_list2] ...[ELSE statement_list]END CASE;

2).案例

根据传入的月份,判定月份所属的季节(要求采用case结构)。

1-3月份,为第一季度4-6月份,为第二季度7-9月份,为第三季度10-12月份,为第四季度

#创建存储过程create procedure p6(in month int)begin#定义局部变量declare result varchar(10);casewhen month >= 1 and month <= 3 thenset result := '第一季度';when month >= 4 and month <= 6 thenset result := '第二季度';when month >= 7 and month <= 9 thenset result := '第三季度';when month >= 10 and month <= 12 thenset result := '第四季度';elseset result := '非法参数';end case ;#查询局部变量:借助concat函数拼接字符串select concat('您输入的月份为: ',month, ', 所属的季度为: ',result);end;#调用存储过程call p6(16);

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

4.2.7 while循环

1).介绍

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

特点:满足条件才循环

-- 先判定条件,如果条件为true,则执行逻辑,否则,不执行逻辑WHILE 条件 DOSQL逻辑...END WHILE; #结束语句

2).案例

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

-- A. 定义局部变量, 记录累加之后的值;-- B. 每循环一次, 就会对n进行减1 , 如果n减到0, 则退出循环create procedure p7(in n int)begindeclare total int default 0;while n>0 doset total := total + n;set n := n - 1;end while;select total;end;call p7(100);

4.2.8 repeat循环

1).介绍

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

特点:满足条件退出循环

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

2).案例

计算从1累加到n的值,n为传入的参数值。(使用repeat实现)

-- A. 定义局部变量, 记录累加之后的值;-- B. 每循环一次, 就会对n进行-1 , 如果n减到0, 则退出循环create procedure p8(in n int)begindeclare total int default 0;repeatset total := total + n;set n := n - 1;until n <= 0end repeat;select total;end;call p8(10);call p8(100);

4.2.9 loop循环

1).介绍

LOOP 实现简单的循环,如果不在SQL逻辑中增加退出循环的条件,可以用其来实现简单的死循环。

LOOP可以配合一下两个语句使用:

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

#[begin_label:] :指定标记[begin_label:] LOOPSQL逻辑...END LOOP [end_label];

LEAVE label; -- 退出指定标记的循环体ITERATE label; -- 直接进入下一次循环

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

2).案例一

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

-- A. 定义局部变量, 记录累加之后的值;-- B. 每循环一次, 就会对n进行-1 , 如果n减到0, 则退出循环 ----> leave xxcreate procedure p9(in n int)begindeclare total int default 0;sum:loopif n<=0 thenleave sum;end if;set total := total + n;set n := n - 1;end loop sum;select total;end;call p9(100);

3).案例二

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

-- A. 定义局部变量, 记录累加之后的值;-- B. 每循环一次, 就会对n进行-1 , 如果n减到0, 则退出循环 ----> leave xx-- C. 如果当次累加的数据是奇数, 则直接进入下一次循环. --------> iterate xxcreate procedure p10(in n int)begindeclare total int default 0;sum:loopif n<=0 thenleave sum;end if;if n%2 = 1 thenset n := n - 1;iterate sum;end if;set total := total + n;set n := n - 1;end loop sum;select total;end;call p10(100);

4.2.10 游标

1)场景分析:

这个存储过程的sql是把SELECT执行结果 count(*)总记录数,赋值给stu_count这个变量。

即:单个字段的值 赋值给一个变量---------成功。

delimiter $$-- 定义存储过程create procedure p2()begin#定义局部变量declare stu_count int default 0;#赋值:把SELECT执行结果赋值给stu_count这个变量select count(*) into stu_count from student;#查询这个变量select stu_count;end$$#调用存储过程call p2();

count(*)修改为*,此时变为把查询的所有结果赋值给一个变量--------失败。

DELIMITER $$-- 定义存储过程CREATE PROCEDURE p2()BEGIN#定义局部变量DECLARE stu_count INT DEFAULT 0;#赋值:把SELECT执行结果赋值给stu_count这个变量SELECT COUNT * INTO stu_count FROM student;#查询这个变量SELECT stu_count;END$$#调用存储过程CALL p2();

原因:

count(*)返回单列数据,所以可以赋值给一个变量而*返回的数据有很多列,所以不能赋值给一个变量说明:局部变量只能接受一些单行单列类型的数据,如果接收的是一张表或者是一个结果集该如何接收呢???使用游标。

2)介绍

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

A.声明游标:类似于声明局部变量

#把sql语句的查询结果封装到这个游标当中。DECLARE 游标名称 CURSOR FOR 查询语句 ;

B.打开游标:使用游标之前一定要打开游标

OPEN 游标名称 ;

C.获取游标记录

#把游标中的记录赋值给一个个的变量FETCH 游标名称 INTO 变量 [, 变量 ] ;

D.关闭游标

#当我们把游标当中的记录便利获取完毕之后,就可以关闭游标了CLOSE 游标名称 ;

3)案例

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

-- 逻辑:-- A. 声明游标, 存储查询结果集-- B. 准备: 创建表结构-- C. 开启游标-- D. 获取游标中的记录-- E. 插入数据到新表中-- F. 关闭游标DELIMITER $$CREATE PROCEDURE p11(IN uage INT)#begin和end之间可以编写任何的sql语句BEGIN#声明2个局部变量接收遍历游标时的2个参数(注意顺序:要先声明局部变量再声明游标,否则执行时会报错)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#获取游标的记录并赋值给2个字段FETCH u_cursor INTO uname,upro;#之后把这2个字段插入到新创建的表中INSERT INTO tb_user_pro VALUES (NULL, uname, upro);END WHILE;#关闭游标CLOSE u_cursor;END$$CALL p11(30);

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

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

上述的功能,虽然我们实现了,但是逻辑并不完善,而且程序执行完毕,获取不到数据,数据库还报错。 接下来,我们就需要来完成这个存储过程,并且解决这个问题。

要想解决这个问题,也就是说一旦发现游标当中没有数据了 此时就要退出循环关闭游标,就需要通过MySQL中提供的条件处理程序 Handler来解决。

4.2.11 条件处理程序

1)介绍

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

即:我们定义了一个条件处理程序,可以去指定什么时候这个条件处理程序被触发,触发了之后执行什么样的动作。

具体语法为:

# 想要使用条件处理程序,我们就需要声明条件处理程序,再声明的时候还需要指定当前条件处理程序的类型(2个)DECLARE handler_action HANDLER FOR condition_value [, condition_value] ... statement;

handler_action:当前条件处理程序的类型

CONTINUE: 继续执行当前程序EXIT: 终止执行当前程序

condition_value:满足什么样的条件才会执行handler这个动作,执行这个动作之后还可以执行具体的sql逻辑(statement)

SQLSTATE sqlstate_value: 状态码,如 02000

SQLWARNING: 所有以01开头的SQLSTATE代码的简写(警告)

NOT FOUND: 所有以02开头的SQLSTATE代码的简写(找不到数据)

SQLEXCEPTION: 所有没有被SQLWARNING 或 NOT FOUND捕获的SQLSTATE代码的简写

具体的错误状态码,可以参考官方文档:

/doc/refman/8.0/en/declare-handler.html

/doc/mysql-errors/8.0/en/server-error-reference.html

2)案例

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

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

A.通过SQLSTATE指定具体的状态码

-- 逻辑:-- A. 声明游标, 存储查询结果集-- B. 准备: 创建表结构-- C. 开启游标-- D. 获取游标中的记录-- E. 插入数据到新表中-- F. 关闭游标DELIMITER $$create procedure p11(in uage int)#begin和end之间可以编写任何的sql语句begin#声明2个局部变量接收遍历游标时的2个参数(注意顺序:要先声明局部变量再声明游标,否则执行时会报错)DECLARE uname VARCHAR(100);DECLARE upro VARCHAR(100);#声明游标,存储查询的结果集declare u_cursor cursor for select name,profession from tb_user where age <=uage;#声明条件处理程序:满足这个状态码就执行退出操作,退出的时候还需要关闭游标#执行流程:即使下面的while true死循环,循环走到最后抓取不到数据会报错,报错之后# 它就会进入到我们的条件处理程序,在条件处理程序中它会关闭游标,然后执行退出操作。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#获取游标的记录并赋值给2个字段fetch u_cursor into uname,upro;#之后把这2个字段插入到新创建的表中insert into tb_user_pro values (null, uname, upro);end while;#关闭游标close u_cursor;end$$call p11(30);

再次执行发现不会在报错了,查询新创建的表发现数据插入成功

B.通过SQLSTATE的代码简写方式NOT FOUND

02 开头的状态码,代码简写为 NOT FOUND

DELIMITER $$create procedure p12(in uage int)#begin和end之间可以编写任何的sql语句begin#声明2个局部变量接收遍历游标时的2个参数(注意顺序:要先声明局部变量再声明游标,否则执行时会报错)DECLARE uname VARCHAR(100);DECLARE upro VARCHAR(100);#声明游标,存储查询的结果集declare u_cursor cursor for select name,profession from tb_user where age <=uage;#声明条件处理程序:满足这个状态码就执行退出操作,退出的时候还需要关闭游标#执行流程:即使下面的while true死循环,循环走到最后抓取不到数据会报错,报错之后# 它就会进入到我们的条件处理程序,在条件处理程序中它会关闭游标,然后执行退出操作。declare exit handler for not found 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#获取游标的记录并赋值给2个字段fetch u_cursor into uname,upro;#之后把这2个字段插入到新创建的表中insert into tb_user_pro values (null, uname, upro);end while;#关闭游标close u_cursor;end$$call p12(30);

同样:再次执行发现不会在报错了,查询新创建的表发现数据插入成功

效果:

4.3 存储函数

4.3.1 介绍

存储函数是有返回值的存储过程,存储函数的参数只能是IN类型的。

也就是说:定义一个存储函数必须要指定返回值,并且它的参数列表当中所有的参数类型必须是输入类型in类型。

说明:存储函数和存储过程的整体语法结构和流程控制基本都是一致的,只是里面的基础语法结构略有不同。

具体语法如下:

RETURNS type :指定返回时的类型可选参数characteristic :当前存储参数的特性

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

characteristic说明:

DETERMINISTIC:相同的输入参数总是产生相同的结果 即:传入的参数一样,返回值也是一样的。 NO SQL :不包含 SQL 语句。 当前的存储函数中不包含 SQL 语句,就可以通过这个描述符来描述当前存储函数。 READS SQL DATA:只包含读取数据的语句,但不包含写入数据的语句。

4.3.2 案例

计算从1累加到n的值,n为传入的参数值。(通过存储函数来实现

#默认就是increate function fun1(n int)returns intbegindeclare total int default 0;while n>0 doset total := total + n;set n := n - 1;end while;return total;end;select fun1(50);

会报错:

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

解决:

DELIMITER $$create function fun1(n int)returns int deterministicbegindeclare total int default 0;while n>0 doset total := total + n;set n := n - 1;end while;return total;end$$select fun1(50);

4.3.3 缺点

存储函数呢相对来说用的比较少一些,因为存储函数能做的事情那么存储过程也能做到,而且存储函数还有一个弊端他必须要用返回值。假如我要去执行一段逻辑而这段逻辑有返回值,我可不可以使用存储过程来实现???

可以:因为存储过程我们可以指定参数的类型为out,就可以接收我存储过程执行的结果,所以可以使用存储函数的地方都可以使用存储过程来代替。

4.4 触发器

4.4.1 介绍

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

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

行级触发器:比如执行一条update语句,他影响了5行,那么这个时候这个触发器会被触发5次。语句级触发器:比如执行一条update语句,不管这条update影响了多少行,只触发一次。

4.4.2 语法

1).创建

#创建触发器并指定名称CREATE TRIGGER trigger_name#指定触发的时机 指定触发器的类型BEFORE/AFTER INSERT/UPDATE/DELETE# on 表的名字:哪一张表的数据进行增删改之后触发#FOR EACH ROW:指定行级触发器,mysql目前只支持行级触发器。ON tbl_name FOR EACH ROW #编写触发器的业务逻辑BEGINtrigger_stmt ;END;

2).查看

#查看当前数据库中所有的触发器SHOW TRIGGERS ;

3).删除

schema_name:数据库名

trigger_name:触发器名

# 删除指定数据库下的触发器# 如果没有指定 schema_name数据库名字,默认为当前数据库 。DROP TRIGGER [schema_name.]trigger_name ; --

4.4.3 案例

通过触发器记录 tb_user 表的数据变更日志,将变更日志插入到日志表user_logs中, 包含增加, 修改 , 删除 ;

也就是说:只要tb_user 表进行了增删改就会触发触发器,然后把变更日志记录在日志表user_logs中。

日志表结构准备:

字段说明:

id:主键operation 操作类型:当前操作是插入还是更新还是删除operate_time 操作时间:触发器执行的当前时间operate_id 操作的ID:操作的原始记录的id是多少,你操作的是tb_user 表当中的那一条数据。operate_params 操作的参数:因为记录的是日志就需要知道我当前插入数据,数据里面的每一个参数值是什么样的。更新数据,更新之前 更新之后是什么样的。删除数据,删除的是哪一块数据。

-- 准备工作 : 日志表 user_logscreate table user_logs(id int(11) not null auto_increment,operation varchar(20) not null comment '操作类型, insert/update/delete',operate_time datetime not null comment '操作时间',operate_id int(11) not null comment '操作的ID',operate_params varchar(500) comment '操作参数',primary key(`id`))engine=innodb default charset=utf8;

A.插入数据触发器

# 插入触发器DELIMITER $$create trigger tb_user_insert_triggerafter insert on tb_user for each rowbegin#operate_id:操作的原始记录的id,通过之前学习的别名new拿到插入到数据库的id(new.id)#operate_params:插入时传递了哪些参数,通过字符串拼接函数指定insert into user_logs(id, operation, operate_time, operate_id, operate_params) VALUES(null, 'insert', now(), new.id, concat('插入的数据内容为:id=',new.id,',name=',new.name, ', phone=', NEW.phone, ', email=', NEW.email, ',profession=', NEW.profession));end$$

测试:

-- 查看show triggers ;-- 插入数据到tb_user:验证在插入数据后会不会往日志表当中保存日志insert into tb_user(id, name, phone, email, profession, age, gender, status,createtime) VALUES (26,'三皇子','18809091212','erhuangzi@','软件工程',23,'1','1',now());-- 查看日志表当中有没有数据SELECT * FROM user_logs;

测试完毕之后,检查日志表中的数据是否可以正常插入,以及插入数据的正确性。

B.修改数据触发器

# 修改触发器DELIMITER $$create trigger tb_user_update_triggerafter update on tb_user for each rowbegininsert into user_logs(id, operation, operate_time, operate_id, operate_params) VALUES(null, 'update', now(), new.id,concat('更新之前的数据: id=',old.id,',name=',old.name, ', phone=',old.phone, ', email=', old.email, ', profession=', old.profession,' | 更新之后的数据: id=',new.id,',name=',new.name, ', phone=',NEW.phone, ', email=', NEW.email, ', profession=', NEW.profession));end$$

测试:

-- 查看触发器show triggers ;-- 更新数据看是否会触发触发器,把日志保存到日志表update tb_user set profession = '会计' where id = 23;update tb_user set profession = '会计' where id <= 5; -- 因为是行级触发器,所以会触发5次-- 查看日志表当中有没有数据SELECT * FROM user_logs;

测试完毕之后,检查日志表中的数据是否可以正常插入,以及插入数据的正确性。

C.删除数据触发器

# 删除触发器DELIMITER $$CREATE TRIGGER tb_user_delete_triggerAFTER DELETE ON tb_user FOR EACH ROWBEGIN#operate_id原始数据的id:删除之前有数据删除之后没有数据,所以只能使用old拿到删除之前的id是多少。INSERT INTO user_logs(id, operation, operate_time, operate_id, operate_params) VALUES(NULL, 'delete', NOW(), old.id,CONCAT('删除之前的数据: id=',old.id,',name=',old.name, ', phone=',old.phone, ', email=', old.email, ', profession=', old.profession));END$$

测试:

-- 查看触发器SHOW TRIGGERS ;-- 删除数据看是否会触发触发器,把日志保存到日志表DELETE FROM tb_user WHERE id = 26;-- 查看日志表当中有没有数据SELECT * FROM user_logs;

测试完毕之后,检查日志表中的数据是否可以正常插入,以及插入数据的正确性。

4.5 总结

MySql进阶篇---006:存储引擎 索引 SQL优化 视图 存储过程 变量 流程控制 游标 存储函数 触发器

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。