一条更新语句如:update T set c = c + 1 where ID = 2;和查询的流程差很少,可是更新流程还设计到两个重要的日志模块:分别是redo log(重作日志)和binlog(归档日志).mysql
当有一条记录须要更新的时候,InnoDB引擎就会把记录写到redo log(粉板)里面,并更新内存,InnoDB引擎会在适当的时候,将这个操做记录更新到磁盘里面,而这个更新每每是在系统比较闲的时候作,这就像打烊之后的掌柜作的事,sql
InnoDB的redo log是固定大小的,好比能够配置一组4个文件,每一个文件的大小是1GB,那么这个“粉板”总共就能够记录4GB的操做,从头开始写,写到末尾就又回到开头循环写,以下图:数据库
有了redo log,InnoDB就能够保证即便数据库发生异常重启,以前提交的记录都不会丢失,这个能力称之为crash-safe;数组
##binlog redo log是引擎特有的日志,server层也有本身的日志,称为binlog(归档日志)安全
这两种日志的不一样点:性能优化
update语句的内部流程:bash
将redo log的写入拆成两个步骤:prepare和commit ,这就是两阶段提交.服务器
若是不采用两阶段提交产生的后果:数据结构
哈希表架构
是一种以键-值(key-value)存储数据的结构,咱们只要输入待查找的值即key,就能够找到其对应的值即value.哈希的思路:把值放在数组里,用一个哈希函数把key换算成一个肯定的位置,而后把value放在数组的这个位置.
不可避免,多个key值通过哈希函数的换算,会出现同一个值的状况.处理这种状况的一种方法是,拉出一个链表.
哈希表这种结构适用于只有等值查询的场景,好比Memcached及其余一些NoSQL引擎.
有序数组 假设身份证号没有重复,这个数组就是按照身份证号递增的顺序保存的.这时候若是要查ID_card_n2对应的名字,用二分法就能够快速获得,这个时间复杂度是O(log(N)).
有序数组支持范围查询,要查询身份证号在[ID_car_X,ID_card_Y]区间的User,能够先用二分法找到ID_card_X(若是不存在ID_card_X,就找到大于ID_card_X的第一个User),而后向右遍历,直到查到第一个大于ID_card_Y的身份证号,推出循环.
若是仅仅看查询效率,有序数组就是最好的数据结构,可是,在须要更新数据的时候就麻烦了,往中间插入一个记录就必须得挪动后面全部的记录,成本过高.
因此,有序数组只适用于静态存储引擎,好比要保存的是2017年某个城市的全部人口信息,这类不会再修改的数据
二叉搜索树
二叉搜索树的特色是:每一个节点的左儿子小于父节点,父节点又小于右儿子.这样要查询ID_card_n2的话,按照图中搜索顺序就是按照UserA->UserC->UserF->User2这个路径获得,这个时间复杂度是O(log(N)).
固然为了维持O(log(N))的复杂度,须要保持这棵树是平衡二叉树,为了作这个保证,更新的时间复杂度也是O(log(N)).
二叉树是搜索效率最高的,可是实际上大多数的数据库存储不是使用二叉树,缘由是索引不止存在内存中,还要写到磁盘上.能够想象一颗100万节点的平衡二叉树,树高20,一次查询可能须要访问20个数据快.在机械硬盘时代,从机械硬盘随机读取一个数据块须要10ms左右的寻址时间.也就是说,对于一个100万行的表,若是使用二叉树来存储,单独访问一行可能须要20个10ms的时间,这样查询就比较慢了
为了让一个查询尽可能少的读磁盘,就必须让查询过程访问尽可能少的数据块.那么,咱们就不该该使用二叉树,而是使用"N"叉树,这里的N取决于数据块的大小.
以InnoDB的一个整数字段索引为例,这个N差很少是1200,这棵树高是4的时候,就能够存1200的2次方个值,这已是17亿了.考虑到树根的数据块老是在内存中,一个10亿行的表上一个整数字段的索引,查找一个值最多只须要访问3次磁盘.
在InnoDB中,表都是根据主键顺序以索引的形式存放的,这种存储方式的表称为索引组织表.
每个索引在InnoDB里面对应一颗B+树
假设,咱们有一个主键列为 ID 的表,表中有字段 k,而且在 k 上有索引。
建表语句为:
mysql> create table T(
id int primary key,
k int not null,
name varchar(16),
index (k))engine=InnoDB;
复制代码
表中 R1~R5 的 (ID,k) 值分别为 (100,1)、(200,2)、(300,3)、(500,5) 和 (600,6),两棵树的示意图以下:
根据叶子节点的内容,索引类型分为主键索引和非主键索引
主键索引的叶子节点存的是整行数据,在InnoDB里,主键索引也被称为聚簇索引
非主键索引的叶子节点内容是主键的值,在InnoDB里,非主键索引也被称为二级索引
主键索引和普通索引的查询区别:
自增主键的插入数据模式,符合递增插入,没插入一条新纪录,都是追加操做,都不涉及到挪动其余记录,也不会触发叶子结点的分裂
而用有业务逻辑的字段作主键,每每不容易保证有序插入,这样写数据成本相对较高
从存储空间的角度看,假设表中确实有一个惟一的字段,好比字符串类型的身份证号,那就应该用身份证号作主键?因为每一个非主键索引的叶子节点上都是主键的值.因此用身份证号作主键,那么每一个二级索引的叶子节点占用约20个字节,而若是用整型作主键,则只要4个字节,若是是长整型(bigint)则是8个字节
显然,主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也越小
有些场景适合用业务字段直接作主键:
InnoDB采用的是B+树结构,B+树可以很好的配合磁盘的读写特性,减小单次查询的磁盘访问次数,因为InnoDB是索引组织表,通常状况下建立一个自增主键,这样非主键索引占用的空间最小
如select * from T where k between 3 and 5; 建表语句:
mysql>create table T(
ID int primary key,
k int NOT NULL DEFAULT 0,
s varchar(16) NOT NULL DEFAULT '',
index k(k))
engine = InnoDB;
insert into T values(100,1,'aa'),(200,2,'bb'),(300,3,'cc'),(400,4,'dd'),(500,5,'ee'),(600,6,'ff'),(700,7,'gg')
复制代码
sql语句的执行流程:
在这个过程:回到主键索引树搜索的过程,称为回表
select ID from T where between 3 and 5,这时只须要查ID的值,而ID的值已经在K索引树上了,所以能够直接提供查询结果,不须要回表,也就是说在这个查询里,索引k已经“覆盖了”咱们的查询需求,咱们称为覆盖索引.
覆盖索引能够减小树的搜索次数,显著提高查询性能,因此使用覆盖索引是一个经常使用的性能优化手段
B+树这种索引结构,能够利用索引的‘最左前缀,来定位记录’
在创建联合索引的时候,如何安排索引内的字段的顺序,第一原则是:若是经过调整顺序,能够少维护一个索引,那么这个顺序每每就是须要优先考虑采用的
在MySQL5.6引入的索引下推优化,能够在索引遍历过程当中,对索引中包含的字段先作判断,直接过滤掉不知足条件的记录,减小回表次数
对于例子中的InnoDB表T,若是要重建索引K,sql语句能够是:
alter table T drop index k;
alter table T add index(k);
复制代码
若是重建主键索引,能够这么写
alter table T drop primary key;
alter table T add primary key(id);
复制代码
对于这两种重建索引的作法,重建索引k的作法是合理的,可是重建主键的过程不合理,不管是删除主键仍是建立主键,都会将整个表重建.能够用alter table T engine=InnoDB替代
根据加锁的范围,MySQL里面的锁大体能够分为全局锁、表级锁和行锁三类
mysql加全局读锁的方法:Flush talbes with read lock(FTWRL),
全局锁的典型使用场景,作全库逻辑备份,也就是把整库每一个表都select出来存成文本.
mysql官方自带的逻辑备份工具是mysqldump,当mysqldump使用参数-single-transaction的时候,导数据以前就会启动一个事物,来确保拿到一致性视图,因为mvcc的支持,这个过程当中数据是能够正常更新,可是single-transaction方法只适用于全部的表使用事物引擎的库不支持事物的引擎,那么备份就只能经过FTWRL方法
表级锁有两种:表锁和元数据锁
表锁的语法是lock tables ... read/write ,释放锁:unlock tales
另外一类表级的锁是MDL,MDL的做用是,保证读写的正确性,在mysql 5.5版本中引入来MDL,当对一个表作增删改查操做的时候,加MDL读锁;当对表作结构变动操做的时候,加MDL写锁.
如何安全地给小表加字段: 首先解决长事物,事物不提交,就会一直占着MDL锁;
在alter table语句里面设定等待时间,若是在这个指定的等待时间里面可以拿到MDL写锁最好,拿不到也不阻塞后面的业务语句,先放弃,以后开发人员或者DBA再经过重试命令重复这个过程.
在InnoDB事物中,行锁是在须要的时候才加上的,当并非不须要了就马上释放,而是要等到事物结束时才释放,这个就是两阶段锁协议
若是事物中须要锁多个行,要把最可能形成锁冲突、最可能影响并发度的锁尽可能日后放.
当出现死锁后,有两种策略:
因为惟一索引用不上change buffer的优化机制,所以若是业务能够接收,从性能角度出发建议使用非惟一索引.
若是某次写入使用了change buffe机制,以后主机异常重启,是否会丢失change buffer和数据?
虽然只是更新内存,可是在事物提交的时候,咱们把change buffer的操做也记录到redo log里了,因此崩溃恢复的时候,change buffer也能找回来.
咱们平时不断地删除历史数据和新增数据,MySQL可能会选错索引.
例如,咱们要根据邮箱查学生的信息,若是没有建立索引,那么就会全表扫描,若是在邮箱上建立索引
一、alter table SUser add index index1(email);
二、alter table SUser add index index2(email);
第一个语句建立在index1索引里面,包含了每一个记录的整个字符串;而第二个语句建立的index2索引里面, 对于每一个记录都是只取前6个字节.
index1(即email整个字符串的索引结构):
index2(即email(6)索引结构):
使用前缀索引, 定义好长度,既能够节省空间,又不用额外增长太多的查询成本.
对比如下两个sql语句:
select id,email from SUser where email='zhangssxyz@qq.com'
select id,email,name from SUser where email='zhangssxyz@qq.com'
使用index1能够利用覆盖索引,从index1查询到结果后直接就返回了,不须要回到ID索引再去查一次.而若是使用index2的话,就不得不回到ID索引再去判断email字段的值.
即便将index2定义的定义修改成email(18)的前缀索引,这时候虽然index2已经包含了全部的信息,但InnDB仍是要回到id索引再查一下,由于系统并不肯定前缀索引的定义是否截断了完整信息.
小结:
InnoDB的数据是按页存储的,删掉一个数据页上的全部数据记录,整个数据页就能够被复用. 可是数据页的复用和记录的复用不一样,若是把ID为400的行删除,再插入一个ID是800的行,就不能复用这个位置.而当整个页从B+树里摘掉之后,能够复用到任何位置,若是将数据页pageA上的全部记录删除后,pageA会被标记为可复用,这时候若是要插入一条ID=50的记录须要用新页的时候,pageA是能够被复用的.若是相邻的两个数据页利用率都很小,系统就会把这两个页上的数据合到其中的一个页上,另一个数据页也就被标记为可复用.所以delete命令其实只是把记录的位置或数据页标记为了“可复用”,但磁盘文件大小是不会变的.
重建表能够解决数据空洞的问题(pageA满了,在插入一个ID是500的数据时,就不得不申请一个新的页面pageB来保存数据了,页分裂完成后,pageA的末尾就留下空洞了)
** 解决办法**
使用show processlist查看哪些事物是空闲的,
有些业务代码会在短期内先大量申请数据库链接作备用,若是确认是被数据库打挂了,那么一种可能的作法是让数据库跳过权限验证阶段.
在mysql中,会引起性能问题的慢查询,大致有如下三种可能:
对于索引没有设计好,解决方法:假设服务器是一主一备,主库A、备库B,能够如今备库上执行set sql_log_bin=off ,也就是不写binlog,而后执行alter table语句加上索引;执行主备切换;这时候主库是B,备库是A,在A上执行set sql_log_bin=off,而后执行alter table语句加上索引,更好的方案是相似gh-ost这样的方案,更加稳妥.
针对sql语句没有写好,解决方法:mysql 5.7提供query_rewrite功能,能够把输入的一种语句改写成另一种模式 好比语句错误地写成了select * from t where id + 1=10000,能够经过insert into query_rewrite.rewrite_rules(pattern,replacement,pattern_database) values("select * from t where id + 1= ?","select * from t where id = ? - 1","db1"); call query_rewrite.flush_rewrite_rules();
call query_rewrite.flush_rewrite_rules();这个存储过程,是让插入的新规则生效,也就是“查询重写”
上线前能够在测试环境把慢查询日志(slow log)打开,而且把long_query_time设置成0,确保每一个语句都会被记录入慢查询日志;