菜鸟的mysql高级进阶以及mysql数据库优化

  • 说明
  • 一、mysql的逻辑分层及存储引擎
    • (1)逻辑分层
    • (2)存储引擎
  • 二、事务的ACID原则
  • 三、数据库设计的三大范式
  • 四、索引
    • (1)二叉树搜索树
    • (2)红黑树
    • (3)B树
    • (4)B+树
    • (5)索引
  • 五、SQL解析
  • 六、锁机制
    • (1)行锁
    • (2)表锁
    • (3)范围锁
    • (4)悲观锁
    • (5)乐观锁
    • (6)读写锁
  • 七、JOIN查询
    • (1)7种常见的JOIN查询
    • (2)sql
    • (3)第一种JOIN查询图
    • (4)第二种JOIN查询
    • (5)第三种JOIN查询
    • (6)第四种JOIN查询
    • (7)第五种JOIN查询
    • (8)第六种JOIN查询
    • (9)第七种JOIN查询
    • (10) Union和Union All
  • 八、索引优化
    • (1)索引分类
      • (1)单值索引
      • (2)唯一索引
      • (3)主键索引
      • (4)复合索引
      • (5)索引的基本语法
    • (2)Explain性能分析
      • (1)sql
      • (2)id字段
      • (3)select_type字段
      • (4)table字段
      • (5)type字段
      • (6)possible_keys字段 和 key字段
      • (7)key_len字段
      • (8)ref字段
      • (9)rows 字段
      • (10)Extra字段
    • (3)索引优化入门案例
      • (1)驱动表与被驱动表
      • (2)单表索引优化
      • (3)两表索引优化
      • (4)三表及以上索引优化
    • (4)索引失效分析
      • (1)最佳左前缀法则
      • (2)避免在索引字段上做计算
      • (3)避免在索引字段上做范围查询
      • (4)查询字段和索引字段尽量一致
      • (5)慎用is null和is not null
      • (6)like 的前后模糊匹配
      • (7)使用union或union all替代or
      • (8)补充
    • (5)分组排序优化
      • (1)order by之前先使用where等条件
      • (2)where和order by所用到的索引按照顺序
      • (3)排序的方向必须一致
    • (6)总结(核心)
      • (1)单索引优化
      • (2)复合索引优化
    • (7)截取查询分析
      • (1)开启慢sql查询日志
      • (2)千万级别数据插入
      • (3)show profile
  • 九、SQL优化
    • (1)sql语句优化

说明

更新时间:2020/10/17 23:49,更新了截取查询分析
更新时间:2020/10/16 23:16,更新了索引优化入门案例和索引失效分析
更新时间:2020/10/15 00:15,更新了join查询和索引优化
更新时间:2020/7/3 17:32,更新了锁机制和SQL语句优化
更新时间:2020/7/2 17:29,更新了索引
更新时间:2020/7/1 22:35,更新了SQL解析及优化,未完待续…

一直想学习一下mysql进阶的相关知识,刚好趁着前段时间redis学完,可以学习一下mysql的进阶,简单了解了一下进阶的相关知识,还挺多的,感觉之前的mysql学的好像只是简单入门而已。本文会持续更新,不断地扩充

本文仅为记录学习轨迹,如有侵权,联系删除

一、mysql的逻辑分层及存储引擎

(1)逻辑分层

一次sql的查询主要包括两个端的操作,客户端client和服务器端server,很多情况下,我们输入增删改查对应的查询语句,即可从服务器端操作数据,像insert、delete、update、select等常见的语句,对应到服务器端其实只是一个api,只是一个接口,从下图可以看到服务器端主要有连接层、服务层、引擎层和存储层,并且有相对应的功能。
其中引擎层就是下面要重点讲的存储引擎。

(2)存储引擎

MySQL中的数据用各种不同的技术存储在文件(或者内存)中。这些技术中的每一种技术都使用不同的存储机制、索引技巧、锁定水平并且最终提供广泛的不同的功能和能力。通过选择不同的技术,你能够获得额外的速度或者功能,从而改善你的应用的整体功能。

查看mysql支持的存储引擎
命令show engines;

我这边的mysql版本是8.0以上的,可以看到mysql支持的存储引擎还不少,它默认的存储引擎是InnoDB,当然,这个跟我的mysql的配置文件有关

我这边设置了默认的引擎和字符串,所以不同配置的mysql可能跟我这个有些不同。

MyISAM与InnoDB区别
这里重点提出来学习一下这个两个引擎,因为这两个好像是最常见的两个。

MyISAM InnoDB
文件构成 每个MyISAM在磁盘上存储成三个文件。第一个文件的名字以表的名字开始,扩展名指出文件类型。.frm文件存储表定义。数据文件的扩展名为.MYD (MYData)。 索引文件的扩展名是.MYI (MYIndex)。 基于磁盘的资源是InnoDB表空间数据文件和它的日志文件,InnoDB 表的大小只受限于操作系统文件的大小,一般为 2GB
事务 不提供事务支持 InnoDB提供事务支持事务
执行速度 MyISAM类型的表强调的是性能,其执行数度比InnoDB类型更快 InnoDB相较速度慢了一点
CURD操作 如果执行大量的SELECT,MyISAM是更好的选择 如果你的数据执行大量的INSERT或UPDATE,出于性能方面的考虑,应该使用InnoDB表
外键 不支持外键 支持外键
主键 允许没有主键的表存在。 如果没有设定主键,就会自动生成一个 6 字节的主键(用户不可见)。
MyISAM 只支持表级锁 InnoDB 支持行级锁和表级锁,默认是行级锁,行锁大幅度提高了多用户并发操作的性能。
全文索引 MyISAM 支持全文索引。 InnoDB 不支持全文索引 ,但innodb 从 mysql5.6 版本开始提供对全文索引的支持。

经过本人查询之后,发现它们之间的区别还不止这些,这些只是相对比较重要的区别。

MyISAM与InnoDB选择
关于这两种存储引擎的选择,网上有很多说法,我个人比较认可的是要根据需求和存储引擎来决定。比如像一些简单的需求,没有复杂的数据表的关系,数据大多以查询和插入为主,并且对安全性没有特别高的要求,这个时候就建议用MyISAM引擎,如果是涉及到的表有很多,而且各种表还有一些复杂的关联,平时除了查询之外,更新数据也挺频繁的,更重要的是安全性要求特别高,这个时候出于各种综合考虑,建议用InnoDB引擎,因为该引擎支持事务等高级操作。

总之,对于存储引擎的选择,需要结合各种引擎的特点以及需求来进行选择。

二、事务的ACID原则

这里给大家推荐篇两篇博客,这一部分的内容都是基于这两篇博客的整合
博客一和博客二

什么是ACID

ACID 说明
原子性(Atomicity) 原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
一致性(Consistency) 事务前后数据的完整性必须保持一致。
隔离性(Isolation) 事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。
持久性(Durability) 持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响

有一个非常经典的银行转账的例子就可以很好的体现了这4个原则。银行转账主要分两个步骤,假设有A和B账户,A账户现有800元,B账户现有200元

转账过程:A减200元,B加200元两个步骤要么都成功,要么都失败,不会出现A减200后,B却不加200的情况,这就是原子性,转账前A加B总额1000元,转账后也是1000元,这就是一致性,如果有多个用户并发的在执行转账,那么它们之前的事务应该是互补干扰的,这就是隔离性,事务执行成功后,即转账成功后,数据就被持久化到数据库了,这就是持久性

脏读、不可重复读和幻读
在进行web系统的开发的时候,有一个必须要考虑的重点,那就是并发处理,虽然自己平时学校里面的一些实训很少会做并发的处理,但是学到现在,自己会有意识的考虑并发的一些情况。额,扯远了,就是说在高并发的情况,可能会出现脏读、不可重复读和幻读的情况,下面给出相应的概述。

  • 脏读
    所谓脏读,就是指事务A读到了事务B还没有提交的数据,比如银行取钱,事务A开启事务,此时切换到事务B,事务B开启事务–>取走100元,此时切换回事务A,事务A读取的肯定是数据库里面的原始数据,因为事务B取走了100块钱,并没有提交,数据库里面的账务余额肯定还是原始余额,这就是脏读。

  • 不可重复读
    所谓不可重复读,就是指在一个事务里面读取了两次某个数据,读出来的数据不一致。还是以银行取钱为例,事务A开启事务–>查出银行卡余额为1000元,此时切换到事务B事务B开启事务–>事务B取走100元–>提交,数据库里面余额变为900元,此时切换回事务A,事务A再查一次查出账户余额为900元,这样对事务A而言,在同一个事务内两次读取账户余额数据不一致,这就是不可重复读。

  • 幻读
    所谓幻读,就是指在一个事务里面的操作中发现了未被操作的数据。比如学生信息,事务A开启事务–>修改所有学生当天签到状况为false,此时切换到事务B,事务B开启事务–>事务B插入了一条学生数据,此时切换回事务A,事务A提交的时候发现了一条自己没有修改过的数据,这就是幻读,就好像发生了幻觉一样。幻读出现的前提是并发的事务中有事务发生了插入、删除操作。

事务隔离级别
其实在应对上面的并发问题,我第一个想到的是用锁。。。,为了应对上面的出现的并发问题,就有了事务隔离级别,因为事务隔离级别越高,在并发下会产生的问题就越少,但同时付出的性能消耗也将越大,因此很多时候必须在并发性和性能之间做一个权衡。
事务隔离级别有4种,但是像Spring会提供给用户5种

事务隔离级别 说明
DEFAULT 默认隔离级别,每种数据库支持的事务隔离级别不一样,如果Spring配置事务时将isolation设置为这个值的话,那么将使用底层数据库的默认事务隔离级别。顺便说一句,如果使用的MySQL,可以使用"select @@tx_isolation"来查看默认的事务隔离级别,mysql8以上的用这个语句查询默认事务隔离级别“select @@transaction_isolation;”
READ_UNCOMMITTED 读未提交,即能够读取到没有被提交的数据,所以很明显这个级别的隔离机制无法解决脏读、不可重复读、幻读中的任何一种,因此很少使用
READ_COMMITED 读已提交,即能够读到那些已经提交的数据,自然能够防止脏读,但是无法限制不可重复读和幻读
REPEATABLE_READ 重复读取,即在数据读出来之后加锁,类似"select * from XXX for update",明确数据读取出来就是为了更新用的,所以要加一把锁,防止别人修改它。REPEATABLE_READ的意思也类似,读取了一条数据,这个事务不结束,别的事务就不可以改这条记录,这样就解决了脏读、不可重复读的问题,但是幻读的问题还是无法解决
SERLALIZABLE 串行化,最高的事务隔离级别,不管多少事务,挨个运行完一个事务的所有子事务之后才可以执行另外一个事务里面的所有子事务,这样就解决了脏读、不可重复读和幻读的问题了

给出网上整理的图

级别越高效率越低,一般情况下会设置为READ_COMMITED,此时避免了脏读,并发性也还不错,之后再通过一些别的手段去解决不可重复读和幻读的问题就好了。

三、数据库设计的三大范式

关于数据库设计的三大范式,首推这篇博客

第一范式(1NF)
要求数据库表的每一列都是不可分割的原子数据项。
例子:

像这种情况是不行的,因为家庭信息和学校信息的数据是可以再分的,比如家庭地址,家里人口数等,是属于可分割的原子数据项,所以上面这张表是不符合第一范式的,最好的做法是再拆分出家庭信息表和学校信息表

第二范式(2NF)
在满足第一范式的前提下,非码属性必须完全依赖于候选码(在1NF基础上消除非主属性对主码的部分函数依赖),第二范式需要确保数据库表中的每一列都和主键相关,而不能只与主键的某一部分相关(主要针对联合主键而言)。

一句话总结就是,一张表只能存放对应的一件事情
例子

很明显,这种设计是不符合第二范式的,单从字段上看就知道,这张表描述了两件事情,产品和订单,所以,这种设计违反了第二范式。

第三范式(3NF)
在满足第一和第二范式的前提下,任何非主属性不依赖于其它非主属性(在2NF基础上消除传递依赖),第三范式需要确保数据表中的每一列数据都和主键直接相关,而不能间接相关。

一句话总结就是,表的每一个字段都应该直接与主键相关,间接相关是不行的
例子

很明显,后面班主任相关的字段好像跟主键学号不是直接相关的,而像是间接相关的,所以这也是不符合第三范式的,应该通过外键,将班主任与学生关联起来,两张表相关联。

规范与性能
既然三大范式是规范的话,那数据库设计是不是都要符合三大范式才行呢,显然不是的,说到规范就必须要说到性能了,很多情况下,规范和项目的性能往往不能兼得,为了规范化,得牺牲一部分性能,尤其是在海量数据的情况下,所以,在规范和性能之间要做一个权衡。

不过就个人而已,我会优先考虑性能,然后再考虑规范化,我宁愿查询的性能高一点,哪怕是不符合三大范式,最常见的就是给表增加一些冗余字段,将多表查询变为单表查询,虽然可能不符合三大范式,但在大量数据的情况下,这能提高效率。

四、索引

索引可以简单理解为一种数据结构,这种结构可以帮我们实现数据库的快速查找,所以,一般加了索引,可以极大的提高数据库的查找效率,在开始学习所以的前,有必要先学习一下下面的几种数据据结构。

在学习数据结构的时候,我们老师重点讲过树,作为一种数据结构,树是重中之重,只可惜当时学的时候没有重视它,现在才知道原来树结构的一些应用,像mysql数据库的索引,底层用的就是B+树,用这种结构可以实现快速查找,这个后面会详细展开,先简单的复习一下二叉树。

二叉树又可以分为一般二叉树、完全二叉树、满二叉树、线索二叉树、霍夫曼树、二叉排序树、平衡二叉树、红黑树、B树,二叉搜索树等等。

(1)二叉树搜索树

定义

(1)每个节点有一个唯一的key值,且所有结点互不相同;
(2)左子树所有key值小于根的key值;
(3)右子树所有key值大于根的key值;
(4)左右子树都是二叉搜索树。

利用这种结构可以实现快速的查找,下面以mysql没加索引时的查找效率和二叉搜索树的查找效率比较。

案例
假设下面这张表名为:test

查找:select * from test where Col2 = 23

正常情况下如果执行上面这条语句的话,它会从头开始搜索,一共需要执行7次才可以定位到23这行数据,但是如果用二叉搜索树的话,搜索过程是先查询34->22->23,只需3次就可以定位到23这行数据,查询效率提高了不少。

这种二叉树搜索树的结构似乎很完美,然而,它也有它的短板,假设如果将上面的图中的Col1用二叉树搜索树存储的话,查找23需要多少次呢?

结果是7次,这种情况下,查询的效率就很低了。

(2)红黑树

红黑树是一个平衡的二叉树,也叫二叉平衡树,但不是一个完美的平衡二叉树。红黑树是在二叉搜索树的基础上,引入了红和黑两种颜色,并且结合红黑树的5条性质,来保证树的平衡。

性质

(1)每个结点要么是红的要么是黑的。
(2)根结点是黑的。
(3)每个叶结点(叶结点即指树尾端NIL指针或NULL结点)都是黑的。
(4)如果一个结点是红的,那么它的两个儿子都是黑的。
(5)任意结点到叶子结点的每条路径都包含相同数目的黑结点。

什么,这些性质有点难理解,ok,下面逐条介绍每一条性质


注意:性质 3 中指定红黑树的每个叶子节点都是空节点,而且并叶子节点都是黑色。但 Java 实现的红黑树将使用 null 来代表空节点,因此遍历红黑树时将看不到黑色的叶子节点,反而看到每个叶子节点都是红色的。


像这个图,上面为NIL的均为叶子节点,上图也是符合5条性质的。

红黑树的左旋右旋
红黑树主要通过性质5进行左旋右旋,从而保证平衡,具体怎么操作看下图

可惜不会发动图,用动图的话可能更加直观。这样就解决了上面的二叉搜索树短板问题。

(3)B树

关于B树这里推荐一篇文章

B树又叫为B-树,这两个是同一种树,只是叫法不同而已。B树和平衡二叉树稍有不同的是B树属于多叉树又名平衡多路查找树(查找路径不只两个),B树的查询有点像二分法查询。

特征

一个m阶的B树具有如下几个特征:
1、每个节点最多有m-1个关键字(可以存有的键值对)。2、根节点最少可以只有1个关键字。3、非根节点至少有m/2个关键字。4、每个节点中的关键字都按照从小到大的顺序排列5、所有叶子节点都位于同一层,或者说根节点到每个叶子节点的长度都相同。6、每个节点都存有索引和数据,也就是对应的key和value。7、所有的索引元素不重复

插入

遵循规则:
这里以一个4阶的B树为例,插入数据1,2,3,4,5,6(1)节点拆分规则:当前是要组成一个4阶B树,那么此时m=4,关键字数必须小于等于4-1=3,所以当一个节点的值超过3就要进行拆分(2)拆分规则:节点中中间的值往上个节点提,左右两边独立出来两个节点(3)排序规则:满足节点本身比左边节点大,比右边节点小的排序规则;

只要满足拆分规则就会进行拆分

查询
查询就更简单了,以下面构建的B树为例查询数值3

先查根节点,3小于11,所以往左边走,直接定位到3,效率及其高

又比如下面的B树,查询数值3


先查根节点,3小于9,往左边走,3位于2和6之间,往中间走,定位到3和5,从而定位到数值3。

注意:这里有一个重点,那就是每一个节点都是用key-value的方式存在的,key对应值,value对应key所在的地址,正是这样的结构才能有高效率的查询

(4)B+树

B+树可以看成是B树的一个变种,他们之前存在很多相同点,当然也有不同点,而且相对于B树,B+树查询的效率更高。斜体样式


插入
B+树的插入跟B树的插入相类似,只是这里会有一个索引节点的存在

插入的规则:当节点元素数量大于m-1的时候,按中间元素分裂成左右两部分,中间元素分裂到父节点当做索引存储,但是,本身中间元素还是分裂右边这一部分的。

这里以一个5阶的B+树为例

查询
以下面的例子,查询3,首先查询根节点

然后3小于8,查询左边节点

3位于2和5之间,所以查询2和5之间的节点

注意:查询的方式好像跟B树差不多,但是细分的话还是有不同的
(1)B+树中间的索引节点不存放数据,只存放索引,所以占的空间相对B树少,因此同样大小的磁盘页,B+树可以存放更多数据,所以在数据量相同的情况下,B+树相对于B树显得更加“矮胖”,查询的I/O次数也较少,查询效率也比较高。

(2)范围查找,B树需要用中值遍历等操作,而B+树只需要遍历叶子节点做链表的遍历即可,这一点上,B+树的效率就比B树要高得多

总结

(1)B树和B+树根节点至少一个元素,非根节点元素范围:m/2 <= k <= m-1(2)B树的所有节点都存放有数据,而B+树有两种类型的节点:索引结点和叶子结点。索引结点只存储索引,数据都存储在叶子节点。(3)单一节点存储的元素更多,使得查询的IO次数更少,所以也就使得它更适合做为数据库MySQL的底层数据结构了(4)B+树叶子节点之间有指针指向下一个叶子节点

(5)索引

索引的分类与创建
主要有主键索引,唯一索引,单值索引和复合索引

底层原理
索引为什么那么快呢?这就需要探究一下它的底层原理,目前mysql的索引底层都是基于B+树的数据结构存储的,但又不是完全是B+树,是经过改造的B+树,B树和B+树就像上面介绍的那样,在查询的时候效率是及其高效的,除此之外底层原理还有Hash结构,也是高效查询,但Hash不支持范围查询。

优势与劣势
关于mysql的索引的概念这里就不再叙述,这里简单介绍一下索引的优势和劣势

优势
(1)高效的查询,增加了索引的字段,查询效率及其高,足以支撑千万级别的数据量,即使是千万级别的数据量查询效率依旧很快
(2)因为mysql的索引底层用的是B+树,好像之前是B树来着,这种树结构本身就是排好序的,所以需要排序的时候可以直接拿过来用

劣势
(1)需要额外的空间来存储索引,索引本身就是一种数据结构,所以需要占据一定的空间,而且所需空间比本身的表还大,相当于用空间换取时间
(2)会降低增删改的效率

五、SQL解析

这里推荐这篇博客里面的内容讲得挺全的,看一下会有收获的。

我们在使用Mysql的时候,其实主要包括我们只负责编写sql语句,而sql语句的解析就由mysql内部自动解析,并且查询数据。

sql编写过程

select distinct ... from .. join ...on ...where...group by ... having ...order by... limit ...

sql解析过程

from... on ... join...where ...group by ...having ... select distinct ...order by ...limit...

可以看到sql的解析跟编写的顺序是不一样的。

六、锁机制

(1)行锁

当mysql处于高并发的情况下,容易出现某一行被锁住的情况,最经典的就是多个客户端同时操作某一行数据,客户端A操作这一行数据后,事务还未提交,客户端B又同时在操作该行数据,此时,由于客户端A在操作此行数据,而且事务还未提交,这个时候这行数据是被锁住的,客户端B如果同时操作该行数据就会进入阻塞状态,下面开始模拟行锁的情况。

两个客户端:客户端A Navicat,客户端B doc窗口的mysql

根据用户id更新数据

客户端A操作id为1的这行数据,此时未提交事务,再用客户端B操作这一行数据


可以看到操作这一行数据时已经阻塞了,只有等客户端A的事务提交后,客户端B才会开始执行

但是如果操作其他行数据呢?

可以看到操作其他行数据是没有问题的,这就是行锁,只锁定某一行数据。

(2)表锁

与行锁对应的还有表锁,顾名思义就是锁定整张表。下面开始模拟表锁的情况。

这次根据用户名更新用户,事务还未提交

此时再操作该行数据,发现该行锁住了,进入了阻塞状态

那操作其他行数据呢,发现也是锁住状态,同样进入阻塞状态

注意,这里跟上面的模拟行锁有点不同,上面的行锁是根据用户id更新数据,而表锁这里是根据用户名更新用户,这就是问题所在,用户id是有主键索引,用户名没有,这就是说,索引会对锁产生影响,如果在有索引的情况下,数据库就只会锁住一行,即行锁,如果没有索引,则会锁住整张表,即表锁。

(3)范围锁

范围锁就是指锁定某一范围内的数据,下面开始模拟范围锁的情况。

更新id小于等于3的数据,未提交事务

此时再分别操作id为1,2,3的数据行,发现都进入了阻塞状态

除此之外,如果操作id为4的数据也会阻塞,明明范围是小于等于3,可能这是它的范围锁的机制吧

如果操作id为5或6的数据行呢,发现id为5或6的数据行可以操作

这就是范围锁,因为更新的数据是id小于等于3,所以在id小于等于3(包括4都会阻塞)这个范围内的数据行都被锁住了,不在这个范围内的数据可以照常操作,这就是范围锁。

(4)悲观锁

悲观锁的定义就不用说了,做什么操作都必须加锁,这就是悲观锁,mysql实现悲观锁的方式可以通过for update语句实现。

格式select * from xxxxxxx where namemc='筛查' for update

for update实现了行级锁,一旦用户对某个行施加了行级加锁,则该用户可以查询也可以更新被加锁的数据行,其它用户只能查询但不能更新被加锁的数据行.如果其它用户想更新该表中的数据行,则也必须对该表施加行级锁.即使多个用户对一个表均使用了共享更新,但也不允许两个事务同时对一个表进行更新,真正对表进行更新时,是以独占方式锁表,一直到提交或复原该事务为止。行锁永远是独占方式锁。

只有当出现如下之一的条件,便释放共享更新锁:
(1)执行提交(COMMIT)语句;
(2)退出数据库(LOG OFF)
(3)程序停止运行。

悲观锁应用之高并发商品的抢购
悲观锁可以用来处理商品高并发的处理,虽然可以会影响性能,假设有多个人同时抢购一件商品,商品刚好剩一件,那么这个时候如果没做相应的处理,就会出现很严重的问题,这个时候的一种处理方式是加悲观锁。下面用一张用户表模拟一下悲观锁的使用。

先查询商品的库存量,在做现应的处理,这里就简单用一张用户表简单模拟一下

加悲观锁后,发现加了悲观锁的数据行只能查询,不能更新

只有提交了事务后,释放了锁后,才可以进行更新操作

这就是mysql的悲观锁的实现。

(5)乐观锁

与悲观锁相对的乐观锁,表示做什么操作都不加锁,有问题了再处理,大概就是这个意思,它的实现也很简单,只需要在表里面加个字段即可。

字段version就可以用来实现乐观锁。

乐观锁应用之高并发商品的抢购
这里也是通过一张用户表来模拟,每次查询完库存后,更新操作时需要对应的版本号,同时版本号加1

假设两个用户同时抢购,同时执行了select * from user WHERE id = 1;查询库存量

这个时候查询到的version同样是0,此时,不管那一个用户先抢到了商品,即执行了UPDATE user set name = 'Tonny',version = version+1 WHERE version = 0 and id =1;语句,版本号都进行了加1,version=1,这时另一个用户由于之前查询的version=0,执行update语句时按照之前的version=0来更新就会失败,因为商品已经被另一人买了,并且version已经等于1

这就是mysql简单实现乐观锁的基本操作。

(6)读写锁

我们应该都明白这样的问题,针对数据库表中的数据,当同一时刻多个用户并发读取同一个数据时,不会出现任何问题,因为没有涉及对表中数据的增加删除和修改的操作,但是当多个用户需要在同一时刻对表内容进行增删改,或者一个用户在进行查询,另一个用户同时需要增删改的情况下,对于读取信息的那个用户来说,就会发生前后读取内容不一致的问题,所以就需要针对这类的并发问题,就可以使用两种类型的锁系统来解决,分别称为共享锁(shared lock)和排它锁(exclusive lock),或者也可以叫做读锁(read lock)和写锁(write lock)。

  先简单解释一下这两种锁的概念,读锁是共享的,是异步的,是相互不阻塞的,高并发下,多个用户同时读取数据库中的同一个资源,可以相互不干扰;而写锁是排他的,同步的,是会阻塞其他的用户的写锁与读锁,只有这样才能保证,在一个时间段内,只有一个用户在对数据库进行操作,从而防止了其他用户访问到了正在修改的数据。

读锁
读锁:是一种共享锁,一个事务持有读锁时,不会阻塞其它的读锁,其他事务都可以对该数据进行读取;

加上读锁后,经过测试,只能查询,增删改均失败,同时在并发的情况下,加上读锁,其余线程对该表的操作也只能查询,增删改则会阻塞。


解锁更新成功

写锁
写锁:是一种排他锁,一个锁持有写锁会阻塞其他的写锁和读锁,从而保证了一个只有一个事务进行写操作,并且防止其他事务读取正在写入资源,避免了脏读;

当持有写锁的一端,可以进行增删改查

同时再打开一个mysql,在user被加上写锁的情况下,在其他端进行增删改查,发现都阻塞了

七、JOIN查询

(1)7种常见的JOIN查询


上面是比较常见的几种join连接查询的情况。

(2)sql

CREATE TABLE `t_dept` (
`id` INT ( 11 ) NOT NULL AUTO_INCREMENT,
`deptName` VARCHAR ( 30 ) DEFAULT NULL,
`address` VARCHAR ( 40 ) DEFAULT NULL,
PRIMARY KEY ( `id` )
) ENGINE = INNODB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;CREATE TABLE `t_emp` (
`id` INT ( 11 ) NOT NULL AUTO_INCREMENT,
`name` VARCHAR ( 20 ) DEFAULT NULL,
`age` INT ( 3 ) DEFAULT NULL,
`deptId` INT ( 11 ) DEFAULT NULL,
empno INT NOT NULL,
PRIMARY KEY ( `id` ),
KEY `idx_dept_id` ( `deptId` ) #CONSTRAINT `fk_dept_id` FOREIGN KEY (`deptId`) REFERENCES `t_dept` (`id`)) ENGINE = INNODB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;INSERT INTO t_dept(deptName,address) VALUES('华山','华山');
INSERT INTO t_dept(deptName,address) VALUES('丐帮','洛阳');
INSERT INTO t_dept(deptName,address) VALUES('峨眉','峨眉山');
INSERT INTO t_dept(deptName,address) VALUES('武当','武当山');
INSERT INTO t_dept(deptName,address) VALUES('明教','光明顶');
INSERT INTO t_dept(deptName,address) VALUES('少林','少林寺');
INSERT INTO t_emp(NAME,age,deptId,empno) VALUES('风清扬',90,1,100001);INSERT INTO t_emp(NAME,age,deptId,empno) VALUES('岳不群',50,1,100002);
INSERT INTO t_emp(NAME,age,deptId,empno) VALUES('令狐冲',24,1,100003);
INSERT INTO t_emp(NAME,age,deptId,empno) VALUES('洪七公',70,2,100004);
INSERT INTO t_emp(NAME,age,deptId,empno) VALUES('乔峰',35,2,100005);
INSERT INTO t_emp(NAME,age,deptId,empno) VALUES('灭绝师太',70,3,100006);
INSERT INTO t_emp(NAME,age,deptId,empno) VALUES('周芷若',20,3,100007);
INSERT INTO t_emp(NAME,age,deptId,empno) VALUES('张三丰',100,4,100008);
INSERT INTO t_emp(NAME,age,deptId,empno) VALUES('张无忌',25,5,100009);
INSERT INTO t_emp(NAME,age,deptId,empno) VALUES('韦小宝',18,null,100010);

上述sql语句是为下面的sql实战做准备的

(3)第一种JOIN查询图

命令:select * from t_dept d left join t_emp e on d.id = e.deptId;

(4)第二种JOIN查询

命令:select * from t_dept d left join t_emp e on d.id = e.deptId;

(5)第三种JOIN查询

命令:select * from t_dept d inner join t_emp e on d.id = e.deptId;

(6)第四种JOIN查询

命令:select * from t_dept d left join t_emp e on d.id = e.deptId where e.id is null;

(7)第五种JOIN查询

命令:select * from t_dept d right join t_emp e on d.id = e.deptId where d.id is null;

(8)第六种JOIN查询

命令:select * from t_dept d left join t_emp e on d.id = e.deptId union select * from t_dept d right join t_emp e on d.id = e.deptId where d.id is null;

(9)第七种JOIN查询

命令:select * from t_dept d left join t_emp e on d.id = e.deptId where e.id is null union select * from t_dept d right join t_emp e on d.id = e.deptId where d.id is null;

(10) Union和Union All

Union 对两个结果集进行并集操作,不包括重复行,同时进行默认规则的排序;
Union All 对两个结果集进行并集操作,包括重复行,不进行排序;

在网上看到一个例子:

使用union和union all的区别:

八、索引优化

(1)索引分类

索引的分类详细分的话主要有主键索引,唯一索引,单值索引和复合索引;其中单值索引和复合索引都是属于普通索引

主键名 命名规范
主键索引 pk_字段名
唯一索引 uk_字段名
单值索引 idx_字段名
复合索引 idx_字段名1_字段名2_…

(1)单值索引

概念::即一个索引只包含单个列,一个表可以有多个单列索引

-- 表创建的同时创建:
CREATE TABLE customer (
id INT ( 10 ) UNSIGNED AUTO_INCREMENT,
customer_no VARCHAR ( 200 ),
customer_name VARCHAR ( 200 ),
PRIMARY KEY ( id ),
KEY ( customer_name )
);-- 单独创建索引
CREATE INDEX idx_customer_name ON customer(customer_name);

(2)唯一索引

概念:索引列的值必须唯一,但允许有空值

-- 随表一起创建
CREATE TABLE customer (
id INT ( 10 ) UNSIGNED AUTO_INCREMENT,
customer_no VARCHAR ( 200 ),
customer_name VARCHAR ( 200 ),
PRIMARY KEY ( id ),
UNIQUE ( customer_name )
);-- 单独创建唯一索引
create unique index uk_customer_no on customer(customer_no);

(3)主键索引

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

-- 随表一起建索引
CREATE TABLE customer (
id INT ( 10 ) UNSIGNED AUTO_INCREMENT,
customer_no VARCHAR ( 200 ),
customer_name VARCHAR ( 200 ),
PRIMARY KEY ( id )
);-- 单独建主键索引:
ALTER TABLE customer add PRIMARY KEY customer(id);-- 删除建主键索引:有两种情况,下面会细讲
--(1)
ALTER TABLE customer modify id int;
ALTER TABLE customer drop PRIMARY KEY ;--(2)
ALTER TABLE customer drop PRIMARY KEY ;-- 注意:修改建主键索引:必须先删除掉(drop)原索引,再新建(add)索引

主键的创建很常见,基本没什么问题,主要是主键的删除,这里有两种情况。

  • 可以直接使用drop删除主键的情况。
  • 如果带有主键的列还有AUTO_INCREMENT属性,需要间接方式去掉。

(4)复合索引

概念:即一个索引包含多个列

-- 随表一起建索引
CREATE TABLE customer (
id INT ( 10 ) UNSIGNED AUTO_INCREMENT,
customer_no VARCHAR ( 200 ),
customer_name VARCHAR ( 200 ),
PRIMARY KEY ( id ),
KEY ( customer_no, customer_name )
);-- 单独创建复合索引
CREATE INDEX idx_no_name ON customer(customer_no,customer_name);

(5)索引的基本语法

操作 命令
创建 CREATE [UNIQUE ] INDEX [indexName] ON table_name(column))
删除 DROP INDEX [indexName] ON mytable;
查看 SHOW INDEX FROM table_name
使 用 Alter命令1 (1)ALTER TABLE tbl_name ADD PRIMARY KEY (column_list) : 该语句添加一个主键,这意味着索引值必须是唯一的,且不能为 NULL。
使 用 Alter命令2 (2)ALTER TABLE tbl_name ADD PRIMARY KEY (column_list)
使 用 Alter命令3 (3)ALTER TABLE tbl_name ADD INDEX index_name (column_list): 添加普通索引,索引值可出现多次。
使 用 Alter命令4 (4)ALTER TABLE tbl_name ADD FULLTEXT index_name (column_list):该语句指定了索引为 FULLTEXT ,用于全文索引。

(2)Explain性能分析

我们知道mysql在执行代码的时候,有自己的sql性能分析器,会自动根据输入的sql语句进行sql语句的优化,使用 EXPLAIN 关键字可以模拟优化器执行 SQL 查询语句,从而知道 MySQL 是如何处理你的 SQL 语句的。分析你的查询语句或是表结构的性能瓶颈。

使用的方式:explain sql语句

通过上面的简单例子,下面开始进行详细讲解。

(1)sql

在开始之前,需要先提前准备一下数据库以及表

CREATE TABLE t1(id INT(10) AUTO_INCREMENT,content VARCHAR(100) NULL , PRIMARY KEY (id));
CREATE TABLE t2(id INT(10) AUTO_INCREMENT,content VARCHAR(100) NULL , PRIMARY KEY (id));
CREATE TABLE t3(id INT(10) AUTO_INCREMENT,content VARCHAR(100) NULL , PRIMARY KEY (id));
CREATE TABLE t4(id INT(10) AUTO_INCREMENT,content VARCHAR(100) NULL , PRIMARY KEY (id));
INSERT INTO t1(content) VALUES(CONCAT('t1_',FLOOR(1+RAND()*1000)));
INSERT INTO t2(content) VALUES(CONCAT('t2_',FLOOR(1+RAND()*1000)));
INSERT INTO t3(content) VALUES(CONCAT('t3_',FLOOR(1+RAND()*1000)));
INSERT INTO t4(content) VALUES(CONCAT('t4_',FLOOR(1+RAND()*1000)));

(2)id字段

这里的id字段有以下几种情况:

id相同的情况

命令:explain select * from t1,t2,t3 where t1.id = t2.id and t2.id = t3.id;


这里可以看到id字段都是1,执行顺序是自上而下的,对应的表是t1,t2,t3,所以这里的执行顺序是t1,t2,t3;

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

命令:explain select t1.* from t1 where t1.id  = (select t2.id from t2 where t2.id = 1);


这里的执行顺序是先执行t2再执行t1

注意:网上还有一种情况是既有id相同和id不同的情况,这个不知道是版本的问题还是我操作的问题,我用的mysql8.0以上的,模拟不出那种情况

(3)select_type字段

表示查询的类型,主要有以下几种

simple 简单的 select 查询,查询中不包含 子查询 或者 union。
primary 查询中若包含任何复杂的子查询,最外层查询则被标记为 primary 。
subquery 在 select 或 where 列表中的子查询。
derived (衍生) DERIVED 在 FROM 列表中包含的子查询被标记为 DERIVED(衍生)MySQL 会递归执行这些子查询, 把结果放在临时表里。
union 若第二个SELECT出现在UNION之后,则被标记为UNION;若UNION包含在FROM子句的子查询中,外层SELECT将被标记为:DERIVED
DEPEDENT SUBQUERY 在SELECT或WHERE列表中包含了子查询,子查询基于外层
UNCACHEABLE SUBQUERY 无法使用缓存的子查询
UNION RESULT 从UNION表获取结果的SELECT

(4)table字段

表示用到哪一张表

(5)type字段

访问类型,是我们检查当前语句是否需要优化,能够继续优化的重要指标

从最优到最差如下(完整版)

system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index >all

从最优到最差如下(简洁版)

system > const > eq_ref > ref > range > index > all

详细参数说明

访问类型 说明
ALL Full Table Scan, MySQL将遍历全表以找到匹配的行
index Full Index Scan,index与ALL区别为index类型只遍历索引树
range 只检索给定范围的行,使用一个索引来选择行
ref 表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值
eq_ref 类似ref,区别就在使用的索引是唯一索引,对于每个索引键值,表中只有一条记录匹配,简单来说,就是多表连接中使用primary key或者 unique key作为关联条件
const、system 当MySQL对查询某部分进行优化,并转换为一个常量时,使用这些类型访问。如将主键置于where列表中,MySQL就能将该查询转换为一个常量,system是const类型的特例,当查询的表只有一行的情况下,使用system

备注:一般来说,得保证查询至少达到range级别,最好能达到ref。

(6)possible_keys字段 和 key字段

possible_keys : 当前表查询可能使用到的索引, key :实际使用的索引

(7)key_len字段

表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度。 key_len 字段能够帮你检查是否充分的
利用上了索引。ken_len 越长,说明索引使用的越充分。

(8)ref字段

显示索引的哪一列被使用了,如果可能的话,是一个常数。哪些列或常量被用于查找索引列上的值。

(9)rows 字段

rows 列显示 MySQL 认为它执行查询时必须检查的行数。越少越好!

(10)Extra字段

其他额外信息,里面有几个十分重要的数据段,
Using filesort
说明 mysql 会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取。MySQL 中无法利用索引完成的排序操作称为“文件排序”。

注意:出现这种情况的话是十分需要优化,这种会严重拖慢查询效率

模拟这种情况的出现,新建t_emp创建复合索引idx_name_age,然后执行下面的语句

explain select name,age from t_emp order by age;

查询中排序的字段,排序字段若通过索引去访问将大大提高排序速度。但是这是复合索引,排序的时候要按照索引的顺序来查询,例如:order by name,age,或者不要跨顺序查询。例如:order by name,这样的查询就按照索引顺序来查,就不会出生上面的情况

Using temporary
使了用临时表保存中间结果,MySQL 在对查询结果排序时使用临时表。常见于排序 order by 和分组查询 groupby。这样查询效率是最慢的。
例如

这个比上面的更慢,因为它创建了一个临时表,效率更慢,更需要优化;

Using index
Using index 代表表示相应的 select 操作中使用了覆盖索引(Covering Index),避免访问了表的数据行,效率不错!如果同时出现 using where,表明索引被用来执行索引键值的查找;如果没有同时出现 using where,表明索引只是用来读取数据而非利用索引执行查找。

除此之外还有其他字段。

(3)索引优化入门案例

(1)驱动表与被驱动表

先了解在join连接时哪个表是驱动表,哪个表是被驱动表:
1.当使用left join时,左表是驱动表,右表是被驱动表
2.当使用right join时,右表时驱动表,左表是驱动表
3.当使用join时,mysql会选择数据量比较小的表作为驱动表,大表作为被驱动表

先了解什么是驱动表什么是被驱动表,下面的多表索引优化会详细讲到。

(2)单表索引优化

查询命令:explain select id,name from t_emp where age = 70 and empno>100001 order by empno limit 1;
还是用上面用到过的表t_emp,先删除所有的除主键外的索引,并且执行查询命令

上图用explain分析发现是全表扫描,而且用到了外部的索引排序Using filesort,这种情况下是需要优化的,优化的思路是加索引,根据上面的用到查询字段age,先给age加个单值索引

这个时候type不再是all全表查询了,而是ref,这个性能比all要高得多,而且这里key字段也表示用到新建的这个索引,但是最后Extra还是出现了外部排序Using filesort,还得优化,那么对两个字段建复合索引的情况下会不会更好?

对比上图中的两条sql语句,一个用了>一个用了=,这里重点强调一下,范围查询会使得索引失效,使用>的索引上面的复合索引idx_age_empno没有被完美应用

(3)两表索引优化

先创建sql相关的表

CREATE TABLE IF NOT EXISTS `class` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`card` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY (`id`)
);CREATE TABLE IF NOT EXISTS `book` (
`bookid` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`card` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY (`bookid`)
);INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO class(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card) VALUES(FLOOR(1 + (RAND() * 20)));

在不创建除主键外的任何索引的情况下,看一下该表的基本情况,已经左连接查询的性能

这种情况下type的all,即全表扫描,下面看优化后的性能分析

分别在class表和book表中创建idx_card索引(记得在创建新的索引之前把旧的索引删掉)

从上面的结果来看,有下面两个结论

①在优化关联查询时,只有在被驱动表上建立索引才有效!
②left join 时,左侧的为驱动表,右侧为被驱动表!
③小表驱动大表原创,这个后面会细讲


(4)三表及以上索引优化

再加一个phone,这些表没有逻辑上的意义,只是为了让它们关联,所以字段名都有card,作为连接的标志


CREATE TABLE IF NOT EXISTS `phone` (
`phoneid` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`card` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY (`phoneid`)
);INSERT INTO phone(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO phone(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO phone(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO phone(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO phone(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO phone(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO phone(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO phone(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO phone(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO phone(card) VALUES(FLOOR(1 + (RAND() * 20)));
INSERT INTO phone(card) VALUES(FLOOR(1 + (RAND() * 20)));

在进行三表索引优化之前,先把这三张表的索引删掉

在没进行优化前,基本都全表扫描

优化思路基本跟上面的两表联立一样,将索引建在被驱动表上即可,这里有两个left join,对应的也需要建两个索引,即class表和phone表上建索引

(4)索引失效分析

创建了索引后,有可能会出现索引失效的情况,这种情况是要避免的。

(1)最佳左前缀法则

使用复合索引,需要遵循最佳左前缀法则,即如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始并且不跳过索引中的列。

以t_emp表为例,创建复合索引idx_name_age_empno,对name、age、empno三个字段创建索引。

在正常按照顺序使用该索引的全部字段的时候是最优的

如果拆开使用的情况呢,即单独使用索引的一个字段,这种情况就需要用到最佳左前缀法则

索引idx_name_age_empno顺序对应字段是name、age和empno,所以在使用该所以查询的时候必须查询条件必须以name为第一个查询条件,并且后面的条件必须按照索引的顺序字段来,这是最佳的情况,即最佳左前缀法则

如果第一个条件以索引的第一个字段为查询条件,后面跟任一或任多个该索引对应的字段,这样也使用了该索引的部分字段,也有一定的优化效果,但还是违背了最佳左前缀法则,如果查询的第一个条件不是该索引的第一个字段name,都将造成该索引失效。如下图

注意,上面说的在精准匹配的情况下,如果查询条件不是精准匹配,而是范围查找,这样会直接造成索引失效,直接跳过该法则,范围查找包括大于小于不等于等条件

结论:过滤条件要使用索引必须按照索引建立时的顺序,依次满足,一旦跳过某个字段,索引后面的字段都无法被使用

(2)避免在索引字段上做计算

不在索引列上做任何操作(计算、函数、(自动 or 手动)类型转换),会导致索引失效而转向全表扫描。
如下,在t_dept表为deptName字段创建单值索引,在查询的时候对该索引字段进行计算操作,直接导致索引失效,进行全表扫描

(3)避免在索引字段上做范围查询

就像上面说的,大于、小于和不等于等查询条件均属于范围查询,以t_dept表为例,只要使用了范围查找,该索引字段前面的索引没有失效,该字段索引不会失效,,但字段后面的条件索引就会失效,。

(4)查询字段和索引字段尽量一致

在查询的时候,查询的数据尽量跟索引字段一致,这样的会有额外信息Extra字段里会出现using index,即覆盖索引,直接从索引拿数据,效率最高(虽然这种情况在实际开发中很难保证数据),尽量不要用select *.

(5)慎用is null和is not null

注意这里的字段必须是不为null的情况,在使用is null或者is not null关键字的时候,索引失效

当这里的字段允许为null时,使用is null索引没有失效,而is not null索引失效

(6)like 的前后模糊匹配

模糊匹配有如下3种情况, ‘%aa’、'aa%'和‘%aa%’,根据%不同位置有3种模糊匹配,以t_emp表为例,注意先将之前的该表的除主键之外的索引都删掉再进行测试

首先是第一种,先对name字段创建单值索引idx_name,并且用%aa的方式进行模糊匹配,发现%aa的模糊匹配即使aa字段是索引字段也会失效

第二种,aa%的模糊匹配,索引未失效

第三种,%aa%的模糊匹配,索引失效

重点:解决%aa%模糊匹配索引失效问题
解决这个问题可以用覆盖索引,即上面用到的,查询的字段包含在所建的复合索引字段内即可,例如创建索引idx_name_age,即对t_emp表的name和age字段创建复合索引


但是如果查询的字段出现了非索引字段的话索引会失效

(7)使用union或union all替代or

(8)补充

(1)如果创建idx_name_age_empno,查询条件只要包括有name、age和empno三个索引就会有效,不用管顺序,因为数据库会有自己的优化器自动调整顺序

(2)如果创建idx_name_age_empno,查询条件包含所有的索引字段的话,只要有一个字段是采用的范围查找的话,该字段索引不会失效,但该字段后面的索引失效,相当于只使用了部分索引。

对于索引idx_name_age_empno,尽管我们的sql是先查name,再查empno,最后age,但是sql的内部的优化器会默认修改为先name,再age,再empno,而empno是范围查找,那么empno后面的索引字段失效,但是empno已经是索引字段的最后,所以,相当于这3个索引字段都没有失效,都用上了,看key_len的数字就可以看出来。

(5)分组排序优化

(1)order by之前先使用where等条件

where 条件和 on 的判断这些过滤条件,是要被先考虑的!只有先使用上where等条件,先使用上索引,再进行order by排序

这个的思路也简单,事先创建索引idx_name_age_empno,分别对应这3个索引字段,再使用排序时,先用where,用上了索引,再进行排序,效率就会非常高,因为索引底层用的B+树本身就是排好序的结构。

(2)where和order by所用到的索引按照顺序

这个的意思是where和order by所用到的全部索引,必须严格按照复合索引的顺序来使用,不能出现中间断层的情况

这里的索引是idx_name_age_empno,where加order by所用的的索引字段必须按照复合索引的顺序来,可以不用上所有的索引字段,但是不能出现断层,否则会出现外部排序。

注意:where后面的条件顺序只要符合最佳左前缀法则,顺序交换,sql优化器会自动调整,而order by后面的条件一定要按照顺序来

(3)排序的方向必须一致

这个就比较好理解了,要么都是倒序要么都是正序

(6)总结(核心)

这里简单将索引优化分为单索引优化和复合索引优化,前提知道explain性能分析器里面的所有字段信息,下面以mysql8.0及以上版本进行测试。

(1)单索引优化

还是以t_emp这张表进行测试,删除该表上所有除主键之外的所有索引,保证没有其他索引的干扰

以最简单的查询某一个字段为例进行单索引优化,为了避免全表扫描,可以对特定字段创建索引

之后在where后面的其他条件用and连接,只要包含该索引字段,条件的顺序可以任意排,只要包含该索引字段就行,mysql就会先去查改索引字段,之后再判断其他条件,这样查询type都是ref级别,基本是最优的

在上图的例子中如果使用范围查询(>= ,<=,!=)的话,就会有两种情况,一种是范围查找,一种是全表扫描,如果是 “<=” 的情况,查询的级别是range级别,即范围查询,如果是 ”>=“ 或者 ”!=“ 会直接进行全表扫描,即all级别

如果是or的情况,像上面的情况的话,用or连接条件,而且只有一个单值索引的情况下,索引会失效,进而扫描全表

但是如果or两边的字段都是索引字段的话,会对多个索引分别进行条件扫描,然后将它们各自的结果进行合并(intersect/union)。这种也是一种最优的优化,避免了全表扫描,达到index_merge级别()

排序的情况,如果where后面的条件是索引字段,而order by后面是非索引字段的话,mysql会先利用索引字段进行查找,但order by后面会进行外部排序,没有利用到B数据索引的排序功能,当然要解决外部排序,利用到B树索引的排序功能,就需要复合索引,在上面的order by排序优化部分有详细介绍这里简单提一


复合索引解决order by外部排序问题

除此之外还有两表和多表连接查询,这个具体可以参考上面的”索引优化入门案例“部分

(2)复合索引优化

复合索引优化在多条件查询的时候的用法,用and进行查询条件连接

复合索引idx_name_age_empno,3个字段的索引,这样的复合索引在使用的时候一定要以该复合索引的第一个索引字段开头,这里是name字段,在8.0版本只要where后面的查询条件包含有该复合索引的第一个索引字段即可,sql优化器会自己默认将复合索引字段作为第一个查询条件去查询索引

复合索引包含有or的情况下,基本就是全表扫描

使用范围查询的情况下,在上图的条件下,索引idx_name_age_empno,进行范围查询大于等于不等于的情况,会进行范围查找,即range级别,前提是查询条件有复合索引的第一个索引字段,即最佳左前缀法则。

还有关于排序order by情况,一种是复合索引的全部字段都用上,order by后面跟任意该索引字段都是最优的,如果排序的时候用到非索引字段会进行外部排序 Using filesort,这是需要优化的

排序的时候必须要么正序或者倒序,并且排序的顺序跟where后面的条件字段符合复合索引的顺序

(7)截取查询分析

(1)开启慢sql查询日志

mysql默认是没有开启慢sql查询日志的,通过慢sql查询日志可以准确定位到某一条慢sql,至于sql执行了多少秒以上算慢sql,这个可以自己设置,默认是10秒

详细参数设置

sql 描述 备注
SHOW VARIABLES LIKE '%slow_query_log% 查看慢sql查询日志详细信息 默认情况下 slow_query_log 的值为 OFF,表示慢查询日志是禁用的
set global slow_query_log=1; 开启慢查询日志 /
set long_query_time=1; 设置慢查询的阈值 单位秒
SHOW VARIABLES LIKE ‘long_query_time%’; 查看慢查询的阈值 单位秒

查看慢sql查询日志详细信息

开启并设置慢查询的阈值为1秒

上述操作在mysql服务重启后失效,如果需要永久生效,需要修改配置文件 my.cnf 中[mysqld]下配置

[mysqld]
slow_query_log=1
slow_query_log_file=/var/lib/mysql/atguigu-slow.log
long_query_time=3
log_output=FILE

下面可以测试一下,故意执行一条超过慢查询阈值(1秒)的sql

(2)千万级别数据插入

为了下面mysql索引优化,这里需要模拟大量的数据进行测试,先创建两个表

CREATE TABLE `dept` (
`id` INT ( 11 ) NOT NULL AUTO_INCREMENT,
`deptName` VARCHAR ( 30 ) DEFAULT NULL,
`address` VARCHAR ( 40 ) DEFAULT NULL,
ceo INT NULL,
PRIMARY KEY ( `id` )
) ENGINE = INNODB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;
CREATE TABLE `emp` (
`id` INT ( 11 ) NOT NULL AUTO_INCREMENT,
`empno` INT NOT NULL,
`name` VARCHAR ( 20 ) DEFAULT NULL,
`age` INT ( 3 ) DEFAULT NULL,
`deptId` INT ( 11 ) DEFAULT NULL,
PRIMARY KEY ( `id` ) ) ENGINE = INNODB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;

MySQL创建一个函数用于随机产生多少到多少的编号,以及两个存储过程,用于插入数据


-- 函数:用于随机产生多少到多少的编号
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$$-- 存储过程:插入emp表
-- 第一个参数:对应员工的部门编号从哪里开始递增
-- 第二个参数:插入多少条数据
DELIMITER $$
CREATE PROCEDURE insert_emp ( START INT, max_num INT ) BEGIN
DECLAREi INT DEFAULT 0;#set autocommit =0 把 autocommit 设置成 0SET autocommit = 0;
REPEATSET i = i + 1;INSERT INTO emp ( empno, NAME, age, deptid ) VALUES   ( ( START + i ), uuid(), rand_num ( 30, 50 ), rand_num ( 1, 10000 ) );UNTIL i = max_num
END REPEAT;
COMMIT;
END $$-- 存储过程:插入dept表
-- 参数:要插入的数据条数
DELIMITER $$
CREATE PROCEDURE `insert_dept`( max_num INT )
BEGIN
DECLARE i INT DEFAULT 0;
SET autocommit = 0;
REPEAT
SET i = i + 1;
INSERT INTO dept ( deptname,address,ceo ) VALUES (uuid(),uuid(),rand_num(1,500000));
UNTIL i = max_num
END REPEAT;
COMMIT;
END$$

部门表dept插入100万条模拟数据,花了100多秒

员工表emp插入100万条数据

为了方便后面的测试·,这里再向emp表插入500万条数据

(3)show profile

show profile是mysql提供的可以用来分析当前会话中的语句执行的资源消耗情况,用于sql的调优的测量。

开启show profile
show profile默认是关闭的,可以查看一下详细情况,命令:show variables like 'profiling';

开启命令:set profiling = on;

执行并查看sql详情
开启后随便执行sql语句,这里列出了部分sql,这些语句基本都是在0.3秒左右的sql

下面来一条执行时间5秒左右的慢sql

如果在执行group by语句的时候遇到下面这个错误

解决方案如下,敲完命令后重新进入就行了

set @@GLOBAL.sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';

现在就可以通过命令show profiles;查看sql的执行情况

诊断sql
命令:show profile 参数1,参数2,...... for query 查询id

参数列表如下


这里以诊断cpu和io开销为例 ,诊断第一条sql,命令:show profile cpu,block io for query 1;

上图展示了sql的整个生命周期,重点关注下面几个参数,出现了下面的任意参数就要注意优化了

上面在测试sql的时候,测了一条5秒的慢sql,可以诊断一下该sql

这里出现了creating tmp table,创建了临时表,后面还有remove tmp table,一般来讲出现了创建临时表的情况是需要进行优化的。

九、SQL优化

据我了解,优化主要包括sql语句的优化和索引的优化,大多情况下索引优化是重点,这里先简单记录一下sql语句的优化,索引优化有点难度,需要定位慢sql,分析sql效率等,比较复杂,等有时间再去深入学习一下索引的优化。

(1)sql语句优化

1、应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。

2、应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描
如:

select id from t where num is null

可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:

select id from t where num=0

3、where子句使用or的优化
通常使用 union all 或 union 的方式替换“or”会得到更好的效果。where子句中使用了or关键字,索引将被放弃使用。
如:

SELECT id FROM A WHERE num = 10 or num = 20;

建议改成

SELECT id FROM A WHERE num = 10 union all SELECT id FROM A WHERE num=20;

4、where子句中对字段进行表达式操作的优化
不要在where子句中的“=”左边进行函数、算数运算或其他表达式运算,否则系统将可能无法正确使用索引。
如:

select id from t where num/2=100

应改为:

select id from t where num=100*2

5、利用limit 1 、top 1 取得一行
对于确定要取的只有一行数据时,建议在sql语句后加上limit 1来终止[数据库索引]继续扫描整个表或索引。
如:

SELECT id FROM A LIKE 'abc%'

应改为

SELECT id FROM A LIKE 'abc%' limit 1

6、任何情况都不要用 select * from table ,用具体的字段列表替换"*",不要返回用不到的字段,避免全盘扫描!

7、批量插入优化
如:

INSERT into person(name,age) values('A',24)
INSERT into person(name,age) values('B',24)
INSERT into person(name,age) values('C',24)

应改为

INSERT into person(name,age) values('A',24),('B',24),('C',24)

8、like语句的优化
反例

SELECT id FROM A WHERE name like '%abc%'

由于abc前面用了“%”,因此该查询必然走全表查询,除非必要(模糊查询需要包含abc),否则不要在关键词前加%

正例

SELECT id FROM A WHERE name like 'abc%'

9、Inner join 和 left join、right join、子查询

第一:inner join内连接也叫等值连接,left/right join是外连接。

SELECT A.id,A.name,B.id,B.name FROM A LEFT JOIN B ON A.id =B.id;SELECT A.id,A.name,B.id,B.name FROM A RIGHT JOIN ON B A.id= B.id;SELECT A.id,A.name,B.id,B.name FROM A INNER JOIN ON A.id =B.id;

经过来之多方面的证实 inner join性能比较快,因为inner join是等值连接,或许返回的行数比较少。但是我们要记得有些语句隐形的用到了等值连接,如:

SELECT A.id,A.name,B.id,B.name FROM A,B WHERE A.id = B.id;

推荐:能用inner join连接尽量使用inner join连接

第二:子查询的性能又比外连接性能慢,尽量用外连接来替换子查询。
反例

mysql是先对外表A执行全表查询,然后根据uuid逐次执行子查询,如果外层表是一个很大的表,我们可以想象查询性能会表现比这个更加糟糕。

Select* from A where exists (select * from B where id>=3000 and A.uuid=B.uuid);

执行时间:2s左右

正例

Select* from A inner join B ON A.uuid=B.uuid where b.uuid>=3000;

这个语句执行测试不到一秒;
执行时间:1s不到

第三:使用JOIN时候,应该用小的结果驱动大的结果
left join 左边表结果尽量小,如果有条件应该放到左边先处理,right join同理反向。如:

反例

Select * from A left join B A.id=B.ref_id where  A.id>10

正例

select * from (select * from A wehre id >10) T1 left join B on T1.id=B.ref_id;

10、很多时候用 exists 代替 in 是一个好的选择
如:

select num from a where num in(select num from b)

用下面的语句替换:

select num from a where exists(select 1 from b where num=a.num)

11、字段的优化

第一:尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。

第二:尽可能的使用 varchar/nvarchar 代替 char/nchar ,因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。

菜鸟的mysql高级进阶以及mysql数据库优化相关推荐

  1. MySQL高级篇知识点——其它数据库日志

    目录 1.其他数据库日志 1.1.日志类型 1.2.日志的弊端 2.慢查询日志 (slow query log) 3.通用查询日志 (general query log) 3.1.问题场景 3.2.查 ...

  2. MySQL高级部分( 二: MySQL架构、引擎、索引)

    MySQL高级 二: MySQL架构.引擎.索引.事务 MySQL架构 MySQL 的完整架构图 各层介绍 连接层 服务层 存储引擎层 Pluggable Storage Engine 物理文件存储层 ...

  3. 【MySQL高级篇笔记-MySQL事务日志(下) 】

    此笔记为尚硅谷MySQL高级篇部分内容 目录 一.redo日志 1.为什么需要REDO日志 2.REDO日志的好处.特点 3.redo的组成 4.redo的整体流程 5.redo log的刷盘策略 6 ...

  4. MySQL高级 - 常用工具 - mysql

    mysql 该mysql不是指mysql服务,而是指mysql的客户端工具. 语法 : mysql [options] [database] 连接选项 参数 : -u, --user=name 指定用 ...

  5. MySQL高级-索引的使用及优化

    索引的使用 1 验证索引提升查询效率 2 索引的使用 2.1 准备环境 2.2 避免索引失效 1). 全值匹配 ,对索引中所有列都指定具体值. 2). 最左前缀法则(复合索引) 3). 范围查询右边的 ...

  6. MySql高级:explain及索引优化

    一.mysql安装linux版本rpm安装 查看是否安装了mysl rpm -qa | grep -i mysql 一定要下载指定的64位,因为电脑是64位的否则会安装失败 https://www.j ...

  7. mysql数据库查询要注意事项_三种mysql高级查询技巧_数据库_mysql函数_课课家

    大家都知道GROUP BY,但是大家知道GROUP BY后面可以带哪些函数吗?今天给大家介绍下GROUP BY后面可以带的函数. 1GROUP_CONCAT 在MySQL中,你可以获取表达式组合的连接 ...

  8. MYSQL高级进阶:运算符、逻辑运算符、条件语句等使用

    一.运算符.逻辑运算符使用参考这里:mysql大于等于_MySQL 运算符_一只特立独行的cherry的博客-CSDN博客 二.mysql中使用case when  else  end 方法:(注意: ...

  9. MySQL高级篇知识点——MySQL 事务日志

    目录 1.redo 日志 1.1.为什么需要 REDO 日志? 1.2.REDO 日志的好处与特点 1.3.redo 的组成 1.4.redo 的整体流程 1.5.redo log 的刷盘策略 1.6 ...

最新文章

  1. D. Colored Rectangles[思维dp]
  2. WSAGetLastError返回的可能错误代码
  3. numpy的基本使用3
  4. 过程重要,还是结果重要?
  5. “不服跑个分?” 是噱头还是实力?
  6. Oracle读取log日志,使用log miner 分析oracle日志
  7. Session和EL表达式实现登陆验证
  8. 利用监听器实现在线人数统计
  9. centos6 安装glibc-2.14.1
  10. fluidsim元件库下载_fluidsim手册.pdf
  11. Drools教程 —— 简介
  12. openwrt mesh网络设置
  13. 实现内网穿透,个人电脑秒变服务器
  14. 大佬们抖音带货流水都过亿 普通人有什么抖音变现的好方式
  15. python的requests爬取Uniprot中蛋白序列和N-糖基化位点
  16. ncbi查找目的基因序列_干货 | 如何查找目标基因序列?掌握这几招就够了!(NCBI篇)...
  17. 【计算机网络学习笔记】计算机网络
  18. iMeta | 扬州大学杜予州团队揭示同域内同食物的两种昆虫肠道微生物群落装配机制...
  19. 电脑鼠标右击刷新一直转圈
  20. vim自定义设置-配置文件

热门文章

  1. audioread函数用法(matlab)
  2. Visual Studio 2022如何安装和使用MSDN
  3. 并联四足机器人项目开源教程(三)--- 使用webots搭建仿真环境
  4. matlab 经典pid,经典-先进PID控制及其MATLAB仿真(刘金锟)-315页.pdf
  5. 【Matlab】简单的滑模控制程序及Simulink仿真
  6. CS61A Proj 4
  7. 国密:生成SM2秘钥、加解密及加验签
  8. 利用Python转换密文
  9. java 三元运算符
  10. 主定理(Master theorem)与Akra–Bazzi定理