学习笔记:MySQL高阶知识体系(下)——索引、锁、日志、隔离级别与MVCC

转载自https://www.ydlclass.com/doc21xnv/database/mysqladvance/mysqlAdvance2.html

MySQL高阶知识体系(下)


6. 索引

6.1 数据结构

一方面mysql的数据是存储在磁盘上的,另一方面还要满足对日常操作如【增删改查】的高效稳定的支持,我们当然可以采用更好的硬件来提升性能,但是选用合适的数据结构也很关键,innodb采用的是一种名为【B+树】的数据结构。

我们之前已经学习过innodb中的数据是以【行】为单位,存在一个个大小为16k的【页】中,刚才的b+树的作用就是按照一个的组织形式,将所有的【页】组织关联起来。

6.1.1 B- 树

我们要了解【B+树】,首先要了解一下【B-树】,这里的 B 表示 balance( 平衡的意思),B-树是一种【多路自平衡的搜索树】,它类似普通的平衡二叉树,不同的一点是B-树允许每个节点有更多的子节点。下图是 B-树的简化图:
在这里插入图片描述
B-树有如下特点:

  1. 所有键值分布在整颗树中;
  2. 任何一个关键字出现且只出现在一个结点中;
  3. 搜索有可能在非叶子结点结束;
  4. 在关键字全集内做一次查找,性能逼近二分查找;

6.1.2 B+ 树

【B+树】是【B-树】的变体,也是一种多路搜索树, 它与 B- 树的不同之处在于:

  1. 所有关键字存储在叶子节点
  2. 为所有叶子结点增加了一个双向指针

简化 B+树 如下图:
在这里插入图片描述

6.1.3 选型缘由

问题一:为什么在b-树或b+树中选择?

  • mysql数据模型更适合用这类数据结构,一条数据中通常包含【id】+【其他列数据】,我们可以很轻松的根据id组织一颗B+树。
  • 我们知道innodb使用【页】(这是inndb管理数据的最小单位)保存数据,一页(16KB),b+树中的每个节点都是一页数据。

问题二:为什么选择B+树?

  • 相同的空间,不存放【整行数据】就能存【更多的id】,b+树能使每个节点能检索的【范围更大、更精确,极大的减少了I/O操作,保证b+树的层高较低,通常3到4层的层高就能支持百万级别的访问】。
  • MySQL是一种关系型数据库,【区间访问】是很常见的一种情况,B+树叶节点增加的双向指针,加强了区间访问性,可使用在范围区间查询的情况。

6.1.4 发现索引

我们发现当使用id去查询数据时,效率很高,因为使用id可以利用B+树的特性,加速查询,请看以下两条sql的执行效率:

select * from user_table where id = 1                              -- 使用时间0.011s
select * from user_table where email = '[email protected]'          -- 使用时间4.284s

我们发现,查询相同的记录,使用【id列】比使用【emil列】快了389倍,原因如下:

  • 使用id列可以利用B+树的特性,由上自下查询。
  • 使用email列只能从叶子节点进行【全表扫描】,一个一个的比较。

那么如果我想提升使用其他字段的查询效率,应该怎么做呢?

首先,我们应该想到的思路就是,按照这个逻辑再给其他的字段也创建一个这样的结构不就好了,如下:
在这里插入图片描述

但是我们会发现,如果我们不断的创建类似的结构,数据会保存很多次,1个G的数据可以膨胀为5G甚至10G,所以我们可以进行优化,在叶子节点中只【保存id】而不保存全部数据,查到id后再【回表】(回到原来的结构中根据id进行查询)查询整条记录,其结构如下:

在这里插入图片描述

其实这就是我们日常工作中经常创建的【索引】。

6.2 分类与创建

6.2.1 聚簇索引与非聚簇索引

我们在上边的例子中,【主键和数据】共存的索引被称之为【聚簇索引】,其他的,比如我们使用【姓名列+主键】建立的索引,可以称为【非聚簇索引】,或者【辅助索引】,或者【二级索引】,同时聚簇索引只有在innodb引擎中才存在,而在myIsam中是不存在的,如下图:
在这里插入图片描述
InnoDB使用的是【聚簇索引】,他会将【主键】组织到一棵B+树中,而【行数据】就储存在叶子节点上,若使用where id = 14这样的条件查找主键,则按照B+树的检索算法即可查找到对应的叶节点,之后获得行数据。

若对Name列进行条件搜索,且name列已建立【索引】,则需要两个步骤:

  1. 在辅助索引B+树中检索Name,到达其叶子节点获取对应的主键。
  2. 使用主键在主索引B+树种再执行一次B+树检索操作,最终到达叶子节点即可获取整行数据。(重点在于通过其他键需要建立辅助索引)

如下图:
在这里插入图片描述

MyIsam使用的是【非聚簇索引】,非聚簇索引的两棵B+树看上去没什么不同,节点的结构完全一致只是存储的内容不同而已,主键索引B+树的节点存储了主键,辅助键索引B+树存储了辅助列。表数据存储在独立的地方,这两颗B+树的叶子节点都使用一个【地址指向真正的表数据】,对于表数据来说,这两个键没有任何差别。由于索引树是独立的,通过辅助键检索无需访问主键的索引树。

tips:

  • 聚簇索引【默认使用主键】,如果表中没有定义主键,InnoDB 会选择一个【唯一且非空】的列代替。如果没有这样的列,InnoDB 会隐式定义一个主键【类似oracle中的RowId】rowid来作为聚簇索引的列。
  • 如果涉及到大数据量的排序、全表扫描、count之类的操作的话,还是MyIsam占优势些,因为索引所占空间小,这些操作是需要在内存中完成的。

小问题:主键为什么建议使用自增id?

  • 主键最好不要使用uuid,因为uuid的值太过离散,不适合排序且可能出现新增加记录的uuid,会插入在索引树中间的位置,出现页分裂,导致索引树调整复杂度变大,消耗更多的时间和资源。
  • 聚簇索引的数据的物理存放顺序与索引顺序是一致的,即:只要索引是相邻的,那么对应的数据一定也是相邻地存放在磁盘上的。如果主键不是自增id,它会不断地调整数据的物理地址、分页,当然也有其他一些措施来减少这些操作,但却无法彻底避免。但如果是自增的id,它只需要一 页一页地写,索引结构相对紧凑,磁盘碎片少,效率也高。

本章节中讲述了聚簇索引和二级键索引,对于【二级索引】而言,根据其不同的特性,我们又可以分为普通索引、唯一索引、复合索引等,接下来会一一讲解。

6.2.2 普通索引 (NORMAL)

就是普普通通的索引,没有什么特殊要求,理论上任何列都可以当做普通索引,创建方式如下:

第一步:创建索引前先执行下列语句,观察执行时间:

select * from user_table where user_name ='Dorothy William Harris';  -- 整个执行时间为4.297s

第二步:创建user_name列的索引:

create index idx_user_name on user_table(user_name);   -- 整个索引创建时间为24.502s

结论:创建索引是一个很费时间的操作。

第三步:再次执行下列语句:

select * from user_table where user_name ='Dorothy William Harris';   -- 执行时间0.031s

结论:创建索引后,我们的执行效率提升了138倍。

第四步:删除索引:

drop index idx_user_name on user_table; 

其他创建索引的方法,如下:

(1)创建email列的索引,索引可以截取length长度,只使用这一列的前几个字符

create index idx_email on user_table(email(5));     --执行时间16.174s

重点:有的列【数据量比较大】,使用前几个字符就能【很快标识】出来一行数据,那我们就可以使用这种方式建立索引,比如我们的邮箱,邮箱很多后缀是相同的我们完全可以忽略。

(2)使用修改表的方式添加索引

alter table user_table add index idx_email (email);

(3)建表时,同时创建索引

create table tbl_name(
    tid int,
    tname varchar(20),
    gender varchar(1),
    index [indexName] (fieldName(length))
)

6.2.3 唯一索引(UNIQUE )

对列的要求:索引列的值不能重复

创建表的同时,创建索引:

create table tbl_name(
    tid int,
    tname varchar(20),
    gender varchar(1),
    unique index unique_index_tname (tname)
)

独立的sql语句创建索引,我们的邮箱,用户名就应该创建唯一索引,姓名就应该是普通索引:

create unique index idx_email on user(email);

通过alter语句添加索引:

ALTER table mytable ADD UNIQUE [ux_indexName] (username(length))

唯一索引和主键的区别:

  1. 唯一索引列允许空值,而主键列不允许为空值。
  2. 主键列在创建时,已经默认为非空值 + 唯一索引了。
  3. 主键可以被其他表引用为外键,而唯一索引不能。
  4. 一个表最多只能创建一个主键,但可以创建多个唯一索引。
  5. 主键更适合那些不容易更改的唯一标识,如自动递增列、身份证号等。

唯一约束和唯一索引的区别:

  1. 唯一约束和唯一索引,都可以实现列数据的唯一,列值可以为null。
  2. 创建唯一约束,会自动创建一个同名的唯一索引,该索引不能单独删除,删除约束会自动删除索引。唯一约束是通过唯一索引来实现数据唯一。
  3. 创建一个唯一索引,这个索引就是独立的索引,可以单独删除。
  4. 如果一个列上想有约束和索引,且两者可以单独的删除。可以先建唯一索引,再建同名的唯一约束。

6.2.4 多个二级索引的组合使用

记住一点:mysql在执行查询语句的时候一般只会使用【一个索引】,除非是使【用or连接的两个索引列】会产生索引合并。

我们针对某电商平台的检索功能做了优化,添加了三个索引,三个字段分别为【品牌】、【价格】、【销量】这三个的索引结构如下:

(1)品牌的索引结构:
在这里插入图片描述
(2)价格的索引结构:
在这里插入图片描述
(3)销量的索引结构:
在这里插入图片描述

针对以上的索引我们进行如下的查询,分析检索过程:

我们要检索品牌为阿玛尼(Armani)的包包:

第一步: 通过【品牌索引】检索出所有阿玛尼的商品id,回表查询,得到结果。
结论: 会使用一个索引。

我们要检索名称为阿玛尼(Armani),价格在1万到3万之间的包包。查询的步骤如下:

第一步: 通过【品牌索引】检索出所有阿玛尼的商品id。
第二步: 直接回表扫描,根据剩余条件检索结果。
结论: 只会使用第一个索引。

我们要检索名称为阿玛尼(Armani),价格为26800,且销量在50以上的包包。查询的步骤如下:

第一步: 通过【品牌索引】检索出所有阿玛尼的商品id,进行缓存。
第二步: 直接回表扫描,根据剩余条件检索结果。
结论: 只会使用第一个索引。

我们要检索名称为阿玛尼(Armani)或名称为LV的包包:

第一步: 通过【品牌索引】检索出所有阿玛尼的商品id,得到结果。
结论: 我们经常听说,有or索引会失效,但是像这样的【type =‘Armani’ or type = ‘LV’】并不会,他相当于一个in关键字,会使用一个索引。

我们要检索名称为阿玛尼(Armani)或价格大于8000的包包:

第一步: 通过【品牌索引】检索出所有阿玛尼的商品id,进行缓存。
第二步: 通过【价格索引】检索出价格在5万到7万之间的商品id,这是一个连接条件带有【or的查询】,所以需要和上一步的结果进行【并集】,得到结果。
结论: 这个过程叫【索引合并】当检索条件有or但是所有的条件都有索引时,索引不失效,可以走【两个索引】。

我们要检索名称为阿玛尼(Armani),且价格大于8000,且【产地(该列无索引)】在北京的包包:

第一步: 通过【品牌索引】检索出所有阿玛尼的商品id。
第二步: 直接回表扫描,根据剩余条件检索结果。
结论: 只会使用第一个索引。

我们要检索名称为阿玛尼(Armani)或价格大于8000,或【产地(该列无索引)】在北京的包包:

第一步: 优化器发现【产地列】无索引,同时连接的逻辑是【or】没有办法利用【索引】优化,只能全表扫描,索引失效。
结论: 发生全表扫描,索引失效,条件中有没建立索引的列,同时关联条件是or。

6.2.5 复合索引(联合索引)重要※

当【查询语句】中包含【多个查询条件,且查询的顺序基本保持一致】时,我们推荐使用复合索引,索引的【组合使用】效率是低于【复合索引】的。

比如:我们经常按照A列、B列、C列进行查询时,通常的做法是建立一个由三个列共同组成的【复合索引】而不是对每一个列建立【普通索引】。

创建联合索引的方式如下:

alert table test add idx_a1_a2_a3 table (a1, a2, a3) 
-- 28.531s
create index idx_user_nick_name on ydl_user(user_name, nick_name,email(7));

复合索引的结构如下,复合索引会优先按照第一列排序,第一列相同的情况下会按照第二列排序,以此类推,如下图:
在这里插入图片描述
我们不妨把上边的图,转化为下边的表格,看起来会好一些:

品牌 价格 销量 id
Armani 16800 35 13, 24, 76
Armani 26800 35 12, 14, 16
Armani 26800 100 34, 56, 17
Armani 68888 15 1, 4, 5, 6, 7
GUCCI 8999 135 78, 92
LV 9999 326 55, 63
LV 12888 99 57, 99
LV 42888 69 11, 22
PRADA 9588 125 111, 202

认真阅读了上边的介绍和图形,我们再次思考以下几个问题:

我们要检索名称为阿玛尼(Armani)的包包:

第一步: 通过【品牌索引】检索出所有阿玛尼的商品id,回表查询,得到结果。
结论: 会使用第一部分索引。

我们要检索名称为阿玛尼(Armani),价格在1万到3万之间的包包,查询的步骤如下:

第一步: 通过【品牌索引】检索出所有阿玛尼的叶子节点。
第二步: 在【满足上一步条件的叶子节点中】查询价格在1万到3万之间的包包的列,查询出对应的id,回表查询列数据。
结论: 会使用复合索引的两个部分。

我们要检索名称为阿玛尼(Armani)或价格大于8000的包包:

第一步: 优化器发现我们并没有一个【价格列】的单独的二级索引,此时要查询价格大于8000的包,必须进行全表扫描。
结论: 但凡查询的条件中没有【复合索引的第一部分】,索引直接【失效】,全表扫描。

我们要检索名称为阿玛尼(Armani),且价格大于8000,且【产地(该列无索引)】在北京的包包:

第一步: 通过【品牌索引】检索出所有阿玛尼的叶子节点。
第二步: 在【满足上一步条件的叶子节点中】查询价格大于8000元的包包的叶子节点。
第三步: 因为【产地列】无索引,但是是【and】的关系,我们只需要将上一步得到的结果回表查询,在这个很小的范围内,检索产地是不是北京即可。
结论: 可以使用复合索引的两个部分。

我们要检索名称为阿玛尼(Armani)和LV之间,价格为在1万到3万的包包,查询的步骤如下:

第一步: 通过【品牌索引】检索出所有阿玛尼和LV的所有叶子节点。
第二步: 我们本想在第一步的结果中,快速定位价格的范围,但是发现一个问题,由于第一步不是等值查询,会导致后边的结果不连续,必须对【上一步的结果】全部遍历,才能拿到对应的结果。
结论: 只会使用复合索引的第一个部分,这个就引出了【复合索引中特别重要的一个概念】-【最左前缀原则】。

重点※: 最左前缀原则:

  • 最左前缀匹配原则,非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 ,如果建立(a,b,c,d)顺序的联合索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。

  • =和in可以乱序,比如a = 1 and b < 2 and c = 3 ,咱们建立的索引就可以是(a,c,b)或者(c,a,b)。

思考: 为什么联合索引的性能会比索引的组合使用效率高。

6.2.6 全文索引(FULLTEXT)

做全文检索(不如百度的搜索功能)使用的索引,但是这种场景,我们有更好的替代品,如:ElacticSearch,所以实际使用不多,只当了解。

使用 like + % 实现的模糊匹配有点类似全文索引。但是对于大量的文本数据检索,全文索引比 like + % 快 N 倍,速度不是一个数量级,但是全文索引可能存在【精度问题】。同时普通索引在使用like时如果%放在首位,索引会失效。

全文索引的版本支持:

  1. MySQL 5.6 以前的版本,只有 MyIsam 存储引擎支持全文索引。
  2. MySQL 5.6 及以后的版本,MyIsam 和 InnoDB 存储引擎均支持全文索引。
  3. 只有字段的数据类型为 char、varchar、text 及其系列才可以建全文索引。

使用全文索引的注意:

  1. 使用全文索引前,搞清楚版本支持情况。
  2. 全文索引比 like + % 快 N 倍,但是可能存在精度问题。
  3. 如果需要全文索引的是大量数据,建议先添加数据,再创建索引。
  4. 对于中文,可以使用 MySQL 5.7.6 之后的版本,或者第三方插件。

(1)创建表时创建全文索引

create table ydlclass_user (    
    ..   
    FULLTEXT KEY fulltext_text(text)  
) 

(2)在已存在的表上创建全文索引

create fulltext index fulltext_text  on ydlclass_user(text);

本次创建用时143s:
在这里插入图片描述
(3)通过 SQL 语句 ALTER TABLE 创建全文索引

alter table ydlclass_user add fulltext index fulltext_text(text);

(4)直接使用 DROP INDEX 删除全文索引

drop index fulltext index on ydlclass_user;

(5)全文检索的语法

select * from ydlclass_user where match(text) against('高号便法还历只办二主厂向际');

6.2.8 hash索引

hash索引是Memory存储引擎的默认方式,而且只有memory引擎支持hash索引,memory的数据是放在内存中的,一旦服务关闭,表中的数据就会丢失,我们可以使用如下的sql创建一张表:

CREATE TABLE `hash_user`  (
  `user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  `user_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户账号',
  ......
) ENGINE = Memory CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户信息表' ROW_FORMAT = Dynamic;

合理的使用memory引擎可以极大的提升性能,针对memory引擎的特点重启丢失),我们最好在其中存储一些公共的、常用的、不经常发生改变的数据,比如一些字典数据、配置数据等。同时,这些数据最好持久化在一些其他的地方,比如配置文件、其他的表,在程序启动的时候,主动的进行加载,我们可以使用如下sql,将一张表的数据加载到内存中:

insert into hash_user select * from ydl_user where user_id < 2000000;

我们在执行的过程种,可能有如下错误:
在这里插入图片描述
他告诉我,这个表使用的内存满了,放不下了,我们只需要调节下边两个参数,修改配置文件重启即可:

tmp_table_size = 4096M
max_heap_table_size = 4096M

基础工作完成,写几个sql语句尝试一下,我们发现真的一个字:快。

我们执行一下的sql

select * from hash_user where email = '[email protected]'  -- 0.189s

创建一个hash索引

create index hash_idx_user_name using hash on hash_user(email);

再次查询

select * from hash_user where email = '[email protected]'  -- 0.017s

也有不错的效果。

关于hash索引需要了解的几点:

  1. hash是一种key-value形式的数据结构。实现一般是数组+链表的结构,通过hash函数计算出key在数组中的位置,然后如果出现hash冲突就通过链表来解决。当然还有其他的解决hash冲突的方法。hash这种数据结构是很常用的,比如我们系统使用HashMap来构建热点数据缓存,存取效率很好。
  2. 即使是相近的key,hash的取值也完全没有规律,索引hash索引不支持范围查询。
  3. hash索引存储的是hash值和行指针,所以通过hash索引查询数据需要进行两次查询(首先查询行的位置,然后找到具体的数据)。
  4. hash索引查询数据的前提就是计算hash值,也就是要求key为一个能准确指向一条数据的key,所以对于like等一类的匹配查询是不支持的。
  5. 只要是只需要做等值比较查询,而不包含排序或范围查询的需求,都适合使用哈希索引。

6.2.8 空间索引(SPATIAL)

MySQL在5.7之后的版本支持了空间索引,而且支持OpenGIS几何数据模型。这是在地理位置领域使用的一种索引,其他场景用的很少,所以不需要深入学习。

6.3 explain的用法

在这里插入图片描述

explain关键字可以模拟MySQL优化器执行SQL语句,可以很好的分析SQL语句或表结构的性能瓶颈。

explain的使用很简单,只需要在目标sql前加上这个关键字就可以了:
在这里插入图片描述

执行explain会产生以下11列内容,如下:

列号 说 明
1 id select查询的序列号,包含一组数字,表示查询中执行select子句或操作表的顺序
2 select_type 查询类型
3 table 正在访问哪个表
4 partitions 匹配的分区
5 type 访问的类型
6 possible_keys 显示可能应用在这张表中的索引,一个或多个,但不一定实际使用到
7 key 实际使用到的索引,如果为NULL,则没有使用索引
8 key_len 表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度
9 ref 显示索引的哪一列被使用了,如果可能的话,是一个常数,哪些列或常量被用于查找索引列上的值
10 rows 根据表统计信息及索引选用情况,大致估算出找到所需的记录所需读取的行数
11 filtered 查询的表行占表的百分比
12 Extra 包含不适合在其它列中显示但十分重要的额外信息

6.3.1 id字段

select查询的序列号,包含一组数字,表示查询中执行select子句或操作表的顺序

  1. id相同
    id如果相同,可以认为是一组,执行顺序从上至下,如下查询语句:

    explain select * from student s, scores sc where s.id = sc.s_id
    

    在这里插入图片描述

  2. id不同
    如果是子查询,id的序号会递增,id的值越大优先级越高,越先被执行例子

    explain select * from student where age > (
    	select age from student where name = '连宇栋'
    );
    

    在这里插入图片描述

  3. id部分相同部分不同
    id如果相同,可以认为是一组,从上往下顺序执行在所有组中,id值越大,优先级越高,越先执行例子:

    explain 
    select * from student s, scores sc where s.id = sc.s_id
    union
    select * from student s, scores sc where s.id = sc.s_id;
    

    在这里插入图片描述

6.3.2 select_type字段

  1. SIMPLE:简单查询,不包含子查询或Union查询的sql语句。

  2. PRIMARY:查询中若包含任何复杂的子部分,最外层查询则被标记为主查询。

  3. SUBQUERY:在select或where中包含子查询。

  4. UNION:若第二个select出现在uion之后,则被标记为UNION。

  5. UNION RESULT:从UNION表获取结果的合并操作。

6.3.3 type字段

最好到最差:掌握以下10种常见的即可NULL > system > const > eq_ref > ref > ref_or_null > index_merge > range>index > ALL

  1. NULL
    MySQL能够在优化阶段分解查询语句,在执行阶段用不着再访问表或索引,比如通过id没有找到例子:

    explain select min(id) from student;
    

    在这里插入图片描述

  2. system
    表只有一行记录(等于系统表),这是const类型的特列,平时不大会出现,可以忽略,我也没有实测出来。

    explain select * from mysql.proxies_priv
    

    我实测一个只有一行记录的系统表,同样是all。
    在这里插入图片描述

  3. const
    表示通过索引一次就找到了,const用于比较primary key或uique索引,因为只匹配一行数据,所以很快,如主键置于where列表中,MySQL就能将该查询转换为一个常量例子:

    explain select * from student where id = 1;
    

    在这里插入图片描述

  4. eq_ref
    唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配,常见于主键或唯一索引扫描例子:

    被驱动表使用主键索面,结果唯一

    explain select * from scores sc left join student s on s.id = sc.s_id
    

    在这里插入图片描述

  5. ref
    非唯一性索引扫描,返回匹配某个单独值的所有行,本质上也是一种索引访问,返回所有匹配某个单独值的行,然而可能会找到多个符合条件的行,应该属于查找和扫描的混合体例子:

    explain select * from student where name = '白杰'
    explain select * from student s left join scores sc on s.id = sc.s_id
    

    在这里插入图片描述

  6. ref_or_null
    类似ref,但是可以搜索值为NULL的行例子:

    explain select * from student s where name = '白杰' or name is null
    

    在这里插入图片描述

  7. index_merge
    表示使用了索引合并的优化方法例子:

    explain select * from student where id = 1 or name ='李兴';
    

    在这里插入图片描述

  8. range
    只检索给定范围的行,使用一个索引来选择行,key列显示使用了哪个索引一般就是在你的where语句中出现between、<>、in等的查询。例子:

    explain select * from student where id between 4 and 7;
    

    在这里插入图片描述

  9. index(全索引扫描)
    Full index Scan,Index与All区别:index只遍历索引树,通常比All快因为索引文件通常比数据文件小,也就是虽然all和index都是读全表,但index是从索引中读取的,而all是从硬盘读的。例子:

    explain select name from student;
    

    在这里插入图片描述

  10. ALL(全表扫)
    Full Table Scan,将遍历全表以找到匹配行例子:

    explain select * from student;
    

    在这里插入图片描述

6.3.4 table字段

表示数据来自哪张表

6.3.5 possible_keys字段

显示可能应用在这张表中的索引,一个或多个查询涉及到的字段若存在索引,则该索引将被列出,但不一定被实际使用

6.3.6 key字段

实际使用到的索引,如果为NULL,则没有使用索引查询中若使用了覆盖索引(查询的列刚好是索引),则该索引仅出现在key列表

6.3.7 key_len字段

表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度在不损失精确度的情况下,长度越短越好key_len显示的值为索引字段最大的可能长度,并非实际使用长度即key_len是根据定义计算而得,不是通过表内检索出的

6.3.8 ref字段

哪些列或常量被用于查找索引列上的值

6.3.9 rows字段

根据表统计信息及索引选用情况,大致估算出找到所需的记录所需读取的行数

6.3.10 partitions字段

匹配的分区

6.3.11 filtered字段

它指返回结果的行占需要读到的行(rows列的值)的百分比

6.3.12 Extra字段

该列包含不适合在其它列中显示,但十分重要的额外信息,我们列举几个例子:

  1. Using filesort
    只要使用非索引字段排序,就会出现这样的内容。

  2. Using temporary
    使用了临时表保存中间结果,MySQL在对结果排序时使用临时表,常见于排序order by 和分组查询group by

  3. Using where
    使用了where条件

  4. impossible where
    where子句的值总是false,不能用来获取任何数据:

    explain select * from student where name = '白洁' and name = '李兴';
    
  5. Select tables optimized away
    SELECT操作已经优化到不能再优化了(MySQL根本没有遍历表或索引就返回数据了)例子:

    explain select min(id) from student;
    
  6. no matching row in const table

    explain select * from student where id < 100 and id > 200;
    

6.4 索引使用的问题

设计好MySql的索引可以让你的数据库飞起来。但是,不合理的创建索引同样会产生很多问题?我们在设计MySql索引的时候有一下几点注意:

6.4.1 哪些情况下适合建索引

  1. 频繁作为where条件语句查询的字段

  2. 关联字段需要建立索引

  3. 分组,排序字段可以建立索引

  4. 统计字段可以建立索引,例如count()、max()等

小案例:还记得在学习临时表时,分析过group by的执行流程吗(分组字段没有索引)?有了索引之后的分组执行流程如下:

在这里插入图片描述
直接使用索引信息,统计每个组的人数,直接返回。

6.4.2 哪些情况下不适合建索引

  1. 频繁更新的字段不适合建立索引

  2. where条件中用不到的字段不适合建立索引

  3. 表数据可以确定比较少的不需要建索引

  4. 数据重复且发布比较均匀的的字段不适合建索引(唯一性太差的字段不适合建立索引),例如性别,真假值

  5. 参与列计算的列不适合建索引,索引会失效

6.4.3 其他情况

  1. 能用复合索引的要使用复合索引

  2. null值也是可以走索引的,他被处理成最小值放在b+树的最左侧

  3. 使用短索引
    对字符串的列创建索引,如果可能,应该指定一个前缀长度。例如,如果有一个CHAR(255)的 列,如果在前10 个或20 个字符内,多数值是惟一的,那么就不要对整个列进行索引。短索引不仅可以提高查询速度而且可以节省磁盘空间和I/O操作。

  4. 排序的索引问题
    mysql查询只使用一个索引,因此如果where子句中已经使用了索引的话,那么order by中的列是不会使用索引的。因此数据库默认排序可以符合要求的情况下不要使用排序操作;尽量不要包含多个列的排序,如果需要,最好给这些列创建复合索引。

6.4.4 索引失效的几种情况

  1. 如果条件中有or,即使其中有条件带索引也不会使用走索引,除非全部条件都有索引

  2. 复合索引不满足最左原则就不能使用全部索引

  3. like查询以%开头

  4. 存在列计算

    explain select * from student where age = (18-1)
    
  5. 如果mysql估计使用全表扫描要比使用索引快,则不使用索引,比如结果的量很大

  6. 存在类型转化

    -- 索引不失效
    explain select * from student where age = '18'  
    explain select * from ydl_user where login_date = '2008-05-31 17:20:54'
    -- 索引失效 本来是字符串,你使用数字和他比较
    explain select * from student where gander = 1
    

    在这里插入图片描述在这里插入图片描述


7. 锁机制

锁是为了保证数据库中数据的一致性,使各种【共享资源】在被访问时变得【有序】而设计的一种规则。

MysQL中不同的存储引擎支持不同的锁机制。 InoDB支持【行锁】,有时也会升级为表锁,MyIsam只支持表锁。

  • 【表锁】的特点就是开销小、加锁快,不会出现死锁。锁粒度大,发生锁冲突的概率小,并发度相对低。

  • 【行锁】的特点就是开销大、加锁慢,会出现死锁。锁粒度小,发生锁冲突的概率高,并发度高。

今天我们讲锁主要从InnoDB引擎来讲,因为它既支持行锁、也支持表锁。

7.1 InnoDB的锁类型

InnoDB的锁类型按不同角度可以划分为共享锁、排他锁,行锁、页锁、表锁,临键锁、间隙锁,意向锁以及MDL锁。

7.1.1 共享锁

读锁(共享锁,shared lock)简称S锁。一个事务获取了一个数据行的读锁,其他事务也能获得该行对应的读锁,但不能获得写锁,即一个事务在读取一个数据行时,其他事务也可以读,但不能对该数行增删改的操作。

注: 读锁是共享锁,多个事务可以同时持有,当有一个或多个事务持有共享锁时,被锁的数据就不能修改。

简而言之:就是可以多个事务读,但只能一个事务写。

读锁是通过 select ... lock in share mode语句给被读取的行记录或行记录的范围上加一个读锁,让其他事务可以读,但是要想申请加写锁,那就会被阻塞。

事务一:

begin;
select * from ydl_student where id = 1 lock in share mode;

事务二:

begin;
update ydl_student set score = '90' where id = 1;

卡住了,说明程序被阻塞,确实加了锁。

在这里插入图片描述

s锁是可以被多个事务同时获取的,我们在两个不同的事务中分别对同一行数据加上s锁,结果都可以成功,如下图:

在这里插入图片描述

7.1.2 排他锁

写锁,也叫排他锁,或者叫独占所,简称x锁(exclusive)。一个事务获取了一个数据行的写锁,既可以读该行的记录,也可以修改该行的记录。但其他事务就不能再获取该行的其他任何的锁,包括s锁,直到当前事务将锁释放。【这保证了其他事务在当前事务释放锁之前不能再修改数据】。

注: 写锁是独占锁,只有一个事务可以持有,当这个事务持有写锁时,被锁的数据就不能被其他事务修改。

(1)一些DML语句的操作都会对行记录加写锁。

事务一:

begin;
update ydl_student set score = '90' where id = 1;

事务二:

begin;
update ydl_student set score = '88' where id = 1;

卡住了,说明程序被阻塞,确实加了锁。但是,我们发现其他事务还能读,有点不符合逻辑,这是应为MySQL实现了MVCC模型,后边会详细介绍。

(2)比较特殊的就是select ... for update,它会对读取的行记录上加一个写锁,那么其他任何事务不能对被锁定的行上加任何锁了,要不然会被阻塞。

事务一:

begin;
select * from ydl_student where id = 1 for update;

事务二:

begin;
update teacher set name = 'lucy2' where id = 1;

卡住了,说明加了锁了。

(3)x锁是只能被一个事务获取,我们在两个不同的事务中分别对同一行数据加上x锁,发现后者会被阻塞,如下图:

在这里插入图片描述

7.1.3 行锁(Record Lock)

行锁就是我们常说的记录锁,只有InnoDB才支持,我们使用以下四个案例来验证记录锁的存在:

(1)两个事务修改【同一行】记录,该场景下,where条件中的列不加索引。

事务一:

begin;
update ydl_student set score = '77' where name = 'jack';

事务二:

begin;
update ydl_student set score = '80' where name = 'jack';

发现事务二卡住了,只有事务一提交了,事务二才能继续执行,很明显,这一行数据被【锁】住了。

(2)两个事务修改同表【不同行】记录,此时where条件也不加索。

事务一:

begin;
update ydl_student set score = '76' where name = 'hellen';

事务二:

begin;
update ydl_student set score = '66' where name = 'jack';

现事务二卡住了,只有事务一提交了,事务二才能继续执行,很明显,表被【锁】住了。

(3)两个事务修改【同一行】记录,where条件加索引

事务一:

begin;
update ydl_student set score = '99' where name = 'jack';

事务二:

begin;
update ydl_student set score = '79' where name = 'jack';

现事务二卡住了,只有事务一提交了,事务二才能继续执行,很明显,这一行数据被【锁】住了。

(4)两个事务修改同表【不同行】记录,此时where条件加索。

事务一:

begin;
update ydl_student set score = '77' where name = 'hellen';

事务二:

begin;
update ydl_student set score = '77' where name = 'jack';

发现都可以顺利修改,说明锁的的确是行。

证明: 行锁是加在索引上的,这是标准的行级锁。

7.1.4 间隙锁(GAP Lock)

间隙锁帮我们解决了MySQL在RR级别下的一部分幻读问题。间隙锁锁定的是记录范围,不包含记录本身,也就是不允许在某个范围内插入数据。

间隙锁生成的条件:

1、A事务使用where进行范围检索时未提交事务,此时B事务向A满足检索条件的范围内插入数据。

2、where条件必须有索引。

第一步把teacher表的id的4改成8

事务一:

begin;
select * from ydl_student where id between 3 and 7 lock in share mode;

事务二:

begin;
insert into ydl_student values (5,'tom',66,'d');

发现卡住了,第一个事务会将id在3到7之间的数据全部锁定,不允许在缝隙间插入。

事务三:

begin;
insert into ydl_student values (11,'tom',66,'d');

插入一个id为11的数据,竟然成功了,因为11不在事务一的检索的范围。

7.1.5 临键锁(next-key lock)

临键锁,是记录锁与间隙锁的组合,它的封锁范围,既包含【索引记录】,又包含【索引区间】。

注:临键锁的主要目的,也是为了避免幻读(Phantom Read)。如果把事务的隔离级别降级为RC,临键锁则也会失效。

7.1.6 MDL锁

MySQL 5.5引入了meta data lock,简称MDL锁,用于保证表中元数据的信息。在会话A中,表开启了查询事务后,会自动获得一个MDL锁,会话B就不可以执行任何DDL语句,不能执行为表中添加字段的操作,会用MDL锁来保证数据之间的一致性。

元数据就是描述数据的数据,也就是你的表结构。意识是在你开启了事务之后获得了意向锁,其他事务就不能更改你的表结构。MDL锁都是为了防止在事务进行中,执行DDL语句导致数据不一致。

7.1.7 死锁问题

发生死锁的必要条件有4个,分别为互斥条件、不可剥夺条件、请求与保持条件和循环等待条件:

  • 互斥条件,在一段时间内,计算机中的某个资源只能被一个进程占用。此时,如果其他进程请求该资源,则只能等待。
  • 不可剥夺条件,某个进程获得的资源在使用完毕之前,不能被其他进程强行夺走,只能由获得资源的进程主动释放。
  • 请求与保持条件,进程已经获得了至少一个资源,又要请求其他资源,但请求的资源已经被其他进程占有,此时请求的进程就会被阻塞,并且不会释放自己已获得的资源。
  • 循环等待条件,系统中的进程之间相互等待,同时各自占用的资源又会被下一个进程所请求。例如有进程A、进程B和进程C三个进程,进程A请求的资源被进程B占用,进程B请求的资源被进程C占用,进程C请求的资源被进程A占用,于是形成了循环等待条件,如图所示。

模拟死锁场景,如下:

在这里插入图片描述

InnoDB使用的是行级锁,在某种情况下会产生死锁问题,所以InnoDB存储引擎采用了一种叫作等待图(wait-for graph)的方法来自动检测死锁,如果发现死锁,就会自动回滚一个事务。

在这里插入图片描述
在MySQL中,通常通过以下几种方式来避免死锁。

  • 如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会;
  • 尽量在事务中一次锁定所需要的所有资源,减少死锁产生概率;
  • 尽量减少查询条件的范围,尽量避免间隙锁或缩小间隙锁的范围;
  • 尽量控制事务的大小,减少一次事务锁定的资源数量,缩短锁定资源的时间;
  • 尽量使需要加锁的SQL在整个事务的最后执行(因为行锁在使用时获取,事务提交时释放);
  • 对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率。

7.2 表锁

1、对于InnoDB表,在绝大部分情况下都应该使用【行级锁】,因为事务和行锁往往是我们之所以选择InnoDB表的理由。但在个另特殊事务中,也可以考虑使用表级锁。

  • 第一种情况是:事务需要更新【大部分或全部数据】,表又比较大,如果使用默认的行锁,不仅这个事务执行效率低,而且可能造成其他事务长时间锁等待和锁冲突,这种情况下可以考虑使用表锁来提高该事务的执行速度。

  • 第二种情况是:事务涉及多个表,比较复杂,很可能引起死锁,造成大量事务回滚。这种情况也可以考虑一次性锁定事务涉及的表,从而避免死锁、减少数据库因事务回滚带来的开销。

2、在InnoDB下 ,主动上表锁的方式如下:

lock tables teacher write,student read;
select * from teacher;
commit;
unlock tables;

使用时有几点需要额外注意:

  • 使用【LOCK TALBES】虽然可以给InnoDB加表级锁,但必须说明的是,表锁不是由InnoDB存储引擎层管理的,而是由其上一层MySQL Server负责的,仅当autocommit=0、innodb_table_lock=1(默认设置)时,InnoDB层才能感知MySQL加的表锁,MySQL Server才能感知InnoDB加的行锁,这种情况下,InnoDB才能自动识别涉及表级锁的死锁;否则,InnoDB将无法自动检测并处理这种死锁。

  • 在用LOCAK TABLES对InnoDB加锁时要注意,事务结束前,不要用UNLOCAK TABLES释放表锁,因为UNLOCK TABLES会隐含地提交事务;COMMIT或ROLLBACK不能释放用LOCAK TABLES加的表级锁,必须用UNLOCK TABLES释放表锁,正确的方式见如下语句。

  • 表锁的力度很大,慎用。

7.3 从另一个角度区分锁的分类

7.3.1 乐观锁

乐观锁大多是基于数据【版本记录机制】实现,一般是给数据库表增加一个"version"字段。

读取数据时,将此版本号一同读出。更新时,对此版本号加一。
此时将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

事务一:

select * from ydl_student where id = 1;

事务二:

select * from ydl_student where id = 1;
update ydl_student set score = 99,version = version + 1 where id = 1 and version = 1;
commit;

事务一:

update ydl_student set score = 100,version = version + 1 where id = 1 and version = 1;
commit;

发现更新失败,应为版本号被事务二、提前修改了,这使用了不加锁的方式,实现了一个事务修改期间,禁止其他事务修改的能力。

7.3.2 悲观锁

“总有刁民想害朕”

悲观锁依靠数据库提供的锁机制实现。MySQL中的共享锁和排它锁都是悲观锁。数据库的增删改操作默认都会加排他锁,而查询不会加任何锁。此处不赘述。


8. 日志系统

8.1 bin log日志

8.2 其他日志

8.3 redo log日志

8.4 undo log日志


9. 隔离级别与MVCC

【MVCC】,全称Multi-Version Concurrency Control,即【多版本并发控制】。MVCC在MySQL InnoDB中的实现主要是为了提高数据库的并发性能,用更好的方式去处理【读-写冲突】,做到即使有【读写冲突】时,也能做到不加锁,非阻塞并发读,学习mvcc之前我们需要学习一些新的概念。

9.1 Read View(读视图)

在学习MVCC多版本并发控制之前,我们必须先了解一下,什么是MySQL InnoDB下的【当前读】和【快照读】,我们都知道undo log会记录事务对一条数据的所有修改,并形成版本链:

  • 当前读:像select lock in share mode(锁)、 select for updateupdateinsertdelete(排他锁)这些操作都是【当前读】,他读取的是记录的【最新版本】,读取时还要保证其他【并发事务】不能修改当前记录,会对读取的记录进行加锁。

  • 快照读:像不加锁的select操作就是快照读,即不加锁的【非阻塞读】;快照读的前提是【隔离级别不是串行级别】,串行级别下的快照读会【退化成当前读】,顾名思义,快照读读取的是【快照】,他是通过readView实现的。

9.1.1 实现原理

Read View就是事务进行【快照读】的时候生产的【读视图】(Read View),在该事务【执行快照读】的那一刻,会生成数据库系统当前的一个快照。

注意:【快照】不是说将数据库复制一份,【Read View】的主要作用是做【可见性判断】, 快照的实现逻辑是通过undo log的【版本链】,配合一些【参数】,比如事务id,来确定当前事务可以读取的版本。

9.1.2 ReadView的结构

举一个列子,当前有事务id为12、13、14、16、20的五个事务,他们在同时修改一条数据,此时,事务13发生读取行为,在【事务13】读取之前【事务14】已经提交,当前场景下,将产生一个readview如下:

一个readView就是一个【结构体】,你甚至可以理解成为java里的实例(readview)和属性,包含属性如下:

  • m_ids:生成该readview时,当前系统中【活跃的事务】id列表。对于当前案例,因为14已经提交,就不活跃了,所以该变量的值为[12,13,16,20]。
  • min_trx_id:当前系统【活跃事务】中最小的【事务id】,他也是m_ids的最小值,当前案例的值就是12。
  • max_trx_id:当前系统中计划分配给下一个事务的id,他可能是m_ids的最大值+1,也可能比他大。当前案例值假设为22。
  • creator_trx_id:生成这个readView的事务id,当前案例的值为12。

以上readview配合undo log就可以形成一个【快照】,那他是怎么读取的呢?

9.2 快照读原理解析

在一个事务读取数据时,会根据当前数据形成一个readview,读取时会按照以下逻辑进行读取:

  • 如果【被访问数据的事务trx_id】和readView中的【creator_trx_id值】相同,意味着自己在访问自己修改过的记录,当然可以被访问。

  • 如果【被访问数据的事务trx_id】小于readView中的【min_trx_id】值,说明生成这个版本数据的事务,在生成readview前已经提交,这样的数据也可以访问。

  • 通俗一点: 这个数据之前被其他的事务修改过,但是事务已经提交,所以这个版本的数据是可以使用的,这样不会产生脏读。

  • 如果【被访问数据的事务trx_id】大于等于readView中的max_trx_id值,说明生成这个版本数据的事务,是在生成readview后开启,这样的数据不应该被访问。

  • 通俗一点: 你读取数据之后,有人修改了当前数据,那人家后边修改的数据,你也不能读。

  • 如果【被访问数据的事务trx_id】如果在min_trx_id和max_trx_id范围内,则需要判断是不是在【m_ids】中(目的是判断这个数据是不是已经提交)。如果在,说明生成这个版本的事务还是活跃的,没有提交的事务产生的数据当然不能读,如果不在,说明事务已经提交,该数据可以被访问。

  • 通俗一点: 这个数据被现在活跃的其他事务正在修改中,读取时要看此时这个事务是不是已经提交,目的也是为了不要读取别人未提交的事务。

我们用下边的案例来看一下这个过程:
在这里插入图片描述

9.3 解决脏读和不可重复读

对于RU隔离级别的事务来说,由于可以读取到未提交的事务,所有直接读取【最新的记录】(当前读)就可以,对于serializable的事务来说,必须使用加锁的方式来访问。

9.3.1 解决脏读

先思考一个问题,脏读指的是在当前事务中读取到了其他事务未提交的数据,那解决的思路是什么:

(1)没有undo+mvcc

一个事务读取了数据之后,立马给这个数据加写锁,不允许其他事务进行修改,这是加锁解决脏读。

(2)使用undo+mvcc

所有事务对数据的修改,记录成版本链,使用readview进行版本选择,每个事务只能读取满足条件的数据,这个过程不需要加锁。

使用mvcc很好的解决了【读写操作】的并发执行,而且采用了无锁机制。

9.3.2 解决不可重复读

RC和RR两个隔离级别解决不可重复读是通过生成readview时间不同

(1)RC隔离级别,同一个事务中【每次读取数据时都生成一个新的ReadView】,两次读取时,如果中间有其他事务进行提交,可能会生成两个不同的readview,导致当前事务中,两次读取的数据不一致,这就是不可重复读。具体的执行流程如下:
在这里插入图片描述(2)RR隔离级别,同一个事务中【只在第一次读取数据时生成一个ReadView】,以后这个事务中一直使用这个readview,那么同一个事务中就能保证多次读取的数据是一致的,具体的执行流程如下:

在这里插入图片描述

9.3.3 解决幻读

他是通过间隙锁实现的,一旦锁定某一个范围的数据,就会对这个范围的数据加锁,间隙锁保证我们不能在这个范围内插入新的数据。


10. 其他知识

10.1 触发器

与表有关的数据对象,在满足某种条件的时候,被动执行的SQL语句。

10.1.1 触发器的特性

  1. 有begin、end的结构体(多条sql语句)
  2. 需要指定触发的条件:INSERT,UPDATE,DELETE
  3. 有指定的触发时间:BEFORE,AFTER

10.1.2 触发器的创建

(1)单条业务逻辑的触发器创建

/*
CREATE TRIGGER 触发器名称 BEFORE|AFTER INSERT|UPDATE|DELETE ON 表名
FOR EACH ROW 业务逻辑
*/
#当b_user表中插入数据后,b_log表中也插入一条数据
CREATE TRIGGER trigger_insert AFTER INSERT ON b_user
FOR EACH ROW INSERT INTO b_log(字段) VALUES('插入数据')

(2)多条业务逻辑的触发器

/*
DELIMITER $
CREATE TRIGGER 触发器名称 BEFORE|AFTER INSERT|UPDATE|DELETE ON 表名
FOR EACH ROW
BIGIN
INSERT...;
UPDATE...;
END;$
*/
#在b_user表中插入数据前,b_log表中插入2条数据
DELIMITER $
CREATE TRIGGER trigger_ insert_before BEFORE INSERT ON b_user
FOR EACH ROW
BEGIN
INSERT INTO b_log(comments,name) values('insert1' ,NEW.name);
INSERT INTO b_log(comments,name) values('insert2' , NEW.name) ;
END;$

总结:

  • BEFORE|AFTER INSERT用于获取将要插入的数据
  • BEFORE|AFTER UPDATE|DELETE用于获取已经修改或删除的数据

10.1.3 删除触发器

DROP TRIGGER 触发器名称

10.2 存储过程

10.2.1 变量

1、系统变量:

由mysql数据库管理系统提供的,变量名称固定,可以修改和查看值,分为全局变量会话变量

全局变量:当mysql服务没有重启时,我们可以查看和修改的变量

会话变量:和MySQL连接形成的会话,生命周期在整个会话过程中

全局变量用global修饰,会话变量用session修饰,通常session可以省略

(1)查看系统变量

SHOW GLOBAL variables; -- 查看全局变量
SHOW SESSION variables; -- 查看会话变量
SHOW variables; -- 查看会话变量
SHOW GLOBAL variables like '%dir%'; -- 模糊查询环境变量
SELECT @@datadir; -- 查看全局系统变量
SELECT @@session_track_transaction_info;

(2)修改系统变量

SHOW GLOBAL variables like 'autocommit'; -- 全局系统变量中为自动提交事务
SET GLOBAL autocommit=0; -- 将全局的自动提交的事务改为手动提交
SHOW SESSION variables link 'autocommit'; -- 查看会话变量中自动提交事务
SET SESSION autocommit=0; -- 将会话变量中自动提交的事务改为手动提交
SET @@session.autocommit=1;
SET @@global.autocommit=1;

全局变量在修改后,在不同的会话中都会立即生效,但是在重新启动mysql服务后,全局变量会恢复为默认值,如果想让全局变量依旧有效,需要去修改.ini文件(MySQL配置文件)

会话变量在修改后只对当前会话有效。一般在开发过程中修改会话变量。如:字符编码格式等可以在ini文件中进行设置

2、用户变量

MySQL允许用户自定义变量,分为用户变量和局部变量

  • 用户变量

    作用域:当前会话有效

    #设置方式1,先去声明并初始化用户变量,赋值操作既可以使用=进行赋值,也可以使用:=进行赋值
    SET @变量名=;
    SET @变量名:=;
    SELECT @变量名:=;
    #设置方式2
    SELECT 字段 into @变量名 FROM 表名;
    
  • 局部变量

    作用域:在begin end的结构体中,声明必须是begin end结构体的第一句

    #声明方式,必须在begin后面从第一行开始
    DECLARE 变量名 类型;
    DECLARE 变量名 类型 DEFAULT;
    
    #局部变量的赋值
    SET 变量名:=;
    SELECT @变量名:=;
    SELECT 字段 into 变量名 FROM 表名;
    

10.2.2 存储过程的创建

存储过程是一组已经预先编译好的sql语句的集合,理解为批处理语句(增加流程控制语句),一般在复杂逻辑中才会使用存储过程

  • 存储过程的优点

    • 提供了代码的可用性
    • 简化了数据库操作,将业务逻辑的细节隐藏在存储过程中
    • 减少了编译次数,减少了网络IO的次数,从而提高操作效率
  • 存储过程的创建

    /*
    DELIMITER $
    CREATE PROCEDURE 存储过程的名称(参数列表)
    BEGIN
    局部变量的定义
    多条sql语句
    流程控制语句
    END;$
    */
    

    如果存储过程中只有一条SQL语句可以省略BEGIN END

参数列表:

参数模式 形参名称 参数类型
IN username mysql数据库中的数据类型(数值型,字符型,日期型)
OUT pwd mysql数据库中的数据类型(数值型,字符型,日期型)
INOUT xxx mysql数据库中的数据类型(数值型,字符型,日期型)
  • IN:声明该参数是一个输入姓参数(类似于java中的形参)

  • OUT:声明该参数为一个输出型参数(类似于java中的返回值),在一个存储过程中可以定义多个out类型的参数

  • INOUT:声明该参数可以为输入型参数,也可以为输出型参数

存储过程调用

CALL 存储过程的名称(实参列表) 
-- 实参列表中包含由输出类型的参数

存储过程演示

无参的存储过程

#用于向b_user表中插入2条数据
DELIMITER $
CREATE PROCEDURE pro_insert()
BEGIN
INSERT INTO b_user(name,sex) VALUES('1','1');
INSERT INTO b_user(name,sex) VALUES('2','2');
END;$

CALL pro_insert();

带有IN模式参数的存储过程

#用于向b_user插入2条数据,性别由客户输入
DELIMITER $
CREATE PROCEDURE pro_insert2(IN sex CHAR(1))
BEGIN
INSERT INTO b_user(name,sex) VALUES('1',sex);
INSERT INTO b_user(name,sex) VALUES('2',sex);
END;$

CALl pro_insert2('男');

多个带有IN参数的存储过程

#用于向b_user插入2条数据,用户名和密码由客户输入
DELIMITER $
CREATE PROCEDURE pro_insert3(IN name VARCHAR(10),IN sex VARCHAR(20))
BEGIN
INSERT INTO b_user(name,sex) VALUES(name,sex);
INSERT INTO b_user(name,sex) VALUES(name,sex);
END;$

CALL pro_insert2('uname','男');

带IN,OUT参数的存储过程

#判断用户登录,如果用户名和密码输入正确登录成功,否则登录失败
#根据输入的用户名和密码作为条件去b_user表中查询,如果查询总行数==1,则认为登录成功,让result返回登录成功,否则登录失败
DELIMITER $
CREATE PROCEDURE pro_login(IN name VARCHAR(20),IN pwd VARCHAR(20),OUT result VARCHAR(20))
BEGIN
DECLARE total INT DEFAULT 0;-- 用于存放查询总行数
select count(*) from b_user u where u.name=name and u.pwd=pwd;-- 将查询结果赋值给total局部变量
SET result:=IF(total=1,'登录成功','登录失败');
END;$
#存储过程如何执行
-- 解决判断,使用自定义变量
SET @result:='';
CAll pro_login('李四','123',@result);
select @result;

删除存储过程

DROP PROCEDURE 存储过程名称

查看存储过程

SHOW CREATE PROCEDURE 存储过程名称;

修改存储过程

DROP
CREATE

2.1 流程控制语句

选择结构

  • IF函数

    • 功能:三目运算
    • 语法:IF(逻辑表达式,表达式1,表达式2)
  • IF结构

    • 功能:实现多路选择

    • 注意:只能用在BEGIN END结构体中

      /*
      IF 逻辑表达式 THEN 语句1;
      ELSEIF 逻辑表达式2 THEN 语句2;
      ...
      ELSE 语句n;
      END IF;
      */
      
  • CASE结构

    • 等值选择
      CASE 字段|变量|表达式
      WHENTHEN|语句
      WHENTHEN...
      ELSEEND
      
    • 不等值选择
      CASE
      WHEN 逻辑表达式 THEN 语句1
      ...
      ELSE 语句n
      END
      

循环结构

WHILE

/*
WHILE 逻辑表达式 DO
循环体
END WHILE;
*/
#需求:创建存储过程,输入一个值,返回1到该值的和
#分析:一个输入参数,一个返回值,在结构体中,从1循环到输入的值,求和
DELIMITER //
CREATE PROCEDURE pro_sum(IN input INT,OUT total INT)
BEGIN
DECLARE i INT DEFAULT 1;
DECLARE sum_ INT DEFAULT 0;
WHILE i<=input do
SET sum_=sum_+i;
SET i=i+1;
END WHILE;
SET totle:=sum_;
END;//

SET @result=0;
CALL por_sun(10,@result);
SELECT @result;

LOOP

#Loopnaem是定义的循环名称,为了跳出循环时指定跳出的循环
loopname:LOOP;
	IF 逻辑表达式 THEN
	LEAVE loopname; -- 跳出当前指定的循环,类似于java中的break
	END IF;
END LOOP;

DElIMITER //
CREATE PROCEDURE pro_sum_loop(IN input INT,OUT total INT)
BEGIN 
DECLARE i INT DEFAULT 1;
DECLARE sum_ INT DEFAULT 0;
a:LOOP;
SET sum_:=sum_+i;
SET i:=i+1;
IF i>input THEN
LEAVE a;
END IF;
END LOOP;
SET total:=sum_;
END;//

SET @result=0;
CALL por_sum_loop(10,@result);
SELECT @result;

REPEAT

REPEAT
循环体
UNTIL 逻辑表达式 -- 当满足逻辑表达式,跳出循环
END REPEAT;

DELIMITER //
CREATE PROCEDURE pro_sum_loop(IN input INT,OUT total INT)
BEGIN 
DECLARE i INT DEFAULT 1;
DECLARE sum_ INT DEFAULT 0;
REPEAT
SET sum_:=sum_+i;
SET i:=i+1
UNTIL i>input
END REPEAT;
SET total:=sum_;
END;//

SET @result=0;
CALL por_sum_loop(10,@result);
SELECT @result;

10.3 存储函数

函数也是一组预先编译好的sql的集合,基本和存储过程相似

函数和存储过程的区别

  • 存储过程可以有0个,1个或多个返回值,适用于insert、update、delete操作
  • 函数只能有一个返回值,适用于在处理数据以后,返回一个已知的结果

10.3.1 创建函数

CREATE FUNCTION 函数名称(参数列表) RETURNS 返回类型 BINLOG参数
BEGIN
函数体
END

参数列表:参数名称 参数类型

BINLOG参数

  • NO SQL:函数体中没有sql语句, 也不会改参数
  • READS SQL DATE:函数体中存在sql语句,但是整个数据是只读的,不会修改数据
  • MODIFIES SQL DATE :函数体中存在SQL语句,并且会修改数据
  • CONTAINS SQL:函数体中包含有SQL语句

函数体:在函数体汇总必须包含return语句,将return放在函数体最后一行执行

#写一个函数,用于求两数之和
DELIMITER //
CREATE FUNCTION sum_(input1 INT,input2 INT) RETURNS INT NO SQL
BEGIN
return input1+input2;
END;//

10.3.2 使用函数

SELECT 函数名(参数列表);

3、查看函数

SHOW CREATE FUNCTION 函数名;

4、删除函数

DROP FUNCTION 函数名;

10.4 定时任务

10.4.1 查看定时策略是否开启

show variables like '%event_sche%';

开启定时策略:

set global event_scheduler=1;

10.4.2 创建定时任务

create event run_event
on schedule every 1 minute
on completion preserve disable
do call test_procedure ();

1、create event day_event:是创建名为run_event的事件
2、创建周期定时的规则,意思是每分钟执行一次
3、on completion preserve disable是表示创建后并不开始生效。
4、do call test_procedure ()是该event(事件)的操作内容

10.4.3 定时任务操作

1、查看定期任务

SELECT event_name,event_definition,interval_value,interval_field,status 
FROM information_schema.EVENTS;

2、开启或关闭定时任务

alter event run_event on completion preserve enable;//开启定时任务
alter event run_event on completion preserve disable;//关闭定时任务

10.4.4 定时规则

1、周期执行–关键字 EVERY 单位有:second、minute、hour、day、week(周)、quarter(季度)、month、year

on schedule every 1 week // 每周执行1次

2、在具体某个时间执行–关键字 AT

on schedule at current_timestamp()+interval 5 day // 5天后执行
on schedule at '2019-01-01 00:00:00' // 在2019年1月1日,0点整执行

3、在某个时间段执行–关键字STARTS ENDS

on schedule every 1 day starts current_timestamp()+interval 5 day ends current_timestamp()+interval 1 month // 5天后开始每天都执行执行到下个月底
on schedule every 1 day ends current_timestamp()+interval 5 day // 从现在起每天执行,执行5天

附录:

MYSQL配置文件的举例

#[client]
#MySQL默认密码
#password=88888888
[mysqld]
#MySQL以什么用户运行
#user=mysql
#MySQL运行在哪个端口
#port=3306
#改参数指定了安装MySQL的安装路径,填写全路径可以解决相对路径所造成的问题
#basedir
#指定MySQL的数据库文件放在什么路径下
datadir=/usr/local/mysql/data
#mysql以socket方式运行的sock文件位置
socket=/usr/local/mysql/mysql.sock
#是否支持符号链接,即数据库或表可以存储在my.cnf中指定datadir之外的分区或者目录,为0不开启
symbolic-links=0
#服务器使用的字符集
character-set-server=utf8
#默认存储引擎
default-storage-engine=INNODB
#表示默认将日志文件存入文件,默认值是'FILE' 
#如果时候log-output=TABLE表示将日志存入数据库,这样日志信息就会被写入到mysql.slow_log表中
log-output=FILE
#1开启,0关闭 将所有到达MySQL Server的SQL语句记录下来
general-log=0
#设置日志文件保存位置
general_log_file="JOYWANG.log"
#慢查询日志是否开启1,0
slow-query-log=1
#慢查询日志文件保存
slow_query_log_file="JOYWANG-slow.log"
#慢查询日志设置时间单位秒 S
long_query_time=10
#是否启用错误日志的功能和错误日志的存储位置
log-error="JOYWANG.err"
#如果不设置则server-id是根据服务器ip地址后2位生成的,默认0或1
#当配置MySQL复制时,是必填项,用来区分复制拓扑中的各个实例
server-id=1
#限制导入和导出的目录权限NULL表示禁止、如果是文件目录,允许该目录下文件(测试子目录不行)、如果为空则表示不限制目录,一定要有等号,否则mysql无法启动
secure-file-priv=
#最大并发连接数,mysql会为每个连接提供缓冲区,会开销越多的内存,所以要适当的调整该值,不能盲目的提高设置值
max_connections=151
#指定高速缓存的大小,每当MySQL访问一个表时,如果在表缓冲区中还有空间,该表就被打开并放入其中,这样可以更快地访问表内容单位M
table_open_cache=2000
#增加一张临时表大小,提高查询速度
tmp_table_size=16M
#线程池缓存大小,当客户端断开连接后,将当前线程缓存起来,当在接到新的连接请求时快速响应,无需创建新的线程
thread_cache_size=10
#MySQL重建索引时允许使用的临时文件最大大小
MyIsam_max_sort_file_size=100G
#设置在REPAIR TABLE,或者用 CREATE INDEX 创建索引或 ALTER TABLE 的过程中排序索引所分配的缓冲区大小。可设置范围4Bytes 至 4GB,默认为8MB。
MyIsam_sort_buffer_size=8M
#指定索引缓冲区的大小,决定了索引处理的速度,尤其是索引读的速度,建议设置成物理内存的1/4,甚至物理内存的30%-40%,如果设置太大,系统就会频繁的换页,降低系统性能
key_buffer_size=8M
#MySQL读入缓冲区大小,对表进行顺序扫描的请求将分配一个读入缓冲区,MySQL会为它分配一段内存缓冲区。read_buffer_size变量控制这一缓冲区的大小。如果对表的顺序扫描请求非常频繁,并且你认为频繁扫描进行得太慢,可以通过增加该变量值以及内存缓冲区大小提高其性能。
read_buffer_size=0
#参数用在sort查询之后 ,以保证获取以顺序的方式获取到查询的数据。如果你有很多order by 查询语句,增长这值能够提升性能
read_rnd_buffer_size=0
#0:log buffer将每秒一次地写入log file中,并且log file的flush(刷到磁盘)操作同时进行。该模式下在事务提交的时候,不会主动触发写入磁盘的操作。
#1:每次事务提交时MySQL都会把log buffer的数据写入log file,并且flush(刷到磁盘)中去,该模式为系统默认。
#2:每次事务提交时MySQL都会把log buffer的数据写入log file,但是flush(刷到磁盘)操作并不会同时进行。该模式下,MySQL会每秒执行一次 flush(刷到磁盘)操作。
innodb_flush_log_at_trx_commit=1
#确保有足够大的日志缓冲区来保存脏数据在被写入到日志文件之前
innodb_log_buffer_size=1M
#指定表数据和索引存储的空间,可以是一个或者多个文件。最后一个数据文件必须是自动扩充的,也只有最后一个文件允许自动扩充。这样,当空间用完后,自动扩充数据文件就会自动增长(以8MB为单位)以容纳额外的数据。例如: innodb_data_file_path=/disk1/ibdata1:900M;/disk2/ibdata2:50M:autoextend两个数据文件放在不同的磁盘上。数据首先放在ibdata1中,当达到900M以后,数据就放在ibdata2中。一旦达到50MB,ibdata2将以 8MB为单位自动增长。如果磁盘满了,需要在另外的磁盘上面增加一个数据文件。
innodb_data_file_path=/disk1/ibdata1:900M;/disk2/ibdata2:50M:autoextend
#这是InnoDB最重要的设置,对InnoDB性能有决定性的影响。默认的设置只有8M,所以默认的数据库设置下面InnoDB性能很差。在只有InnoDB存储引擎的数据库服务器上面,可以设置60-80%的内存。更精确一点,在内存容量允许的情况下面设置比InnoDB tablespaces大10%的内存大小。
innodb_buffer_pool_size=8M
#放置表空间数据的目录,默认在mysql的数据目录,设置到和MySQL安装文件不同的分区可以提高性能。
innodb_data_home_dir=
#该参数决定了recovery speed。太大的话recovery就会比较慢,太小了影响查询性能,一般取256M可以兼顾性能和recovery的速度
innodb_log_file_size=48M
#该参数设定了事务提交时内存中log信息的处理。
1) =1时,在每个事务提交时,日志缓冲被写到日志文件,对日志文件做到磁盘操作的刷新。Truly ACID。速度慢。
2) =2时,在每个事务提交时,日志缓冲被写到文件, 但不对日志文件做到磁盘操作的刷新。只有操作系统崩溃或掉电才会删除最后一秒的事务,不然不会丢失事务。
3) =0时, 日志缓冲每秒一次地被写到日志文件,并且对日志文件做到磁盘操作的刷新。任何mysqld进程的崩溃会删除崩溃前最后一秒的事务
innodb_flush_logs_at_trx_commit=2
#设置InnoDB同步IO的方式:
) Default – 使用fsync()。
2) O_SYNC 以sync模式打开文件,通常比较慢。
3) O_DIRECT,在Linux上使用Direct IO。可以显著提高速度,特别是在RAID系统上。避免额外的数据复制和double buffering(mysql buffering 和OS buffering)。
innodb_flush_method=Default
#InnoDB kernel最大的线程数。
1) 最少设置为(num_disks+num_cpus)*2。
2) 可以通过设置成1000来禁止这个限制
innodb_thread_concurrency=25
#此配置项作用主要是当tablespace 空间已经满了后,需要MySQL系统需要自动扩展多少空间,每次tablespace 扩展都会让各个SQL 处于等待状态。增加自动扩展Size可以减少tablespace自动扩展次数。
innodb_autoextend_increment=64
#可以开启多个内存缓冲池,把需要缓冲的数据hash到不同的缓冲池中,这样可以并行的内存读写。
innodb_buffer_pool_instances=8
#这个参数设置为一种tickets,默认是5000,我也不清楚到底它代表多久,从官方文档来看它和事物处理的行数有关,大事物需要处理的行数自然更多,小事物当然也就越少至少我们可以想成获得CPU的时间,干活期间他会不断减少,如果减少到0,这个线程将被提出innodb层次,进入前面说的等待队列,当然也就在队尾部了,这里假设有一个小的事物正在排队进入innodb层,又或者它已经进入了innodb层没有获得CPU时间轮片,突然一个大的事物tickets耗尽被提出了innodb层,那么这个小事物就自然而然能够获得CPU轮片干活,而小事物执行非常快,执行完成后,另外的事物又能尽快的获得CPU干活,不会由于OS线程调度不均匀的问题而造成的小事物饥饿问题,这很好理解。也就是前面我说的与其依赖OS的调度策略不如自己设置一种规则,让用到了一定时间轮片的线程先处于睡眠态放弃CPU的使用
innodb_concurrency_tickets=5000
innodb_old_blocks_time=1000
innodb_open_files=300
innodb_stats_on_metadata=0
#可以存储每个InnoDB表和它的索引在它自己的文件中。
innodb_file_per_table=1
#如果应用程序可以运行在READ-COMMITED隔离级别,做此设定会有一定的性能提升。
transaction-isolation=READ-COMITTED
#这个参数用来表示 页读取到mid位置后,需要等待多久才会被加入到LRU列表的热端。使LRU列表中的热点数据不被刷出
innodb_checksum_algorithm=0
#MySQL暂时停止回答新请求之前的短时间内多少个请求可以被存在堆栈中
back_log=80
flush_time=0
#如果按照检索的性能方式来细分,那么无论是两表 INNER JOIN 还是多表 INNER JOIN,都大致可以分为以下几类:1.JOIN KEY 有索引,主键2.JOIN KEY 有索引, 二级索引3.JOIN KEY 无索引;JOIN BUFFER 是 MySQL 用来缓存以上第二、第三这两类 JOIN 检索的一个 BUFFER 内存区域块。
join_buffer_size=256K
#可以增大此值以便于server端接收更大的SQL
max_allowed_packet=4M
#同一主机最大连续请求错误次数,如果还没成功建立连接,mysql服务器会组织这台主机后续的所有请求
max_connect_errors=100
#限制mysqld能打开文件的最大数
open_files_limit=4161
#一个connection级参数,在每个connection第一次需要使用这个buffer的时候,一次性分配设置的内存
sort_buffer_size=256K
#就是控制总frm文件的数量,还是个hash表,内部维护。如果打开的表实例的数量超过了table_definition_cache设置,LRU机制将开始标记表实例以进行清除,并最终将它们从数据字典缓存中删除。简单通俗点frm文件有多少,就设置多少了
table_definition_cache=1400
#指定基于行的二进制日志事件的最大大小
binlog_row_event_max_size=8K
#本参数用于主从库中配置从库大于0作用为每个命令之后刷盘,小与0作为为永不刷盘,默认均为1000
sync_master_info=10000
#这个参数和sync_binlog是一样的,当设置为1时,slave的I/O线程每次接收到master发送过来的binlog日志都要写入系统缓冲区,然后刷入relay log中继日志里,这样是最安全的,因为在崩溃的时候,你最多会丢失一个事务,但会造成磁盘的大量I/O。当设置为0时,并不是马上就刷入中继日志里,而是由操作系统决定何时来写入,虽然安全性降低了,但减少了大量的磁盘I/O操作。这个值默认是0,可动态修改,建议采用默认值。
sync_relay_log=10000
#这个参数和sync_relay_log参数一样,当设置为1时,slave的I/O线程每次接收到master发送过来的binlog日志都要写入系统缓冲区,然后刷入relay-log.info里,这样是最安全的,因为在崩溃的时候,你最多会丢失一个事务,但会造成磁盘的大量I/O。当设置为0时,并不是马上就刷入relay-log.info里,而是由操作系统决定何时来写入,虽然安全性降低了,但减少了大量的磁盘I/O操作。这个值默认是0,可动态修改,建议采用默认值
sync_relay_log_info=10000
#参数不可动态修改,必须重启数据库
#1:存储在磁盘是小写,比较时不区分大小写
#2:存储为给定的大小写但是比较时是小写
#0:存储为给定的大小写和比较时区分大小写的
lower_case_table_names = 1
#ONLY_FULL_GROUP_BY:归于GROUP BY聚合操作,如果在SELECT中的列,没有在GROUP BY中出现,那么这个SQL是不合法的,因为列不在GROUP BY从句中
#NO_AUTO_VALUE_ON_ZERO:该值影响自增常烈的插入。默认设置下,插入0或者NULL代表生成下一个自增长值。如果用户希望插入的值为0,而该列又是自增长的,那么这个选项就有用了
#STRICT_TRANS_TABLES:如果一个值不能插入到一个事物中,则中断当前操作,对非事物不做限制
#NO_ZERO_IN_DATE:在严格模式下,不允许日期和月份为零
#NO_ZERO_DATE:mysql不允许插入零日期,插入零日期会抛出错误而不是警告
#ERROR_FOR_DIVISION_BY_ZERO:在insert或update过程中,如果数据被清除,则产生错误而非警告。如果未给出该模式,那么数据被清除时Mysql返回NULL
#NO_AUTO_CREATE_USER:禁止GRANT创建密码为空的用户
#NO_ENGINE_SUBSTITUTION:如果需要的存储引擎被禁用或未编译,那么抛出错误。不设置此值时,用默认的存储引擎替代,并抛出一个异常
#PIPES_AS_CONCAT:将“||”是为字符串的链接操作符而非运算符,这和Oracle数据库是一样是,也和字符串的拼接函数Concat相类似
#ANSI_QUOTES:不能用双引号来引用字符串,因为它被解释为识别符
sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION

猜你喜欢

转载自blog.csdn.net/qq_45867699/article/details/129060382