MySQL学习笔记整理

很久没写博客了, 感觉自己越来越懒,这样子不行 -0-,明明那么菜还懒,还是写点东西,加深自己的印象。

MySql是目前最主流的数据库,有太多值得深入学习地方。

Mysql逻辑架构

MySQL学习笔记整理_第1张图片

第一层:最上层非mysql独有,客户端连接到服务端,大多基于网络连接的工具都有,为连接处理、授权认证、安全,

第二层:服务层比较特别特别,mysql很多核心功能在此,包含查询解析,优化、内置函数、存储过程,触发器、视图等等

比如说解析优化:mysql会解析查询,并创建内部数据结构(解析树),对查询做各种优化,比如决定读表的顺序,使用什么索引,优化器不关心使用何种存储引擎,但存储引擎会影响查询的速率,某些特定存储引擎有特定查询优化方式。
用户可通过hint优化器影响它的决策,也可以根据优化器解释explain知道服务器如何优化决策。

第三层:主要包含存储引擎负责mysql数据存储和提取

数据库引擎: 用于存储、处理和保护数据的核心服务。

数据库事务,mysql隔离级别

事务不是mysql特有的,事务就是一组原子性的sql操作或者说独立的工作单元。

数据库操作的最小工作单元,是作为单个逻辑工作单元执行的一系列操作;这些操作作为一个整体一起向系统提交,要么都执行、要么都不执行。

事务四大特性(ACID)
atomicity:一个事务被视为一个不可分割的最小单元,整个事务的所有操作要么全部提交成功,要么失败全部回滚,对于一个事务来说不能只执行其中一部分操作。
consistency:数据库总是从一致的某个状态转为另外一个一致的某个状态,比如转账,一个少了100一个必须要多100
isolation:通常来说,一个事务在做最后提交之前对另外一个事务是不可见的,但也有可能存在脏读、幻读、不可重复读等
durability:一旦事务提交,将持久化保存起来,即使系统崩溃数据也不会丢失,但持久性也是个比较模糊的概念,不可能做到100%,就算备份也要保证备份的持久性。
隔离级别

查询当前mysql事务隔离级别

SELECT @@tx_isolation 
四大隔离级别
1.未提交读 read uncommit:
没提交就可读,很容易出现脏读,一般很少用


2.提交读 read commit
大多数数据库默认级别(如oracle,但mysql不是) 提交后可读 不会出现脏读 但是会出现不可重复读 两次读可能不一样,一个事务开始时,只能看见已提交的事务做的更改,一个事务开始到提交前做的修改对其它事务是不可见的

导致的问题:
两次读取结果不一样
比如说事务1读取个数据事务1未结束,同时事务2也访问该数据并修改,事务1又读了该数据两次结果不一样

3.可重复读 reoeatable read[mysql默认隔离级别]
保证同一个事务多次读取同样的记录是一致的,可重复读无法解决的问题是幻读问题。
一个事务做某个范围的查询记录,另外一个事务又加了新的记录,当之前事务读取该范围记录时会产生幻行

4.可串行化 serializable
mysql最大的隔离级别
指的是在同一事务下,连续执行两次同样的SQL语句第二次的SQL语句可能返回之前不存在的行;
可重复读指的是同一行在同一个事务下无论怎么读取都是同一个结果(除非自己把它改了)
事务隔离级别 脏读 不可重复读 幻读
读未提交(read-uncommitted)
不可重复读(read-committed)
可重复读(repeatable-read)
串行化(serializable)

MYSQL数据库引擎

查看当前使用的数据库引擎n种方案。

1.show table status like 'mall_cart' 查询当前表信息 行数 创建时间 引擎 使用字符集

MySQL学习笔记整理_第2张图片
其它重要字段
name 表名
create_time 创建时间
collation 字符集
engine 表使用存储引擎
index_length 索引的大小 以字节为单位

2.show create table xxx 表字段使用、使用引擎信息

MYSQL存储引擎

主要有myisam和innodb

myisam

mysql5.1版本前,默认存储引擎。

myisam提供大量特性,包括全文索引,压缩等,但不像innodb支持事务和行级锁。
myisam有个很大的缺陷就是崩溃后难以安全恢复。对于只读表/小表来说,有repair操作可以修复。
建表的时候尽量不要默认使用myisam而是innodb

存储

基于该引擎创建的表会把表存在两个文件中
①数据文件 .MYD后缀
②索引文件 .MYI后缀
加锁与并发

myisam会对整张表加锁,非针对单行,读取时会加共享锁,写表时加排它锁,有读取时也支持并发插入

修复

对于myisam表,mysql支持修复,这里的修复不是崩溃修复。

check table mytable检查表错误

repaire table mytable修复表(如果服务关闭 通过myisamchk命令做检查和修复)

压缩表

myisam支持压缩表

可以使用myisampack对基于myisam创建的表做压缩(也叫打包pack),但是压缩表的话是不能修改只读的,如果要编辑只能先解除压缩修改再做压缩,压缩表也支持索引但是索引也不能编辑。
优点:压缩表可以极大的减小磁盘占用,减少磁盘io。
缺点:不能编辑
myisam性能
myisam引擎设计相较来说比较简单,数据以紧密格式存储,用于只读或大部分读的场景性能可能是很不错的选择,但是做并发编辑的话可能会出现'表锁问题',使表查询长期处于'Locked'状态,这是myisam典型的性能问题。

Innodb

mysql默认引擎,也是最重要使用最广泛的存储引擎,它可以被用于处理大量的短期事务情况,除非有特定需求,一般都使用该数据引擎。

innodb的前身是2008年发布的innodb plugin插件,适用于mysql5.1版本,Oracle收购mysql后,mysql5.5版本后innodb plugin已经是原生编译了而不是作为插件。

innodb采用mvcc(Multi Version Concurrency Control,一种多版本并发控制,InnoDB在每行数据都增加两个隐藏字段,一个记录创建的版本号,一个记录删除的版本号,没做深究 - -)来支持高并发,实现了4种隔离级别,通过间隙锁防止幻读。

innodb基于聚簇索引建立,innodb的索引结构和其它存储引擎有很大不同

聚簇索引对主键查询有很高的性能,不过它的二级索引(非主键索引),必须包含主键列,如果主键太大会导致其他索引也很大,因此表上索引较多的话,主键应当尽可能小,后面如何建索引会说明。

innodb内部做很多优化,包括读取磁盘数据使用预测性预读,能够自动在内存中创建hash索引加速读操作,以及能够加速插入操作的插入缓存区。

作为事务型的存储引擎,innodb通过一些机制和工具真正实现热备份,oracle官方提供了一些插件和某些开源插件支持。myisam等其他引擎不支持。

其它引擎

作为一个小开发,以下几种用的不多,应该只要了解就好了。

csv引擎:不支持索引,可以把csv文件转换为表,逗号隔开,如果将数据存入,其它外部程序可以快速读到csv文件

merge引擎:可以把多个myisam表merge,用于日志或者数据仓库类使用,但引入分区后很少使用

memory引擎:需要快速访问,且数据不会被修改,重启后丢失也没关系。也成heap表,基于内存比myisam快一个数量级,不需要经过磁盘ip,memory表结构重启会保留,但数据会丢失

faderated:远程访问,会创建一个远程访问客户端连接,向远程数据库发起请求得到结果

如何转换数据引擎

Alter table mytable ENGINE = INNODB

Innodb数据页

页是innodb用于存储和管理数据的最小磁盘单位。innodb数据会存储在磁盘上,在真正处理数据时需先将数据加载到内存,取记录时,innodb无需从磁盘中一条一条取出数据,innodb方式,将数据划分为若干页,以页作为磁盘和内存之间交互的基本单位。即每次读数据至少读16kb内容到内存中,写时同理

常见的页类型有数据页、系统页、事务数据页等。

数据库采用数据页的形式组织数据
Mysql默认的非压缩数据页为16kb,在ibd文件中,0-16kb即为0号数据页,依次往下。

查看数据页状态

SHOW GLOBAL STATUS like 'Innodb_page_size' -- 数据页大小 默认16384b 即16kb

数据页格式:

名称 中文名 占用空间(字节) 描述
File Header 文件头部 38 页的一些通用信息[可以记录上下页的页号,页类型,页是双向链表,每页会记录上一页和下一页]
Page Header 页面头部 56 数据页专有的一些信息,比如本页行记录数量,槽数量
Infimum + Supremum 最小记录和最大记录 26 两个虚拟的行记录
User Records 用户记录 不确定 实际存储的行记录内容【真正存储数据的地方,数据以单向链表的形式存储若干条行记录】
Free Space 空闲空间 不确定 页中尚未使用的空间
Page Directory 页面目录 不确定 对User Records创建的稀疏目录,用于提升查询效率【由于行记录太多的话会导致查询变慢需要一个目录,内部使用二分法快速查找行记录中的数据】
File Trailer 文件尾部 8 校验页是否完整
MySQL学习笔记整理_第3张图片
在这里插入图片描述

在页中查找指定的一条记录

InnoDB的页目录(Page Directory)会将一个页中所有记录(User Records)划分为若干组,正常每组4-8个记录,首尾比较特殊,将每组最大行记录(根据rowid)的偏移量计算出来,放到槽中也称为槽,Page Directory由若干个槽组成。

换句话说,槽是每组最大那行的偏移量组成的数组,有多少个组就有多少个,这些槽组成了页目录,就像是一本书,找行记录的时候先通过rowid(主键)根据二分法找到槽在哪里,再遍历组寻找。

此处参考

在一个页中根据主键查找记录是很快的,查找步骤
1.二分法确定该记录所在的槽,并找到该槽所在分组主键值最小的记录。
2.通过next_record属性遍历组内单链表查找到记录(因为一组1-8个 所以边的遍历一般最多也就1-8次)

需要注意的是分组规则:

  • 「最小记录」自成一组
  • 包含「最大记录」的组一般为 1~8 条记录
  • 其它记录一般是 4~8 条分为一组

页合并和页分裂

页合并:不同页的数据合并到一个数据页中
页分裂:原来同一页的数据分裂到不同页去
为何会导致页分裂和页合并

页合并

当删除一行时,不会立马删除该记录,而是标记为已删除状态

当执行足够多删除操作,达到mege_threshold(页大小50%),mysql进行合并页优化空间

页分裂

页可以为空也可以被填充满,但是页中的数据又是按主键顺序排序的,所以如果按不规律的插入,因为必须排序,假如一页数据满了但新插入的主键id刚好又是在这页行数据的中间,就会产生页分裂的问题

MYSQL锁

加锁、锁的各种操作需要耗费资源,有些情况锁定的数据量越少,系统的并发度越高。

锁策略:锁的开销和数据的安全性寻求平衡,mysql提供多种选择,mysql存储引擎有可以实现自己的锁策略和锁粒度。

两种重要锁策略

表锁:

表锁是mysql最基本的锁策略,开销最小,锁定整张表,没有写锁时其它读取用户才能取到,读锁时共享的不会有阻塞。

尽管存储引擎可以管理自己的锁,但特定场景mysql会使用表锁达到目的,比如执行alter table之类的语句则会忽略存储锁机制。

行锁:

行锁可以最大程度的支持并发处理,同时也带来最大的锁开销,InnoDB和Xtra等存储引擎使用行级锁

索引

以前听过这么一句话,如果你不能一句话去解释一门技术,那说明你对它的了解还不够。如果要用一句话描述什么是索引。

索引也称为键,是存储引擎快速找到记录的一种数据结构。

为什么要学习索引,是因为索引对于良好的性能来说非常关键,特别是表数据量越来越大的时候,索引对性能的影响越来越大,反之不恰当的建索引或滥用索引可能还会导致性能变差,因此如何建索引也是一门学问。

索引的工作过程有点像日常看书的时候,根据目录去找主题,然后再定位到主题的内容或说页码。

mysql用类似的方式先根据索引找到值,再通过索引匹配记录找到对应行。

比如说用户表,要查出所有名为皮卡丘的用户行记录,用户名name带有索引,mysql先根据name找到值为皮卡丘,然后再返回对应的所有行。

常用索引类别

B树

b-tree:

按顺序存储,每个叶子节点到跟距离相同。

网上描述b-tree的文章很多,如https://www.cnblogs.com/vincently/p/4526560.html

b+tree

b+tree也成b+树,是基于b-tree的一种变种。如果不特殊说明,mysql表建索引都是采取该类型索引。需要注意的是聚簇索引不是索引类型而是一种数据存储方式,还是innodb特有的。

b+树的好处

1.可以加快访问数据的速度,单一节点存储更多元素,较少io次数
2.每个叶子节点到根距离相同,时间复杂度一样查询性能稳定
3.叶子节点存放所有数据,且是有序链表便于范围查询
b-tree通常可以支持“只访问索引的查询”,即查询只需要访问索引无需访问数据行

b-tree查询类型

b-tree只适用于全键值、键值范围、键前缀查找

其中键前缀查找也就是平时常说的最左匹配原则查找,这也算是mysql的一种特色。

关于最左匹配原则的原理,其实和索引结构密切相关,可参考这里

哈希索引

hash索引基于哈希表实现,只有精确匹配索引所有列的查询才有效

对于每一行记录都会计算一个hash码,hash索引将所以哈希码存在索引中

优点:访问特别快,除非有hash冲突

查找过程

比如name列建索引,peter为name字段值,8784位hash运算后得到的hash值

f(‘peter’)=8784

先根据某个字段计算hash值,根据hash值寻找对应记录指针,找到第三行指针再比较第三行的值是否为peter

缺点

hash索引只包含hash值和行指针不存字段值

hash不是按索引顺序做排序,无法做排序等范围查询,只能做等值,不能满足in

hash索引不支持索引列匹配查找,如果在数据列(A,B)上建索引,如果只查数据列A则无法使用索引

hash冲突多,查询性能和一些增删操作代价比较高,性能不一定高

自适应哈希索引

innodb其中一个特性,SHOW ENGINE INNODB STATUS可以查看当前自适应哈希索引使用情况。

如果开启该索引,innodb会监控常用的等值查询,利用buffer pool在b+tree的结构上建一个hash索引,提高查询效率。

查看当前自适应哈希索引:show variables like '%ap%hash_index';默认开启

参考InnoDB关键特性之自适应hash索引

全文索引

MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较是否相等。

查找条件使用 MATCH AGAINST,而不是普通的 WHERE。

全文索引使用倒排索引实现,它记录着关键词到其所在文档的映射。

InnoDB 存储引擎在 MySQL 5.6.4 版本中也开始支持全文索引。

空间数据索引

MyISAM 存储引擎支持空间数据索引(R-Tree),可以用于地理数据存储。空间数据索引会从所有维度来索引数据,可以有效地使用任意维度来进行组合查询。

必须使用 GIS 相关的函数来维护数据。

索引的优缺点

优点
1:大大减少扫描的数据量
2:查询时避免排序已自动排好序
3:可以将随机io变为顺序io,比如做范围查询时是顺序读取磁盘
4:通过建唯一索引,可以保证行数据唯一
5:表之间关联,可以加快查询查询

缺点
1.索引需要占用磁盘空间,而且数据量小不需要反而更慢,太多的索引反而降低性能,比如100页的书有50页的目录
2.对于如delete、update、insert的操作,索引会降低速度,因为mysql不仅要把改数据文件,还要改索引文件
3.建立索引会增加额外索引开销,维护索引也需要成本

在大多数场景下,建索引还是利大于弊,因此建表初期就应该尽量建好索引,否则数据量大的时候改动成本可能就会变高。

关于聚簇索引、辅助索引、覆盖索引、前缀索引

聚簇索引(聚集索引):不是一种单独的索引类型,而是一种数据存储方式,innodb的聚簇索引就是指b-tree索引和数据行组成的数据存储方式。

这边聚簇指的就是数据行和键值紧凑的存储在一起。

需要注意的是,一张innodb表只有一个聚簇索引,而且是主键,如果没设主键,innodb会隐式帮忙定义生成一个类似rowid的东西作为聚簇索引。

当表有聚簇索引时,它的数据行实际存放在索引的叶子页中,由于无法把数据行保存在两个不同地方,所以一个表只有一个聚簇索引,覆盖索引可以模拟多个聚簇索引的情况。

聚簇索引使用的索引列为主键列,在innodb中如果没有定义主键,

聚簇索引的优点:
1可以把相关数据保存一起,只需从磁盘读取少量数据页就可以获取某个用户全部邮件,通过id获取邮件,如果没有可能一个邮件就有一次io
2数据访问更快 聚簇索引和数据一起存在b-tree 因此从聚簇索引中获取数据比非聚簇索引查询更快,减少一次回表
3使用覆盖索引可以直接使用叶节点中的主键值

缺点:
主要是提高io性能,且是顺序,但是对比速度快且随机性能高的内存,没什么优势
插入速度严重依赖插入顺序,如果不按主键列插入 速度会有点慢
更新聚簇索引代价很高,会强制innodb的每个行移动到新的位置 
基于聚簇索引的表再插入新行时,或主键更新需要移动行时,可能出现页分裂问题。
聚簇索引可能导致全表扫描变慢,特别在行稀疏,或者由于页分裂导致行数据存储不连续时

辅助索引(二级索引、非聚集索引):在innodb中,除了基于主键创建的聚集索引,其它索引如唯一索引、普通索引都是辅助索引。

二级索引可能比想象大因为除了自身索引值,二级索引叶子节点还包含引用行的主键值而不是指向行的指针。
这意味着使用二级索引查找行数据时,存储引擎会先根据二级索引的叶子节点获取主键值,在根据主键值去聚簇索引中找到对应的行记录并返回。

覆盖索引:如果在一个查询中,一个索引包含(或者说覆盖)说要查询字段的值,我们就称之为覆盖索引,覆盖索引准确来说也不应该算是一种索引类型,而是一种提高性能的查询方式或者说工具。

如果一条sql使用覆盖查询,则可以极大的提升性能,该查询只要查询索引而无需回表。这也是为什么推荐使用select 索引字段 from table而不是select * from table ,select * 会导致没必要的回表。

覆盖索引带来的好处
①只读取索引,mysql可以极大减小数据访问量,不用再去查询数据行并返回数据行,一方面可以减少io,也对缓存的性能有所提高,mysql不用花更多的时间做数据拷贝,只需拷贝索引数据即可,且索引比数据更小,更容易放在内存中代价更小。
②对于b+树来说,索引是按照值顺序存储的(至少对单个数据页来说),顺序io往往是快的

**前缀索引: **当要索引的列字符很多时 索引则会很大且变慢,使用前缀索引能有效减小索引文件大小,提高索引速度,适用于大字段

建立前缀索引的语法

ALTER TABLE table_name ADD KEY(column_name(prefix_length));

比如:ALTER TABLE city ADD KEY(cityname(7));

前缀索引缺点
1.一般只能用于普通索引中而不能使用unique,强行使用会有问题
2.只支持英文和数字
3.mysql不能再order by或group by使用前缀索引,也不能作为覆盖索引

关于联合索引(多列索引)

由多列组成的的索引。遵循最左前缀规则。对where,order by,group by 都生效,在一些常用的组合查询场景中,建立联合索引可以很大程度提高查询性能。

需要注意的是

1.联合索引也需要遵循最左匹配原则,这也是由于它的结构,比如建了联合索引(name,age)

①select * from stu where name=?②select * from stu where name=? and age=? ③select * from stu where age=? and name=? (优化器会优化)这3条sql都会走联合索引,但是select * from stu where age=?则不行,因此建议联合索引把经常查询的字段放在左边。

2.联合索引的数据只要有一列是null值就不生效

3.联合索引有"最左前缀"原则,遇到范围查询(>.<.between)这样无效

4.当创建(a,b, c)联合索引时,相当于创建了(a),(a,b),(a,b,c)三个索引,想要生效的话也只有这三个组合,但是很神奇的ac,ca(会有优化)也可以,原因在于有带a就会去走a单列索引,总结一下就是abc3个最左边的带头大哥(a)不能没,如果是bc是不行的。

联合索引优点像一种结构的电话簿,电话簿首先按姓做排序,然后再按名排序,如果只知道姓可以很快定位到电话,只知道名就没啥帮助了。

什么是三星索引

如果与一个查询相关的索引行是相邻的,或者至少足够靠近的话,那这个索引就可以被标记上第一颗星
三星索引是一种理想的索引设计方式,真实情况往往很难三颗星全都达到
三星系统的索引
1:索引将相关的记录放在一起,一星
2:索引的顺序和查找数据的顺序一致 二星
3:索引包含要查询数据的全部列避免去聚簇索引进行一次随机的IO查询 三星

MYSQL优化

优化这个词比较泛,对mysql而言,优化主要分为

1.查询优化: sql查询优化

2.索引优化: 构建高性能索引,选择合适索引

3.库表优化: 建表设计、库表设计等优化

4.其它(如存储优化(分库分表)、碎片优化、读写分离等等)

1.查询优化

查询优化主要是对sql查询语句优化

定位慢sql

遇到慢查询时,可以在查询语句前加explain关键字定位慢查询的原因。

使用explain定位

描述这个的文章太多了,比如这个整理的比较详细

https://www.cnblogs.com/butterfly100/archive/2018/01/15/8287569.html

高性能mysql这本书在附录D也有大量篇幅做介绍。

比较重要的字段

  • select_type : 查询类型,有简单查询、联合查询、子查询等
  • key : 使用的索引
  • rows : 扫描的行数

查看慢查询日志

MySQL慢查询日志总结

1.show variables  like '%slow_query_log%'; -- 查看是否开启慢查询日志
2.show variables like 'long_query_time%'; -- 查看慢查询设置秒数
3.show variables like 'slow_query_log_file'; --查看慢查询生成文件路径
如果开启慢查询日志1 sql查询到达指定时间2 就会在指定目录下3生成日志用作分析

优化数据访问

1.减小请求数据量

  • 只返回必要的列:最好不要使用 SELECT * 语句。
  • 只返回必要的行:使用 LIMIT 语句来限制返回的数据。
  • 缓存重复查询的数据:使用缓存可以避免在数据库中进行查询,特别在要查询的数据经常被重复查询时,缓存带来的查询性能提升将会是非常明显的。【一般缓存可用redis】

2.减少服务器端扫描行数

最有效的方式是使用索引

重构查询方式

1.切分大查询或者大操作

比如有张日志表要做定时清理,每次清理一个月前全部数据,如果一次性执行,很有可能会一次性锁住很多数据,沾满整个事务日志、耗尽系统资源,阻塞其它小而重要的查询

DELEFT FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH);

优化为

rows_affected = 0
do {
    rows_affected = do_query(
    "DELETE FROM messages WHERE create  < DATE_SUB(NOW(), INTERVAL 3 MONTH) LIMIT 10000")
} while rows_affected > 0
2.分解大连接查询
阿里开发规约中不建议三张表的连接查询,因为多表join容易出现多种问题

将大连接分解成若干个单表查询,然后做关联,这样做的好处有

  • 让缓存更高效。对于连接查询,如果其中一个表发生变化,那么整个查询缓存就无法使用。而分解后的多个查询,即使其中一个表发生变化,对其它表的查询缓存依然可以使用。
  • 分解成多个单表查询,这些单表查询的缓存结果更可能被其它查询使用到,从而减少冗余记录的查询。
  • 减少锁竞争;
  • 更灵活,在应用层进行连接,可以更容易对数据库进行拆分,从而更容易做到高性能和可伸缩。
  • 查询本身效率也可能会有所提升。例如下面的例子中,使用 IN() 代替连接查询,可以让 MySQL 按照 ID 顺序进行查询,这可能比随机的连接要更高效。
SELECT * FROM tab
JOIN tag_post ON tag_post.tag_id=tag.id
JOIN post ON tag_post.post_id=post.id
WHERE tag.tag='mysql';

优化为

-- 得到之前的结果 再把结果作为下个查询的条件
SELECT * FROM tag WHERE tag='mysql';
SELECT * FROM tag_post WHERE tag_id=1234;
SELECT * FROM post WHERE post.id IN (123,456,567,9098,8904);

常见sql查询优化方式

1.避免大表全表查询,可用explain查看扫描行数,使用索引、limit等方式,如果多个字段频繁查询可考虑使用合并索引[有些情况容易导致全表扫描,比如查询中使用like、or、在where =前使用函数等,in有时候也会导致,对于连续的数值可考虑between不用in]
2.尽可能使用覆盖索引,不查询无用字段
3.统计数量时使用count替代sum,count条件为true统计数量
4.做关联查询的时候小表驱动大表,有时候子查询使用exist替代in
5.有时候不走索引可以在sql中使用force强制走
6.优化limit分页,limit分页查询查询到后面容易出问题。下面会说
7.减少扫描行数,最理想的情况扫描行数=返回行数,这需要适用合适的索引。explain的rows可以查看扫描了多少行(注意只是预估可能并非实际扫描)
小表驱动大表

即小的数据集驱动大的数据集

mysql关联算法是通过驱动表的结果为循环基础数据,然后一条条通过该结果集数据作为过滤条件到下一个表查询,合并结果

例子

-- 假设user表 10w条数据 class表20条
select * from user u left join class c u.userid=c.userid -- 循环10w次
select * from class c left join user u c.userid=u.userid -- 循环20次

合理的使用exists可以达到小表驱动大表的效果,拓展

EXISTS(subquery)只返回TRUE或者FALSE,因此子查询中的SELECT * 也可以是SELECT 1或者SELECT 'X',官方说法是实际执行时会忽略SELECT清单,因此没有区别

EXISTS子查询的实际执行过程可能经过了优化而不是我们理解上的逐条对比,子查询中是可以用到索引的

如果两个表中一个较小,一个是大表,则子查询表大的用exists,子查询表小的用in, 快速记忆方式exists比in大(长) ,exists右边跟着大表快,in右边跟着小表快

例如:表A(小表),表B(大表)

select * from A where cc in (select cc from B) ;  # 效率低,用到了A表上cc列的索引;
select * from A where exists(select cc from B where cc = A.cc) ;  # 效率高,用到了B表上cc列的索引。 

select * from B where cc in (select cc from A) ;  # 效率高,用到了B表上cc列的索引;
select * from B where exists(select cc from A where cc = B.cc) ;  # 效率低,用到了A表上cc列的索引。

需要注意的是,not in查询是不会使用到索引,而not exists的子查询能使用到索引,所以无论哪个表更大not exists都比not in快

对limit查询优化【limit查询到后面会越来越慢,如何优化】

常见的limit分页问题,即随着分页页数变大,查询效率低下的问题

比如

select * from user limit 0,10 -- 刚开始10行很快
select * from user limit 100000,10 --当查到后面的页数时显著变慢
之所以会变慢是因为limit 100000,10会扫描前面的100010行然后扔掉前面的数据最后返回10行,会导致扫描过多行数的问题

解决方式:可以用覆盖索引查询(只包含id)并用id>=xxx的形式做优化

以上sql优化为

select * from user where id >= (select id from product limit 10000, 1) limit 10
或者
select * from user a join (select id from user limit 10000, 10) b on a.id = b.id

2.库表优化

库表优化主要是对数据库组织结构。如表结构,字段数据类型等设计的优化。

良好的逻辑和物理设计是高性能的基石。【高性能mysql第四章】

schema

什么是schema:是数据库的组织和结构,数据库对象的集合,包括表、视图、存储过程、索引

在MySQL数据库中,我们可以通过CREATE SCHEMA语句来创建一个数据库

一个schema可以等同理解为一个数据库,定义数据库的模式

常见数据类型优化

1.mysql数据字段类型非常多,选择合适的数据类型对提高性能帮助很大。合理的情况,更小的通常更好,因为占用的磁盘、内存、cpu缓存更少,而且处理需要的cpu周期也更少。[比如一个数就0-200 可以设计为tinyint]
2.简单的字段,通常整型比字符型操作代价更低,因为字符集和字符串的排序规则(或者说比较)比整形更为复杂,可以用整形存储ip地址,
32为无符号整数,mysql提供inet_aton()和inet_ntoa()函数ip和整形做转换。
3.尽量避免使用null,如果查询结果包含null对mysql来说不好优化,因为null列使索引计算和值比较变复杂,
4.选择合适类型,比如datatime和timesmap都可以存时间,timesmap占用一般的存储空间,并且会根据时区变化具有特殊的自动更新能力,另一方面因为时间范围比较小也会受到限制。
datetime:1001年到9999年 精度为秒 yyyymmddhhmmss与时区无关 用8个字节存储
timestamp:1970.1.1午夜到2038年前,空间效率更高
5.使用无符号 tinyint默认-128-127 使用unsigned可以存0-255
6.选择合适类型如 varchar类型和char类型
varchar节约存储空间对性能帮助更大,合适的类型在做关联查询时可以提高性能
7.选择合适大小:更长的列会消耗更多的内存,最好分配真正需要的空间
8.使用枚举(enum)代替字符串类型
可以使用枚举代替,mysql存储枚举很紧凑,会做一些压缩处理,做一个数字-字符串关系映射 在表的.frm文件中会存在映射表
实际存整数,可以通过字段+数字方式查出数字。
但是注意的是双重性容易导致混乱,所以尽量避免枚举的时候使用数字,应用字符串
9.使用bit为数据,5.0前和tinyint一样,mysql把bit当做字符串非整数
比如00111001 得到57的字符串 对应ascii码为9 但用作数字是57

合理使用范式和反范式【或者说混用】

什么是范式:范式是最小冗余的表结构,可减少数据库中数据冗余
所谓的范式是一种理想的情况,实际上真实的应用场景很少会用完美的范式,一般都是混用舍弃部分范式。

数据库三范式

①一范式即属性不可分割,每列都是不可再分的最小单元,第一范式的目标是为了确保每列的原子性,比如城市地址如果只有一列存中国北京,这是违反第一范式的,要满足第一范式的话应该使用两列,国家字段和城市字段

②二范式即需要有主键,并且非主键的列和主键不存在部分依赖而是完全依赖,一张表只描述一件事

为什么需要主键:没有主键就没唯一性

其它字段为什么要依赖主键:不依赖主键就找不到他们

③三范式即满足第一第二的同时并消除传递依赖,消除传递依赖即消除冗余,各个信息应该只在一个地方存储不出现在多个表中

错误的事例:Orders订单表同时含订单编号,订单人编号,订单人姓名,这个时候订单人姓名依赖订单人编号,应该在其它表里有了依赖关系,为了消除冗余需要把订单人姓名该列删掉【但实际上为了查找快点减少关联查询,有些时候订单表依然会保存订单人姓名破坏第三范式,这就是反范式】

范式优点
关联小,范式的更新操作通常更快
范式的表通常也比较小,执行操作更快
但数据范式化的话,没有太多冗余,更新操作比较容易,只要修改更少数据即可
可以很少使用distinct group by这样的语句
范式缺点
通常需要表关联,如果复杂的话要经过多次表关联,范式化会时列尽可能的在不同表中。

反范式优点
在一个表里可以尽量避免关联,某些场景查询效率可能更高,上面的订单例子即是。

加快alter操作速度

一种技巧 影子拷贝 用新表交换。备机执行alter操作完成之后切换为主库【因为很耗时】

使用分区优化

什么是分区:指按一定规则,同一个表不同的行记录被分配到不同的物理文件中,分区是将一个表或索引分解成更多更小可以管理的部分,每个区都是独立可管理的,每个区的聚簇索引和非聚簇索引都放在各自区的物理文件里

比如使用hash分区的规则建表
CREATE TABLE employees (
    id INT NOT NULL,
    fname VARCHAR(30),
    lname VARCHAR(30),
    hired DATE NOT NULL DEFAULT '1970-01-01',
    separated DATE NOT NULL DEFAULT '9999-12-31',
    job_code INT,
    store_id INT
)
PARTITION BY HASH(store_id)
PARTITIONS 4;
分区类型
range分区最为常用连续区间列值最好为整型,如果是日期最好也使用函数切换为整型
list 与range类似,区别在于list是集合,range是连续
hash分区 基于给定的分区个数 将数据分配到不到分区 只能支持整数 取模运算
key分区 和hash差不多 但是支持字符串 基于列的md5值 做hash运算 可以使用字符串,具体差别参考https://www.cnblogs.com/shibazi/p/3832852.html
CREATE TABLE `access_log` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `access_time` datetime NOT NULL,
  PRIMARY KEY (`id`,`access_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
/*!50100 PARTITION BY RANGE (to_days(access_time))
(PARTITION p1 VALUES LESS THAN (to_days(20190101)) ENGINE = InnoDB,
 PARTITION p2 VALUES LESS THAN (to_days(20190102)) ENGINE = InnoDB,
 PARTITION p3 VALUES LESS THAN (to_days(20190103)) ENGINE = InnoDB) */;

分区后每个分区都对应一个ibd文件

新增
alter table access_log add partition(
    partition p4 values less than (to_days('20190105'))
);

删除
alter table access_log drop partition p1;

拆分
alter table access_log reorganize partition p4 into(
-> partition s0 values less than(to_days('20190104')),
-> partition s1 values less than(to_days('20190105'))
-> );

合并
alter table access_log reorganize partition s0,s1 into ( 
partition p4 values less than (to_days('20190105')) 
);  
注意点
mysql如果存在主键和唯一键,分区列必须包含在其中
分区字段不能为null 不然没法区分 所以尽量避免not null
分区数不能超过1024
不支持外键
只能对数据表的整形做分区,如果不是通过分区函数转为整形
分区表不影响自增列
分区的优点
1.与单个磁盘或文件系统分区相比可以存储更多数据,性能高,添加数据的过程可以为新数据新开辟一个分区
2.查询得到优化,找到匹配的没必要再去其它分区查找
3.一些sql执行速度快 比如sum count 不同分区可以并行
4.吞吐量高

索引优化

创建高性能的索引,选择合适的索引

  • 创建高效的索引加快查询,使用合适的索引列,比如整形列,比如经常查询的列建索引,联合索引,比如一些大字段如blob、text和大的varchar类型的列,需使用前缀索引,即只匹配索引开始的字符

  • 将选择性高的列放到最前列(冲突少或没冲突)

  • 索引的长度尽可能短,可提高查询性能,特别是主键索引,如果主键索引过长,不但会导致查询性能变差,同时由于二级索引都带主键索引,也会导致占用更大的磁盘空间。

注意点:索引也并非是最好的解决方案,小表不适用索引,全表反而更快,当帮助存储引擎快速找到记录的好处大于额外工作时建索引,但是对于特大型的表,建索引和使用索引的代价也会增长,io次数变多,树太大,这个时候可以考虑使用上面说的分区。

索引常见优化方式

1.优先考虑在常用在where和order by的字段上建索引
2.频繁用来查询的组合字段可考虑联合索引,即索引合并
3.索引字段尽量用小的,比如无符号的int
4.选择合适的索引,常用innodb,一些场景也可以考虑hash比如只用于做唯一查询没涉及到范围查询等情况。

在一张大表里修改索引的代价是很高的,会导致表底层结构重建,因此在建表前就应该考虑好索引的创建

分库分表

因为当数据库量过大大于一定量的时候,可以考虑分库分表

数据库是存在瓶颈的,这里的瓶颈有

①io瓶颈,磁盘io瓶颈,查询大量数据产生多次磁盘io读写,网络io,请求数据太多导致网络io带宽不够

②cpu瓶颈,mysql在做复杂操作时可能会比较耗cpu

不管是磁盘还是cpu等瓶颈都会使用数据库负载变高,进而也会导致活跃连接数变多达到阈值,有时候可以考虑使用分库分表可以有效缓解io/cpu/并发等瓶颈问题。

水平分库、分表

水平分库:以字段为依据,将一个表的数据分到不同的库中,每个库可以在不同服务器上

比如根据user_id 根据hash分到不同的表中,满足

1.所有表的数据结构相同

2.数据不同

3.全部数据为所有表的并集

水平分表:类似水平分库,不过水平分表是放在同一个库的不同表中,比如用户信息同时存在user_1,user_2,user_3,3张表中,根据用户id取模分析存在哪张表中。也满足水平分库的3个条件

垂直分库、分表

垂直分库:将一张表切分多个表,每个表存不同列,并在不同的库中,每个库可以在不同服务器上

在数据库层面是使用垂直切分将表密集程度部署在不同库中,比如电商数据库分为商品数据库、用户数据库等等

垂直分表:垂直切分表在同一个库不同表里

分库分表的优缺点
1.提高数据松散度,表数据量在可控范围内可提高查询效率,否则所有数据都在单表可能查询会很慢,同时也可降低行锁竞争等
2.提高数据库并发能力,水平在不同服务器上,提高并发能力并可横向扩容

如果要说缺点,可能就是维护起来比较麻烦吧

常用的分库分表工具

很多都是听说过,并没实操过。。

  • Cobar
  • TDDL
  • Atlas
  • Sharding-jdbc
  • Mycat

读写分离

比如主数据库负责写操作和实时性要求高的读操作,而从数据库处理读操作

为何读写分离

1.主从服务器分表负责渡河写,极大缓解锁竞争
2.从服务器可以使用myisam,提高读性能和结余系统开销
3.可能也方便做权限控制,比如某些场景不能写则不能配置主数据库账号密码

碎片优化

碎片产生原因

什么会产生空间碎片:上面讲数据页的时候有说过mysql删除数据页的数据后并不会彻底从磁盘删除而是留个标志,这将导致表空间不被回收,ibd文件越变越大,这些占着茅坑不拉屎的数据就可以理解为碎片。

删除数据和添加数据后回滚都会产生碎片

频繁页分裂也会产生碎片:因为频繁的页分裂会使数据页变得稀疏,被不规律的填充

碎片的危害

碎片率过高,不但会占用磁盘空间,并且mysql做读取的时候,仍然会扫到留白空间,因此碎片过多可能影响查询性能。

比如一张表占10万字节大小,刚开始碎片为0,删除大量数据后,产生了9万字节的空间碎片,后续mysql在查询的时候依然会扫描到这9万字节,把这张表当做10万字节来处理,这样的话多少回影响性能。

列出所有存在空间碎片的表

select table_schema db, table_name, data_free, engine from information_schema.tables where table_schema not in ('information_schema', 'mysql') and data_free > 0; -- 该查询可能比较慢

表空间、碎片率

表空间=data_length+index_length+data_free

碎片率=data_free/表空间

查看现innodb表空间存储方式

show variables like '%per_table%';

如果为on代表使用共享空间,yes代表独立表空间,老版本的mysql中,innodb都是采用共享空间的方式存储数据,现在一般都是用独立的,mysql5.6以后默认使用独立表空间,所以基本也看不到ibdata1文件。

切换存储方式为独立表空间

修改配置文件my.cnf中的参数innodb_file_per_table = 1,重启服务。

但是只对新建的表生效,以前的表还是存储在ibdata1文件里。

独立表空间优缺点

优点:
1.每个表的数据、索引放在自己的表空间中,独立
2.独立的表空间,碎片影响较小
3.空间可以回收,需要注意的是drop、truncate无法回收,使用下面说的

缺点:
单表对比共享,表空间占用可能更大

总结:
共享表空间在insert操作上性能更好,但是综合来说,还是独立开来更好。

需要注意的是,如果使用的是共享表空间并且有碎片的情况,即使使用optimize table也无法使ibdata1文件变小,但是如果是独立表空间,那么idb文件将会变小。

碎片处理

mysql提供了一种简便的方式清空data_free

optimize table 表名;  -- 对大表来说是个漫长的过程,因为它会对重建表,释放聚簇索引未使用的空间,[我猜可能还会做一些页合并的操作]

执行后data_free将会清0,如果是myisam表,.MYD文件变小,如果是innodb表,.IDB文件变小

注意点
1.OPTIMIZE TABLE只对myisam、InnoDB、BDB引擎的表起作用
2.OPTIMIZE TABLE运行过程中,MySQL会锁定表,如果是大表这将是个漫长的过程。官方不建议经常执行,正常情况也比较少用,可按周、月执行一次,具体应该要看表的规模大小。

常见疑问

MyISAM和Innodb区别

①innodb支持事务,myisam不支持,对于innodb的每条sql都默认封装成事务,自动提交,所以最好多条放在begin和commit之间组成一个事务。
②innodb支持外键,myisam不支持,对一个包含外键的innodb转为myisam会失败。
③innodb使用聚簇索引,数据和主键索引绑在一起没通过主键索引查询效率很高,辅助索引的话需要两次做回表,先查询主键,再根据主键查询到数据,因此主键不能太大,主键太大会导致其他索引也比较大。
innodb主键索引的叶子节点存有数据文件,辅助索引的叶子节点存主键值,而myisam的b+数主键索引和辅助索引的叶子节点都是数据文件的地址指针。
④innodb不保存具体行数,select count(*)需要全表扫描[虽然走索引但也要一条一条计算] 而myisam用一个变量保存整个表的行数,在最新一行
[为何innodb没有这个变量,由于事务特性 innodb对于每个事务中返回多少行不确定 只能一行一行读出来判断总数  粗略统计可以用show status from product; 缓存计数虽然效率快 但无法保证一致性]
⑤myisam可以压缩后做查询
⑥innodb默认行锁也支持表锁,myisam仅支持表锁 innodb行锁实现在索引上非物理行上,那么访问没有命中行锁的话将会退化成表锁锁住全表。
⑦innodb表必须要有主键[用于根据主键存放数据],没有的话自己生成一个rowid,一般用自增列或者唯一标识符的唯一主键
⑧innodb存储文件有frm ibd myisam是frm myd myi [frm是表定义文件]

如何选择这两种存储引擎

支持事务使用innodb

只读、读多写少并且能忍受崩溃使用myisam

innodb适合增删改频繁、可靠性要求高要求事务的场景

尽量使用innodb

冷知识

1.二级索引最多只能建64个
2.联合索引最多只能16个,超过将报错
3.一个表最多可以有1024列
4.mysql高点的版本建表varchar指定过大的话(比如60000+),会自动转换成mediumtext等类型
innodb行数据如果没blob/text这样的可变字段,一行行数据最多只能占8126字节 至于为啥

这个数据页有关系,我们知道innodb数据存储在一个叫*.ibd的文件中,前面也说了很多关于数据页的概念

当page是默认大小16kb时时,一个extent大小为1M即最多有64个page数据页,一个页包含2-n个数据行,innodb有个强制规定,每个页至少含两个数据行(2-n行),这样的话一行最大则不能超过8KB【单个数据行的最大限制为(16kb(16384)-header等元数据信息/2 = 8126】,否则插入会导致行溢出报如下错误。

SQLSTATE[42000]: Syntax error or access violation: 1118 Row size too large (> 8126). Changing some columns to TEXT or BLOB or using ROW_FORMAT=DYNAMIC or ROW_FORMAT=COMPRESSED may help. In current row format, BLOB prefix of 768 bytes is stored inline

根据报错提示,是因为因为数据行过大,建议一些字段声明为text或blob【如果为可变的长字段格式(text/blob/varchar),innodb默认会将前768个字节数据和20字节的指针数据存储在innodb page的行中,其它放在特殊的地方(指针指向,用于放出溢出数据)】

Compact 和 Redundant ,这两种格式数据页行记录数据该字段会存放768个字前缀字节和20字节指针数据指向溢出页,其余数据放在溢出页

Compressed 和 Dynamic和上面的主要区别在于数据页中只是放20个字节的指针,实际数据放在一个叫off-page的页中

参考行格式模式

![image.png](https://upload-images.jianshu.io/upload_images/10006199-beb5d0f3f7a65b6a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

![image.png](https://upload-images.jianshu.io/upload_images/10006199-f133ba3084f29811.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
![image.png](https://upload-images.jianshu.io/upload_images/10006199-b14bbbe77f044ef3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)


根据上述报错提示,解决方式有两种
①字段改为blob/text格式,这样行数据一般不会太大,因为额外数据放在溢出页【但是768也挺大的,必要时使用方案2】
②using ROW_FORMAT=DYNAMIC or ROW_FORMAT=COMPRESSED 设置为这两种行格式,节省空间,即使有很多大字段在行数据中一列也才占20字节的指针数据。
show table status like '表名'; -- row_format属性

-- mysql行格式5.7以前
show variables like "innodb_file_format";

-- 5.7以后 
show variables like "innodb_default_row_format"; -- 默认值为DYNAMIC
也可以使用show table status like '表名'; 【查看row_format属性】  
mysql单行限制到底是8126还是65535
主要原因是mysql分server层和存储引擎层,mysql在server层做了次限制还会在innodb存储引擎层加个限制【简单来说就是mysql约定一行数据里字段最大的长度是64kb,由于innodb结构的特殊性,限制(8192)】
具体参考:https://mp.weixin.qq.com/s?__biz=MzAwMjkyMjEwNg==&mid=2247483785&idx=1&sn=1d90a44915d1028c6dc150367e1af033#rd

总结

mysql细碎的知识点太多,即使花了几天时间整理笔记,也只是很小的一块内容,后续也会继续做更新,做笔记的同时也能分享给别人,笔记里的大部分也都是从高性能mysql第1-7章内容,如果大家想深入学习,最好还是系统的看书和官方文档,本人水平有限还是小菜,如果有啥不对的地方欢迎指出。

参考链接

高性能mysql

官方文档

什么是DB的三星索引

Mysql慢查询日志总结

如何理解关系型数据库的常见设计范式

mysql表碎片化的原因

你可能感兴趣的:(MySQL学习笔记整理)