MySQL学习笔记

一、 MySQL索引

1.1 MySQL索引数据结构

索引是对数据库表中一列或多列的值进行排序的一种结构,使用索引可快速访问数据库表中的特定信息。
是一种单独的、物理的对数据库表中一列或多列的值进行排序的一种存储结构,它是某个表中一列或若干列值的集合和相应的指向表中物理标识这些值的数据页的逻辑指针清单。

MySQL索引的数据结构是B+树,MongoDB索引的数据结构是B-树。

1.1.1 B-树

MySQL学习笔记_第1张图片

特点:多路,非二叉树;每个节点既保存索引,又保存数据;搜索时相当于二分查找。

1.1.2 B+树

MySQL学习笔记_第2张图片

特点:多路,非二叉树;只有叶子节点保存数据;搜索时相当于二分查找;增加了相邻接点的指向指针。

1.1.3 B-树和B+树的区别

B+树查询时间复杂度固定是O(log(n)),B-树查询复杂度最好是 O(1)。
B+树相邻接点的指针可以大大增加区间访问性,可使用在范围查询等,而B-树每个节点key和data在一起,则无法区间查找。
B+树更适合外部存储,也就是磁盘存储。由于内节点无data域,每个节点能索引的范围更大更精确。
注意这个区别相当重要,是基于前面三点的,B-树每个节点即保存数据又保存索引,所以磁盘IO的次数很少,B+树只有叶子节点保存,磁盘IO多,但是区间访问比较好。

1.1.4 MySQL使用B+树作为索引原因解释

MongoDB使用B-树,所有节点都有Data域,只要找到指定索引就可以进行访问,无疑单次查询平均快于MySQL。
MySQL作为一个关系型数据库,数据的关联性是非常强的,区间访问是常见的一种情况,B+树由于数据全部存储在叶子节点,并且通过指针串在一起,这样就很容易的进行区间遍历甚至全部遍历。

1.1.5 哈希索引

哈希表有着O(1)的查找时间复杂度,又称散列表,由直接寻址表改进而来,对于字典类型查找非常快速,但是对于范围查找无能为力。
InnoDB存储引擎使用哈希算法来对字典进行查找,其冲突机制采用链表方式。

InnoDB的哈希索引是自适应的,仅是数据库自身创建并使用,DBA本身不能对其进行干预。

1.1.6 全文检索

InnoDB从1.2.x版本开始支持全文检索,支持MyISAM存储引擎全文索引的全部功能。

1.1.6.1 倒排索引

全文检索通常使用倒排索引来实现,其和B+树索引一样也是一种索引结构。它在辅助表种存储了单词与单词自身在一个或多个文档中所在位置之间的映射。这通常利用关联数组实现,其拥有两种表现形式。

  1. inverted file index,表现形式为{单词,单词所在文档的ID}
  2. full inverted index,表现形式为{单词,(单词所在文档的ID,在具体文档中的位置)}

如下全文检索表:

DocumentID表时进行全文检索文档的ID,Text表时存储的内容,用户需要对存储的这些文档内容进行全文检索。例如,查找出现过Some单词的文档ID,又或者查找单个文档中出现过两个Some单词的文档ID,等等。

对于inverted file index的关联数组,其存储内容如下表:
MySQL学习笔记_第3张图片

可以看到单词code存在于文档1和4中,单词days存在于文档3和6中,之后在要进行全文查询就简单了,可以直接根据Documents的ID包含查询关键字的文档。对于inverted file index,其仅存去文档ID,而ull inverted index存储的是对(pair),即(DocumentID,Position),因此起存储的倒排索引如下表:

MySQL学习笔记_第4张图片

1.1.6.2 InnoDB全文检索

InnoDB从1.2.x版本开始支持全文检索,其采用full inverted index的方式。在InnoDB中将(DocumentID,Position)视为一个“ilist”,因为在全文检索的表中有两个列,一个是word字段,另一个是ilist字段,并且在word上设有索引。此外,由于InnoDB存储引擎在ilist字段中存放了Position信息,故可以进行Proximity Search,而MyISAM存储引擎不支持该特性。

InnoDB将word存放到Auxiliary Table(辅助表),InnoDB共有6张Auxiliary Table,目前每张表根据word的Latin编码进行分区。

Auxiliary Table是持久的表,存放于磁盘,InnoDB还使用FTS Index Cache(全文检索索引缓存)来提高全文检索性能。

FTS Index Cache是一个红黑树,根据(word,ilist)排序,这意味着插入数据时已更新了对应的表,但是对全文索引的更新可能在分词操作后还在FTS Index Cache中,Auxiliary Table可能没有更新。InnoDB回批量对Auxiliary Table更新,而不是每次插入就更新一次,进行检索查询时,首先将FTS Index Cache中的word合并到Auxiliary Table再进行查询。

1.1.6.3 全文检索语法

Natural Language

SELECT * FROM tfs_a WHERE MATCH(body) AGAINST ('Porridge' IN Natural Language MODE);

MySQL学习笔记_第5张图片

由于Natural Language MODE时默认的全文检索查询模式,因此可以省略。

SELECT * FROM tfs_a WHERE MATCH(body) AGAINST ('Porridge');

Natural Language模式的相关性计算依据以下条件:

  1. word是否再文档中出现
  2. word在文档中出现的次数
  3. word再索引列中的数量
  4. 多少个文档包含该word

Boolean

SELECT * FROM tfs_a WHERE MATCH(body) AGAINST ('+Pease -hot' IN Boolean MODE);

MySQL学习笔记_第6张图片

  1. +表示该word必须存在
  2. -表示该word必须不存在
  3. (no operator)表示该word时可选的,如果出现相关性更高
  4. @distance表示查询的多个单词之间的距离是否再distance之内,单位是字节这种称为Proximity Search,如MATCH(body) AGAINST (’“Pease pot”@30’ IN BOOLEAN MODE)表示字符串Pease和pot之间的距离需在30字节内。
  5. >表示出现该单词时增加相关性
  6. <表示出现该单词时减少相关性
  7. ~表示允许出现该单词,出现时减少相关性
  8. *表示以该单词开头的单词
  9. "表示短语

Query Expansion
MySQL支持全文检索的扩展查询,这种查询通常在查询的关键字太短,用户需要隐含知识时进行。比如单词database的查询,用户可能希望查询的不仅是包含database的文档,可能还指那些包含MySQL、Oracle的单词。
使用Query Expansion查询分为两个阶段:

  1. 根据搜索的单词进行全文索引查询
  2. 根据第一阶段产生的分词再进行一次全文检索查询

MySQL学习笔记_第7张图片

MySQL学习笔记_第8张图片

可以看到Query Expansion最后得到8条结果,除了包含之前使用Natural Language的结果,还有一些title或body包含MySQL、DB2的文档,这就是Query Expansion。

1.2 聚簇索引与非聚簇索引

1.2.1 聚簇索引

在叶子节点下挂载了索引和行数据的索引。
如果表设置了主键,则主键就是聚簇索引。如果表没有主键,则会默认第一个NOT NULL,且唯一(UNIQUE)的列作为聚簇索引。以上都没有,则会默认创建一个隐藏的row_id作为聚簇索引。
InnoDB的聚簇索引的叶子节点存储的是行记录(其实是页结构,一个页包含多行数据),InnoDB必须要有至少一个聚簇索引。

1.2.2 非聚簇索引

普通索引也叫二级索引,除聚簇索引外的索引,即非聚簇索引。
InnoDB的普通索引叶子节点存储的是主键(聚簇索引)的值,而MyISAM的普通索引存储的是记录指针。

例:
建表:

mysql> create table user(
  -> id int(10) auto_increment,
  -> name varchar(30),
  -> age tinyint(4),
  -> primary key (id),
  -> index idx_age (age)
  -> )engine=innodb charset=utf8mb4;

id 字段是聚簇索引,age 字段是普通索引(二级索引):

insert into user(name,age) values('张三',30);
insert into user(name,age) values('李四',20);
insert into user(name,age) values('王五',40);
insert into user(name,age) values('刘八',10);
 
mysql> select * from user;
+----+--------+------+
| id | name | age |
+----+--------+------+
| 1 | 张三 | 30 |
| 2 | 李四 | 20 |
| 3 | 王五 | 40 |
| 4 | 刘八 | 10 |
+----+--------+------+

如果查询条件为主键(聚簇索引),则只需扫描一次B+树即可通过聚簇索引定位到要查找的行记录数据。
如:select * from user where id = 1;

如果查询条件为普通索引(非聚簇索引),需要扫描两次B+树,第一次扫描通过普通索引定位到聚簇索引的值,然后第二次扫描通过聚簇索引的值定位到要查找的行记录数据。
如:select * from user where age = 30;

  1. 先通过普通索引 age=30 定位到主键值 id=1
  2. 再通过聚集索引 id=1 定位到行记录数据

1.3 回表查询

先通过普通索引的值定位聚簇索引值,再通过聚簇索引的值定位行记录数据,需要扫描两次索引B+树,它的性能较扫一遍索引树更低。

1.4 联合索引

1.4.1 联合索引B+树结构

这是一张表格,col1 是主建,col2和col3 是普通字段:

MySQL学习笔记_第9张图片

那么主索引 对应的 B+树 结构是这样子的:

MySQL学习笔记_第10张图片

对col3 建立一个第二索引(辅助索引):

MySQL学习笔记_第11张图片

如果对 col3 和 col2 建立 联合索引(顺序是先col3后 col2):

MySQL学习笔记_第12张图片

原文例子中的数据没有重复数据,所以改了下表数据:

MySQL学习笔记_第13张图片

还是对 col3 ,col2建立联合索引,那么 B+树 如下:

MySQL学习笔记_第14张图片

1.4.2 联合索引最左前缀原理

对于复合索引(多列b+tree,使用多列值组合而成的b+tree索引)。遵循最左侧原则,从左到右的使用索引中的字段,一个查询可以只使用索引中的一部份,但只能是最左侧部分。例如索引是key index (a,b,c). 可以支持a、a,b、a,b,c3种组合进行查找,但不支持b,c进行查找。当使用最左侧字段时,索引就十分有效。

这是因为B+树的结构决定的,索引是a,b,c,那么是先对a进行B+树索引的,然后再对b排序再对c排序,只有先找到a才能找到b,c。

1.4.3 联合索引使用

  1. 联合索引在and查询中应用

    1. select a,b,c from test where a=? and b=? and c=?;查询效率最高,索引全覆盖。
    2. select a,b,c from test where a=? and b=?;索引覆盖a和b。
    3. select a,b,c from test where b=? and a=?;经过mysql的查询分析器的优化,索引覆盖a和b。
    4. select a,b,c from test where a=?;索引覆盖a。
    5. select a,b,c from test where b=? and c=?;没有a列,不走索引,索引失效。
    6. select a,b,c from test where c=?;没有a列,不走索引,索引失效。
  2. 联合索引在范围查询中应用

    1. select * from test where a=? and b between ? and ? and c=?;索引覆盖a和b,因b列是范围查询,因此c列不能走索引。
    2. select * from test where a between ? and ? and b=?;a列走索引,因a列是范围查询,因此b列是无法使用索引。
    3. select * from test where a between ? and ? and b between ? and ? and c=?;a列走索引,因a列是范围查询,b列是范围查询也不能使用索引。
  3. 联合索引在排序中应用

    1. select * from test where a=? and b=? order by c;a、b、c三列全覆盖索引,查询效率最高。
    2. select * from test where a=? and b between ? and ? order by c;a、b列使用索引查找,因b列是范围查询,因此c列不能使用索引,会出现file sort。

1.4.4 联合索引总结

联合索引的使用在写where条件的顺序无关,mysql查询分析会进行优化而使用索引。但是减轻查询分析器的压力,最好和索引的从左到右的顺序一致。
使用等值查询,多列同时查询,索引会一直传递并生效。因此等值查询效率最好。
索引查找遵循最左侧原则。但是遇到范围查询列之后的列索引失效。
排序也能使用索引,合理使用索引排序,避免出现file sort。

1.5 覆盖索引

InnoDB存储引擎支持覆盖索引(covering index,或称索引覆盖),即从辅助索引中就可以得到查询的记录,而不需要查询聚集索引中的记录。使用覆盖索引的一个好处是付出索引不包含整行记录的所有信息,故其大小要远小于聚集索引,因此可以减少大量的IO操作。

只需要在一棵索引树上就能获取SQL所需的所有列数据,无需回表,速度更快。

例如:select id,age from user where age = 10;

1.5.1 如何实现覆盖索引

常见的方法是:将被查询的字段,建立到联合索引里去。

  1. 如实现:select id,age from user where age = 10;
    explain分析:因为age是普通索引,使用到了age索引,通过一次扫描B+树即可查询到相应的结果,这样就实现了覆盖索引。
    MySQL学习笔记_第15张图片

  2. 实现:select id,age,name from user where age = 10;
    explain分析:age是普通索引,但name列不在索引树上,所以通过age索引在查询到id和age的值后,需要进行回表再查询name的值。此时的Extra列的NULL表示进行了回表查询。
    MySQL学习笔记_第16张图片
    为了实现索引覆盖,需要建组合索引idx_age_name(age,name)

drop index idx_age on user;
create index idx_age_name on user(`age`,`name`);

explain分析:此时字段age和name是组合索引idx_age_name,查询的字段id、age、name的值刚刚都在索引树上,只需扫描一次组合索引B+树即可,这就是实现了索引覆盖,此时的Extra字段为Using index表示使用了索引覆盖。

1.5.2 特殊举例

摘自《MySQL技术内幕InnoDB存储引擎第2版》5.6.3节。
联合索引userid_2(userid,buy_date),一般情况,我们按照buy_date是无法使用该索引的,但特殊情况下:查询语句是统计操作,且是覆盖索引,则按照buy_date当做查询条件时,也可以使用该联合索引

mysql> explain select count(*) from buy_log where buy_date >= '2011-01-01' and buy_date < '2011-02-01';
+--+-----------+-------+-----+-------------+--------+-------+----+----+------------------------+
|id|select_type| table |type |possible_keys| key    |key_len|ref |rows|Extra                   |
+--+-----------+-------+-----+-------------+--------+-------+----+----+------------------------+
| 1| SIMPLE    |buy_log|index| NULL        |userid_2| 8     |NULL|  7 |Using where; Using index|
+--+-----------+-------+-----+-------------+--------+-------+----+----+------------------------+
1 row in set (0.00 sec)

1.6 索引失效

1.6.1 创建索引

普通的索引的创建:
CREATE INDEX indexName ON TableName(field);

复合索引的创建:
CREATE INDEX indexName ON TableName(field1, field2 ...);

删除索引:
DROP INDEX indexName;

查看索引:
SHOW INDEX FROM TableName;

MySQL学习笔记_第17张图片

1.6.2 explain

id: SELECT 查询的标识符. 每个 SELECT 都会自动分配一个唯一的标识符
select_type: SELECT 查询的类型
table: 查询的是哪个表
partitions: 匹配的分区
type: join 类型
possible_keys: 此次查询中可能选用的索引
key: 此次查询中确切使用到的索引
ref: 哪个字段或常数与 key 一起被使用
rows: 显示此查询一共扫描了多少行. 这个是一个估计值
filtered: 表示此查询条件所过滤的数据的百分比
extra: 额外的信息

1.6.3 索引查询失效的几种情况

  1. like以%开头,索引无效;当like前缀没有%,后缀有%时,索引有效
    MySQL学习笔记_第18张图片

  2. or语句前后没有同时使用索引。当or左右查询字段只有一个是索引,该索引失效,只有当or左右查询字段均为索引时,才会生效
    MySQL学习笔记_第19张图片
    MySQL学习笔记_第20张图片

  3. 组合索引,不是使用第一列索引,索引失效。
    MySQL学习笔记_第21张图片

  4. 数据类型出现隐式转化。如varchar不加单引号的话可能会自动转换为int型,使索引无效,产生全表扫描。
    MySQL学习笔记_第22张图片

  5. 在索引列上使用 IS NULL 或 IS NOT NULL操作。索引是不索引空值的,所以这样的操作不能使用索引,可以用其他的办法处理,例如:数字类型,判断大于0,字符串类型设置一个默认值,判断是否等于默认值即可。
    MySQL学习笔记_第23张图片

  6. 在索引字段上使用not,<>,!=。不等于操作符是永远不会用到索引的,因此对它的处理只会产生全表扫描。 优化方法: key<>0 改为 key>0 or key<0。
    MySQL学习笔记_第24张图片

  7. 对索引字段进行计算操作、字段上使用函数。(索引为 emp(ename,empno,sal))
    MySQL学习笔记_第25张图片
    MySQL学习笔记_第26张图片

  8. 当全表扫描速度比索引速度快时,mysql会使用全表扫描,此时索引失效。

索引失效分析工具:
可以使用explain命令加在要分析的sql语句前面,在执行结果中查看key这一列的值,如果为NULL,说明没有使用索引。
explain命令的详细用法,可以查看这篇文章:MySQL 性能优化神器 Explain 使用分析

二、 MySQL键约束

2.1 主键约束

主键是非空且唯一的,只允许一个主键,主键可以是单个字段或多字段的组合(联合主键)。
可以根据主键定位到唯一一行的数据。
如果表设置了主键,则主键就是聚簇索引。如果表没有主键,则会默认第一个NOT NULL,且唯一(UNIQUE)的列作为聚簇索引。以上都没有,则会默认创建一个隐藏的row_id作为聚簇索引。

CREATE TABLE user(
    id INT PRIMARY KEY,
    name VARCHAR(20),
    password VARCHAR(20)
);

CREATE TABLE user(
    id INT,
    name VARCHAR(20),
    password VARCHAR(20),
    PRIMARY KEY(id, name)
);

以上是创建表时定义主键、联合主键的方法。
下面是在表已经创建后添加、删除主键约束。

ALTER TABLE user ADD PRIMARY KEY(id);
ALTER TABLE user ADD PRIMARY KEY(id, name);
ALTER TABLE user MODIFY id INT PRIMARY KEY;

ALTER TABLE user DROP PRIMARY KEY;

2.2 自增约束

AUTO_INCREMENT可以约束任何一个字段,该字段不一定是PRIMARY KEY字段,也就是说自增的字段并不等于主键字段。

CREATE TABLE user(
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(20),
    password VARCHAR(20)
);

2.3 唯一约束

被修饰的字段的值不可重复。

CREATE TABLE user(
    id INT,
    name VARCHAR(20) UNIQUE,
    UNIQUE(id)
);

ALTER TABLE user ADD UNIQUE(name);
ALTER TABLE user MODIFY name VARCHAR(20) UNIQUE;
ALTER TABLE user DROP INDEX name;

2.4 非空约束

被修饰的字段不能为空。

CREATE TABLE user(
    id INT,
    name VARCHAR(20) NOT NULL
);

2.5 默认约束

被修饰的字段如果在插入字段时没有定义其值就使用默认值。

CREATE TABLE user(
    id INT,
    name VARCHAR(20) DEFAULT '张三'
);

2.6 外键约束

上面介绍的键约束都只涉及一个表,外键约束设计两个表,一个主表一个副表。
副表外键不可有主表字段不存在的值,主表中的数据如果被附副表引用了,那么是不可以删除主表对应数据的。

CREATE TABLE classes(
    id INT PRIMARY KEY,
    name VARCHAR(20)
);

CREATE TABLE students(
    id INT PRIMARY KEY,
    name VARCHAR(20),
    class_id int,
    FOREIGN KEY(class_id) references classes(id)
);

三、 MySQL锁

3.1 数据库中的锁

在数据库中lock和latch都可以被称为“锁”。
它们锁的对象不一样。latch是用来保证并发线程操作临界资源的,lock用来锁定数据库中的表、页、行等对象。
这里主要介绍的是lock,一般锁定的对象仅在事务commit或rollback后进行释放(不同隔离级别释放的时间不能不同),具有死锁机制。

MySQL学习笔记_第27张图片

3.2 锁的类型

InnoDB存储引擎实现了两种标准的行级锁:
共享锁(S Lock),允许事务读一行数据。
排他锁(X Lock),允许事务删除或更新一行数据。

兼容指的是当一个事务获得行r的锁,另一个事务可以立即获得锁,如果不能立即获得则必须等待事务释放锁才能再获得,则不兼容。
X锁与任何的锁都不兼容,而S锁仅和S锁兼容。需要特别注意的是,S和X锁都是行锁,兼容只对同一记录(row)的兼容性情况。

此外,InnoDB支持多粒度锁定,这种锁定允许事务在行级和表级的锁同时存在。为了支持在不同粒度上进行加锁,InnoDB支持意向锁。
将对象按粒度划分为数据库、表、页、行,如果需要对页上的记录r上X锁,那么需要对数据库、表、页上意向锁,最后对记录r上X锁。

InnoDB支持两种的意向锁:
共享意向锁(IS Lock),允许事务读一行数据。
排他意向锁(IX Lock),允许事务删除或更新一行数据。

由于InnoDB支持的是行级别的锁,因此意向锁其实不会阻塞除全表扫描以外的任何请求。

MySQL学习笔记_第28张图片

3.3 一致性非锁定读(快照读)

一致性的非锁定行读(consistent nonlocking read,简称CNR)是指InnoDB存储引擎通过行多版本控制(multi versioning)的方式来读取当前执行时间数据库中运行的数据。如果读取的行正在执行DELETE或UPDATE操作,这时读取操作不会因此而会等待行上锁的释放,相反地,InnoDB会去读取行的一个快照数据。

MySQL学习笔记_第29张图片

之所以称为非锁定读,是因为不需要等待访问数据行上的X锁的释放。快照数据是指该行之前版本的数据,通过undo段来实现。而undo用来在事务中回滚数据,因此快照数据本身是没有额外开销的,此外,读取快照数据是不需要上锁的,因为没有事务需要对历史的数据进行修改操作。

在READ COMMITTED和REPEATABLE READ模式下,InnoDB存储引擎使用默认的一致性非锁定读。

READ COMMITTED隔离级别下,对于快照数据,一致性非锁定读总是读取被锁定行的最新一份快照数据。
REPEATABLE READ隔离级别下,对于快照数据,一致性非锁定读总是读取事务开始时的行数据版本。

用下面这个实例举例区别READ COMMITTED和REPEATABLE READ。

MySQL学习笔记_第30张图片

MySQL学习笔记_第31张图片

最后在第5条,会话A不管是哪种隔离模式,读出来的应该都是一样的,因为会话B的事务还未提交。

但是在第7条的时候,在READ COMMITTED读已提交,由于会话B的事务已提交,所以第7行读出来如下:

在REPEATABLE READ模式,一致性非锁定读总是读取事务开始时的行数据版本所以读出来如下:

MySQL学习笔记_第32张图片

3.4 一致性锁定读(当前读)

在REPEATABLE READ的默认隔离级别下,InnoDB的SELECT使用一致性非锁定读,但是在某些情况下,用户需要显示地对数据库读取操作进行加锁以保证数据逻辑的一致性。

如何解释这句话,READ COMMITTED是最新快照读,REPEATABLE READ是事务开始时快照读,默认使用一致性非锁定读能读到数据且都拿不到锁。

InnoDB对于SELECT语句支持两种一致性锁定读操作。
SELECT ··· FOR UPDATE;// 读取时对记录加S锁,直到事务结束。
SELECT ··· LOCK IN SHARE MODE;// 读取时对记录加X锁,直到事务结束。
SELECT ··· FOR UPDATE对读取的行记录加X锁,其它事务不能再加任何锁。
SELECT ··· LOCK IN SHARE MODE加S锁,其它事务只能再加S锁,加X锁会被阻塞。

有点类似读写锁的概念,一个线程拿了写锁,其它线程就都拿不到锁,一个线程拿了读锁,那么其它线程请求读锁依旧可以拿到,但是拿不到写锁。
对于已被执行了SELECT ··· FOR UPDATE的行数据,也是可以使用SELECT一致性非锁定读进行读取的,因为SELECT读快照,不请求锁。

3.5 锁的算法

InnoDB存储引擎有3种行锁的算法,分别是:
Record Lock:单个行记录上的锁
Gap Lock:间隙锁,锁定一个范围,但不包含记录本身
Next-Key Lock:Record Lock+Gap Lock,锁定一个范围,并且锁定记录本身

Record Lock总是会锁住索引记录,如果InnoDB存储引擎表在建立的时候没有设置任何一个索引,那么这时InnoDB存储引擎会使用隐式的主键来进行锁定。
Gap Lock是为了解决Phantom Problem(幻象/幻读),利用这种锁技术,锁定的不是单个值,而是一个范围。
Next-Key Lock是结合了Gap Lock和Record Lock的一种锁定算法,在此种算法下,InnoDB对于行的查询都是采用这种锁定算法。

例如:
有10、11、13、20这四个值,那么索引可能被Next-Key Locking的区间为:

  1. (-∞,10]
  2. (10,11]
  3. (11,13]
  4. (13,20]
  5. (20,+∞)

除了Next-Key Locking,还有Previous-Key Locking,如上面的例子,可锁定的区间为:

  1. (-∞,10)
  2. [10,11)
  3. [11,13)
  4. [13,20)
  5. [20,+∞)

然而,当查询的索引含有唯一属性时,InnoDB存储引擎对Next-Key Lock进行优化,将其降级为Record Lock,即仅锁住索引本身,而不是范围。如表中有1,2,5的id数据:

MySQL学习笔记_第33张图片

会话A首先对a=5进行X锁。而由于a是主键且唯一,因此锁定的仅是5这个值,而不是(2,5)的范围,这样在B中插入值4不会阻塞,可以立即插入并返回。即锁定由Next-Key Lock算法降级为Record Lock,从而提高了并发性。

Next-Key Lock降级为Record Lock仅在查询的列是唯一索引的情况下。

若是辅助索引,则情况会完全不同如:

CREATE TABLE z(a INT,b INT,PRIMARY KEY(a),KEY(b))
INSERT INTO z SELECT 1,1;
INSERT INTO z SELECT 3,1;
INSERT INTO z SELECT 5,3;
INSERT INTO z SELECT 7,6;
INSERT INTO z SELECT 10,8;

表z的b列是辅助索引,若在会话A中执行下面的SQL:
SELECT * FROM z WHERE b=3 FOR UPDATE;

这是SQL语句通过索引列b进行查询,因此其使用传统的Next-Key Locking加锁,并且由于两个索引,其需要分别进行锁定。对于聚集索引,其仅对a等于5的索引加上record Lock。对于辅助索引,其加上的是Next-Key Lock,锁定的范围是(1,3],需要注意的是,InnoDB存储引擎还会对辅助索引下一个键值加上gap lock,即还有一个辅助索引的范围为(3,6)的锁,因此,在B会话中运行下面的SQL语句,都会被阻塞:

SELECT * FORM z WHERE a=5 LOCK IN SHARE MODE;
INSERT INTO z SELECT 4,2;
INSERT INTO z SELECT 6,5;

而执行下面的语句,不会阻塞:
INSERT INTO z SELECT 8,6;
INSERT INTO z SELECT 2,0;
INSERT INTO z SELECT 6,7;

3.6 解决幻读(Phantom Problem)

Phantom Problem是指在同一事物下,连续执行两次同样的SQL语句可能导致不同的结果,第二次的SQL语句可能会返回之前不存在的行。

比如表t由1、2、5这三个值组成。

MySQL学习笔记_第34张图片
MySQL学习笔记_第35张图片

上图在READ COMMITTED下会出现幻读。第3行和第7行的两次SELECT结果不一致。

InnoDB采用Next-Key Lock避免Phantom Problem。对于上述的SQL语句SELECT * FROM T WHERE a >2 FOR UPDATE,其锁住的不是5这单个值,而是对(2,+∞)这个范围加了X锁,因此对于这个范围的插入都是不被允许的,从而避免了幻读,如下图所示:

MySQL学习笔记_第36张图片

使用SELECT ··· LOCK IN SHARE MODE避免了幻读.

单纯的REPEATABLE READ允许幻读,要使用一致性锁定读,才能在REPEATABLE READ下解决幻读。

3.7 锁问题

3.7.1 脏读

事务A读到了事务B还未提交的内容。

MySQL学习笔记_第37张图片

3.7.2 不可重复读

不可重复读是指在A事务内多次读取同一数据集合,在A事务还没有结束时,B事务也访问了相同集合,并做了DML操作,导致A事务两次读这个集合督导的是不一样的数据。
不可重复读和脏读的区别:脏读是读到未提交的数据,而不可重复读读到的是已经提交的数据。

MySQL学习笔记_第38张图片

3.7.3 幻读

Phantom Problem是指在同一事物下,连续执行两次同样的SQL语句可能导致不同的结果,第二次的SQL语句可能会返回之前不存在的行。

3.7.4 丢失更新

更新丢失是指并发下两次更新同时进行,后一次更新覆盖了前一次更新的情况,更新丢失是数据没有保证一致性导致的。

用户A在银行卡有100元钱,某一刻用户B向A转账50元(称为B操作),同时有用户C向A转账50元(称为C操作);
B操作从数据库中读取他此时的余额100,计算新的余额为100+50=150
C操作也从数据库中读取他此时的余额100,计算新的余额为100+50=150
B操作将balance=150写入数据库,之后C操作也将balance=150写入数据库
最终A的余额变为150

上面的例子,A同时收到两笔50元转账,最后的余额应该是200元,但却因为并发的问题变为了150元,原因是B和C向A发起转账请求时,同时打开了两个数据库会话,进行了两个事务,后一个事务拿到了前一个事务的中间状态数据,导致更新丢失。
常用的解决思路有两种:

悲观锁\排他锁
悲观锁在读取数据的时候都会认为会有别人去修改,于是在取数据的时候会对当前数据加一个锁,在操作结束前,不允许其余操作更改。要注意悲观锁和乐观锁都是业务逻辑层次的定义,不同的设计可能会有不同的实现。在MySQL层常用的悲观锁实现方式是采用一致性锁定锁。

begin;
select * from account where id = 1 for update;
update account set balance=150 where id =1;
commit;

乐观锁
乐观锁是指在获取数据时候不加锁,乐观的认为操作不会有冲突,在update的时候再去检查冲突。
有的可以通过版本号实现乐观锁。

begin;
select balance from account where id=1;
-- 得到balance=100;然后计算balance=100+50=150
update account set balance = 150 where id=1 and balance = 100;
commit;

四、 MySQL事务

4.1 事务定义

一个最小的不可再分的工作单元,通常一个事务对应一个完整的业务。
在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务。
事务处理可以用来维护数据库的完整性,保证成批的 SQL 语句要么全部执行,要么全部不执行。
事务用来管理 insert,update,delete 语句。

事务四大特性ACID
原子性(Atomicity,或称不可分割性)、一致性(Consistency)、隔离性(Isolation,又称独立性)、持久性(Durability)。

原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。

隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(Read committed)、可重复读(Repeatable read)和串行化(Serializable)。

持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

在 MySQL 命令行的默认设置下,事务都是自动提交的,即执行 SQL 语句后就会马上执行 COMMIT 操作,使用SELECT @@AUTOCOMMIT可以获得自动提交的状态值。因此要显式地开启一个事务务须使用命令 BEGIN 或 START TRANSACTION,或者执行命令 SET AUTOCOMMIT=0,用来禁止使用当前会话的自动提交。

4.2 事务控制语句

由于MySQL 命令行的默认设置下,事务都是自动提交的,如果修改默认配置又会导致每次都需要手动COMMIT,所以一般采用事务控制语句手动控制事务。

BEGIN 或 START TRANSACTION 显式地开启一个事务;
COMMIT 也可以使用 COMMIT WORK,不过二者是等价的。COMMIT 会提交事务,并使已对数据库进行的所有修改成为永久性的;
ROLLBACK 也可以使用 ROLLBACK WORK,不过二者是等价的。回滚会结束用户的事务,并撤销正在进行的所有未提交的修改;
SAVEPOINT identifier,SAVEPOINT 允许在事务中创建一个保存点,一个事务中可以有多个 SAVEPOINT;
RELEASE SAVEPOINT identifier 删除一个事务的保存点,当没有指定的保存点时,执行该语句会抛出一个异常;
ROLLBACK TO identifier 把事务回滚到标记点;
SET TRANSACTION 用来设置事务的隔离级别。InnoDB 存储引擎提供事务的隔离级别有READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ 和 SERIALIZABLE。

4.3 四种事务隔离级别

先讲一下事务并发会引发什么问题,这个在上一节讲到过。

1、脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据。
2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。
3、幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。

事务隔离级别 脏读 不可重复读 幻读
读未提交(read-uncommitted)
读已提交(read-committed)
可重复读(repeatable-read)
串行化(serializable)

1、事务隔离级别为读已提交时,写数据只会锁住相应的行。
2、事务隔离级别为可重复读时,如果检索条件有索引(包括主键索引)的时候,默认加锁方式是next-key 锁;如果检索条件没有索引,更新数据时会锁住整张表。一个间隙被事务加了锁,其他事务是不能在这个间隙插入记录的,这样可以防止幻读。
3、事务隔离级别为串行化时,读写数据都会锁住整张表。
4、隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。

五、 慢查询日志

MySQL中一般有以下几种日志:

MySQL学习笔记_第39张图片

慢查询日志(slow query log)是MySQL自带的几种日志文件中非常重要的一种日志(另还有错误日志、查询日志、二进制日志。)。

当MySQL性能下降时,通过开启慢查询来获得哪条SQL语句造成的响应过慢,进行分析处理。当然开启慢查询会带来CPU损耗与日志记录的IO开销,所以我们要间断性的打开慢查询日志来查看MySQL运行状态。

慢查询日志可帮助DBA定位可能存在问题的SQL语句,从而进行SQL语句层面的优化。

在默认情况下,MySQL数据库不启动慢查询日志,需要用户手动将log_slow_queries参数设置为ON。

SHOW VARIABLES LIKE 'log_slow_queries'\G;
MySQL学习笔记_第40张图片

SHOW VARIABLES LIKE 'long_query_time'\G;
MySQL学习笔记_第41张图片

SHOW VARIABLES LIKE “slow_query_log”\G; # 查看是否开启慢查询日志
SET slow_query_log = ON|OFF; # 开启|关闭慢查询日志

SHOW VARIABLES LIKE “log_output”\G # 查看慢查询日志记录到文件还是表中
SET log_output=TABLE|FILE; # 设置慢查询日志输出到table or files中

SHOW VARIABLES LIKE “slow_query_log_file”\G; # 查看慢查询日志文件路径
SET slow_query_log_file=/usr/local/mysql/data/localhost-slow.log;设置慢查询日志文件路径

SHOW VARIABLES LIKE “long_query_time”\G; # 查看慢查询阀值
SET long_query_time=10; # 设置慢查询阀值为10s

SHOW VARIABLES LIKE “log_queries_not_using_indexes”\G; # 查看是否开启,没有使用索引也记录到慢查询日志中
SET log_queries_not_using_indexes=ON|OFF; # 开启or关闭

SHOW VARIABLES LIKE “log_throttle_queries_not_using_indexes”\G; # 每分钟 允许【因为没有使用索引】而记录到慢查询日志中的sql语句数
SET log_throttle_queries_not_using_indexes = 0; 表示不限制数量,可能会频繁记录,要小心

5.1 如何分析慢查询日志

5.1.1 mysqldumpslow

该工具是慢查询自带的分析慢查询工具,一般只要安装了mysql,就会有该工具。

Usage: mysqldumpslow [ OPTS... ] [ LOGS... ]  -- 后跟参数以及log文件的绝对地址;

  -s            what to sort by (al, at, ar, c, l, r, t), 'at' is default
                al: average lock time
                ar: average rows sent
                at: average query time
                c: count
                l: lock time
                r: rows sent
                t: query time

  -r           reverse the sort order (largest last instead of first)
  -t NUM       just show the top n queries
  -a           don't abstract all numbers to N and strings to 'S'
  -n NUM       abstract numbers with at least n digits within names
  -g PATTERN   grep: only consider stmts that include this string
  -h HOSTNAME  hostname of db server for *-slow.log filename (can be wildcard),
               default is '*', i.e. match all
  -i NAME      name of server instance (if using mysql.server startup script)
  -l           don't subtract lock time from total time

常见用法

mysqldumpslow -s c -t 10 /var/run/mysqld/mysqld-slow.log # 取出使用最多的10条慢查询

mysqldumpslow -s t -t 3 /var/run/mysqld/mysqld-slow.log # 取出查询时间最慢的3条慢查询

mysqldumpslow -s t -t 10 -g “left join/database/mysql/slow-log # 得到按照时间排序的前10条里面含有左连接的查询语句

 mysqldumpslow -s r -t 10 -g 'left join' /var/run/mysqld/mysqld-slow.log # 按照扫描行数最多的

注意: 使用mysqldumpslow的分析结果不会显示具体完整的sql语句,只会显示sql的组成结构.
假如: SELECT * FROM sms_send WHERE service_id=10 GROUP BY content LIMIT 0, 1000;
mysqldumpslow来显示

Count: 1  Time=1.91s (1s)  Lock=0.00s (0s)  Rows=1000.0 (1000), vgos_dba[vgos_dba]@[10.130.229.196]
SELECT * FROM sms_send WHERE service_id=N GROUP BY content LIMIT N, N;

由此可以得出哪些SQL语句执行的时间比较长。

5.1.2 pt-query-digest

pt-query-digest是用于分析mysql慢查询的一个工具,它可以分析binlog、General log、slowlog,也可以通过SHOWPROCESSLIST或者通过tcpdump抓取的MySQL协议数据来进行分析。可以把分析结果输出到文件中,分析过程是先对查询语句的条件进行参数化,然后对参数化以后的查询进行分组统计,统计出各查询的执行时间、次数、占比等,可以借助分析结果找出问题进行优化。
pt-query-digest是一个perl脚本,只需下载并赋权即可执行。

这里不做详细介绍。

5.2 如何进行SQL优化

5.2.1 使用explain查询sql的执行计划

explain select comic_id,name,pen_name,cover,last_verify_time from comic;
MySQL学习笔记_第42张图片

参数分析:
table:表示属于哪张数据表
type:最重要的参数,表示连接使用了何种类型。从最好到最差的连接类型为const,eq_reg,ref,range,index和ALL。
possible_keys:显示可能应用在这张表中的索引。如果为null,则表示没有可能的索引。
key:实际使用的索引。如果为null,则表示没有使用索引。
key_len:使用的索引的长度,在不损失精确性的情况下,长度越短越好。
ref:表示索引的哪一列被使用了,如果可能的话,是一个常数。
rows:Mysql认为必须检查的用来返回请求数据的行数。

5.2.2 count() 和 Max() 的优化方法

(1)优化前,是没有为last_update_time字段建立索引的情况,查询最大的时间戳
MySQL学习笔记_第43张图片

(2)优化后,是为last_update_time字段建立索引的情况,查询最大的时间戳
create index update_time on comic(last_update_time);
MySQL学习笔记_第44张图片
对比,可以看到,在没有为字段建立索引的情况下,查询时间是11秒多,建立索引之后,查询时间变成0秒了。

所以总结就是,如果经常用于count和max操作的字段,可以为其添加索引。

还有,值得注意的地方是:count() 计算时,count(*)会将这一列中的null值但也算进去,而count(comic_id)则不会将null算进去。

5.2.3 子查询的优化

通常情况下,需要把子查询优化为join查询,但在优化时要注意关联键是否有一对多的关系,如果有,是可能会出现重复数据的。所以如果存在一对多关系,则应该使用distinct进行限制。

例如:
select t.id from t where t.id in (select k.kid from k);
优化成:
select distinct t.id from t join k on t.id = k.kid;

5.2.4 limit 1 优化

如果确定结果只有一条数据,可以使用SELECT xxx LIMIT 1;在找到该条记录之后不再往下扫描表。

六、 binlog

6.1 binlog 简介

MySQL 的二进制日志 binlog 可以说是 MySQL 最重要的日志,它记录了所有的 DDL 和 DML 语句(除了数据查询语句select、show等),以事件形式记录,还包含语句所执行的消耗的时间,MySQL的二进制日志是事务安全型的。binlog 的主要目的是复制和恢复。

二进制日志记录了对MySQL数据库执行更改的所有操作,也就是不包括SELETE和SHOW这类操作,因为这类操作对数据本身并没有修改。然后,若操作本身并没有导致数据库发生变成,那么该操作可能也会写入二进制日志,比如修改的新值与旧值相等。

6.1.1 Binlog日志的两个最重要的使用场景

MySQL主从复制:MySQL Replication在Master端开启binlog,Master把它的二进制日志传递给slaves来达到master-slave数据一致的目的。

数据恢复:通过使用 mysqlbinlog工具来使恢复数据。

6.1.2 如何启用Binlog

一般来说开启binlog日志大概会有1%的性能损耗。

启用binlog,通过配置 /etc/my.cnf 或 /etc/mysql/mysql.conf.d/mysqld.cnf 配置文件的 log-bin 选项:

在配置文件中加入 log-bin 配置,表示启用binlog,如果没有给定值,写成 log-bin=,则默认名称为主机名。(注:名称若带有小数点,则只取第一个小数点前的部分作为名称)

[mysqld]
log-bin=my-binlog-name

也可以通过 SET SQL_LOG_BIN=1 命令来启用 binlog,通过 SET SQL_LOG_BIN=0 命令停用 binlog。启用 binlog 之后须重启MySQL才能生效。

6.1.3 常用的Binlog操作命令

# 是否启用binlog日志
show variables like 'log_bin';

# 查看详细的日志配置信息
show global variables like '%log%';

# mysql数据存储目录
show variables like '%dir%';

# 查看binlog的目录
show global variables like "%log_bin%";

# 查看当前服务器使用的biglog文件及大小
show binary logs;

# 查看主服务器使用的biglog文件及大小

# 查看最新一个binlog日志文件名称和Position
show master status;


# 事件查询命令
# IN 'log_name' :指定要查询的binlog文件名(不指定就是第一个binlog文件)
# FROM pos :指定从哪个pos起始点开始查起(不指定就是从整个文件首个pos点开始算)
# LIMIT [offset,] :偏移量(不指定就是0)
# row_count :查询总条数(不指定就是所有行)
show binlog events [IN 'log_name'] [FROM pos] [LIMIT [offset,] row_count];

# 查看 binlog 内容
show binlog events;

# 查看具体一个binlog文件的内容 (in 后面为binlog的文件名)
show binlog events in 'master.000003';

# 设置binlog文件保存事件,过期删除,单位天
set global expire_log_days=3; 

# 删除当前的binlog文件
reset master; 

# 删除slave的中继日志
reset slave;

# 删除指定日期前的日志索引中binlog日志文件
purge master logs before '2019-03-09 14:00:00';

# 删除指定日志文件
purge master logs to 'master.000003';

6.1.4 写binlog的时机

对支持事务的引擎如InnoDB而言,必须要提交了事务才会记录binlog。所有未提交的二进制日志会被记录到一个缓存中,等该事务提交时直接将缓存中的二进制日志写入二进制日志文件中。binlog 什么时候刷新到磁盘跟参数 sync_binlog 相关。

如果设置为0,则表示MySQL不控制binlog的刷新,由文件系统去控制它缓存的刷新;
如果设置为不为0的值,则表示每 sync_binlog 次事务,MySQL调用文件系统的刷新操作刷新binlog到磁盘中。
设为1是最安全的,在系统故障时最多丢失一个事务的更新,但是会对性能有所影响。
如果 sync_binlog=0 或 sync_binlog大于1,当发生电源故障或操作系统崩溃时,可能有一部分已提交但其binlog未被同步到磁盘的事务会被丢失,恢复程序将无法恢复这部分事务。

在MySQL 5.7.7之前,默认值 sync_binlog 是0,MySQL 5.7.7和更高版本使用默认值1,这是最安全的选择。一般情况下会设置为100或者0,牺牲一定的一致性来获取更好的性能。

6.1.5 binlog文件以及扩展

binlog日志包括两类文件:

  1. 二进制日志索引文件(文件名后缀为.index)用于记录所有有效的二进制文件
  2. 二进制日志文件(文件名后缀为.00000*)记录数据库所有的DDL和DML语句事件

binlog是一个二进制文件集合,每个binlog文件以一个4字节的魔数开头,接着是一组Events:
魔数:0xfe62696e对应的是0xfebin;
Event:每个Event包含header和data两个部分;
header提供了Event的创建时间,哪个服务器等信息,data部分提供的是针对该Event的具体信息,如具体数据的修改;
第一个Event用于描述binlog文件的格式版本,这个格式就是event写入binlog文件的格式;
其余的Event按照第一个Event的格式版本写入;
最后一个Event用于说明下一个binlog文件;
binlog的索引文件是一个文本文件,其中内容为当前的binlog文件列表。

当遇到以下3种情况时,MySQL会重新生成一个新的日志文件,文件序号递增:

  1. MySQL服务器停止或重启时。
  2. 使用 flush logs 命令。
  3. 当 binlog 文件大小超过 max_binlog_size 变量的值时。

max_binlog_size 的最小值是4096字节,最大值和默认值是 1GB (1073741824字节)。事务被写入到binlog的一个块中,所以它不会在几个二进制日志之间被拆分。因此,如果你有很大的事务,为了保证事务的完整性,不可能做切换日志的动作,只能将该事务的日志都记录到当前日志文件中,直到事务结束,你可能会看到binlog文件大于 max_binlog_size 的情况。

6.1.6 binlog的日志格式

记录在二进制日志中的事件的格式取决于二进制记录格式。支持三种格式类型:

  • STATEMENT:基于SQL语句的复制(statement-based replication, SBR)
  • ROW:基于行的复制(row-based replication, RBR)
  • MIXED:混合模式复制(mixed-based replication, MBR)

在MySQL 5.7.7之前,默认的格式是STATEMENT,在MySQL 5.7.7及更高版本中,默认值是ROW。日志格式通过binlog-format指定,如binlog-format=STATEMENTbinlog-format=ROWbinlog-format=MIXED

Statement
每一条会修改数据的sql都会记录在binlog中

优点:不需要记录每一行的变化,减少了binlog日志量,节约了IO, 提高了性能。

缺点:由于记录的只是执行语句,为了这些语句能在slave上正确运行,因此还必须记录每条语句在执行的时候的一些相关信息,以保证所有语句能在slave得到和在master端执行的时候相同的结果。另外mysql的复制,像一些特定函数的功能,slave与master要保持一致会有很多相关问题。

Row
5.1.5版本的MySQL才开始支持row level的复制,它不记录sql语句上下文相关信息,仅保存哪条记录被修改。

优点: binlog中可以不记录执行的sql语句的上下文相关的信息,仅需要记录那一条记录被修改成什么了。所以row的日志内容会非常清楚的记录下每一行数据修改的细节。而且不会出现某些特定情况下的存储过程,或function,以及trigger的调用和触发无法被正确复制的问题.

缺点:所有的执行的语句当记录到日志中的时候,都将以每行记录的修改来记录,这样可能会产生大量的日志内容。

注:将二进制日志格式设置为ROW时,有些更改仍然使用基于语句的格式,包括所有DDL语句,例如CREATE TABLE, ALTER TABLE,或 DROP TABLE。

Mixed
从5.1.8版本开始,MySQL提供了Mixed格式,实际上就是Statement与Row的结合。
在Mixed模式下,一般的语句修改使用statment格式保存binlog,如一些函数,statement无法完成主从复制的操作,则采用row格式保存binlog,MySQL会根据执行的每一条具体的sql语句来区分对待记录的日志形式,也就是在Statement和Row之间选择一种。

6.1.7 binlog事件的结构

一个事件对象分为事件头和事件体,事件的结构如下:

+=====================================+
| event  | timestamp         0 : 4    |
| header +----------------------------+
|        | type_code         4 : 1    |
|        +----------------------------+
|        | server_id         5 : 4    |
|        +----------------------------+
|        | event_length      9 : 4    |
|        +----------------------------+
|        | next_position    13 : 4    |
|        +----------------------------+
|        | flags            17 : 2    |
|        +----------------------------+
|        | extra_headers    19 : x-19 |
+=====================================+
| event  | fixed part        x : y    |
| data   +----------------------------+
|        | variable part              |
+=====================================+

如果事件头的长度是 x 字节,那么事件体的长度为 (event_length - x) 字节;设事件体中 fixed part 的长度为 y 字节,那么 variable part 的长度为 (event_length - (x + y)) 字节

6.2 使用binlog进行复制

复制是mysql最重要的功能之一,mysql集群的高可用、负载均衡和读写分离都是基于复制来实现的;从5.6开始复制有两种实现方式,基于binlog和基于GTID(全局事务标示符);接下来介绍基于binlog的主一从复制;其复制的基本过程如下:

  1. Master将数据改变记录到二进制日志(binary log)中
  2. Slave上面的IO进程连接上Master,并请求从指定日志文件的指定位置(或者从最开始的日志)之后的日志内容
  3. Master接收到来自Slave的IO进程的请求后,负责复制的IO进程会根据请求信息读取日志指定位置之后的日志信息,返回给Slave的IO进程。返回信息中除了日志所包含的信息之外,还包括本次返回的信息已经到Master端的bin-log文件的名称以及bin-log的位置
  4. Slave的IO进程接收到信息后,将接收到的日志内容依次添加到Slave端的relay-log文件的最末端,并将读取到的Master端的bin-log的文件名和位置记录到master-info文件中,以便在下一次读取的时候能够清楚的告诉Master从某个bin-log的哪个位置开始往后的日志内容
  5. Slave的Sql进程检测到relay-log中新增加了内容后,会马上解析relay-log的内容成为在Master端真实执行时候的那些可执行的内容,并在自身执行

接下来使用实例演示基于binlog的主从复制:
a.配置master
主要包括设置复制账号,并授予REPLICATION SLAVE权限,具体信息会存储在于master.info文件中,及开启binlog;

mysql> CREATE USER 'test'@'%' IDENTIFIED BY '123456';
mysql> GRANT REPLICATION SLAVE ON *.* TO 'test'@'%';
mysql> show variables like "log_bin";
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| log_bin       | ON    |
+---------------+-------+
查看master当前binlogmysql状态:mysql> show master status;
+------------------+----------+--------------+------------------+-------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+------------------+----------+--------------+------------------+-------------------+
| mysql-bin.000003 |      120 |              |                  |                   |
+------------------+----------+--------------+------------------+-------------------+

建表插入数据:

CREATE TABLE `tb_person` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(36) NOT NULL,                           
`address` varchar(36) NOT NULL DEFAULT '',    
`sex` varchar(12) NOT NULL DEFAULT 'Man' ,
`other` varchar(256) NOT NULL ,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
insert into tb_person  set name="name1", address="beijing", sex="man", other="nothing";
insert into tb_person  set name="name2", address="beijing", sex="man", other="nothing";
insert into tb_person  set name="name3", address="beijing", sex="man", other="nothing";
insert into tb_person  set name="name4", address="beijing", sex="man", other="nothing";

b.配置slave
Slave的配置类似master,需额外设置relay_log参数,slave没有必要开启二进制日志,如果slave为其它slave的master,须设置bin_log
c.连接master

mysql> CHANGE MASTER TO
MASTER_HOST='10.108.111.14',
MASTER_USER='test',
MASTER_PASSWORD='123456',
MASTER_LOG_FILE='mysql-bin.000003',
MASTER_LOG_POS=120;

d.show slave status;

mysql> show slave status\G
*************************** 1. row ***************************
Slave_IO_State:   ---------------------------- slave io状态,表示还未启动
Master_Host: 10.108.111.14  
Master_User: test  
Master_Port: 20126  
Connect_Retry: 60   ------------------------- master宕机或连接丢失从服务器线程重新尝试连接主服务器之前睡眠时间
Master_Log_File: mysql-bin.000003  ------------ 当前读取master binlog文件
Read_Master_Log_Pos: 120  ------------------------- slave读取master binlog文件位置
Relay_Log_File: relay-bin.000001  ------------ 回放binlog
Relay_Log_Pos: 4   -------------------------- 回放relay log位置
Relay_Master_Log_File: mysql-bin.000003  ------------ 回放log对应maser binlog文件
Slave_IO_Running: No
Slave_SQL_Running: No
Exec_Master_Log_Pos: 0  --------------------------- 相对于master从库的sql线程执行到的位置
Seconds_Behind_Master: NULL

Slave_IO_State, Slave_IO_Running, 和Slave_SQL_Running为NO说明slave还没有开始复制过程。
e.启动复制
start slave
f.再次观察slave状态

mysql> show slave status\G
*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event -- 等待master新的event
Master_Host: 10.108.111.14
Master_User: test
Master_Port: 20126
Connect_Retry: 60
Master_Log_File: mysql-bin.000003
Read_Master_Log_Pos: 3469  ---------------------------- 3469  等于Exec_Master_Log_Pos,已完成回放
Relay_Log_File: relay-bin.000002                    ||
Relay_Log_Pos: 1423                                ||
Relay_Master_Log_File: mysql-bin.000003                    ||
Slave_IO_Running: Yes                                 ||
Slave_SQL_Running: Yes                                 ||
Exec_Master_Log_Pos: 3469  -----------------------------3469  等于slave读取master binlog位置,已完成回放
Seconds_Behind_Master: 0

可看到slave的I/O和SQL线程都已经开始运行,而且Seconds_Behind_Master=0。Relay_Log_Pos增加,意味着一些事件被获取并执行了。
最后看下如何正确判断SLAVE的延迟情况,判定slave是否追上master的binlog:
1、首先看 Relay_Master_Log_File 和 Maser_Log_File 是否有差异;
2、如果Relay_Master_Log_File 和 Master_Log_File 是一样的话,再来看Exec_Master_Log_Pos 和 Read_Master_Log_Pos 的差异,对比SQL线程比IO线程慢了多少个binlog事件;
3、如果Relay_Master_Log_File 和 Master_Log_File 不一样,那说明延迟可能较大,需要从MASTER上取得binlog status,判断当前的binlog和MASTER上的差距;
4、如果以上都不能发现问题,可使用pt_heartbeat工具来监控主备复制的延迟。
g.查询slave数据,主从一致

mysql> select * from tb_person;
+----+-------+---------+-----+---------+
| id | name  | address | sex | other   |
+----+-------+---------+-----+---------+
|  5 | name4 | beijing | man | nothing |
|  6 | name2 | beijing | man | nothing |
|  7 | name1 | beijing | man | nothing |
|  8 | name3 | beijing | man | nothing |
+----+-------+---------+-----+---------+

关于mysql复制的内容还有很多,比如不同的同步方式、复制格式情况下有什么区别,有什么特点,应该在什么情况下使用…这里不再一一介绍。

6.3 使用Binlog进行数据恢复

恢复是binlog的两大主要作用之一,接下来通过实例演示如何利用binlog恢复数据:
a.首先,看下当前binlog位置

mysql> show master status;
+------------------+----------+--------------+------------------+-------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+------------------+----------+--------------+------------------+-------------------+
| mysql-bin.000008 |     1847 |              |                  |                   |
+------------------+----------+--------------+------------------+-------------------+

b.向表tb_person中插入两条记录:

insert into tb_person  set name="person_1", address="beijing", sex="man", other="test-1";
insert into tb_person  set name="person_2", address="beijing", sex="man", other="test-2";

c.记录当前binlog位置:

mysql> show master status;
+------------------+----------+--------------+------------------+-------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+------------------+----------+--------------+------------------+-------------------+
| mysql-bin.000008 |     2585 |              |                  |                   |
+------------------+----------+--------------+------------------+-------------------+

d.查询数据

mysql> select *  from tb_person where name ="person_2" or name="person_1";
+----+----------+---------+-----+--------+
| id | name     | address | sex | other  |
+----+----------+---------+-----+--------+
|  6 | person_1 | beijing | man | test-1 |
|  7 | person_2 | beijing | man | test-2 |
+----+----------+---------+-----+--------+

e.删除一条: delete from tb_person where name =“person_2”;

mysql> select *  from tb_person where name ="person_2" or name="person_1";
+----+----------+---------+-----+--------+
| id | name     | address | sex | other  |
+----+----------+---------+-----+--------+
|  6 | person_1 | beijing | man | test-1 |
+----+----------+---------+-----+--------+

f. binlog恢复(指定pos点恢复/部分恢复)

mysqlbinlog   --start-position=1847  --stop-position=2585  mysql-bin.000008  > test.sql
mysql> source /var/lib/mysql/3306/test.sql

g.数据恢复完成

mysql> select *  from tb_person where name ="person_2" or name="person_1";
+----+----------+---------+-----+--------+
| id | name     | address | sex | other  |
+----+----------+---------+-----+--------+
|  6 | person_1 | beijing | man | test-1 |
|  7 | person_2 | beijing | man | test-2 |
+----+----------+---------+-----+--------+

h.总结
恢复,就是让mysql将保存在binlog日志中指定段落区间的sql语句逐个重新执行一次而已。

七、 redo log和undo log

innodb事务日志包括redo log和undo log。
redo log是重做日志,提供前滚操作。
undo log是回滚日志,提供回滚操作。

undo log不是redo log的逆向过程,其实它们都算是用来恢复的日志:

  1. redo log通常是物理日志,记录的是数据页的物理修改,而不是某一行或某几行修改成怎样怎样,它用来恢复提交后的物理数据页(恢复数据页,且只能恢复到最后一次提交的位置)。
  2. undo用来回滚行记录到某个版本。undo log一般是逻辑日志,根据每行记录进行记录。

7.1 redo log

7.1.1 redo log和二进制日志的区别

redo log不是二进制日志。虽然二进制日志中也记录了innodb表的很多操作,也能实现重做的功能,但是它们之间有很大区别。

  1. 二进制日志是在存储引擎的上层产生的,不管是什么存储引擎,对数据库进行了修改都会产生二进制日志。而redo log是innodb层产生的,只记录该存储引擎中表的修改。并且二进制日志先于redo log被记录。具体的见后文group commit小结。
  2. 二进制日志记录操作的方法是逻辑性的语句。即便它是基于行格式的记录方式,其本质也还是逻辑的SQL设置,如该行记录的每列的值是多少。而redo log是在物理格式上的日志,它记录的是数据库中每个页的修改。
  3. 二进制日志只在每次事务提交的时候一次性写入缓存中的日志"文件"(对于非事务表的操作,则是每次执行语句成功后就直接写入)。而redo log在数据准备修改前写入缓存中的redo log中,然后才对缓存中的数据执行修改操作;而且保证在发出事务提交指令时,先向缓存中的redo log写入日志,写入完成后才执行提交动作。
  4. 因为二进制日志只在提交的时候一次性写入,所以二进制日志中的记录方式和提交顺序有关,且一次提交对应一次记录。而redo log中是记录的物理页的修改,redo log文件中同一个事务可能多次记录,最后一个提交的事务记录会覆盖所有未提交的事务记录。例如事务T1,可能在redo log中记录了T1-1,T1-2,T1-3,T1共4个操作,其中T1表示最后提交时的日志记录,所以对应的数据页最终状态是T1对应的操作结果。而且redo log是并发写入的,不同事务之间的不同版本的记录会穿插写入到redo log文件中,例如可能redo log的记录方式如下:T1-1,T1-2,T2-1,T2-2,T2,T1-3,T1。
  5. 事务日志记录的是物理页的情况,它具有幂等性,因此记录日志的方式极其简练。幂等性的意思是多次操作前后状态是一样的,例如新插入一行后又删除该行,前后状态没有变化。而二进制日志记录的是所有影响数据的操作,记录的内容较多。例如插入一行记录一次,删除该行又记录一次。

7.1.2 redo log基本概念

redo log包括两部分:一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;二是磁盘上的重做日志文件(redo log file),该部分日志是持久的。

在概念上,innodb通过force log at commit机制实现事务的持久性,即在事务提交的时候,必须先将该事务的所有事务日志写入到磁盘上的redo log file和undo log file中进行持久化。

为了确保每次日志都能写入到事务日志文件中,在每次将log buffer中的日志写入日志文件的过程中都会调用一次操作系统的fsync操作(即fsync()系统调用)。因为MariaDB/MySQL是工作在用户空间的,MariaDB/MySQL的log buffer处于用户空间的内存中。要写入到磁盘上的log file中(redo:ib_logfileN文件,undo:share tablespace或.ibd文件),中间还要经过操作系统内核空间的os buffer,调用fsync()的作用就是将OS buffer中的日志刷到磁盘上的log file中。

也就是说,从redo log buffer写日志到磁盘的redo log file中,过程如下:

MySQL学习笔记_第45张图片

在此处需要注意一点,一般所说的log file并不是磁盘上的物理日志文件,而是操作系统缓存中的log file,官方手册上的意思也是如此(例如:With a value of 2, the contents of the InnoDB log buffer are written to the log file after each transaction commit and the log file is flushed to disk approximately once per second)。但说实话,这不太好理解,既然都称为file了,应该已经属于物理文件了。所以在本文后续内容中都以os buffer或者file system buffer来表示官方手册中所说的Log file,然后log file则表示磁盘上的物理日志文件,即log file on disk。

另外,之所以要经过一层os buffer,是因为open日志文件的时候,open没有使用O_DIRECT标志位,该标志位意味着绕过操作系统层的os buffer,IO直写到底层存储设备。不使用该标志位意味着将日志进行缓冲,缓冲到了一定容量,或者显式fsync()才会将缓冲中的刷到存储设备。使用该标志位意味着每次都要发起系统调用。比如写abcde,不使用O_DIRECT将只发起一次系统调用,使用O_DIRECT将发起5次系统调用。

MySQL支持用户自定义在commit时如何将log buffer中的日志刷log file中。这种控制通过变量innodb_flush_log_at_trx_commit的值来决定。该变量有3种值:0、1、2,默认为1。但注意,这个变量只是控制commit动作是否刷新log buffer到磁盘。

  • 当设置为1的时候,事务每次提交都会将log buffer中的日志写入os buffer并调用fsync()刷到log file on disk中。这种方式即使系统崩溃也不会丢失任何数据,但是因为每次提交都写入磁盘,IO的性能较差。
  • 当设置为0的时候,事务提交时不会将log buffer中日志写入到os buffer,而是每秒写入os buffer并调用fsync()写入到log file on disk中。也就是说设置为0时是(大约)每秒刷新写入到磁盘中的,当系统崩溃,会丢失1秒钟的数据。
  • 当设置为2的时候,每次提交都仅写入到os buffer,然后是每秒调用fsync()将os buffer中的日志写入到log file on disk。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mozK8Op9-1628852871521)(https://upload-images.jianshu.io/upload_images/17711648-e7c2d709bb74bdb8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

注意,有一个变量innodb_flush_log_at_timeout的值为1秒,该变量表示的是刷日志的频率,很多人误以为是控制innodb_flush_log_at_trx_commit值为0和2时的1秒频率,实际上并非如此。测试时将频率设置为5和设置为1,当innodb_flush_log_at_trx_commit设置为0和2的时候性能基本都是不变的。关于这个频率是控制什么的,在后面的"刷日志到磁盘的规则"中会说。

在主从复制结构中,要保证事务的持久性和一致性,需要对日志相关变量设置为如下:

  • 如果启用了二进制日志,则设置sync_binlog=1,即每提交一次事务同步写到磁盘中。
  • 总是设置innodb_flush_log_at_trx_commit=1,即每提交一次事务都写到磁盘中。

上述两项变量的设置保证了:每次提交事务都写入二进制日志和事务日志,并在提交时将它们刷新到磁盘中。

选择刷日志的时间会严重影响数据修改时的性能,特别是刷到磁盘的过程。下例就测试了innodb_flush_log_at_trx_commit分别为0、1、2时的差距。

#创建测试表
drop table if exists test_flush_log;
create table test_flush_log(id int,name char(50))engine=innodb;
#创建插入指定行数的记录到测试表中的存储过程
drop procedure if exists proc;
delimiter $$
create procedure proc(i int)
begin
    declare s int default 1;
    declare c char(50) default repeat('a',50);
    while s<=i do
        start transaction;
        insert into test_flush_log values(null,c);
        commit;
        set s=s+1;
    end while;
end$$
delimiter ;

当前环境下,innodb_flush_log_at_trx_commit的值为1,即每次提交都刷日志到磁盘。测试此时插入10W条记录的时间。

mysql> call proc(100000);
Query OK, 0 rows affected (15.48 sec)

结果是15.48秒。

再测试值为2的时候,即每次提交都刷新到os buffer,但每秒才刷入磁盘中。

mysql> set @@global.innodb_flush_log_at_trx_commit=2;    
mysql> truncate test_flush_log;

mysql> call proc(100000);
Query OK, 0 rows affected (3.41 sec)

结果插入时间大减,只需3.41秒。

最后测试值为0的时候,即每秒才刷到os buffer和磁盘。

mysql> set @@global.innodb_flush_log_at_trx_commit=0;
mysql> truncate test_flush_log;

mysql> call proc(100000);
Query OK, 0 rows affected (2.10 sec)

结果只有2.10秒。

最后可以发现,其实值为2和0的时候,它们的差距并不太大,但2却比0要安全的多。它们都是每秒从os buffer刷到磁盘,它们之间的时间差体现在log buffer刷到os buffer上。因为将log buffer中的日志刷新到os buffer只是内存数据的转移,并没有太大的开销,所以每次提交和每秒刷入差距并不大。可以测试插入更多的数据来比较,以下是插入100W行数据的情况。从结果可见,值为2和0的时候差距并不大,但值为1的性能却差太多。

MySQL学习笔记_第46张图片

尽管设置为0和2可以大幅度提升插入性能,但是在故障的时候可能会丢失1秒钟数据,这1秒钟很可能有大量的数据,从上面的测试结果看,100W条记录也只消耗了20多秒,1秒钟大约有4W-5W条数据,尽管上述插入的数据简单,但却说明了数据丢失的大量性。更好的插入数据的做法是将值设置为1,然后修改存储过程,将每次循环都提交修改为只提交一次,这样既能保证数据的一致性,也能提升性能,修改如下:

drop procedure if exists proc;
delimiter $$
create procedure proc(i int)
begin
    declare s int default 1;
    declare c char(50) default repeat('a',50);
    start transaction;
    while s<=i DO
        insert into test_flush_log values(null,c);
        set s=s+1;
    end while;
    commit;
end$$
delimiter ;

测试值为1时的情况。

mysql> set @@global.innodb_flush_log_at_trx_commit=1;
mysql> truncate test_flush_log;

mysql> call proc(1000000);
Query OK, 0 rows affected (11.26 sec)

7.1.3 日志块(log block)

innodb存储引擎中,redo log以块为单位进行存储的,每个块占512字节,这称为redo log block。所以不管是log buffer中还是os buffer中以及redo log file on disk中,都是这样以512字节的块存储的。

每个redo log block由3部分组成:日志块头、日志块尾和日志主体。其中日志块头占用12字节,日志块尾占用8字节,所以每个redo log block的日志主体部分只有512-12-8=492字节。

MySQL学习笔记_第47张图片

因为redo log记录的是数据页的变化,当一个数据页产生的变化需要使用超过492字节()的redo log来记录,那么就会使用多个redo log block来记录该数据页的变化。

日志块头包含4部分:

  • log_block_hdr_no:(4字节)该日志块在redo log buffer中的位置ID。
  • log_block_hdr_data_len:(2字节)该log block中已记录的log大小。写满该log block时为0x200,表示512字节。
  • log_block_first_rec_group:(2字节)该log block中第一个log的开始偏移位置。
  • lock_block_checkpoint_no:(4字节)写入检查点信息的位置。

关于log block块头的第三部分log_block_first_rec_group,因为有时候一个数据页产生的日志量超出了一个日志块,这是需要用多个日志块来记录该页的相关日志。例如,某一数据页产生了552字节的日志量,那么需要占用两个日志块,第一个日志块占用492字节,第二个日志块需要占用60个字节,那么对于第二个日志块来说,它的第一个log的开始位置就是73字节(60+12)。如果该部分的值和log_block_hdr_data_len相等,则说明该log block中没有新开始的日志块,即表示该日志块用来延续前一个日志块。

日志尾只有一个部分:log_block_trl_no,该值和块头的log_block_hdr_no相等。

上面所说的是一个日志块的内容,在redo log buffer或者redo log file on disk中,由很多log block组成。如下图:

MySQL学习笔记_第48张图片

7.1.4 log group和redo log file

log group表示的是redo log group,一个组内由多个大小完全相同的redo log file组成。组内redo log file的数量由变量innodb_log_files_group决定,默认值为2,即两个redo log file。这个组是一个逻辑的概念,并没有真正的文件来表示这是一个组,但是可以通过变量innodb_log_group_home_dir来定义组的目录,redo log file都放在这个目录下,默认是在datadir下。

mysql> show global variables like "innodb_log%";
+-----------------------------+----------+
| Variable_name               | Value    |
+-----------------------------+----------+
| innodb_log_buffer_size      | 8388608  |
| innodb_log_compressed_pages | ON       |
| innodb_log_file_size        | 50331648 |
| innodb_log_files_in_group   | 2        |
| innodb_log_group_home_dir   | ./       |
+-----------------------------+----------+

[root@xuexi data]# ll /mydata/data/ib*
-rw-rw---- 1 mysql mysql 79691776 Mar 30 23:12 /mydata/data/ibdata1
-rw-rw---- 1 mysql mysql 50331648 Mar 30 23:12 /mydata/data/ib_logfile0
-rw-rw---- 1 mysql mysql 50331648 Mar 30 23:12 /mydata/data/ib_logfile1

可以看到在默认的数据目录下,有两个ib_logfile开头的文件,它们就是log group中的redo log file,而且它们的大小完全一致且等于变量innodb_log_file_size定义的值。第一个文件ibdata1是在没有开启innodb_file_per_table时的共享表空间文件,对应于开启innodb_file_per_table时的.ibd文件。

在innodb将log buffer中的redo log block刷到这些log file中时,会以追加写入的方式循环轮训写入。即先在第一个log file(即ib_logfile0)的尾部追加写,直到满了之后向第二个log file(即ib_logfile1)写。当第二个log file满了会清空一部分第一个log file继续写入。

由于是将log buffer中的日志刷到log file,所以在log file中记录日志的方式也是log block的方式。

在每个组的第一个redo log file中,前2KB记录4个特定的部分,从2KB之后才开始记录log block。除了第一个redo log file中会记录,log group中的其他log file不会记录这2KB,但是却会腾出这2KB的空间。如下:

MySQL学习笔记_第49张图片

redo log file的大小对innodb的性能影响非常大,设置的太大,恢复的时候就会时间较长,设置的太小,就会导致在写redo log的时候循环切换redo log file。

7.1.5 redo log的格式

因为innodb存储引擎存储数据的单元是页(和SQL Server中一样),所以redo log也是基于页的格式来记录的。默认情况下,innodb的页大小是16KB(由innodb_page_size变量控制),一个页内可以存放非常多的log block(每个512字节),而log block中记录的又是数据页的变化。

其中log block中492字节的部分是log body,该log body的格式分为4部分:

  • redo_log_type:占用1个字节,表示redo log的日志类型。
  • space:表示表空间的ID,采用压缩的方式后,占用的空间可能小于4字节。
  • page_no:表示页的偏移量,同样是压缩过的。
  • redo_log_body表示每个重做日志的数据部分,恢复时会调用相应的函数进行解析。例如insert语句和delete语句写入redo log的内容是不一样的。

如下图,分别是insert和delete大致的记录方式。

MySQL学习笔记_第50张图片

7.1.6 日志刷盘的规则

log buffer中未刷到磁盘的日志称为脏日志(dirty log)。

在上面的说过,默认情况下事务每次提交的时候都会刷事务日志到磁盘中,这是因为变量innodb_flush_log_at_trx_commit值为1。但是innodb不仅仅只会在有commit动作后才会刷日志到磁盘,这只是innodb存储引擎刷日志的规则之一。

刷日志到磁盘有以下几种规则:

  1. 发出commit动作时。已经说明过,commit发出后是否刷日志由变量innodb_flush_log_at_trx_commit控制。

  2. 每秒刷一次。这个刷日志的频率由变量innodb_flush_log_at_timeout值决定,默认是1秒。要注意,这个刷日志频率和commit动作无关。

  3. 当log buffer中已经使用的内存超过一半时。

  4. 当有checkpoint时,checkpoint在一定程度上代表了刷到磁盘时日志所处的LSN位置。

7.1.7 数据页刷盘的规则及checkpoint

内存中(buffer pool)未刷到磁盘的数据称为脏数据(dirty data)。由于数据和日志都以页的形式存在,所以脏页表示脏数据和脏日志。

上一节介绍了日志是何时刷到磁盘的,不仅仅是日志需要刷盘,脏数据页也一样需要刷盘。

在innodb中,数据刷盘的规则只有一个:checkpoint。但是触发checkpoint的情况却有几种。不管怎样,checkpoint触发后,会将buffer中脏数据页和脏日志页都刷到磁盘。

innodb存储引擎中checkpoint分为两种:

  • sharp checkpoint:在重用redo log文件(例如切换日志文件)的时候,将所有已记录到redo log中对应的脏数据刷到磁盘。
  • fuzzy checkpoint:一次只刷一小部分的日志到磁盘,而非将所有脏日志刷盘。有以下几种情况会触发该检查点:
  • master thread checkpoint:由master线程控制,每秒或每10秒刷入一定比例的脏页到磁盘。
  • flush_lru_list checkpoint:从MySQL5.6开始可通过innodb_page_cleaners变量指定专门负责脏页刷盘的page cleaner线程的个数,该线程的目的是为了保证lru列表有可用的空闲页。
  • async/sync flush checkpoint:同步刷盘还是异步刷盘。例如还有非常多的脏页没刷到磁盘(非常多是多少,有比例控制),这时候会选择同步刷到磁盘,但这很少出现;如果脏页不是很多,可以选择异步刷到磁盘,如果脏页很少,可以暂时不刷脏页到磁盘
  • dirty page too much checkpoint:脏页太多时强制触发检查点,目的是为了保证缓存有足够的空闲空间。too much的比例由变量innodb_max_dirty_pages_pct控制,MySQL 5.6默认的值为75,即当脏页占缓冲池的百分之75后,就强制刷一部分脏页到磁盘。

由于刷脏页需要一定的时间来完成,所以记录检查点的位置是在每次刷盘结束之后才在redo log中标记的。

MySQL停止时是否将脏数据和脏日志刷入磁盘,由变量innodb_fast_shutdown={ 0|1|2 }控制,默认值为1,即停止时只做一部分purge,忽略大多数flush操作(但至少会刷日志),在下次启动的时候再flush剩余的内容,实现fast shutdown。

7.1.8 LSN超详细分析

LSN称为日志的逻辑序列号(log sequence number),在innodb存储引擎中,lsn占用8个字节。LSN的值会随着日志的写入而逐渐增大。

根据LSN,可以获取到几个有用的信息:

  1. 数据页的版本信息。

  2. 写入的日志总量,通过LSN开始号码和结束号码可以计算出写入的日志量。

  3. 可知道检查点的位置。

实际上还可以获得很多隐式的信息。

LSN不仅存在于redo log中,还存在于数据页中,在每个数据页的头部,有一个fil_page_lsn记录了当前页最终的LSN值是多少。通过数据页中的LSN值和redo log中的LSN值比较,如果页中的LSN值小于redo log中LSN值,则表示数据丢失了一部分,这时候可以通过redo log的记录来恢复到redo log中记录的LSN值时的状态。

redo log的lsn信息可以通过show engine innodb status来查看。MySQL 5.5版本的show结果中只有3条记录,没有pages flushed up to。

mysql> show engine innodb stauts
---
LOG
---
Log sequence number 2225502463
Log flushed up to   2225502463
Pages flushed up to 2225502463
Last checkpoint at  2225502463
0 pending log writes, 0 pending chkp writes
3201299 log i/o's done, 0.00 log i/o's/second

其中:

  • log sequence number就是当前的redo log(in buffer)中的lsn;
  • log flushed up to是刷到redo log file on disk中的lsn;
  • pages flushed up to是已经刷到磁盘数据页上的LSN;
  • last checkpoint at是上一次检查点所在位置的LSN。

innodb从执行修改语句开始:

(1).首先修改内存中的数据页,并在数据页中记录LSN,暂且称之为data_in_buffer_lsn;

(2).并且在修改数据页的同时(几乎是同时)向redo log in buffer中写入redo log,并记录下对应的LSN,暂且称之为redo_log_in_buffer_lsn;

(3).写完buffer中的日志后,当触发了日志刷盘的几种规则时,会向redo log file on disk刷入重做日志,并在该文件中记下对应的LSN,暂且称之为redo_log_on_disk_lsn;

(4).数据页不可能永远只停留在内存中,在某些情况下,会触发checkpoint来将内存中的脏页(数据脏页和日志脏页)刷到磁盘,所以会在本次checkpoint脏页刷盘结束时,在redo log中记录checkpoint的LSN位置,暂且称之为checkpoint_lsn。

(5).要记录checkpoint所在位置很快,只需简单的设置一个标志即可,但是刷数据页并不一定很快,例如这一次checkpoint要刷入的数据页非常多。也就是说要刷入所有的数据页需要一定的时间来完成,中途刷入的每个数据页都会记下当前页所在的LSN,暂且称之为data_page_on_disk_lsn。

详细说明如下图:

MySQL学习笔记_第51张图片

上图中,从上到下的横线分别代表:时间轴、buffer中数据页中记录的LSN(data_in_buffer_lsn)、磁盘中数据页中记录的LSN(data_page_on_disk_lsn)、buffer中重做日志记录的LSN(redo_log_in_buffer_lsn)、磁盘中重做日志文件中记录的LSN(redo_log_on_disk_lsn)以及检查点记录的LSN(checkpoint_lsn)。

假设在最初时(12:0:00)所有的日志页和数据页都完成了刷盘,也记录好了检查点的LSN,这时它们的LSN都是完全一致的。

假设此时开启了一个事务,并立刻执行了一个update操作,执行完成后,buffer中的数据页和redo log都记录好了更新后的LSN值,假设为110。这时候如果执行show engine innodb status查看各LSN的值,即图中①处的位置状态,结果会是:

log sequence number(110) > log flushed up to(100) = pages flushed up to = last checkpoint at

之后又执行了一个delete语句,LSN增长到150。等到12:00:01时,触发redo log刷盘的规则(其中有一个规则是innodb_flush_log_at_timeout控制的默认日志刷盘频率为1秒),这时redo log file on disk中的LSN会更新到和redo log in buffer的LSN一样,所以都等于150,这时show engine innodb status,即图中②的位置,结果将会是:

log sequence number(150) = log flushed up to > pages flushed up to(100) = last checkpoint at

再之后,执行了一个update语句,缓存中的LSN将增长到300,即图中③的位置。

假设随后检查点出现,即图中④的位置,正如前面所说,检查点会触发数据页和日志页刷盘,但需要一定的时间来完成,所以在数据页刷盘还未完成时,检查点的LSN还是上一次检查点的LSN,但此时磁盘上数据页和日志页的LSN已经增长了,即:

log sequence number > log flushed up to 和 pages flushed up to > last checkpoint at

但是log flushed up to和pages flushed up to的大小无法确定,因为日志刷盘可能快于数据刷盘,也可能等于,还可能是慢于。但是checkpoint机制有保护数据刷盘速度是慢于日志刷盘的:当数据刷盘速度超过日志刷盘时,将会暂时停止数据刷盘,等待日志刷盘进度超过数据刷盘。

等到数据页和日志页刷盘完毕,即到了位置⑤的时候,所有的LSN都等于300。

随着时间的推移到了12:00:02,即图中位置⑥,又触发了日志刷盘的规则,但此时buffer中的日志LSN和磁盘中的日志LSN是一致的,所以不执行日志刷盘,即此时show engine innodb status时各种lsn都相等。

随后执行了一个insert语句,假设buffer中的LSN增长到了800,即图中位置⑦。此时各种LSN的大小和位置①时一样。

随后执行了提交动作,即位置⑧。默认情况下,提交动作会触发日志刷盘,但不会触发数据刷盘,所以show engine innodb status的结果是:

log sequence number = log flushed up to > pages flushed up to = last checkpoint at

最后随着时间的推移,检查点再次出现,即图中位置⑨。但是这次检查点不会触发日志刷盘,因为日志的LSN在检查点出现之前已经同步了。假设这次数据刷盘速度极快,快到一瞬间内完成而无法捕捉到状态的变化,这时show engine innodb status的结果将是各种LSN相等。

7.1.9 innodb的恢复行为

在启动innodb的时候,不管上次是正常关闭还是异常关闭,总是会进行恢复操作。

因为redo log记录的是数据页的物理变化,因此恢复的时候速度比逻辑日志(如二进制日志)要快很多。而且,innodb自身也做了一定程度的优化,让恢复速度变得更快。

重启innodb时,checkpoint表示已经完整刷到磁盘上data page上的LSN,因此恢复时仅需要恢复从checkpoint开始的日志部分。例如,当数据库在上一次checkpoint的LSN为10000时宕机,且事务是已经提交过的状态。启动数据库时会检查磁盘中数据页的LSN,如果数据页的LSN小于日志中的LSN,则会从检查点开始恢复。

还有一种情况,在宕机前正处于checkpoint的刷盘过程,且数据页的刷盘进度超过了日志页的刷盘进度。这时候一宕机,数据页中记录的LSN就会大于日志页中的LSN,在重启的恢复过程中会检查到这一情况,这时超出日志进度的部分将不会重做,因为这本身就表示已经做过的事情,无需再重做。

另外,事务日志具有幂等性,所以多次操作得到同一结果的行为在日志中只记录一次。而二进制日志不具有幂等性,多次操作会全部记录下来,在恢复的时候会多次执行二进制日志中的记录,速度就慢得多。例如,某记录中id初始值为2,通过update将值设置为了3,后来又设置成了2,在事务日志中记录的将是无变化的页,根本无需恢复;而二进制会记录下两次update操作,恢复时也将执行这两次update操作,速度比事务日志恢复更慢。

7.1.10 和redo log有关的几个变量

  • innodb_flush_log_at_trx_commit={0|1|2} # 指定何时将事务日志刷到磁盘,默认为1。
    • 0表示每秒将"log buffer"同步到"os buffer"且从"os buffer"刷到磁盘日志文件中。
    • 1表示每次事务提交都将"log buffer"同步到"os buffer"且从"os buffer"刷到磁盘日志文件中。
    • 2表示每次事务提交都将"log buffer"同步到"os buffer"但每秒才从"os buffer"刷到磁盘日志文件中。
  • innodb_log_buffer_size:# log buffer的大小,默认8M
  • innodb_log_file_size:#事务日志的大小,默认5M
  • innodb_log_files_group =2:# 事务日志组中的事务日志文件个数,默认2个
  • innodb_log_group_home_dir =./:# 事务日志组路径,当前目录表示数据目录
  • innodb_mirrored_log_groups =1:# 指定事务日志组的镜像组个数,但镜像功能好像是强制关闭的,所以只有一个log group。在MySQL5.7中该变量已经移除。

7.2 undo log

7.2.1 undo log基本概念

undo log有两个作用:提供回滚和多个行版本控制(MVCC)。

在数据修改的时候,不仅记录了redo,还记录了相对应的undo,如果因为某些原因导致事务失败或回滚了,可以借助该undo进行回滚。

undo log和redo log记录物理日志不一样,它是逻辑日志。可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。

当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。有时候应用到行版本控制的时候,也是通过undo log来实现的:当读取的某一行被其他事务锁定时,它可以从undo log中分析出该行记录以前的数据是什么,从而提供该行版本信息,让用户实现非锁定一致性读取。

undo log是采用段(segment)的方式来记录的,每个undo操作在记录的时候占用一个undo log segment。

另外,undo log也会产生redo log,因为undo log也要实现持久性保护。

7.2.2 undo log的存储方式

innodb存储引擎对undo的管理采用段的方式。rollback segment称为回滚段,每个回滚段中有1024个undo log segment。

在以前老版本,只支持1个rollback segment,这样就只能记录1024个undo log segment。后来MySQL5.5可以支持128个rollback segment,即支持128*1024个undo操作,还可以通过变量innodb_undo_logs(5.6版本以前该变量是innodb_rollback_segments)自定义多少个rollback segment,默认值为128。

undo log默认存放在共享表空间中。

[root@xuexi data]# ll /mydata/data/ib*
-rw-rw---- 1 mysql mysql 79691776 Mar 31 01:42 /mydata/data/ibdata1
-rw-rw---- 1 mysql mysql 50331648 Mar 31 01:42 /mydata/data/ib_logfile0
-rw-rw---- 1 mysql mysql 50331648 Mar 31 01:42 /mydata/data/ib_logfile1

如果开启了innodb_file_per_table,将放在每个表的.ibd文件中。

在MySQL5.6中,undo的存放位置还可以通过变量innodb_undo_directory来自定义存放目录,默认值为"."表示datadir。

默认rollback segment全部写在一个文件中,但可以通过设置变量innodb_undo_tablespaces平均分配到多少个文件中。该变量默认值为0,即全部写入一个表空间文件。该变量为静态变量,只能在数据库示例停止状态下修改,如写入配置文件或启动时带上对应参数。但是innodb存储引擎在启动过程中提示,不建议修改为非0的值,如下:

2017-03-31 13:16:00 7f665bfab720 InnoDB: Expected to open 3 undo tablespaces but was able
2017-03-31 13:16:00 7f665bfab720 InnoDB: to find only 0 undo tablespaces.
2017-03-31 13:16:00 7f665bfab720 InnoDB: Set the innodb_undo_tablespaces parameter to the
2017-03-31 13:16:00 7f665bfab720 InnoDB: correct value and retry. Suggested value is 0

7.2.3 和undo log相关的变量

undo相关的变量在MySQL5.6中已经变得很少。如下:它们的意义在上文中已经解释了。

 mysql> show variables like "%undo%";
+-------------------------+-------+
| Variable_name           | Value |
+-------------------------+-------+
| innodb_undo_directory   | .     |
| innodb_undo_logs        | 128   |
| innodb_undo_tablespaces | 0     |
+-------------------------+-------+

7.2.4 delete/update操作的内部机制

当事务提交的时候,innodb不会立即删除undo log,因为后续还可能会用到undo log,如隔离级别为repeatable read时,事务读取的都是开启事务时的最新提交行版本,只要该事务不结束,该行版本就不能删除,即undo log不能删除。

但是在事务提交的时候,会将该事务对应的undo log放入到删除列表中,未来通过purge来删除。并且提交事务时,还会判断undo log分配的页是否可以重用,如果可以重用,则会分配给后面来的事务,避免为每个独立的事务分配独立的undo log页而浪费存储空间和性能。

通过undo log记录delete和update操作的结果发现:(insert操作无需分析,就是插入行而已)

  • delete操作实际上不会直接删除,而是将delete对象打上delete flag,标记为删除,最终的删除操作是purge线程完成的。
  • update分为两种情况:update的列是否是主键列。
    • 如果不是主键列,在undo log中直接反向记录是如何update的。即update是直接进行的。
    • 如果是主键列,update分两部执行:先删除该行,再插入一行目标行。

7.3 binlog和事务日志的先后顺序及group commit

为了提高性能,通常会将有关联性的多个数据修改操作放在一个事务中,这样可以避免对每个修改操作都执行完整的持久化操作。这种方式,可以看作是人为的组提交(group commit)。

除了将多个操作组合在一个事务中,记录binlog的操作也可以按组的思想进行优化:将多个事务涉及到的binlog一次性flush,而不是每次flush一个binlog。

事务在提交的时候不仅会记录事务日志,还会记录二进制日志,但是它们谁先记录呢?二进制日志是MySQL的上层日志,先于存储引擎的事务日志被写入。

在MySQL5.6以前,当事务提交(即发出commit指令)后,MySQL接收到该信号进入commit prepare阶段;进入prepare阶段后,立即写内存中的二进制日志,写完内存中的二进制日志后就相当于确定了commit操作;然后开始写内存中的事务日志;最后将二进制日志和事务日志刷盘,它们如何刷盘,分别由变量sync_binlog和innodb_flush_log_at_trx_commit控制。

但因为要保证二进制日志和事务日志的一致性,在提交后的prepare阶段会启用一个prepare_commit_mutex锁来保证它们的顺序性和一致性。但这样会导致开启二进制日志后group commmit失效,特别是在主从复制结构中,几乎都会开启二进制日志。

在MySQL5.6中进行了改进。提交事务时,在存储引擎层的上一层结构中会将事务按序放入一个队列,队列中的第一个事务称为leader,其他事务称为follower,leader控制着follower的行为。虽然顺序还是一样先刷二进制,再刷事务日志,但是机制完全改变了:删除了原来的prepare_commit_mutex行为,也能保证即使开启了二进制日志,group commit也是有效的。

MySQL5.6中分为3个步骤:flush阶段、sync阶段、commit阶段。

MySQL学习笔记_第52张图片

  • flush阶段:向内存中写入每个事务的二进制日志。
  • sync阶段:将内存中的二进制日志刷盘。若队列中有多个事务,那么仅一次fsync操作就完成了二进制日志的刷盘操作。这在MySQL5.6中称为BLGC(binary log group commit)。
  • commit阶段:leader根据顺序调用存储引擎层事务的提交,由于innodb本就支持group commit,所以解决了因为锁prepare_commit_mutex而导致的group commit失效问题。

在flush阶段写入二进制日志到内存中,但是不是写完就进入sync阶段的,而是要等待一定的时间,多积累几个事务的binlog一起进入sync阶段,等待时间由变量binlog_max_flush_queue_time决定,默认值为0表示不等待直接进入sync,设置该变量为一个大于0的值的好处是group中的事务多了,性能会好一些,但是这样会导致事务的响应时间变慢,所以建议不要修改该变量的值,除非事务量非常多并且不断的在写入和更新。

进入到sync阶段,会将binlog从内存中刷入到磁盘,刷入的数量和单独的二进制日志刷盘一样,由变量sync_binlog控制。

当有一组事务在进行commit阶段时,其他新事务可以进行flush阶段,它们本就不会相互阻塞,所以group commit会不断生效。当然,group commit的性能和队列中的事务数量有关,如果每次队列中只有1个事务,那么group commit和单独的commit没什么区别,当队列中事务越来越多时,即提交事务越多越快时,group commit的效果越明显。

八、 MySQL分库分表

8.1 数据库瓶颈

不管是IO瓶颈,还是CPU瓶颈,最终都会导致数据库的活跃连接数增加,进而逼近甚至达到数据库可承载活跃连接数的阈值。在业务Service来看就是,可用数据库连接少甚至无连接可用。接下来就可以想象了吧(并发量、吞吐量、崩溃)。

8.1.1 IO瓶颈

第一种:磁盘读IO瓶颈,热点数据太多,数据库缓存放不下,每次查询时会产生大量的IO,降低查询速度 ->分库和垂直分表

第二种:网络IO瓶颈,请求的数据太多,网络带宽不够 ->分库

8.1.2 CPU瓶颈

第一种:SQL问题,如SQL中包含join,group by,order by,非索引字段条件查询等,增加CPU运算的操作 -> SQL优化,建立合适的索引,在业务Service层进行业务计算。

第二种:单表数据量太大,查询时扫描的行太多,SQL效率低,CPU率先出现瓶颈 ->水平分表。

8.2 分库分表

8.2.1 水平分库

MySQL学习笔记_第53张图片

  1. 概念:以字段为依据,按照一定策略(hash、range等),将一个中的数据拆分到多个中。
  2. 结果:
    • 每个结构都一样;
    • 每个数据都不一样,没有交集;
    • 所有并集是全量数据;
  3. 场景:系统绝对并发量上来了,分表难以根本上解决问题,并且还没有明显的业务归属来垂直分库。
  4. 分析:库多了,io和cpu的压力自然可以成倍缓解。

8.2.2 水平分表

MySQL学习笔记_第54张图片

  1. 概念:以字段为依据,按照一定策略(hash、range等),将一个中的数据拆分到多个中。
  2. 结果:
    • 每个结构都一样;
    • 每个数据都不一样,没有交集;
    • 所有并集是全量数据;
  3. 场景:系统绝对并发量并没有上来,只是单表的数据量太多,影响了SQL效率,加重了CPU负担,以至于成为瓶颈。
  4. 分析:表的数据量少了,单次SQL执行效率高,自然减轻了CPU的负担。

8.2.3 垂直分库

MySQL学习笔记_第55张图片

  1. 概念:以为依据,按照业务归属不同,将不同的拆分到不同的中。
  2. 结果:
    • 每个结构都不一样;
    • 每个数据也不一样,没有交集;
    • 所有并集是全量数据;
  3. 场景:系统绝对并发量上来了,并且可以抽象出单独的业务模块。
  4. 分析:到这一步,基本上就可以服务化了。例如,随着业务的发展一些公用的配置表、字典表等越来越多,这时可以将这些表拆到单独的库中,甚至可以服务化。再有,随着业务的发展孵化出了一套业务模式,这时可以将相关的表拆到单独的库中,甚至可以服务化。

8.2.4 垂直分表

MySQL学习笔记_第56张图片

  1. 概念:以字段为依据,按照字段的活跃性,将中字段拆到不同的(主表和扩展表)中。
  2. 结果:
    • 每个结构都不一样;
    • 每个数据也不一样,一般来说,每个表的字段至少有一列交集,一般是主键,用于关联数据;
    • 所有并集是全量数据;
  3. 场景:系统绝对并发量并没有上来,表的记录并不多,但是字段多,并且热点数据和非热点数据在一起,单行数据所需的存储空间较大。以至于数据库缓存的数据行减少,查询时会去读磁盘数据产生大量的随机读IO,产生IO瓶颈。
  4. 分析:可以用列表页和详情页来帮助理解。垂直分表的拆分原则是将热点数据(可能会冗余经常一起查询的数据)放在一起作为主表,非热点数据放在一起作为扩展表。这样更多的热点数据就能被缓存下来,进而减少了随机读IO。拆了之后,要想获得全部数据就需要关联两个表来取数据。但记住,千万别用join,因为join不仅会增加CPU负担并且会讲两个表耦合在一起(必须在一个数据库实例上)。关联数据,应该在业务Service层做文章,分别获取主表和扩展表数据然后用关联字段关联得到全部数据。

九、SELECT查询

9.1 四种连接查询

使用的例表结构

create table person(
id int,
name varchar(20),
cardId int
);

create table card(
id int,
name varchar(20)
);

insert into person values(1,'张三',1),(2,'李四',3),(3,'王五',6);
insert into card values(1,'饭卡'),(2,'建行卡'),(3,'农行卡'),(4,'工商卡'),(5,'邮政卡')

9.1.1 内连接 inner join 或 join

就是两张表的数据,通过某个字段对应,查询出相关记录数据。

select * from person inner join card on person.cardId=card.id;

MySQL学习笔记_第57张图片

MySQL学习笔记_第58张图片

9.1.2 左外连接 left join 或者 left outer join

会把左边表里面的所有数据取出来,而右边表中的数据,如果有相等的就取出来如果没有,就会补NULL。

select * from person right join card on person.cardId=card.id;

MySQL学习笔记_第59张图片

MySQL学习笔记_第60张图片

9.1.3 右外连接 rigth join 或者 right outer join

会把右边表里面所有数据取出来,而左边表中的数据,如果有相等的,就显示出来,如果没有就补NULL。

select * from person right join  card on person.cardId=card.id;

MySQL学习笔记_第61张图片

MySQL学习笔记_第62张图片

9.1.4 完全外连接 full join 或者 full outer join

mysql不支持full join,如果要实现全外连接,就使用union将左外连接和右外连接并起来。

select * from person left join card on person.cardId=card.id
union
select * from person right join  card on person.cardId=card.id;

9.2 WHERE和HAVE

  1. 处于的语句不一样
    WHERE可以用于增删改查,但是HAVING只能用于SELECT。

  2. HAVING只能用于SELECT的字段或SELECT AS的临时字段

SELECT DISTINCT(color) FROM xg_base_itemtype HAVING price>=2;

报错:Unknown column ‘price’ in ‘having clause’。

  1. 误区:HAVING只能与GROUP BY一起使用
    这个说法是错误的,比如下面这种情况,在筛选SELECT出来的字段时,其实HAVING和WHERE 的效果是一样的。
SELECT DISTINCT(color) FROM xg_base_itemtype HAVING color>=2;
  1. WHERE不可以使用聚合函数、HAVING可以使用聚合函数
    聚合函数就是例如SUM, COUNT, MAX, AVG等对一组(多条)数据操作的函数,可能需要配合group by 来使用。
    WHERE的条件只能来自表中已有的字段。
SELECT COUNT(*) AS total_num FROM xg_base_itemtype WHERE total_num >10 GROUP BY color;
报错:Unknown column 'total_num' in 'where clause'
SELECT COUNT(*) AS total_num FROM xg_base_itemtype GROUP BY color HAVING total_num >10;

而HAVING可以直接筛选聚合函数的结果。

  1. 运行顺序不一样
    WHERE的过滤在执行语句进行分组之前应用的。
    HAVING的过滤是在分组条件后执行的。
    即如果WHERE和HAVING一起用时,WHERE会先执行,HAVING后执行。

9.3 LIMIT分页查询

  1. 语法规则
    SELECT * FROM table LIMIT [offset,] rows;
    offset:偏移,从offset条之后开始选择数据,如果offset不填,则offset为0。
    rows:从偏移后开始,选择rows条数据。
    特点:左开右闭,即选取(offset,offset+rows]的数据。

  2. 示例

    1. 选取前5条数据(第1~5条数据)
      SELECT * FROM table LIMIT 0,5;
      区间:(0,5]
      offset=0
      rows=5-0=5

    2. 选取第2~5条数据
      SELECT * FROM table LIMIT 2,4;
      区间:(1,5]
      offset=1
      rows=5-1=4

    3. 选取第n~m条数据(n最小为1为第一条数据)
      SELECT * FROM table LIMIT n,m;
      区间:(n-1,m]
      offset=n-1
      rows=m-n+1

9.4 子查询

这个比较基础,就是嵌套SELECT,可以多表,如果多个表中有相同字段要用“表名.字段名”来指定时哪个表的字段。

SELECT * FROM xg_user_animalsoul WHERE pos=7 AND userid NOT IN 
(SELECT xg_user_tempdata.userid FROM xg_user_tempdata WHERE type = 20104 AND data4&(1<<6)>0);

十、 MySQL应用示例

10.1 批量UPDATE

普通的UPDATE语句用来更新一条数据。
UPDATE tableNmae SET columnName = value WHERE columnName = otherValue;

扩充条件的UPDATE语句可以用来更新多条数据,但是只能用来设置相同的值。
UPDATE tableNmae SET columnName = value WHERE columnName IN (otherValue1, otherValue2);
当然还有其他写法,这里只是举个例子。

使用一条UPDATE语句更新多条数据并且分别设置各自的值。

UPDATE tableName SET
    column1 = CASE column2
        WHEN column1Value1 THEN column2Value1
        WHEN column1Value2 THEN column2Value2
        WHEN column1Value3 THEN column2Value3
    END
WHERE column2 IN (column2Value1, column2Value2, column2Value3)

首先要使用IN限定修改的数据,不使用IN的话在此语句中未进行设置值的会变成NULL或者默认值。
该语句会将column2值为相应column2Value的数据column1字段修改为对应的column1Value

UPDATE tableName SET
    column1 = CASE column2
        WHEN column1Value1 THEN column2Value1
        WHEN column1Value2 THEN column2Value2
        WHEN column1Value3 THEN column2Value3
    END,
    column3 = CASE column2
        WHEN column3Value1 THEN column2Value1
        WHEN column3Value2 THEN column2Value2
        WHEN column3Value3 THEN column2Value3
    END
WHERE column2 IN (column2Value1, column2Value2, column2Value3)

10.2 SELECT INTO

制作 “Persons” 表的备份复件:
SELECT * INTO Persons_backup FROM Persons;

IN子句可用于向另一个数据库中拷贝表:
SELECT * INTO Persons IN 'Backup.mdb' FROM Persons;

如果我们希望拷贝某些域,可以在 SELECT 语句后列出这些域:
SELECT LastName,FirstName INTO Persons_backup FROM Persons;

通过从 “Persons” 表中提取居住在 “Beijing” 的人的信息,创建了一个带有两个列的名为 “Persons_backup” 的表:
SELECT LastName,Firstname INTO Persons_backup FROM Persons WHERE City='Beijing';

创建一个名为 “Persons_Order_Backup” 的新表,其中包含了从 Persons 和 Orders 两个表中取得的信息:
SELECT Persons.LastName,Orders.OrderNo INTO Persons_Order_Backup FROM Persons INNER JOIN Orders ON Persons.Id_P=Orders.Id_P;

10.3 AS临时表

  1. 基本语法
    (SELECT * FROM TABLE) AS TEMP,然后在语句中可以直接使用TEMP中SELECT出来的字段。
    SELECT中使用AS临时表
SELECT id,name,server,plat,times FROM xg_user,(SELECT userid, COUNT(userid) AS times FROM xg_user_log_item 
WHERE way=554 AND itemtype=95002153 AND isaward=1 AND amount=40 GROUP BY userid) AS user_times WHERE id=userid;
  1. UPDATE中使用临时表
UPDATE xg_user_tempdata,(SELECT DISTINCT(userid),level AS maxlevel FROM xg_user_animalsoul WHERE pos>0 ORDER BY level DESC) AS A 
SET xg_user_tempdata.data2=A.maxlevel WHERE xg_user_tempdata.type=20104 AND A.userid=xg_user_tempdata.userid;

UPDATE xg_user_tempdata,(SELECT userid,MAX(level) AS maxlevel FROM xg_user_animalsoul WHERE pos>0 GROUP BY userid) AS A 
SET xg_user_tempdata.data2=A.maxlevel WHERE xg_user_tempdata.type=20104 AND A.userid=xg_user_tempdata.userid;

UPDATE xg_user_tempdata SET data4=data4&(~(1<<7)) WHERE type=20104 AND data4&(1<<7)>0 AND userid NOT IN 
(SELECT userid FROM xg_user_log_item WHERE itemtype=95002150 AND way=129 AND amount=9);

UPDATE xg_user_animalsoul SET pos=0 WHERE pos=7 AND userid NOT IN 
(SELECT xg_user_tempdata.userid FROM xg_user_tempdata WHERE type = 20104 AND data4&(1<<6)>0);
  1. 多表联结不适用临时表
UPDATE xg_user_tempdata SET data4=data4&(~(1<<6)) WHERE type=20104 AND data4&(1<<6)>0 AND userid NOT IN 
(SELECT userid FROM xg_user_log_item WHERE itemtype=95002150 AND way=129 AND amount=7);

10.4 VIEW视图

视图是指计算机数据库中的视图,是一个虚拟表,其内容由查询定义。同真实的表一样,视图包含一系列带有名称的列和行数据。但是,视图并不在数据库中以存储的数据值集形式存在。行和列数据来自由定义视图的查询所引用的表,并且在引用视图时动态生成。

DROP VIEW IF EXISTS user_level;
DROP VIEW IF EXISTS user_all_user;
DROP VIEW IF EXISTS user_all_user1;

CREATE VIEW user_all_user1 AS SELECT userid,SUM(amount) AS sum FROM xg_user_log_item WHERE itemtype=95002150 AND xg_user_log_item.way=129 GROUP BY xg_user_log_item.userid;
CREATE VIEW user_all_user AS SELECT userid FROM user_all_user1 WHERE sum>=25;
UPDATE xg_user_tempdata SET data4=data4&(~(1<<(8-1))) WHERE userid NOT IN (SELECT userid FROM user_all_user) AND type=20104;
UPDATE xg_user_animalsoul SET pos=0 WHERE userid NOT IN (SELECT userid FROM user_all_user) AND pos=8;

DROP VIEW IF EXISTS user_all_user;
CREATE VIEW user_all_user AS SELECT userid FROM user_all_user1 WHERE sum>=16;
UPDATE xg_user_tempdata SET data4=data4&(~(1<<(7-1))) WHERE userid NOT IN (SELECT userid FROM user_all_user) AND type=20104;
UPDATE xg_user_animalsoul SET pos=0 WHERE userid NOT IN (SELECT userid FROM user_all_user) AND pos=7;

CREATE VIEW user_level AS SELECT userid,MAX(level) AS maxlevel FROM xg_user_animalsoul WHERE pos>0 GROUP BY userid;
UPDATE xg_user_tempdata JOIN user_level ON user_level.userid=xg_user_tempdata.userid SET xg_user_tempdata.data2=maxlevel WHERE xg_user_tempdata.type=20104;

DROP VIEW IF EXISTS user_level;
DROP VIEW IF EXISTS user_all_user;
DROP VIEW IF EXISTS user_all_user1;

视图的数据变化会影响到基表,基表的数据变化也会影响到视图。
但是如果视图的数据来自多表或者是使用一些COUNT、SUM等基表不存在的数据,则是不允许修改视图的。

10.5 自动更新时间

如下例create_time在插入时使用NULL为自动填充创建时间戳。
update_time在更新时会自动更新当前时间戳。

CREATE TABLE `test` (
  `id` bigint(64) unsigned NOT NULL,
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
);

十一、 存储引擎对比

MySQL学习笔记_第63张图片

特性 MyISAM InnoDB
存储限制 64TB
事务
锁粒度 表锁 行锁
B树索引 支持 支持
哈希索引 不支持 支持
存储消耗
内存消耗
批量插入速度
外键 不支持 支持

参考文章

  1. 《MySQL技术内幕InnoDB存储引擎第2版》
  2. Mysql索引查询失效的情况
  3. MySQL 的覆盖索引与回表的使用方法
  4. mysql索引数据结构
  5. 最简单方式理解为什么MongoDB索引选择B-树,而 Mysql 选择B+树
  6. 复合(联合)索引失效解析
  7. 联合索引(复合索引)在B+树上的结构
  8. MySQL索引背后的数据结构及算法原理
  9. MYSQL使用锁解决并发下的更新丢失问题
  10. MySQL 事务
  11. MySQL优化:定位慢查询的两种方法以及使用explain分析SQL
  12. MySQL定位慢查询步骤
  13. mysql 默认引擎innodb 初探(三)
  14. 详解 慢查询 之 mysqldumpslow
  15. Mysql慢查询日志的使用 和 Mysql的优化
  16. MySQL Binlog 介绍
  17. 腾讯工程师带你深入解析 MySQL binlog
  18. 详细分析MySQL事务日志(redo log和undo log)
  19. MySQL:互联网公司常用分库分表方案汇总!
  20. MySQL四种连接查询
  21. MySQL批量更新数据

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