MySQL高级索引及调优篇

文章目录

  • 六、索引的数据结构
    • 1.概念
    • 2.优缺点
      • a.优点
      • b.缺点
    • 3.InnoDB中的索引
      • a.不使用索引的情况
        • 1)在一个页中的查找
        • 2)在很多页中查找
      • b.设计索引
        • 1)一个简单的索引设计方案
        • 2) InnoDB中的索引方案
      • c.常见索引概念
        • 1)聚簇索引
        • 2)非聚簇索引(辅助索引、二级索引)
        • 3)联合索引
      • d.InnoDB的B+树索引的注意事项
        • 1)根页面位置永不动
        • 2)内节点中目录项记录的唯一性
        • 3)一个页面最少存储2 条记录
    • 4. MyISAM中的索引方案
      • a.MyISAM索引的原理
      • b.MyISAM 与 InnoDB对比
    • 5.索引的代价
    • 6.MySQL数据结构选择的合理性
      • a.全表查询
      • b.Hash查询
      • c.二叉搜索树
      • d.AVL树(平衡二叉树)
      • e.B-Tree
      • f.B+Tree
      • g.R树
      • 附录(时间复杂度)
  • 七、innoDB数据存储结构
    • 1.数据库的存储结构:页
      • a.磁盘与内存交互基本单位:页
      • b.页结构概述
      • c.页的大小
      • d.页的上层结构
    • 2.页的内部结构
      • 第一部分:File Header (文件头部)和 File Trailer (文件尾部)
        • 1)File Header(文件头部)(38字节)
        • 2)File Trailer(文件尾部)(8字节)
      • 第二部分:User Records (用户记录)、最大最小记录、Free Space (空闲空间)
        • 1)Free Space (空闲空间)
        • 2)User Records (用户记录)
        • 3)Infimum + Supremum(最小最大记录)
      • 第三部分:Page Directory (页目录)和 Page Header (页面头部)
        • 1)Page Directory(页目录)
        • 2)Page Header(页面头部)
      • 总结
    • 3.InnoDB行格式(或记录格式)
    • 4.区、段与碎片区
      • a.区
      • b.段
      • c.碎片区
      • d.区的分类
    • 5.表空间
      • a.独立表空间
      • b.系统表空间
    • 附录:数据页加载的三种方式
  • 八、索引的创建和设计原则
    • 1.索引的声明与使用
      • a.索引的分类
        • 1)普通索引
        • 2)唯一索引
        • 3)主键索引
        • 4)单列索引
        • 5)多列(组合、联合)索引
        • 6)全文索引
      • b.创建索引
        • 1)创建表的时候创建索引
        • 2)在已经存在的表上创建索引
      • c.删除索引
    • 2.MySQL8.0索引新特性
      • a.支持降序索引
      • b.隐藏索引
    • 3.索引的设计原则
      • a.数据准备
      • b.哪些情况适合创建索引
        • 1)字段的数值有唯一性的限制
        • 2)频繁作为 WHERE 查询条件的字段
        • 3)经常 GROUP BY 和 ORDER BY 的列
        • 4)UPDATE、DELETE 的 WHERE 条件列
        • 5)DISTINCT 字段需要创建索引
        • 6)多表 JOIN 连接操作时,创建索引注意事项
        • 7)使用列的类型小的创建索引
        • 8)使用字符串前缀创建索引
        • 9)区分度高(散列性高)的列适合作为索引
        • 10)使用最频繁的列放到联合索引的左侧
        • 11)在多个字段都要创建索引的情况下,联合索引优于单值索引
      • c.限制索引的数目
      • d.哪些情况不适合创建索引
  • 九、性能分析工具
    • 1.数据库服务器的优化步骤
    • 2.查看系统性能参数
    • 3.统计SQL的查询成本: last_query_cost
    • 4.定位执行慢的 SQL:慢查询日志
      • a.开启慢查询日志参数
        • 1.开启 slow_query_log
        • 2.修改 long_query_time 阈值
      • b.查看慢查询数目
      • c.案例
      • e.慢查询日志分析工具:mysqldumpslow
      • f.关闭慢查询日志
      • g.删除慢查询日志
    • 5.查看 SQL 执行成本:SHOW PROFILE
    • 6.分析查询语句:EXPLAIN
      • 1.概述
      • 2.基本语法
      • 3.数据准备
      • 4.EXPLAIN各列的作用
        • 1)table
        • 2)id
        • 3)select_type
        • 4)partitions (可略)
        • 5)type ☆
        • 6)possible_keys和key
        • 7)key_len ☆
        • 8)ref
        • 9)rows ☆
        • 10)filtered
        • 11)Extra ☆
    • 7.EXPLAIN的进一步使用
        • 1.传统格式
        • 2. JSON格式
        • 3. TREE格式
        • 4.可视化输出
    • 8.分析优化器执行计划:trace
    • 9.MySQL监控分析视图-sys schema
      • Sys schema视图摘要
  • 十、索引优化和查询优化
    • 1.关联查询优化
      • a.采用左外连接
      • b.采用内连接
      • c. 小结
    • 2.子查询优化
    • 3.排序优化
    • 4.GROUP优化
    • 5.优化分页查询
    • 6.覆盖索引
    • 7.字符串添加索引
    • 8.普通索引与唯一索引的区别
    • 9.其它查询优化策略
      • a.EXISTS 和 IN 的区分
      • b.COUNT(*)与COUNT(具体字段)效率
      • c.SELECT(*)
      • d.LIMIT 1
      • e.COMMIT
  • 十一、数据库设计规范
    • 1.优势
    • 2.范式
      • a.概念
      • b.常见范式
      • c.键和相关属性
      • d.第一范式
      • e.第二范式
      • f.第三范式
      • g.总结
    • 3.反范式化
      • 使用场景
        • 1.增加冗余字段的建议
        • 2.历史快照、历史数据的需要
    • 4. BCNF(巴斯范式)
    • 5.案例
      • 1. 迭代1次:考虑1NF
      • 2. 迭代2次:考虑2NF
      • 3. 迭代3次:考虑3NF
    • 6.ER模型
      • 1.ER模型要素
      • 2. 关系的类型
      • 3.ER模型转换为数据表
    • 7.数据表的设计原则
    • 8.数据库对象编写建议
      • 1.关于库
      • 2.关于表、列
      • 3.关于索引
      • 4. SQL编写
    • 9.PowerDesigner
  • 十二、数据库其他调优策略
    • 1.优化MySQL的参数

六、索引的数据结构

1.概念

索引的本质:是存储引擎用于快速找到数据记录的一种数据结构,MySQL中进行数据查找时,首先查看查询条件是否命中某条索引,符合则通过索引查找相关数据,如果不符合则需要全表扫描,即需要一条一条地查找记录,直到找到与条件符合的记录,建索引,目的就是为了减少磁盘I/O的次数,加快查询速率

索引是在存储引擎中实现的,因此每种存储引擎的索引不一定完全相同,并且每种存储引擎不一定支持所有索引类型。同时,存储引擎可以定义每个表的最大索引数最大索引长度。所有存储引擎支持每个表至少16个索引,总索引长度至少为256字节

2.优缺点

a.优点

  1. 类似大学图书馆建书目索引,提高数据检索的效率,降低数据库的IO成本,这也是创建索引最主要的原因

  2. 通过创建唯一索引,可以保证数据库表中每一行数据的唯一性

  3. 在实现数据的参考完整性方面,可以加速表和表之间的连接,对于有依赖关系的子表和父表联合查询时,可以提高查询速度

  4. 在使用分组和排序子句进行数据查询时,可以显著减少查询中分组和排序的时间,降低了CPU的消耗

b.缺点

  1. 创建索引和维护索引要耗费时间,并且随着数据量的增加,所耗费的时间也会增加

  2. 索引需要占磁盘空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,存储在磁盘上,如果有大量的索引,索引文件就可能比数据文件更快达到最大文件尺寸

  3. 虽然索引大大提高了查询速度,同时却会降低更新表的速度。当对表中的数据进行增加、删除和修改的时候,索引也要动态地维护,这样就降低了数据的维护速度

注意:

  • 索引可以提高查询的速度,但是会影响插入记录的速度。这种情况下,最好的办法是先删除表中的索引,然后插入数据,插入完成后再创建索引

3.InnoDB中的索引

a.不使用索引的情况

SELECT [列名列表] FROM 表名 WHERE 列名= xxx;

1)在一个页中的查找

假设目前表中的记录比较少,所有的记录都可以被存放到一个页中,在查找记录的时候可以根据搜索条件的不同分为两种情况:

  • 以主键为搜索条件

    可以在页目录中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可快速找到指定记录

  • 以其他列作为搜索条件

    在数据页中并没有对非主键列建立所谓的页目录,所以无法通过二分法快速定位相应的槽。这种情况下只能从最小记录开始依次遍历单链表中的每条记录,然后对比每条记录是不是符合搜索条件

2)在很多页中查找

在很多页中查找记录可以分为两个步骤:

  1. 定位到记录所在的页
  2. 从所在的页内中查找相应的记录

在没有索引的情况下,不论是根据主键列或者其他列的值进行查找,由于不能快速的定位到记录所在的页,所以只能从第一个页沿着双向链表一直往下找,在每一个页中根据在一个页中的查找方式去查找指定的记录。因为要遍历所有的数据页,所以这种方式非常耗时的

b.设计索引

建一个表:

CREATE TABLE index_demo(
c1 INT,
c2 INT,
c3 CHAR(1),
PRIMARY KEY(c1)
) ROW_FORMAT = Compact;

index_demo表中有2个INT类型的列,1个CHAR(1)类型的列,而且规定了c1列为主键,这个表使用Compact行格式来实际存储记录的。index_demo表的行格式示意图:

MySQL高级索引及调优篇_第1张图片

  • record_type :表示记录的类型,0 表示普通记录、2 表示最小记录、3 表示最大记录、1 暂时还没用过
  • next_record :表示下一条地址相对于本条记录的地址偏移量
  • 各个列的值:记录在 index_demo 表中的三个列

把一些记录放到页里的示意图就是:

MySQL高级索引及调优篇_第2张图片

1)一个简单的索引设计方案

下一个数据页中用户记录的主键值必须大于上一个数据页中用户记录的主键值

假设:每个数据结构最多能存放3条记录

INSERT INTO index_demo VALUES(1,4,'u'),(3,9,'d'),(5,3,'y');

这些记录按照主键值的大小串联成一个单向链表

MySQL高级索引及调优篇_第3张图片

假设3条记录插入了index_demo 表中编号为10的数据页中。此时再插入一条记录

INSERT INTO index_demo VALUES(4,4,'a');

因为数据结构最多只能放3条记录,所以不得不再分配一个新页

MySQL高级索引及调优篇_第4张图片

注意:新分配的数据页编号可能并不是连续的

页10中用户记录最大的主键值是5,而页28中有一条记录的主键值是4,因为5>4,所以不符合下一个数据页中用户记录的主键值必须大于上一个数据页中用户记录的主键值的要求,所以在插入主键值为4的记录的时候需要把主键值为5的记录移动到页28中,然后再把主键值为4的记录插入到页10中

MySQL高级索引及调优篇_第5张图片

这个过程表明了在对页中的记录进行增删改查操作时,必须通过一些诸如记录移动的操作来始终保证这个状态(下一个数据页中用户记录的主键值必须大于上一个页中用户记录的主键值)一直成立。这个过程称为页分裂

给所有的页建立一个目录项(索引)

由于数据页的编号可能是不连续的,所以在向 index_demo 表中插入许多条记录后,可能是这样的效果:

MySQL高级索引及调优篇_第6张图片

此时需要给它们做个目录,每个页对应一个目录项,每个目录项包括下边两个部分:

  1. key:页的用户记录中最小的主键值
  2. page_on:页号

MySQL高级索引及调优篇_第7张图片

以页28 为例,它对应目录项2 ,这个目录项中包含着该页的页号28 以及该页中用户记录的最小主键值5 。此时只需要把几个目录项在物理存储器上连续存储(比如:数组),就可以实现根据主键值快速查找某条记录的功能

比如:查找主键值为20 的记录,具体查找过程分两步:

  1. 先从目录项中根据二分法快速确定出主键值为20 的记录在目录项3 中(因为12 < 20 < 209 ),它对应的页是页9
  2. 再根据在一个页中查找记录的方式去页9中定位具体的记录

总结

因为每个数据结构允许存放的数据记录有限,当存储大量数据记录时,会导致产生很多数据页,然而在查询数据记录时,需要依次从第一页开始查找数据记录,这样会导致查找效率低下,此时索引的作用就是使用每个数据页中最小主键值为key,每个数据页的页号为page_no,能够快速的定位到所需记录所在的数据页,然后再使用一个数据页查找记录的方式查找记录,这样会使查找效率极大地提高

2) InnoDB中的索引方案

迭代1次:只有一个存放目录项纪录的页

InnoDB使用记录头信息里的record_type属性区分一条记录是普通的用户记录还是目录项记录,它的各自取值代表的意思如下:

  • 0:普通的用户记录
  • 1:目录项记录
  • 2:最小记录
  • 3:最大记录

MySQL高级索引及调优篇_第8张图片

从图中可以看出来,新分配了一个编号为30的页来专门存储目录项记录

目录项记录普通的用户记录不同点

  • 目录项记录的 record_type 值是1,而普通用户记录的 record_type 值是0
  • 目录项记录只有主键值和页的编号两个列,而普通的用户记录的列是用户自己定义的,可能包含很多列,另外还有InnoDB自己添加的隐藏列

目录项记录普通的用户记录相同点

  • 两者用的是一样的数据页,都会为主键值生成Page Directory (页目录)

总结:在存放目录项记录的页中使用二分法匹配所要查找的记录的主键,从而得到所要查找记录所在的存放普通的用户记录的页号,然后再在存放普通的用户记录的页中二分法查找对应的主键值,最终得到想要的结果

以查找主键为20 的记录为例,大致分为两步:

  1. 先到存储目录项记录的页,也就是页30中通过二分法快速定位到对应目录项,因为12 < 20 < 209 ,所以定位到对应的记录所在的页就是页9
  2. 再到存储用户记录的页9中根据二分法快速定位到主键值为20 的用户记录

迭代2次:有多个存放目录项纪录的页

MySQL高级索引及调优篇_第9张图片

从图中可以看出,插入了一条主键值为320的用户记录之后需要两个新的数据页:

  • 为存储该用户记录而新生成了页31
  • 因为原先存储目录项记录的页30的容量已满(假设只能存储4条目录项记录),所以需要一个新的页32来存放页31对应的目录项

总结:存在多个存放目录项记录的页时,相互也保持着一种状态(当前页的最小主键值一定大于上一页的最大主键值),所以只能按顺序,从第一个存放目录项记录的页开始依次查找,得到所需主键所在的存放目录项记录的页,然后在存放目录项记录的页中二分法查找主键对应的存放普通用户记录的页号,得到存放普通用户记录的页号,在对应页中二分法查找对应的主键值,最终得到所需的记录

以查找主键值为20 的记录为例,大致分为三步:

  1. 确定目录项记录页,现在的存储目录项记录的页有两个,即页30 和页32 ,又因为页30表示的目录项的主键值的范围是[1,320),页32表示的目录项的主键值不小于320,所以主键值为20 的记录对应的目录项记录在页30中。
  2. 通过目录项记录页确定用户记录真实所在的页
  3. 在真实存储用户记录的页中定位到具体的记录

迭代3次:存放目录项记录的页的目录页

如果表中的数据非常多则会产生很多存储目录项记录的页,为这些存储目录项记录的页再生成一个更高级的目录

MySQL高级索引及调优篇_第10张图片

如图,生成了一个存储更高级目录项的页33 ,这个页中的两条记录分别代表页30和页32,如果用户记录的主键值在[1,320)之间,则到页30中查找更详细的目录项记录,如果主键值不小于320 的话,就到页32中查找更详细的目录项记录

我们可以用下边这个图来描述它:

MySQL高级索引及调优篇_第11张图片

这个数据结构,它的名称是 B+树

B+Tree

一个B+树的节点其实可以分成好多层,规定最下边的那层,也就是存放用户记录的那层为第0层,之后依次往上加,假设所有存放用户记录的叶子节点代表的数据页可以存放100条用户记录,所有存放目录项记录的内节点代表的数据页可以存放1000条目录项记录,那么:

  • 如果B+树只有1层,也就是只有1个用于存放用户记录的节点,最多能存放100 条记录
  • 如果B+树有x层,最多能存放(x-1)×1000×100=10,0000x条记录

一般情况下,用到的B+树都不会超过4层,通过主键值去查找某条记录最多只需要做4个页面内的查找(查找3个目录项页和一个用户记录页),又因为在每个页面内有Page Directory(页目录),所以在页面内也可以通过二分法实现快速定位记录

c.常见索引概念

索引按照物理实现方式,索引可以分为2种:聚簇(聚集)和非聚簇(非聚集)索引(也称为二级索引或者辅助索引)

1)聚簇索引

聚簇索引将索引和数据保存在同一个B+树中

聚簇:表示当前数据行和相邻的键值聚簇的存储在一起

特点:

  • 使用记录主键值的大小进行记录和页的排序,这包括三个方面的含义:

    • 页内的记录是按照主键的大小顺序排成一个单向链表
    • 各个存放用户记录的页之间也是根据页中用户记录的主键大小顺序排成一个双向链表
    • 存放目录项记录的页分为不同的层次,在同一层次中的页也是根据页中目录项记录的主键大小顺序排成一个双向链表
  • B+树的叶子节点存储的是完整的用户记录(存储了所有列的值(包括隐藏列))

把具有这两种特性的B+树称为聚簇索引,所有完整的用户记录都存放在这个聚簇索引叶子节点处InnDB存储引擎自动的创建聚簇索引

优点:

  • 数据访问更快
  • 聚簇索引对于主键的排序查找范围查找速度非常快
  • 节省了大量的io操作

缺点:

  • 插入速度严重依赖于插入顺序,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。因此,对于InnoDB表,一般都会定义一个自增的ID列为主键
  • 更新主键的代价很高,因为将会导致被更新的行移动。因此,对于InnoDB表,一般定义主键为不可更新
  • 二级索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据

2)非聚簇索引(辅助索引、二级索引)

以别的列作为搜索条件时,可以多建几颗B+树,不同的B+树中的数据采用不同的排列规则,当以别的列作为搜索条件时,建立的B+树中的叶子节点存放的是别的列的值以及所对应的页号,并不会直接存放完整的用户记录

概念:回表

  • 根据以c2列大小排序的B+树只能确定要查找记录的主键值,如果想根据c2列的值查找到完整的用户记录,仍然需要到聚簇索引中再查一遍,这个过程称为回表。也就是根据c2列的值查询一条完整的用户记录需要使用到2棵B+树

概念:叶子节点存放

  • 如果把完整的用户记录放到叶子结点是可以不用回表。但是太占地方了,相当于每建立一课B+树都需要把所有的用户记录再都拷贝一遍,太浪费存储空间

  • 因为这种按照非主键列建立的B+树需要一次回表操作才可以定位到完整的用户记录,所以这种B+树也被称为二级索引,或者辅助索引

非聚簇索引的存在不影响数据在聚簇索引中的组织,所以一张表可以有多个非聚簇索引

MySQL高级索引及调优篇_第12张图片

小结

聚簇索引与非聚簇索引的原理不同,在使用上也有一些区别:

  1. 聚簇索引的叶子节点存储的完整的用户数据,非聚簇索引的叶子节点存储的是数据位置。非聚簇索引不会影响数据表的物理存储顺序
  2. 一个表只能有一个聚簇索引,因为只能有一种排序存储的方式,但可以有多个非聚簇索引,即多个索引目录提供数据检索
  3. 使用聚簇索引的时候,数据的查询效率高,但如果对数据进行插入,删除,更新等操作效率会比非聚簇索引低

3)联合索引

可以同时以多个列的大小作为排序规则,也就是同时为多个列建立索引,比方说想让B+树按照 c2和c3列的大小进行排序,这个包含两层含义:

  • 先把各个记录和页按照c2列进行排序
  • 在记录的c2列相同的情况下,采用c3列进行排序

为c2和c3建立的索引的示意图如下:

MySQL高级索引及调优篇_第13张图片

需要注意:

  • 每条目录项都有c2、c3、页号这三个部分组成,各条记录先按照c2列的值进行排序,如果记录的c2列相同,则按照c3列的值进行排序
  • B+树叶子节点处的用户记录由c2、c3和主键c1列组成

注意一点,以c2和c3列的大小为排序规则建立的B+树称为联合索引,本质上也是一个二级索引。它的意思与分别为c2和c3列分别建立索引的表述是不同的,不同点如下:

  • 建立联合索引只会建立如上图一样的1棵B+树
  • 为c2和c3列分别建立索引会分别以c2和c3列的大小为排序规则建立2棵B+树

d.InnoDB的B+树索引的注意事项

1)根页面位置永不动

实际上B+树的形成过程是这样的:

  • 每当为某个表创建一个B+树索引(聚簇索引不是人为创建的,默认就有)的时候,都会为这个索引创建一个根结点页面。最开始表中没有数据的时候,每个B+树索引对应的根结点中即没有用户记录,也没有目录项记录
  • 向表中插入用户记录时,先把用户记录存储到这个根节点
  • 当根节点中的可用空间用完后继续插入记录,此时会将根节点中的所有记录复制到一个新分配的页,然后对这个新页进行页分裂的操作,得到另一个新页。这时新插入的记录根据键值(也就是聚簇索引中的主键值,二级索引中对应的索引列的值)的大小就会被分配到两个新页中的一页中,而根节点便升级为存储目录项记录的页

这个过程特别注意的是:一个B+树索引的根节点自诞生之日起,便不会再移动。这样只要对某个表建立一个索引,那么它的根节点的页号便会被记录到某个地方。当InnoDB存储引擎需要用到这个索引的时候,都会从哪个固定的地方取出根节点的页号,从而来访问这个索引

2)内节点中目录项记录的唯一性

B+树索引的内节点中目录项记录的内容是索引列+页号的搭配,假设这个表中的数据是这样的:

MySQL高级索引及调优篇_第14张图片

如果二级索引中目录项记录的内容只是索引列+页号的搭配的话,那么为c2列建立索引后的B+树应该长这样:

MySQL高级索引及调优篇_第15张图片

如果想新插入一行记录,其中c1、c2、c3的值分别是: 9、1、c,那么在修改这个为 c2 列建立的二级索引对应的 B+树时:由于页3中存储的目录项记录是由c2列+页号的值构成的,页3中的两条目录项记录对应的 c2 列的值都是1,而新插入的这条记录的 c2 列的值也是1,为了让新插入记录找到自己在那个页面,需要保证在B+树的同一层页节点的目录项记录除页号这个字段以外是唯一的。所以对于二级索引的内节点的目录项记录的内容实际上是由三个部分构成的:

  • 索引列的值
  • 主键值
  • 页号

即把主键值也添加到二级索引内节点中的目录项记录,这样就能保住 B+树每一层节点中各条目录项记录除页号这个字段外是唯一的,所以为c2建立二级索引后的示意图实际上应该是这样子的:

MySQL高级索引及调优篇_第16张图片

当插入记录(9,1,‘c’)时,由于页3中存储的目录项记录是由c2列+主键+页号的值构成的,可以把新纪录的c2列的值和页3中各目录项记录的c2列的值作比较,如果c2列的值相同的话,可以接着比较主键值,因为B+树同一层中不同目录项记录的c2列+主键的值肯定是不一样的,所以最后肯定能定位唯一的一条目录项记录,在本例中最后确定新纪录应该被插入到页5中

3)一个页面最少存储2 条记录

InnoDB 的一个数据页至少可以存放两条记录

4. MyISAM中的索引方案

B树索引使用存储引擎如表所示:

索引/存储引擎 MyISAM InnoDB Memory
B-Tree索引 支持 支持 支持

即使多个存储引擎支持同一种类型的索引,但是他们的实现原理也是不同的。Innodb和MyISAM默认的索引是Btree索引;而Memory默认的索引是Hash索引

MyISAM引擎使用 B+Tree 作为索引结构,叶子节点的data域存放的是数据记录的地址

a.MyISAM索引的原理

InnoDB中索引为数据,即在聚簇索引中b+树的叶子结点中包含了所有完整的用户记录,但MyISAM的索引方案是将索引和数据分开存储:

  1. 表中的记录按照记录的插入顺序单独存储在一个文件中,称为数据文件,这个文件不划分若干个数据页全部数据都存放在一个文件中,由于插入时没有刻意按照主键大小顺序,所有不能使用二分法查找数据
  2. 使用MyISAM存储引擎的表会把索引信息存储到一个称为索引文件的文件中,MyISAM会单独为表的主键创建一个索引,在索引的叶子结点中存储的是主键值+数据记录地址的组合,而不是完整的用户记录

假设表中一共有三列,Col1为主键,图为一个MyISAM表的主索引(primary key),可以看出MyISAM的索引文件仅保存数据记录的地址

MySQL高级索引及调优篇_第17张图片

MyISAM中,主键索引和二级索引在结构上没有区别,唯一的区别为主键索引要求的key唯一,二级索引要求的key可以重复,图为在Col2建立的一个二级索引

MySQL高级索引及调优篇_第18张图片

MyISAM中索引的检索算法:首先按照B+树搜索算法搜索索引,如果指定的key存在,则取出其data域的值,然后以data域的值为地址,读取响应地址存放的数据记录

b.MyISAM 与 InnoDB对比

MyISAM的索引方式都是“非聚簇”的,与InnoDB包含1个聚簇索引是不同的

两种引擎中索引的区别:

  1. 在InnoDB存储引擎中,只需要根据主键值对聚簇索引进行一次查找就能找到对应的记录,在MyISAM中需要进行一次回表操作,意味着MyISAM中建立的索引相当于全部都是二级索引

  2. InnoDB的数据文件本身就是索引文件,而MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址

  3. InnoDB的非聚簇索引data域存储相应记录主键的值,而MyISAM索引记录的是地址

  4. MyISAM是拿着地址偏移量直接到文件中取数据,所以回表操作是十分快速的,InnoDB是通过获取主键之后再去聚簇索引里找记录,速度没有MyISAM快

  5. InnoDB要求表必须有主键( MyISAM可以没有)。如果没有显式指定,则MySQL系统会自动选择一个可以非空且唯一标识数据记录的列作为主键。如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键

小结:

MySQL高级索引及调优篇_第19张图片

5.索引的代价

索引在空间和时间上都会有消耗:

  • 空间上的代价

    每建立一个索引都要为它建立一棵B+树,每一棵B+树的每一个节点都是一个数据页,一个页默认会占用16KB 的存储空间,一棵很大的B+树由许多数据页组成,那就是很大的一片存储空间。

  • 时间上的代价

    每次对表中的数据进行增、删、改操作时,都需要去修改各个B+树索引。而且B+树每层节点都是按照索引列的值从小到大的顺序排序而组成了双向链表。不论是叶子节点中的记录,还是内节点中的记录都是按照索引列的值从小到大的顺序而形成了一个单向链表。而增、删、改操作可能会对节点和记录的排序造成破坏,所以存储引擎需要额外的时间进行一些记录移位,页面分裂、页面回收等操作来维护好节点和记录的排序

一个表上索引建的越多,就会占用越多的存储空间,在增删改记录的时候性能就越差。为了能建立又好又少的索引,我们得学学这些索引在哪些条件下起作用的。

6.MySQL数据结构选择的合理性

磁盘I/O操作次数对索引的使用效率至关重要

a.全表查询

按数组的方式,从数组第一个位置开始依次查找,这样查找,导致磁盘I/O操作次数很多,即索引效率很低

b.Hash查询

Hash函数可以帮助大幅度提升索引的效率,Hash算法是通过某种确定的算法将输入转变为输出

例如:

如果想要验证两个文件是否相同,只需要把一个文件通过Hash函数计算得到的值与另一个文件通过Hash函数计算得到的值做对比,如果值相同,则是同一个文件,否则不是同一个文件

效率方面来说,Hash检索基本只需要一次就可以,B+树检索需要从根结点依次向下查找,多次访问节点才可以找到数据,所以Hash比B+树更快

Hash函数可以根据关键字K,通过hash函数h(k)求出关键字k所在哈希表中槽的位置

当两个不同的关键字映射到相同的位置时,在数据库中一般使用链接法解决,将散列到同一槽位的元素放到一个链表中

MySQL高级索引及调优篇_第20张图片

MySQL高级索引及调优篇_第21张图片

数组方式的全表查询和hash方式的全表查询的效率比较

@Test
    //数组方式检索效率
    public void testSQLplus1() {
        int[] arr = new int[100000];
        for(int i = 0;i < arr.length;i++){
            arr[i]= i +1;
        }
        long start = System.currentTimeMillis();
        for(int j = 1; j<=100000;j++){
            int temp = j;
            for(int i = 0;i < arr.length;i++){
                if(temp == arr[i]){
                    break;
                }
            }
        }
        long end = System.currentTimeMillis();
        System.out.println("time:"+(end - start)); //time:2359
    }
    @Test
    //hash方式检索效率
    public void testSQLplus2(){
        HashSet<Integer> set = new HashSet<>(100000);
        for(int i = 0;i < 100000;i++){
            set.add(i +1);
        }
        long start = System.currentTimeMillis();
        for(int j = 1; j<=100000;j++){
            int temp = j;
            boolean contains = set.contains(temp);
        }
        long end = System.currentTimeMillis();
        System.out.println("time:"+(end - start)); //time:8
    }

Hash索引适用存储引擎如表所示:

索引/存储引擎 MyISAM InnoDB Memory
HASH索引 不支持 不支持 支持

Hash索引的适用性:

  1. Hash索引在一些场景下效率很高,例如键值型数据库(Redis数据库)的核心即为Hash表
  2. InnoDB不支持Hash索引,但提供了自适应Hash索引,如果一个数据经常被访问,当满足一定条件时,就会将这个数据页的地址放到Hash表中,下次查询时,可以直接找到这个页面所在的位置

采用自适应 Hash 索引目的是方便根据 SQL 的查询条件加速定位到叶子节点,特别是当 B+树比较深的时候,通过自适应 Hash 索引可以明显提高数据的检索效率。

MySQL高级索引及调优篇_第22张图片

通过 innodb_adaptive_hash_index 变量来查看是否开启了自适应 Hash

show variables like '%adaptive_hash_index';

c.二叉搜索树

1.二叉搜索树的特点

  • 一个节点只能有两个子节点,也就是一个节点度不能超过2
  • 左子节点< 本节点; 右子节点>= 本节点

2.查找规则

  1. 如果key大于根结点,则在右子树中查找
  2. 如果key小于根结点,则在左子树中查找
  3. 如果key等于根结点,则找到了所需的节点,返回根节点即可

为了提高查询效率,就需要减少磁盘IO数。为了减少磁盘IO的次数,就需要尽量降低树的高度,需要树的每层的分叉越多越好

d.AVL树(平衡二叉树)

在二叉搜索树的基础上增加了约束,左右两个子树的高度差的绝对值不能超过1,并且左右两个子树都是AVL(平衡二叉树)

平衡二叉树包括:平衡二叉搜索树、红黑树、数堆、伸展树,搜索的时间复杂度O(log2n)

因为每访问一次节点就需要进行一次磁盘 I/O 操作,虽然平衡二叉树的效率高,但是树的深度也同样高,这就意味着磁盘 I/O 操作次数多,会影响整体数据查询的效率,为了解决这个问题,可以将二叉树变成M叉树,这样当数据量 N 大的时候,以及树的分叉树 M 大的时候,M叉树的高度会远小于二叉树的高度(M > 2)

e.B-Tree

B 树的英文是 Balance Tree,也就是多路平衡查找树。简写为 B-Tree。它的高度远小于平衡二叉树的高度

B 树称为多路平衡查找树每个节点最多包含M个子节点M称为B树的阶,每个节点包括了关键字和子节点的指针,子节点指针数量=(关键字数量+1)

一个 M 阶的 B 树(M>2)有以下的特性:

  1. 根节点的儿子数的范围是[2,M]
  2. 每个中间节点包含 k-1 个关键字和 k 个孩子,孩子的数量= 关键字的数量+1,k 的取值范围为[ceil(M/2), M]
  3. 叶子节点包括 k-1 个关键字(叶子节点没有孩子),k 的取值范围为[ceil(M/2), M]
  4. 假设中间节点节点的关键字为:Key[1], Key[2],…, Key[k-1],且关键字按照升序排序,即 Key[i]
  5. 所有叶子节点位于同一层

用 B 树进行查找,步骤:每次与根结点关键字,如果比根结点的关键字小,则向左遍历,如果比根结点的关键字大,则向右遍历,如果和根结点关键字相同,则找到需要的关键字返回此根结点即可

注意:

  • B树在插入和删除节点时,会导致树不平衡,B树会自动调整节点位置来保持树的自平衡
  • 关键字存放在叶子节点和非叶子结点中
  • 搜索性能等价于在关键字全集内做二分查找
  • 叶子节点和非叶子结点存放的是关键字、子节点指针、表记录中除了主键以外的数据

f.B+Tree

B+树也是一种多路搜索树,基于B树做出了改进,主流的DBMS都支持B+树的索引方式,相比于B-树,B+树适合文件索引系统,MySQL中采用的是B+树

B+树和 B 树的差异:

  1. B+树中孩子数量= 关键字数,而 B 树中,孩子数量= 关键字数+1
  2. B+树中非叶子节点的关键字也会同时存在在子节点中,并且是在子节点中所有关键字的最大(或最小)
  3. B+树中非叶子节点仅用于索引,不保存数据记录,跟记录有关的信息都放在叶子节点中。B 树中,非叶子节点既保存索引,也保存数据记录
  4. B+树中所有关键字都在叶子节点出现,叶子节点构成一个有序链表,而且叶子节点本身按照关键字的大小从小到大顺序链接

B+树与Hash的差异:

  1. Hash索引不能进行范围查询,B+树可以,因为Hash索引指向的数据是无序的,但B+树的叶子结点是一个有序的链表
  2. Hash索引不支持联合索引的最左侧原则(即联合索引的部分索引无法使用),B+树可以,在联合索引中,Hash索引在计算Hash值时是将索引键合并后再一起计算Hash值,不会针对单个索引单独计算Hash值
  3. Hash索引不支持ORDER BY排序,因为Hash索引指向的数据是无序的,因此无法起到排序优化的作用,B+树索引数据是有序的,可以支持ORDER BY排序
  4. Hash索引不支持模糊查询,B+树支持模糊查询
  5. InnoDB不支持Hash索引

B+树的优势

  1. B+树查询效率更稳定,B+树中只有访问到叶子节点时才能得到数据,而B树在叶子节点和非叶子结点都存放有数据
  2. B+树查询效率更高,B+树比B树的阶数更大,深度更小,所需的磁盘I/O操作也小,同样的磁盘页大小可以存储更多的关键字
  3. B+树的高度一般只有24层,所以在查找某一键值时,只需要13次磁盘I/O操作

g.R树

R-Tree在MySQL很少使用,仅支持 geometry数据类型

附录(时间复杂度)

数据结构 查找 插入 删除 遍历
数组 O(N) O(1) O(N)
有序数组 O(logN) O(N) O(N) O(N)
链表 O(N) O(1) O(N)
有序链表 O(N) O(N) O(N) O(N)
二叉树(一般情况) O(logN) O(logN) O(logN) O(N)
二叉树(最坏情况) O(N) O(N) O(N) O(N)
平衡二叉树(一般情况和最坏情况) O(logN) O(logN) O(logN) O(N)
哈希表 O(1) O() O(1)

七、innoDB数据存储结构

1.数据库的存储结构:页

索引信息以及数据记录都是保存在文件(页结构)中,不同的的存储引擎存放的格式不同,InnoDB是MySQL默认的存储引擎

a.磁盘与内存交互基本单位:页

InnoDB将数据划分为若干页,每一页默认大小为16KB

磁盘和内存之间交互的基本单位为,每次至少在磁盘中读取16KB的内容到内存中,每次至少把内存中16KB的内容刷新到磁盘中,即是磁盘I/O操作的最小单位,记录的存储是以行来存储的,每一页可以存储多个行记录

MySQL高级索引及调优篇_第23张图片

b.页结构概述

各个页在物理结构上不相连,是通过双向链表进行相连的,每一页中的记录按照主键值从小到大说的顺序组成一个单链表,每一页都会为存储的记录生成一个页目录,在通过主键查找记录时可以在页目录中使用二分法快速定位到响应的槽

c.页的大小

show variables like '%innodb_page_size%'

MySQL中默认大小为16KB

d.页的上层结构

MySQL高级索引及调优篇_第24张图片

表空间

  • 是一个逻辑容器,表空间存储的对象是段,在一个表空间中可以有一个或多个段,但一个段只能属于一个表空间,数据库由多个表空间构成,表空间分为系统表空间用户表空间撤销表空间临时表空间

  • 由一个或多个区构成,区在文件系统中是一个连续分配的空间(InnoDB中是连续的64个页),段中的页与页中间不要求相邻,段是数据库的分配单位,不同类型的数据库对象以不同的段形式存在,例如在创建表时就会创建一个表段,创建一个索引时就会创建一个索引段

  • 在InnoDB中一个区会分配64个连续的页,一个区的默认大小为64*16KB=1MB

2.页的内部结构

页按类型划分,常见的有数据页(保存B+树节点)系统表Undo 页事物数据页

数据页的16KB大小的存储空间被划分为七个部分,分别是文件头(File Header)、页头(Page Header)、最大最小记录(Infimum + supremum)、用户记录(User Records)、空闲空间(Free Space)、页目录(Page Directory)和文件尾(File Tailer)

MySQL高级索引及调优篇_第25张图片

第一部分:File Header (文件头部)和 File Trailer (文件尾部)

1)File Header(文件头部)(38字节)

描述各种页的通用信息。(比如页的编号、其上一页、下一页是谁等)

2)File Trailer(文件尾部)(8字节)

  • 前4个字节代表页的校验和:这个部分是和File Header中的校验和相对应的
  • 后4个字节代表页面被最后修改时对应的日志序列位置(LSN):这个部分也是为了校验页的完整性的,如果首部和尾部的LSN值校验不成功的话,就说明同步过程出现了问题

第二部分:User Records (用户记录)、最大最小记录、Free Space (空闲空间)

1)Free Space (空闲空间)

存储的记录会按照指定的行格式存储到User Records部分,在一开始生成页的时候,并没有User Records这个部分,每当插入一条记录,都会从Free Space部分,也就是尚未使用的存储空间中申请一个记录大小的空间划分到User Records部分,当Free Space部分的空间全部被User Records部分替代掉之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新的页

2)User Records (用户记录)

User Records中的这些记录按照指定的行格式一条一条摆在User Records部分,相互之间形成单链表

3)Infimum + Supremum(最小最大记录)

对于一条完整的记录来说,比较记录的大小就是比较主键的大小

第三部分:Page Directory (页目录)和 Page Header (页面头部)

1)Page Directory(页目录)

在页中,记录是以单向链表的形式进行存储的。单向链表的特点就是插入、删除非常方便,但是检索效率不高,最差的情况下需要遍历链表上的所有节点才能完成检索。因此在页结构中专门设计了页目录这个模块,专门给记录做一个目录,通过二分查找法的方式进行检索,提升效率

页目录,二分法查找

  1. 将所有的记录分成几个组,这些记录包括最小记录和最大记录,但不包括标记为“已删除”的记录
  2. 第 1 组,也就是最小记录所在的分组只有 1 个记录
  3. 最后一组,就是最大记录所在的分组,会有 1-8 条记录
  4. 其余的组记录数量在 4-8 条之间
  5. 这样做的好处是,除了第 1 组(最小记录所在组)以外,其余组的记录数会尽量平分
  6. 在每个组中最后一条记录的头信息中会存储该组一共有多少条记录,作为 n_owned 字段
  7. 页目录用来存储每组最后一条记录的地址偏移量,这些地址偏移量会按照先后顺序存储起来,每组的地址偏移量也被称之为槽(slot),每个槽相当于指针指向了不同组的最后一个记录

2)Page Header(页面头部)

为了能得到一个数据页中存储的记录的状态信息,比如本页中已经存储了多少条记录,第一条记录的地址是什么,页目录中存储了多少个槽等等,特意在页中定义了一个叫Page Header的部分,这个部分占用固定的56个字节,专门存储各种状态信息

总结

一颗B+树按照字节类型可以分为两部分:

  1. 叶子节点,B+树最底层的节点,节点的高度为0,存储行记录
  2. 非叶子节点,节点的高度大于0,存储索引键和页面指针,并不存储行记录本身

3.InnoDB行格式(或记录格式)

4.区、段与碎片区

a.区

B+树中每一层中的页都会形成一个双向链表,每个页之间的物理位置不确定,所以有可能进行索引是I/O操作很慢,引入,即在物理位置上连续的64个页,当表中数据量大时,为某个索引分配空间的时候不是按照页为单位分配,而是按照区位单位分配,当不足64个页时也分配一个区,虽然会造成空间的浪费,但会大幅度减少I/O操作

b.段

在B+树中为了区分叶子节点和非叶子结点,InnoDB一般会将叶子节点和非叶子结点分别设立独立的区,存放叶子节点的区的集合算是一个段,存放非叶子结点的区的集合算是一个段,即一个索引会生成两个段,叶子节点段和非叶子结点段

常见的段:数据段(B+树的叶子节点)、索引段(B+树的非叶子节点)、回滚段

c.碎片区

碎片区直属于表空间,不属于任何一个段,一般刚开始向表中插入数据时,段是从某个碎片区以单个页面为单位分配存储空间的,当某个段已经占用了32个碎片区页面之后,就会申请完整的区为单位来分配存储空间

d.区的分类

  • 空闲的区(FREE): 现在还没有用到这个区中的任何页面
  • 有剩余空间的碎片区(FREE_FRAG):表示碎片区中还有可用的页面
  • 没有剩余空间的碎片区(FULL_FRAG):表示碎片区中的所有页面都被使用,没有空闲页面
  • 附属于某个段的区(FSEG):每一个索引都可以分为叶子节点段和非叶子节点段

处于FREE、FREE_FRAG 以及 FULL_FRAG 这三种状态的区都是独立的,直属于表空间。处于 FSEG 状态的区附属于某个段

5.表空间

是一个逻辑容器,表空间存储的对象是段,在一个表空间中可以有一个或多个段,但一个段只能属于一个表空间,数据库由多个表空间构成,表空间分为系统表空间用户表空间撤销表空间临时表空间

a.独立表空间

独立表空间,即每张表有一个独立的表空间,数据和索引信息都会保存在自己的表空间中。独立的表空间(即:单表)可以在不同的数据库之间进行迁移

独立表空间结构

  • 独立表空间由段、区、页组成

真实表空间对应的文件大小

  • 随着表中数据的增多,表空间对应的文件也逐渐增大

b.系统表空间

整个MySQL进程只有一个系统表空间,在系统表空间中会额外记录一些有关整个系统信息的页面,这部分在独立表空间中没有

附录:数据页加载的三种方式

InnoDB从磁盘中读取数据最小单位是数据页

MySQL存放的数据,逻辑概念上称为表,在磁盘等物理层面而言是按数据页形式进行存放的,当其加载到MySQL中称之缓存页

如果缓冲池没有该页数据,缓冲池读取数据的方式,每种方式的读取速率是不同的:

1.内存读取

如果数据存在于内存中,则缓冲池直接在内存中读取数据

2.随机读取

如果数据没有存在于内存中,则需要在磁盘上对该页进行查找

3.顺序读取

针对于批量读取,如果数据没有存在于内存中,则需要在磁盘上批量顺序读取数据

八、索引的创建和设计原则

1.索引的声明与使用

a.索引的分类

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

1)普通索引

  • 创建普通索引时,不附加任何限制条件,只用于提高查询效率,可以创建在任何数据类型中,其值是否唯一和非空,由字段本身的完整性约束条件决定

例如,在表student的字段name上建立一个普通索引,查询记录时就可以根据该索引进行查询

2)唯一索引

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

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

3)主键索引

  • 主键索引是一种特殊的唯一索引,在唯一索引的基础上增加了不为空的条件约束,即NOT NULL+UNIQUE,一张数据表中只能有一个主键索引

例如,在表student的字段id中创建主键索引,那么id的值就必须唯一且不为空,通过主键索引可以更加快速的确定某条记录

4)单列索引

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

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

  • 多列索引是在表的多个字段组合上创建一个索引,该索引指向创建时对应的多个字段,可以通过这几个字段进行查询,但只有查询条件中使用了这些字段中的第一个字段时才会被使用

例如,在表student的字段id、name和gender上建立一个idx_id_name_gender索引,只有在查询条件中使用了字段id时该索引才会被使用,使用多列索引是遵循最左前缀集合

6)全文索引

  • 使用FULLTEXT设置全文索引,在定义索引的列上支持值的全文查找,运行在这些索引列中插入重复值和空值,全文索引只能创建在CHARVARCHARTEXT类型的字段上,当查询数据量较大的字符串类型的字段时,使用全文索引可以提高查询速度

例如,在表student的字段information是TEXT类型,该字段包含了很多文字信息,在字段information上建立全文索引后,可以提高查询速度

b.创建索引

  1. 在创建表的定义语句 CREATE TABLE 中指定索引列
  2. 使用 ALTER TABLE 语句在存在的表上创建索引
  3. 使用 CREATE INDEX 语句在已存在的表上添加索引

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

在CREATE TABLE创建表时,可以定义列的数据类型、主键约束(PRIMARY KEY)、外键约束(FOREIGN KEY)或者唯一性约束(UNIQUE)

举例:

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 tablename(
col_name data_type;...
[UNIQUE|FULLTEXT|SPATIAL][INDEX|KEY] [index_name] (col_name[LENGTH],...)[ASC|DESC]
);
  • UNIQUE 、 FULLTEXT为可选参数,分别表示唯一索引、全文索引
  • INDEX 与 KEY 为同义词,两者的作用相同,用来指定创建索引
  • index_name 指定索引的名称,为可选参数,如果不指定,那么MySQL默认col_name为索引名
  • col_name 为需要创建索引的字段列,该列必须从数据表中定义的多个列中选择
  • length 为可选参数,表示索引的长度,只有字符串类型的字段才能指定索引长度
  • ASC 或 DESC 指定升序或者降序的索引值存储

查看索引

方式一

SHOW INDEX FROM tablename ;

方式二

SHOW CREATE TABLE tablename;

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(year_publication)
);

2.创建唯一索引

CREATE TABLE test1(
id INT NOT NULL,
name varchar(30) NOT NULL,
UNIQUE INDEX uk_idx_id(id)
);

3.主键索引

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

  • 随表一起建索引:
CREATE TABLE student (
id INT UNSIGNED AUTO_INCREMENT ,
student_no VARCHAR(200),
student_name VARCHAR(200),
PRIMARY KEY(id)
);
  • 删除主键索引:
ALTER TABLE student drop PRIMARY KEY;
  • 修改主键索引:必须先删除掉(drop)原索引,再新建(add)索引

    1. 如果设置了一个字段为自增,那么这个字段也必须要设置为主键
    2. 如果需要设置多个主键,其中只能有一个是自增的

4.创建单列索引

引举:

CREATE TABLE test2(
id INT NOT NULL,
name CHAR(50) NULL,
INDEX single_idx_name(name(20))
);

5.创建组合索引

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

CREATE TABLE test3(
id INT NOT NULL,
name CHAR(30) NOT NULL,
age INT NOT NULL,
info VARCHAR(255),
INDEX multi_idx(id,name,age)
);

在test3表中,查询id和name字段,使用EXPLAIN语句查看索引的使用情况:

EXPLAIN SELECT * FROM test3 WHERE id=1 AND name='songhongkang';

在test3表中,查询name和age字段或者单独查询name和age字段,使用EXPLAIN语句查看索引的使用情况:

EXPLAIN SELECT * FROM test3 WHERE NAME='songhongkang' AND AGE = 12;

在这里插入图片描述

可以看到,查询id和name字段时,使用了名称为MultiIdx的索引,如果查询(name, age)组合或者单独查询name和age字段,会发现结果中possible_keys和key值为NULL,并没有使用在t3表中创建的索引进行查询

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),
FULLTEXT INDEX futxt_idx_info(info)
);

举例2:创建了一个给title和body字段添加全文索引的表

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

举例3:

CREATE TABLE papers(
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
title VARCHAR(200) DEFAULT NULL,
content TEXT,
PRIMARY KEY (id),
FULLTEXT KEY title(title,content)
);

不同于like方式的的查询:

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

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

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

明显的提高查询效率

注意

  1. 全文索引比 like +% 快 N 倍,但是可能存在精度问题
  2. 如果需要全文索引的是大量数据,建议先添加数据,再创建索引

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

在已经存在的表中创建索引可以使用ALTER TABLE语句或者CREATE INDEX语句

1.使用ALTER TABLE语句创建索引

ALTER TABLE table_name ADD 
[UNIQUE|FULLTEXT|SPATIAL][INDEX|KEY] [index_name] (col_name[LENGTH],...)[ASC|DESC];

2.使用CREATE INDEX创建索引

CREATE [UNIQUE|FULLTEXT|SPATIAL] INDEX index_name
ON table_name (col_name[LENGTH],...)[ASC|DESC];

c.删除索引

1.使用ALTER TABLE删除索引

ALTER TABLE table_name DROP INDEX index_name;

2.使用DROP INDEX语句删除索引

DROP INDEX index_name ON table_name;

删除表中的列时,如果要删除的列为索引的组成部分,则该列也会从索引中删除。如果组成索引的所有列都被删除,则整个索引将被删除

2.MySQL8.0索引新特性

a.支持降序索引

MySQL 8.x版本才开始真正支持降序索引(仅限于InnoDBc存储引擎)

b.隐藏索引

从MySQL 8.x开始支持隐藏索引(invisible indexes),只需要将待删除的索引设置为隐藏索引,使查询优化器不再使用这个索引,确认将索引设置为隐藏索引后系统不受任何响应,就可以彻底删除索引。这种通过先将索引设置为隐藏索引,再删除索引的方式就是软删除

注意:

  • 主键不能被设置为隐藏索引。当表中没有显式主键时,表中第一个唯一非空索引会成为隐式主键,也不能设置为隐藏索引

1.创建表时直接创建

在MySQL中创建隐藏索引通过SQL语句INVISIBLE来实现,其语法形式如下:

CREATE TABLE tablename(
propname1 type1[CONSTRAINT1],
propname2 type2[CONSTRAINT2],
……
propnamen typen,
INDEX [indexname](propname1 [(length)]) INVISIBLE
);

上述语句比普通索引多了一个关键字INVISIBLE,用来标记索引为不可见索引

2.在已经存在的表上创建

可以为已经存在的表设置隐藏索引,其语法形式如下:

CREATE INDEX indexname
ON tablename(propname[(length)]) INVISIBLE;

3.通过ALTER TABLE语句创建

语法形式如下:

ALTER TABLE tablename
ADD INDEX indexname (propname [(length)]) INVISIBLE;

4.切换索引可见状态

已存在的索引可通过如下语句切换可见状态:

ALTER TABLE tablename ALTER INDEX index_name INVISIBLE; #切换成隐藏索引
ALTER TABLE tablename ALTER INDEX index_name VISIBLE; #切换成非隐藏索引

如果将index_cname索引切换成可见状态,通过explain查看执行计划,发现优化器选择了index_cname索引

注意当索引被隐藏时,它的内容仍然是和正常索引一样实时更新的。如果一个索引需要长期被隐藏,那么可以将其删除,因为索引的存在会影响插入、更新和删除的性能。

通过设置隐藏索引的可见性可以查看索引对调优的帮助

5.使隐藏索引对查询优化器可见

在MySQL 8.x版本中,为索引提供了一种新的测试方式,可以通过查询优化器的一个开关(use_invisible_indexes)来打开某个设置,使隐藏索引对查询优化器可见。如果use_invisible_indexes 设置为off (默认),优化器会忽略隐藏索引。如果设置为on,即使隐藏索引不可见,优化器在生成执行计划时仍会考虑使用隐藏索引

(1)在MySQL命令行执行如下命令查看查询优化器的开关设置

select @@optimizer_switch \G

在输出的结果信息中找到如下属性配置

use_invisible_indexes=off

此属性配置值为off,说明隐藏索引默认对查询优化器不可见

(2)使隐藏索引对查询优化器可见,需要在MySQL命令行执行如下命令:

set session optimizer_switch="use_invisible_indexes=on";
Query OK,0 rows affected (0.00 sec)

SQL语句执行成功,再次查看查询优化器的开关设置

select @@optimizer_switch ;
***************************1. row ***************************
@@optimizer_switch:
index_merge=on,index_merge_union=on,index_merge_sort_union=on,index_merge_
intersection=on,engine_condition_pushdown=on,index_condition_pushdown=on,mrr=on,mrr_co
st_based=on,block_nested_loop=on,batched_key_access=off,materialization=on,semijoin=on
,loosescan=on,firstmatch=on,duplicateweedout=on,subquery_materialization_cost_based=on
,use_index_extensions=on,condition_fanout_filter=on,derived_merge=on,use_invisible_ind
exes=on,skip_scan=on,hash_join=on
1 row in set (0.00 sec)

此时,在输出结果中可以看到如下属性配置

use_invisible_indexes=on

use_invisible_indexes属性的值为on,说明此时隐藏索引对查询优化器可见

(3)使用EXPLAIN查看以字段invisible_column作为查询条件时的索引使用情况

explain select * from classes where cname = '高一2班';

查询优化器会使用隐藏索引来查询数据

(4)如果需要使隐藏索引对查询优化器不可见,则只需要执行如下命令即可

set session optimizer_switch="use_invisible_indexes=off";
Query OK,0 rows affected (0.00 sec)

再次查看查询优化器的开关设置

select @@optimizer_switch ;

此时,use_invisible_indexes属性的值已经被设置为“off”

3.索引的设计原则

a.数据准备

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

CREATE DATABASE mysqlPlus;
USE mysqlPlus;
#1.创建学生表和课程表
CREATE TABLE student_info(
id INT NOT NULL AUTO_INCREMENT,
student_id INT NOT NULL ,
name VARCHAR(20) DEFAULT NULL,
course_id INT NOT NULL ,
class_id INT DEFAULT NULL,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id)
)AUTO_INCREMENT=1;

CREATE TABLE course(
id INT NOT NULL AUTO_INCREMENT,
course_id INT NOT NULL ,
course_name VARCHAR(40) DEFAULT NULL,
PRIMARY KEY (id)
)AUTO_INCREMENT=1;

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

#函数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 ;
#函数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';
  • 命令开启:允许创建函数设置:
set global log_bin_trust_function_creators=1; #不加global只是当前窗口有效。
  • mysqld重启,上述参数又会消失。永久方法:

    • windows下:my.ini[mysqld]加上:

      log_bin_trust_function_creators=1
      
    • linux下:vim /etc/my.cnf 中[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步:调用存储过程

CALL insert_course(100);
CALL insert_stu(1000000);

b.哪些情况适合创建索引

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

  • 如果某个字段是唯一性的,就可以直接创建唯一索引或主键索引

例如,在student表中,字段id具有唯一性,可以为该字段创建唯一索引,可以快速的确定某个学生的信息

2)频繁作为 WHERE 查询条件的字段

  • 某个字段在SELECT语句的 WHERE 条件中经常被使用到,可以为该字段创建索引,普通索引即可

3)经常 GROUP BY 和 ORDER BY 的列

  • 索引可以使数据按照某种顺序进行存储和检索,所以当使用GROUP BY 对数据进行分组查询,或者使用 ORDER BY 对数据进行排序的时候,就需要对分组或者排序的字段进行索引

注意:

  • 只要为字段创建了索引,当查询该字段时,查询结果会自动按照升序或者降序的顺序展示

4)UPDATE、DELETE 的 WHERE 条件列

  • 对数据按照 WHERE 某个条件进行查询后再进行 UPDATE 或 DELETE 的操作,如果对 WHERE 字段创建了索引,就能大幅提升效率

5)DISTINCT 字段需要创建索引

  • 使用 DISTINCT对某个字段进行去重,可以为这个字段创建索引,提升查询效率

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

  • 首先,连接表的数量尽量不要超过3 张

  • 其次,对 WHERE 条件创建索引,因为 WHERE 才是对数据条件的过滤

  • 最后,对用于连接的字段创建索引,并且该字段在多张表中的类型必须一致

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

  • 数据类型的大小指的是该类型表示的数据范围的大小

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

  • 当字符串很长时,可以为字符串的前面一部分建立索引,称为前缀索引
create table shop(address varchar(120) not null);
alter table shop add index(address(12));

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

  • 列的基数指的是某一列中不重复数据的个数,比方说某个列包含值2,5,8,2,5,8,2,5,8,虽然有9条记录,但列的基数却是3。**在记录行数一定的情况下,列的基数越大,该列中的值越分散;列的基数越小,该列中的值越集中。**为列的基数大的列建立索引

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

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

c.限制索引的数目

由于索引需要占用磁盘空间、影响INSERT、DELETE、UPDATE等性能…所以一般一张表中最多设置6个索引

d.哪些情况不适合创建索引

  1. 在where中使用不到的字段,不要设置索引
  2. 数据量小的表最好不要使用索引
  3. 有大量重复数据的列上不要建立索引
  4. 避免对经常更新的表创建过多的索引
  5. 不建议用无序的值作为索引
  6. 删除不再使用或者很少使用的索引
  7. 不要定义夯余或重复的索引

九、性能分析工具

1.数据库服务器的优化步骤

流程划分成了**观察(Show status)行动(Action)**两个部分

MySQL高级索引及调优篇_第26张图片

字母 S 的部分代表观察(会使用相应的分析工具),字母 A 代表的部分是行动(对应分析可以采取的行动)

采用分析工具可以帮助定位到有问题的SQL,这三种分析工具即为SQL调优的三个步骤:慢查询、EXPLAIN、SHOWPROFILING

2.查看系统性能参数

使用SHOW STATUS语句查询一些MySQL数据库服务器的性能参数、执行频率

SHOW [GLOBAL|SESSION] STATUS LIKE '参数';

一些常用的性能参数如下:

  • Connections:连接MySQL服务器的次数
  • Uptime:MySQL服务器的上线时间
  • Slow_queries:慢查询的次数
  • Innodb_rows_read:Select查询返回的行数
  • Innodb_rows_inserted:执行INSERT操作插入的行数
  • Innodb_rows_updated:执行UPDATE操作更新的行数
  • Innodb_rows_deleted:执行DELETE操作删除的行数
  • Com_select:查询操作的次数
  • Com_insert:插入操作的次数。对于批量插入的 INSERT 操作,只累加一次
  • Com_update:更新操作的次数
  • Com_delete:删除操作的次数

3.统计SQL的查询成本: last_query_cost

评价一个查询的执行效率的一个常用指标:last_query_cost,对应的是SQL 语句所需要读取的读页的数量

SHOW STATUS LIKE 'last_query_cost';

使用场景

SQL查询是一个动态的过程,从页加载的角度来看:

  1. 位置决定效率。如果页就在数据库缓冲池中,那么效率是最高的,否则还需要从内存或者磁盘中进行读取,针对单个页的读取来说,如果页存在于内存中,会比在磁盘中读取效率高很多
  2. 批量决定效率。如果从磁盘中对单一页进行随机读,那么效率是很低的,而采用顺序读取的方式,批量对页进行读取,平均一页的读取效率就会提升很多,甚至要快于单个页面在内存中的随机读取

所以首先要考虑数据存放的位置,如果是进程使用的数据就要尽量放到缓冲池中,其次可以充分利用磁盘的吞吐能力,一次性批量读取数据,这样单个页的读取效率也就得到了提升

4.定位执行慢的 SQL:慢查询日志

慢查询日志用来记录MySQL中响应时间超过阈值的语句,具体运行时间超过long_query_time值的SQL,则会被记录在慢查询日志中,long_query_time默认值为10(运行10秒以上的语句)

主要作用:帮助发现执行时间特别长的SQL查询,并且有针对性的进行优化,从而提高系统的整体效率。当数据库服务器发生阻塞、运行变慢等情况时,可以通过检查慢查询日志,找到慢查询语句,一般一条sql语句超过5秒即为慢查询。可以通过结合explain语句收集超过5秒的sql进行全面分析

默认情况下MySQL数据库没有开启慢查询日志,当需要调优时再开启即可

a.开启慢查询日志参数

1.开启 slow_query_log

查看慢查询日志是否开启

mysql > show variables like '%slow_query_log';

显示慢查询日志信息

SHOW VARIABLES LIKE `slow_query_log%`;

开启慢查询日志

set global slow_query_log='ON';

2.修改 long_query_time 阈值

查看慢查询的时间阈值

show variables like '%long_query_time%';

临时性设置慢查询的时间阈值

set global long_query_time = 1;

永久设置慢查询的时间阈值

修改配置文件vim /etc/my.cf

[mysqld]
slow_query_log=ON #开启慢查询日志开关
slow_query_log_file=/var/lib/mysql/atguigu-low.log #慢查询日志的目录和文件名信息
long_query_time=3 #设置慢查询的阈值为3秒,超出此设定值的SQL即被记录到慢查询日志
log_output=FILE

注意:修改慢查询是否开启设置后,需要重启mysql服务器systemctl restart mysqld.service

b.查看慢查询数目

SHOW GLOBAL STATUS LIKE '%Slow_queries%';

c.案例

CREATE TABLE `student`(
`id` INT NOT NULL AUTO_INCREMENT,
`stuno` INT NOT NULL ,
`name` VARCHAR(20) DEFAULT NULL,
`age` INT DEFAULT NULL,
`classId` INT DEFAULT NULL,
 PRIMARY KEY (`id`)
)AUTO_INCREMENT=1;
 

1.设置参数 log_bin_trust_function_creators

  • 命令开启:允许创建函数设置:
set global log_bin_trust_function_creators=1; #不加global只是当前窗口有效。

2.创建函数

随机产生字符串

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 ;

#测试
SELECT rand_string(10);

产生随机数值

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 ;

#测试:
SELECT rand_num(10,100);

3.创建存储过程

DELIMITER //
CREATE PROCEDURE insert_stu1( START INT , max_num INT )
BEGIN
DECLARE 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(10,100),rand_num(10,1000));
 UNTIL i = max_num
 END REPEAT;
 COMMIT; #提交事务
END //
DELIMITER ;

4.调用存储过程

#调用刚刚写好的函数,40000条记录,从100001号开始

CALL insert_stu1(100001,40000);

5.测试及分析

测试

SELECT * FROM student WHERE stuno = 100760;

SELECT * FROM student WHERE NAME = 'WkjrhJ';

分析

show status like 'slow_queries';

在这里插入图片描述

e.慢查询日志分析工具:mysqldumpslow

mysqldumpslow --help

MySQL高级索引及调优篇_第27张图片

mysqldumpslow 命令的具体参数:

  • -a: 不将数字抽象成N,字符串抽象成S
  • -s: 是表示按照何种方式排序:
    • c: 访问次数
    • l: 锁定时间
    • r: 返回记录
    • t: 查询时间
    • al:平均锁定时间
    • ar:平均返回记录数
    • at:平均查询时间(默认方式)
    • ac:平均查询次数
  • -t: 为返回前面多少条的数据
  • -g: 后边搭配一个正则匹配模式,大小写不敏感

eg:按照查询时间排序,查看前五条 SQL 语句

mysqldumpslow -s t -t 5 /var/lib/mysql/ablelynn100-slow.log

f.关闭慢查询日志

临时性关闭

SET GLOBAL slow_query_log=off;

永久性关闭

[mysqld]
slow_query_log=OFF

注意:修改慢查询是否开启设置后,需要重启mysql服务器systemctl restart mysqld.service

g.删除慢查询日志

显示慢查询日志信息

SHOW VARIABLES LIKE 'slow_query_log%'

得到慢查询日志的目录默认为MySQL的数据目录,手动删除慢查询日志文件

使用命令mysqladmin flush-logs来重新生成查询日志文件,执行完毕会在数据目录下重新生成慢查询日志文件

mysqladmin -uroot -p flush-logs slow

提示

慢查询日志都是使用mysqladmin flush-logs命令来删除重建的。使用时一定要注意,一旦执行了这个命令,慢查询日志都只存在新的日志文件中,如果需要旧的查询日志,就必须事先备份

5.查看 SQL 执行成本:SHOW PROFILE

show profile 是 MySQL 提供的可以用来分析当前会话中 SQL 都做了什么、执行的资源消耗工具的情况,可用于 sql 调优的测量。默认情况下处于关闭状态,并保存最近15次的运行结果

show variables like 'profiling';
set profiling = 'ON';

相关查询执行后,查看当前会话都有哪些profiles: show profiles;

查询最近一次查询的开销:show profile;

show profile的常用查询参数:

  1. ALL:显示所有的开销信息

  2. BLOCK IO:显示块IO开销

  3. CONTEXT SWITCHES:上下文切换开销

  4. CPU:显示CPU开销信息

  5. IPC:显示发送和接收开销信息

  6. MEMORY:显示内存开销信息

  7. PAGE FAULTS:显示页面错误开销信息

  8. SOURCE:显示和Source_function,Source_file, Source_line相关的开销信息

  9. SWAPS:显示交换次数开销信息

日常开发需注意的结论:

  1. converting HEAP to MyISAM: 查询结果太大,内存不够,数据往磁盘上搬了
  2. Creating tmp table:创建临时表。先拷贝数据到临时表,用完后再删除临时表
  3. Copying to tmp table on disk:把内存中临时表复制到磁盘上,警惕
  4. locked

如果在show profile诊断结果中出现了以上4条结果中的任何一条,则sql语句需要优化。

6.分析查询语句:EXPLAIN

1.概述

当定位了查询慢的sql后,可以使用EXPLAINDEXCRIBE工具做针对性的分析查询语句(EXPLAINDEXCRIBE功能一样)

EXPLAIN语句帮助查看某个查询语句的具体执行计划

可以得到:

  • 表的读取顺序
  • 数据读取操作的操作类型
  • 哪些索引可以使用
  • 哪些索引被实际使用
  • 表之间的引用
  • 每张表有多少行被优化器查询

2.基本语法

EXPLAIN 或 DESCRIBE语句的语法形式如下:

EXPLAIN SELECT select_options
或者
DESCRIBE SELECT select_options

如果想看看某个查询的执行计划,可以在具体的查询语句前边加一个 EXPLAIN

EXPLAIN SELECT 1;

在这里插入图片描述

列名 描述
id 在一个大的查询语句中每个select关键字对应一个唯一的id
select_type select关键字对应的那个查询的类型
table 表名
partitions 匹配的分区信息
type 针对单表的访问方法
possible_keys 可能用到的索引
key 实际用到的索引
key_len 实际用到的索引长度
ref 当时用索引列等值查询时,与索引列进行等值匹配的对象信息
rows 预估的需要读取的记录条数
filtered 某个表经过搜索条件过滤后剩余记录条数的百分比
extra 一些额外信息

3.数据准备

CREATE TABLE s1 (
 id INT AUTO_INCREMENT,
 key1 VARCHAR(100),
 key2 INT,
 key3 VARCHAR(100),
 key_part1 VARCHAR(100),
 key_part2 VARCHAR(100),
 key_part3 VARCHAR(100),
 common_field VARCHAR(100),
 PRIMARY KEY (id),
 INDEX idx_key1 (key1),
 UNIQUE INDEX idx_key2 (key2),
 INDEX idx_key3 (key3),
 INDEX idx_key_part(key_part1, key_part2, key_part3)
);
CREATE TABLE s2 (
 id INT AUTO_INCREMENT,
 key1 VARCHAR(100),
 key2 INT,
 key3 VARCHAR(100),
 key_part1 VARCHAR(100),
 key_part2 VARCHAR(100),
 key_part3 VARCHAR(100),
 common_field VARCHAR(100),
 PRIMARY KEY (id),
 INDEX idx_key1 (key1),
 UNIQUE INDEX idx_key2 (key2),
 INDEX idx_key3 (key3),
 INDEX idx_key_part(key_part1, key_part2, key_part3)
);

设置参数 log_bin_trust_function_creators,保证允许创建函数

set global log_bin_trust_function_creators=1; 

创建函数

DELIMITER //
CREATE FUNCTION rand_string1(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 ;

创建存储过程

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

DELIMITER //
CREATE PROCEDURE insert_s1 (IN min_num INT,IN max_num INT)
BEGIN
 DECLARE i INT DEFAULT 0;
 SET autocommit = 0;
 REPEAT
 SET i = i +1;
 INSERT INTO s1 VALUES(
(min_num + i),
 rand_string1(6),
(min_num +30 * i +5),
 rand_string1(6),
 rand_string1(10),
 rand_string1(5),
 rand_string1(10),
 rand_string1(10));
 UNTIL i = max_num
 END REPEAT;
 COMMIT;
END //
DELIMITER ;

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

DELIMITER //
CREATE PROCEDURE insert_s2 (IN min_num INT ,IN max_num INT )
BEGIN
 DECLARE i INT DEFAULT 0;
 SET autocommit = 0;
 REPEAT
 SET i = i +1;
 INSERT INTO s2 VALUES(
(min_num + i),
 rand_string1(6),
(min_num +30 * i +5),
 rand_string1(6),
 rand_string1(10),
 rand_string1(5),
 rand_string1(10),
 rand_string1(10));
 UNTIL i = max_num
 END REPEAT;
 COMMIT;
END //
DELIMITER ;

调用存储过程

s1表数据的添加:加入1万条记录:

CALL insert_s1(10001,10000);

s2表数据的添加:加入1万条记录:

CALL insert_s2(10001,10000);

4.EXPLAIN各列的作用

1)table

MySQL规定EXPLAIN语句输出的每条记录都对应着某个单表的访问方法,该条记录的table列代表着该表的表名(有时不是真实的表名字,可能是简称)

单表查询

EXPLAIN SELECT * FROM s1;

在这里插入图片描述

连接查询

EXPLAIN SELECT * FROM s1 INNER JOIN s2;

在这里插入图片描述

多表查询,会显示所有用到的表,其id值都相同,extra显示使用到的查询方法

2)id

在连接查询的执行计划中,每个表都会对应一条记录,这些记录的id列的值是相同的,出现在前边的表表示驱动表,出现在后面的表表示被驱动表

EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key1 FROM s2) OR key3 = 'a';

在这里插入图片描述

注意:每出现一个select关键字,就会为此分配一个新的id值,查询优化器会对涉及子查询的查询语句重写,转换为连接查询,所以会出现不同的id值,但如果是连接查询,则只会出现一个id值

 EXPLAIN SELECT * FROM s1 WHERE key1 IN (SELECT key2 FROM s2 WHERE common_field = 'a');

在这里插入图片描述

小结:

  • id如果相同,可以认为是一组,从上往下顺序执行
  • 在所有组中,id值越大,优先级越高,越先执行
  • 每一个id,表示一趟独立的查询,一个sql的查询趟数越少越好

3)select_type

MySQL为每一个SELECT关键字代表的小查询定义了一个select_type属性,可以通过select_type属性了解小查询在大查询中的作用

名称 描述
SIMPLE 简单的查询(没有使用UNION 或者子查询(subqueries))
PRIMARY 主键查询
UNION 在UNION中第二个或者更后面的查询语句
UNION RESULT UNION的结果
SUBQUERY 子查询中的第一个查询
DEPENDENT SUBQUERY 子查询中第一个查询,依赖外部查询
DEPENDENT UNION 在UNION中第二个或者更后面的查询语句,依赖外部查询
DERIVED 驱动表
MATERIALIZED Mateerialized 子查询
UNCACHEABLE SUBQUERY 子查询的结果不能被缓存,必须被用于外部查询的计算
UNCACHEABLE UNION UNION的第二个或者更后面的查询,数据不能被缓存
  • SIMPLE:查询语句中不包含UNION或者子查询的查询都算作是SIMPLE类型
  • PRIMARY:对于包含UNION、UNION ALL或者子查询的大查询来说,它是由几个小查询组成的,其中最左边的那个查询的select_type的值就是PRIMARY
  • UNION:对于包含UNION或者UNION ALL的大查询来说,它是由几个小查询组成的,其中除了最左边的那个小查询意外,其余的小查询的select_type值就是UNION
  • UNION RESULT:MySQL 选择使用临时表来完成UNION查询的去重工作,针对该临时表的查询的select_type就是UNION RESULT
  • SUBQUERY:如果包含子查询的查询语句是不相关子查询,并且查询优化器决定采用将该子查询物化的方案来执行该子查询时,该子查询的第一个SELECT关键字代表的那个查询的select_type就是SUBQUERY

4)partitions (可略)

代表分区表中的命中情况,非分区表,该项为NULL。一般情况下查询语句的执行计划的partitions列的值为NULL

5)type ☆

type列就表明了这个访问方法(system , const , eq_ref , ref , fulltext , ref_or_null , index_merge , unique_subquery , index_subquery , range , index , ALL)

  • system:当表中只有一条记录并且该表使用的存储引擎的统计数据是精确的,比如MyISAM引擎,那么对该表的访问方法就是system
  • const:当根据主键或者唯一二级索引列与常数进行等值匹配时,对单表的访问方法就是const
  • eq_ref:在连接查询时,如果被驱动表是通过主键或者唯一二级索引列等值匹配的方式进行访问的(如果该主键或者唯一二级索引是联合索引的话,所有的索引列都必须进行等值比较)。则对该被驱动表的访问方法就是eq_ref
  • ref:当通过普通的二级索引列与常量进行等值匹配时来查询某个表,那么对该表的访问方法就可能是ref
  • fulltext:全文索引
  • ref_or_null:当对普通二级索引进行等值匹配查询,该索引列的值也可以是NULL值时,那么对该表的访问方法就可能是ref_or_null
  • index_merge:单表访问方法时在某些场景下可以使用Interseation、union、Sort-Union这三种索引合并的方式来执行查询
  • unique_subqueryunique_subquery是针对在一些包含IN子查询的查询语句中,如果查询优化器决定将IN子查询转换为EXISTS子查询,而且子查询可以使用到主键进行等值匹配的话,那么该子查询执行计划的type列的值就是unique_subquery
  • index_subqueryindex_subquery是针对在一些包含IN子查询的查询语句中,如果查询优化器决定将IN子查询转换为EXISTS子查询,而且子查询可以使用到索引进行等值匹配的话,那么该子查询执行计划的type列的值就是index_subquery
  • range:查询是范围查询时
  • index:需要扫描所有索引记录时
  • ALL:全表查询

结果值从最好到最坏依次是:

  • system > const > eq_ref > ref> fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
  • SQL 性能优化的目标:至少要达到 range 级别,要求是 ref 级别,最好是 consts级别。(阿里巴巴开发手册要求)

6)possible_keys和key

possible_keys列表示在某个查询语句中,对某个列执行单表查询时可能用到的索引有哪些,一般查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用。key列表示实际用到的索引有哪些,如果为NULL,则没有使用索引

7)key_len ☆

实际使用到的索引长度,帮帮助检查是否充分的利用了索引值越大越好

8)ref

显示索引的哪一列被使用,哪些列或常量被用于查找索引列上的值

9)rows ☆

预估的需要读取的记录条数,值越小越好

10)filtered

某个表经过搜索条件过滤后剩余记录条数的百分比

11)Extra ☆

包含不适合在其他列中显示但十分重要的额外信息。我们可以通过这些额外信息来更准确的理解MySQL到底将如何执行给定的查询语句

  • No tables used:查询语句没有FROM子句时将会提示该额外信息
  • Impossible WHERE:查询语句的WHERE子句永远为FALSE时将会提示该额外信息
  • Using where:使用全表扫描对某个表的查询,查询语句的WHERE子句中有针对该表的搜索条件时将会提示该额外信息
  • No matching min/max row:当查询列表处有MIN或者MAX聚合函数,但是并没有符合WHERE子句中的搜索条件的记录时
  • Using index:查询列表以及搜索条件中只包含属于某个索引的列,也就是在可以使用覆盖索引的情况下,在Extra列将会提示该额外信息
  • Using index condition:有些搜索条件中虽然出现了索引列,但却不能使用到索引
  • Using join buffer (Block Nested Loop):在连接查询执行过程中,当被驱动表不能有效的利用索引加快访问速度,MySQL一般会为其分配一块名叫join buffer的内存块来加快查询速度
  • Not exists:使用左(外)连接时,如果WHERE子句中包含要求被驱动表的某个列等于NULL值的搜索条件,而且那个列是不允许存储NULL值的,那么在该表的执行计划的Extra列就会提示这个信息
  • Using intersect(...)、 Using union(...)和 Using sort_union(...)
    • 如果出现Using intersect(...)提示,说明准备使用Intersect索引合并的方式执行查询,括号中的...表示需要进行索引合并的索引名称
    • 如果出现Using union(...)提示,说明准备使用Union索引合并的方式执行查询
    • 如果出现Using sort_union(...)提示,说明准备使用Sort-Union索引合并的方式执行查询
  • Zero limit:当我们的LIMIT子句的参数为0时,表示压根儿不打算从表中读取任何记录,将会提示该额外信息
  • Using filesort:有一些情况下对结果集中的记录进行排序是可以使用到索引的

7.EXPLAIN的进一步使用

EXPLAIN可以输出四种格式:传统格式JSON格式TREE格式以及可视化输出

1.传统格式

传统格式简单明了,输出是一个表格形式,概要说明查询计划

2. JSON格式

JSON格式是四种格式里面输出信息最详尽的格式,里面包含了执行的成本信息

  • JSON格式:在EXPLAIN单词和真正的查询语句中间加上 FORMAT=JSON

3. TREE格式

主要根据查询的各个部分之间的关系各部分的执行顺序来描述如何查询

4.可视化输出

可视化输出,可以通过MySQL Workbench可视化查看MySQL的执行计划。通过点击Workbench的放大镜图标,即可生成可视化的查询计划

8.分析优化器执行计划:trace

可以跟踪优化器做出的各种决策,并将结果记录到INFORMATION_SCHEMA.OPTIMIZER_TRACE表中

默认关闭,开始时设置格式为json,同时设置trace最大能够使用的内存大小

SET optimizer_trace="enabled=on",end_markers_in_json=on;
set optimizer_trace_max_mem_size=1000000;

9.MySQL监控分析视图-sys schema

Sys schema视图摘要

  1. 主机相关:以host_summary开头,主要汇总了IO延迟的信息
  2. Innodb相关:以innodb开头,汇总了innodb buffer信息和事务等待innodb锁的信息
  3. I/O相关:以io开头,汇总了等待I/O、I/O使用量情况
  4. 内存使用情况:以memory开头,从主机、线程、事件等角度展示内存的使用情况
  5. 连接与会话信息:processlist和session相关视图,总结了会话相关信息
  6. 表相关:以schema_table开头的视图,展示了表的统计信息
  7. 索引信息:统计了索引的使用情况,包含冗余索引和未使用的索引情况
  8. 语句相关:以statement开头,包含执行全表扫描、使用临时表、排序等的语句信息
  9. 用户相关:以user开头的视图,统计了用户使用的文件I/O、执行语句统计信息
  10. 等待事件相关信息:以wait开头,展示等待事件的延迟情况

十、索引优化和查询优化

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

一般建议

  • 全值匹配
  • 最佳左前缀法则
  • 主键设置自增
  • where条件中is null可以使用索引,is not null 无法使用索引
  • 在使用LIKE关键字进行查询的查询语句中,如果匹配字符串的第一个字符为’%‘,索引就不会起作用。只有’%'不在第一个位置,索引才会起作用
  • 在WHERE子句中,如果在OR前的条件列进行了索引,而在OR后的条件列没有进行索引,那么索引会失效。也就是说,OR前后的两个条件中的列都是索引时,查询中才使用索引
  • 数据库和表的字符集统一使用utf8mb4

1.关联查询优化

a.采用左外连接

LEFT JOIN 条件用于确定如何从右表搜索行,左边一定都有,所以右边是我们的关键点,一定需要建立索引

b.采用内连接

inner join对于内连接来说,查询优化器可以决定谁作为驱动表,谁作为被驱动表出现的

c. 小结

  • 保证被驱动表的JOIN字段已经创建了索引
  • 需要JOIN 的字段,数据类型保持绝对一致
  • LEFT JOIN 时,选择小表作为驱动表,大表作为被驱动表。减少外层循环的次数
  • INNER JOIN 时,MySQL会自动将小结果集的表选为驱动表。选择相信MySQL优化策略
  • 能够直接多表关联的尽量直接关联,不用子查询。(减少查询的趟数)
  • 不建议使用子查询,建议将子查询SQL拆开结合程序多次查询,或使用 JOIN 来代替子查询
  • 衍生表建不了索引

2.子查询优化

子查询的执行效率不高,原因:

  1. 执行子查询时,MySQL需要为内层查询语句的查询结果建立一个临时表,然后外层查询语句从临时表中查询记录。查询完毕后,再撤销这些临时表。这样会消耗过多的CPU和IO资源,产生大量的慢查询

  2. 子查询的结果集存储的临时表,不论是内存临时表还是磁盘临时表都不会存在索引,所以查询性能会受到一定的影响

  3. 对于返回结果集比较大的子查询,其对查询性能的影响也就越大

**在MySQL中,可以使用连接(JOIN)查询来替代子查询。**连接查询不需要建立临时表,其速度比子查询要快,如果查询中使用索引的话,性能就会更好

3.排序优化

优化建议:

  1. SQL 中,可以在 WHERE 子句和 ORDER BY 子句中使用索引,目的是在 WHERE 子句中避免全表扫描,在 ORDER BY 子句避免使用 FileSort 排序。当然,某些情况下全表扫描,或者 FileSort 排序不一定比索引慢
  2. 尽量使用 Index 完成 ORDER BY 排序。如果 WHERE 和 ORDER BY 后面是相同的列就使用单索引列;如果不同就使用联合索引
  3. 无法使用 Index 时,需要对 FileSort 方式进行调优

4.GROUP优化

  • group by 使用索引的原则几乎跟order by一致,group by 即使没有过滤条件用到索引,也可以直接使用索引
  • group by 先排序再分组,遵照索引建的最佳左前缀法则
  • 当无法使用索引列,增大 max_length_for_sort_data 和 sort_buffer_size 参数的设置
  • where效率高于having,一般将限制条件写在where不写在having中
  • 减少使用order by,Order by、group by、distinct这些语句较为耗费CPU
  • 包含了order by、group by、distinct这些查询的语句,where条件过滤出来的结果集保持在1000行以内,否则SQL会很慢

5.优化分页查询

优化思路

  1. 在索引上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容
  2. 适用于主键自增的表,可以把Limit 查询转换成某个位置的查询

6.覆盖索引

索引是高效找到行的一个方法,但是一般数据库也能使用索引找到一个列的数据,因此它不必读取整个行。毕竟索引叶子节点存储了它们索引的数据;当能通过读取索引就可以得到想要的数据,那就不需要读取行了。一个索引包含了满足查询结果的数据就叫做覆盖索引

7.字符串添加索引

MySQL是支持前缀索引的。默认地,如果创建索引的语句不指定前缀长度,那么索引就会包含整个字符串,一般当字符串很长时,在建立字符串索引是设置前一小部分为索引即可

8.普通索引与唯一索引的区别

唯一索引和普通所用在效率方面几乎没区别

9.其它查询优化策略

a.EXISTS 和 IN 的区分

选择的标准:与表的大小有关,如果EXISTS、IN前的表大,则使用IN,如果EXISTS、IN前的表小,则使用EXISTS

b.COUNT(*)与COUNT(具体字段)效率

一般采用全表扫描的方式统计数据表的行数,select count(*)

c.SELECT(*)

一般不建议使用SELECT(*)

d.LIMIT 1

针对的是会扫描全表的 SQL 语句,如果可以确定结果集只有一条,那么加上 LIMIT 1后,当找到一条结果的时候就不会继续扫描,这样会加快查询速度

e.COMMIT

多使用 COMMIT,这样程序的性能得到提高,需求也会因为 COMMIT 所释放的资源而减少。

COMMIT 所释放的资源:

  • 回滚段上用于恢复数据的信息
  • 被程序语句获得的锁
  • redo / undo log buffer 中的空间
  • 管理上述3 种资源中的内部花费

十一、数据库设计规范

1.优势

  1. 节省数据的存储空间
  2. 能够保证数据的完整性
  3. 方便进行数据库应用系统的开发

2.范式

a.概念

在关系型数据库中,关于数据表设计的基本原则、规则就称为范式

b.常见范式

目前关系型数据库有六种常见范式,按照范式级别,从低到高分别是:第一范式(1NF)、第二范式(2NF)、第三范式(3NF)、巴斯-科德范式(BCNF)、第四范式(4NF)和第五范式(5NF,又称完美范式)

c.键和相关属性

范式的定义会使用到主键和候选键

  • 超键:能够唯一标识元组的属性集称为超键
  • 候选键:如果超键不包括多余的属性,那么这个超键就是候选键
  • 主键:用户可以从候选键中选择一个作为主键
  • 外键:如果数据表R1中的某属性集不是R1的主键,而是另一个数据表R2的主键,则这个属性集为数据表R1的外键
  • 主属性:包含在任一候选键中的属性称为主属性
  • 非主属性:与主属性相对,指的是不包含在任何一个候选键中的属性

d.第一范式

第一范式主要确保数据库中每个字段的值必须具有原子性,也就是说数据表中每个字段的值为不可再次拆分的最小数据单元

e.第二范式

第二范式要求,在满足第一范式的基础上,满足数据库里的每一条数据记录,都是可唯一标识的。而且所有非主键字段,都必须完全依赖主键,不能只依赖主键的一部分。如果知道主键的所有属性的值,就可以检索到任何元组(行)的任何属性的任何值

举例:

成绩表(学号,课程号,成绩)关系中,(学号,课程号)可以决定成绩,但是学号不能决定成绩,课程号也不能决定成绩,所以“(学号,课程号)→成绩”就是完全依赖关系

f.第三范式

第三范式是在第二范式的基础上,确保数据表中的每一个非主键字段都和主键字段直接相关,也就是说,要求数据表中的所有非主键字段不能依赖于其他非主键字段。(即,不能存在非主属性A依赖于非主属性B,非主属性B依赖于主键C的情况,即存在“A->B->C"的决定关系)通俗地讲,该规则的意思是所有非主键属性之间不能由依赖关系,必须相互独立

g.总结

必须遵循:

  1. 第一范式:确保每列保持原子性,每列不可分割
  2. 第二范式:确保每列与主键完全依懒
  3. 第三范式:确保每列和主键直接相关,而不是间接相关

3.反范式化

遵循业务优先的原则,即首先满足业务需求,然后满足范式要求

规范化 vs 性能

  1. 为满足某种商业目标,数据库性能比规范化数据库更重要
  2. 在数据规范化的同时,要综合考虑数据库的性能
  3. 通过在给定的表中添加额外的字段,以大量减少需要从中搜索信息所需的时间
  4. 通过在给定的表中插入计算列,以方便查询

使用场景

当冗余信息有价值或者能大幅度提高查询效率的时候,才会采取反范式的优化

1.增加冗余字段的建议

增加冗余字段一定要符合如下两个条件。只要满足这两个条件,才可以考虑增加夯余字段

1)这个冗余字段不需要经常进行修改

2)这个冗余字段查询的时候不可或缺

2.历史快照、历史数据的需要

在现实生活中,经常需要一些冗余信息,比如订单中的收货人信息,包括姓名、电话和地址等。每次发生的订单收货信息都属于历史快照,需要进行保存,但用户可以随时修改自己的信息,这时保存这些冗余信息是非常有必要的

反范式优化也常用在数据仓库的设计中,因为数据仓库通常存储历史数据,对增删改的实时性要求不强,对历史数据的分析需求强。这时适当允许数据的冗余度,更方便进行数据分析

数据仓库和数据库在使用上的区别:

  1. 数据库设计的目的在于捕捉数据,而数据仓库设计的目的在于分析数据
  2. 数据库对数据的增删改实时性要求强,需要存储在线的用户数据,而数据仓库存储的一般是历史数据
  3. 数据库设计需要尽量避免冗余,但为了提高查询效率也允许一定的冗余度,而数据仓库在设计上更偏向采用反范式设计

4. BCNF(巴斯范式)

在3NF的基础上进行了改进,提出了巴斯范式(BCNF),页脚巴斯-科德范式(Boyce - Codd Normal Form)。BCNF被认为没有新的设计规范加入,只是对第三范式中设计规范要求更强,使得数据库冗余度更小。所以,称为是修正的第三范式,或扩充的第三范式,BCNF不被称为第四范式

若一个关系达到了第三范式,并且它只有一个候选键,或者它的每个候选键都是单属性,则该关系自然达到BC范式

一般来说,一个数据库设符合3NF或者BCNF就可以了

5.案例

商超进货系统中的进货单表进行剖析:

进货单表:

MySQL高级索引及调优篇_第28张图片

这个表中的字段很多,表里的数据量也很惊人。大量重复导致表变得庞大,效率极低

在实际工作场景中,这种由于数据表结构设计不合理,而导致的数据重复的现象并不少见。往往是系统虽然能够运行,承载能力却很差,稍微有点流量,就会出现内存不足、CPU使用率飙升的情况,甚至会导致整个项目失败

1. 迭代1次:考虑1NF

第一范式要求:所有的字段都是基本数据类型,不可进行拆分。这里需要确认,所有的列中,每个字段只包含一种数据。

这张表里,把“property"这一字段,拆分成”specification (规格)“和"unit (单位)”,这两个字段如下:

MySQL高级索引及调优篇_第29张图片

2. 迭代2次:考虑2NF

第二范式要求,在满足第一范式的基础上,还要满足数据表里的每一条数据记录,都是可唯一标识的。而且所有字段,都必须完全依赖主键,不能只依赖主键的一部分

第1步,就是要确定这个表的主键。通过观察发现,字段“listnumber(单号)"+"barcode(条码)"可以唯一标识每一条记录,可以作为主键

第2步,确定好了主键以后,判断哪些字段完全依赖主键,哪些字段只依赖于主键的一部分。把只依赖于主键一部分的字段拆出去,形成新的数据表

首先,进货单明细表里面的"goodsname(名称)““specification(规格)”“unit(单位)“这些信息是商品的属性,只依赖于"batcode(条码)”,不完全依赖主键,可以拆分出去。把这3个字段加上它们所依赖的字段"barcode(条码)”,拆分形成新的数据表"商品信息表”

这样一来,原来的数据表就被拆分成了两个表

商品信息表:

MySQL高级索引及调优篇_第30张图片

进货单表:

MySQL高级索引及调优篇_第31张图片

此外,字段"supplierid(供应商编号)““suppliername(供应商名称)”“stock(仓库)“只依赖于"listnumber(单号)”,不完全依赖于主键,所以,可以把"supplierid”“suppliername”“stock"这3个字段拆出去,再加上它们依赖的字段"listnumber(单号)”,就形成了一个新的表"进货单头表"剩下的字段,会组成新的表,我们叫它"进货单明细表"

原来的数据表就拆分成了3个表

进货单头表:

MySQL高级索引及调优篇_第32张图片

进货单明细表:

MySQL高级索引及调优篇_第33张图片

商品信息表:

MySQL高级索引及调优篇_第34张图片

第3步,在“商品信息表”中,字段“barcode"是有可能存在重复的,比如,用户门店可能有散装称重商品和自产商品,会存在条码共用的情况。所以,所有的字段都不能唯一标识表里的记录。这个时候,必须给这个表加上一个主键,比如说是自增字段"itemnumber"

3. 迭代3次:考虑3NF

进货单头表,还有数据冗余的可能。因为"suppliername"依赖"supplierid",那么就可以按照第三范式的原则进行拆分了。进一步拆分进货单头表,把它拆解陈供货商表和进货单头表

供货商表:

MySQL高级索引及调优篇_第35张图片

进货单头表:

MySQL高级索引及调优篇_第36张图片

这2个表都满足第三范式的要求了

6.ER模型

ER模型也称实体关系模型

1.ER模型要素

ER 模型中有三个要素,分别是实体、属性和关系

实体,数据对象,往往对应于现实生活中的真实存在的个体。在 ER 模型中,用矩形来表示。实体分为两类,分别是强实体和弱实体。强实体是指不依赖于其他实体的实体;弱实体是指对另一个实体有很强的依赖关系的实体

属性,指实体的特性。比如超市的地址、联系电话、员工数等。在 ER 模型中用椭圆形来表示

关系,指实体之间的联系。比如超市把商品卖给顾客,就是一种超市与顾客之间的联系。在 ER 模型中用菱形来表示

注意:实体和属性不容易区分。这里提供一个原则:要从系统整体的角度出发去看,可以独立存在的是实体,不可再分的是属性。也就是说,属性不能包含其他属性

2. 关系的类型

在 ER 模型的3 个要素中,关系又可以分为3 种类型,分别是一对一、一对多、多对多

一对一:指实体之间的关系是一一对应的,比如个人与身份证信息之间的关系就是一对一的关系。一个人只能有一个身份证信息,一个身份证信息也只属于一个人

一对多:指一边的实体通过关系,可以对应多个另外一边的实体。相反,另外一边的实体通过这个关系,则只能对应唯一的一边的实体。比如说,我们新建一个班级表,而每个班级都有多个学生,每个学生则对应一个班级,班级对学生就是一对多的关系

多对多:指关系两边的实体都可以通过关系对应多个对方的实体。比如在进货模块中,供货商与超市之间的关系就是多对多的关系,一个供货商可以给多个超市供货,一个超市也可以从多个供货商那里采购商品。再比如一个选课表,有许多科目,每个科目有很多学生选,而每个学生又可以选择多个科目,这就是多对多的关系

3.ER模型转换为数据表

  • 强实体:不需要依赖其他任何实体
  • 弱实体:虽然都可以独立存在,但是它们都依赖某个强实体
  1. 一个实体转换成一个数据库
  2. 一个多对多的关系转换成一个数据表
    • 一般这种表称为中间表,必须包含两个多的实体的主键
  3. 通过外键来表达1对多的关系
    • 外键的作用主要是保证数据的一致性,但会导致性能降低,一般不建议设置
  4. 把属性转换成表的字段

MySQL高级索引及调优篇_第37张图片

7.数据表的设计原则

“三少一多”

  • 数据表的个数越少越好
  • 数据表中的字段个数越少越好
  • 数据表中联合主键的字段个数越少越好
  • 使用主键和外键越多越好

8.数据库对象编写建议

1.关于库

  1. 【强制】库的名称必须控制在32个字符以内,只能使用英文字母、数字和下划线,建议以英文字母开头
  2. 【强制】库名中英文一律小写,不同单词采用下划线分割。须见名知意
  3. 【强制】库的名称格式:业务系统名称_子系统名
  4. 【强制】库名禁止使用关键字
  5. 【强制】创建数据库时必须显式指定字符集,并且字符集只能是utf8或者utf8mb4。创建数据库SQL举例:CREATE DATABASE xxx DEFAULT CHARACTER SET 'utf8';
  6. 【建议】对于程序连接数据库账号,遵循权限最小原则使用数据库账号只能在一个DB下使用,不准跨库。程序使用的账号原则上不准有drop权限
  7. 【建议】临时库以 tmp_ 为前缀,并以日期为后缀;备份库以 bak_ 为前缀,并以日期为后缀

2.关于表、列

  1. 【强制】表和列的名称必须控制在32个字符以内,表名只能使用英文字母、数字和下划线,建议以英文字母开头

  2. 【强制】表名、列名一律小写,不同单词采用下划线分割。须见名知意

  3. 【强制】表名要求有模块名强相关,同一模块的表名尽量使用统一前缀。比如:crm_fund_item

  4. 【强制】创建表时必须显式指定字符集为utf8或utf8mb4

  5. 【强制】表名、列名禁止使用关键字

  6. 【强制】创建表时必须显式指定表存储引擎类型。如无特殊需求,一律为InnoDB

  7. 【强制】建表必须有comment

  8. 【强制】字段命名应尽可能使用表达实际含义的英文单词或缩写。如:公司 ID,不要使用 corporation_id,而用corp_id

  9. 【强制】布尔值类型的字段命名为 is_描述。如member表上表示是否为enabled的会员的字段命名为 is_enabled

  10. 【强制】禁止在数据库中存储图片、文件等大的二进制数据,通常文件很大,短时间内造成数据量快速增长,数据库进行数据库读取时,通常会进行大量的随机IO操作,文件很大时,IO操作很耗时。通常存储于文件服务器,数据库只存储文件地址信息

  11. 【建议】建表时关于主键:表必须有主键

    1. 强制要求主键为id,类型为int或bigint,且为 auto_increment 建议使用unsigned无符号型
    2. 标识表里每一行主体的字段不要设为主键,建议设为其他字段如user_id,order_id等,并建立unique key索引。因为如果设为主键且主键值为随机插入,则会导致innodb内部页分裂和大量随机I/O,性能下降
  12. 【建议】核心表(如用户表)必须有行数据的创建时间字段(create_time)和最后更新时间字段(update_time),便于查问题

  13. 【建议】表中所有字段尽量都是 NOT NULL 属性,业务可以根据需要定义 DEFAULT值。因为使用 NULL值会存在每一行都会占用额外存储空间、数据迁移容易出错、聚合函数计算结果偏差等问题

  14. 【建议】所有存储相同数据的列名和列类型必须一致(一般作为关联列,如果查询时关联列类型不一致会自动进行数据类型隐式转换,会造成列上的索引失效,导致查询效率降低)

  15. 【建议】中间表(或临时表)用于保留中间结果集,名称以 tmp_ 开头。备份表用于备份或抓取源表快照,名称以 bak_ 开头。中间表和备份表定期清理

  16. 【示范】一个较为规范的建表语句:

CREATE TABLE user_info (
`id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`user_id` bigint(11) NOT NULL COMMENT '用户id',
`username` varchar(45) NOT NULL COMMENT '真实姓名',
`email` varchar(30) NOT NULL COMMENT '用户邮箱',
`nickname` varchar(45) NOT NULL COMMENT '昵称',
`birthday` date NOT NULL COMMENT '生日',
`sex` tinyint(4) DEFAULT '0' COMMENT '性别',
`short_introduce` varchar(150) DEFAULT NULL COMMENT '一句话介绍自己,最多50个汉字',
`user_resume` varchar(300) NOT NULL COMMENT '用户提交的简历存放地址',
`user_register_ip` int NOT NULL COMMENT '用户注册时的源ip',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE
CURRENT_TIMESTAMP COMMENT '修改时间',
`user_review_status` tinyint NOT NULL COMMENT '用户资料审核状态,1为通过,2为审核中,3为未
通过,4为还未提交审核',
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_user_id`(`user_id`),
KEY `idx_username`(`username`),
KEY `idx_create_time_status`(`create_time`,`user_review_status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='网站用户基本信息
  1. 【建议】创建表时,可以使用可视化工具。这样可以确保表、字段相关的约定都能设置上

3.关于索引

  1. 【强制】InnoDB表必须主键为id int/bigint auto_increment,且主键值禁止被更新
  2. 【强制】InnoDB和MyISAM存储引擎表,索引类型必须为 BTREE
  3. 【建议】主键的名称以 pk_ 开头,唯一键以 uni_ 或 uk_ 开头,普通索引以 idx_ 开头,一律使用小写格式,以字段的名称或缩写作为后缀
  4. 【建议】多单词组成的columnname,取前几个单词首字母,加末单词组成column_name。如: sample 表 member_id 上的索引:idx_sample_mid
  5. 【建议】单个表上的索引个数不能超过6个
  6. 【建议】在建立索引时,多考虑建立联合索引,并把区分度最高的字段放在最前面
  7. 【建议】在多表 JOIN 的SQL里,保证被驱动表的连接列上有索引,这样JOIN 执行效率最高
  8. 【建议】建表或加索引时,保证表里互相不存在冗余索引。比如:如果表里已经存在key(a,b),则key(a)为冗余索引,需要删除

4. SQL编写

  1. 【强制】程序端SELECT语句必须指定具体字段名称,禁止写成*
  2. 【建议】程序端insert语句指定具体字段名称,不要写成INSERT INTO t1 VALUES(…)
  3. 【建议】除静态表或小表(100行以内),DML语句必须有WHERE条件,且使用索引查找
  4. 【建议】INSERT INTO…VALUES(XX),(XX),(XX)…这里XX的值不要超过5000个
  5. 【建议】SELECT语句不要使用UNION,推荐使用UNION ALL,并且UNION子句个数限制在5个以内
  6. 【建议】线上环境,多表 JOIN 不要超过5个表
  7. 【建议】减少使用ORDER BY,和业务沟通能不排序就不排序,或将排序放到程序端去做。ORDER BY、GROUP BY、DISTINCT 这些语句较为耗费CPU,数据库的CPU资源是极其宝贵的
  8. 【建议】包含了ORDER BY、GROUP BY、DISTINCT 这些查询的语句,WHERE 条件过滤出来的结果集保持在1000行以内,否则SQL会很慢
  9. 【建议】对单表的多次alter操作必须合并为一次对于超过100W行的大表进行alter table,必须经过DBA审核,并在业务低峰期执行,多个alter需整合在一起。因为alter table会产生表锁,期间阻塞对于该表的所有写入,对于业务可能会产生极大影响
  10. 【建议】批量操作数据时,需要控制事务处理间隔时间,进行必要的sleep
  11. 【建议】事务里包含SQL不超过5个。因为过长的事务会导致锁数据较久,MySQL内部缓存、连接消耗过多等问题
  12. 【建议】事务里更新语句尽量基于主键或UNIQUE KEY,如UPDATE… WHERE id=XX; 否则会产生间隙锁,内部扩大锁定范围,导致系统性能下降,产生死锁

9.PowerDesigner

PowerDesigner是一款开发人员常用的数据库建模工具,用户利用该软件可以方便地制作数据流程图概念数据模型物理数据模型,它几乎包括了数据库模型设计的全过程,是Sybase公司为企业建模和设计提供的一套完整的集成化企业级建模解决方案

十二、数据库其他调优策略

1.优化MySQL的参数

my.cnf的参考配置

[mysqld]
port = 3306 
serverid = 1 
socket = /tmp/mysql.sock 
skip-locking #避免MySQL的外部锁定,减少出错几率增强稳定性。
skip-name-resolve #禁止MySQL对外部连接进行DNS解析,使用这一选项可以消除MySQL进行DNS解析的时间。但需要注意,如果开启该选项,则所有远程主机连接授权都要使用IP地址方式,否则MySQL将无法正常处理连接请求!
back_log = 384
key_buffer_size = 256M 
max_allowed_packet = 4M 
thread_stack = 256K
table_cache = 128K 
sort_buffer_size = 6M 
read_buffer_size = 4M
read_rnd_buffer_size=16M 
join_buffer_size = 8M 
myisam_sort_buffer_size =64M 
table_cache = 512 
thread_cache_size = 64 
query_cache_size = 64M
tmp_table_size = 256M 
max_connections = 768 
max_connect_errors = 10000000
wait_timeout = 10 
thread_concurrency = 8 #该参数取值为服务器逻辑CPU数量*2,在本例中,服务器有2颗物理CPU,而每颗物理CPU又支持H.T超线程,所以实际取值为4*2=8
skip-networking #开启该选项可以彻底关闭MySQL的TCP/IP连接方式,如果WEB服务器是以远程连接的方式访问MySQL数据库服务器则不要开启该选项!否则将无法正常连接!
table_cache=1024
innodb_additional_mem_pool_size=4M #默认为2M 
innodb_flush_log_at_trx_commit=1
innodb_log_buffer_size=2M #默认为1M 
innodb_thread_concurrency=8 #你的服务器CPU有几个就设置为几。建议用默认一般为8 
tmp_table_size=64M #默认为16M,调到64-256最挂
thread_cache_size=120 
query_cache_size=32M

你可能感兴趣的:(mysql,mysql)