《MySQL是怎么样运行的》读书笔记一 数据页+索引

 登录MySQL

cmd命令行中输入:mysql -hlocalhost -uroot -p

之后输入密码。

《MySQL是怎么样运行的》读书笔记一 数据页+索引_第1张图片

对于Windows系统来说,默认的用户名是ODBC,你可以通过设置环境变量USER来添加一个默认用户名。

MySQL采用TCP作为服务器和客户端之间的网络通信协议。

MySQL服务器启动的时候会默认申请3306端口号,之后就在这个端口号上等待客户端进程进行连接,用书面一点的话来说,MySQL服务器会默认监听3306端口。

我们在使用mysql来启动客户端程序时,在-h参数后必须跟随IP地址来作为需要连接的服务器进程所在主机的主机名,如果客户端进程和服务器进程在一台计算机中的话,我们可以使用127.0.0.1来代表本机的IP地址

mysql -h127.0.0.1 -uroot -p中即-hlocalhost变成了-h127.0.0.1

服务器处理客户端请求

《MySQL是怎么样运行的》读书笔记一 数据页+索引_第2张图片

连接管理:客户端进程通过线程与服务器进程进行交互。

查询缓存:查询是否有无之前处理过的相同请求的处理结果 。

语法解析MySQL服务器程序首先要对请求做分析,判断请求的语法是否正确,然后从文本中将要查询的表、各种查询条件都提取出来放到MySQL服务器内部使用的一些数据结构上来。

查询优化:我们写的MySQL语句执行起来效率可能并不是很高,MySQL的优化程序会对我们的语句做一些优化。

存储引擎

存储引擎就是表处理器。对表进行实际操作。  InnoDBMyISAM。

存储引擎是负责对表中的数据进行提取和写入工作的,我们可以为不同的表设置不同的存储引擎,也就是说不同的表可以有不同的物理存储结构,不同的提取和写入方式。

创建表时指定存储引擎

CREATE TABLE 表名(
    建表语句;
) ENGINE = 存储引擎名称;

修改表的存储引擎

ALTER TABLE 表名 ENGINE = 存储引擎名称;

启动选项和配置文件

字符集和比较规则

字符集:ASCII码,GBK,utf8等

比较规则:通过直接比较这两个字符对应的二进制编码的大小

MySQL有4个级别的字符集和比较规则,分别是:

  • 服务器级别
  • 数据库级别
  • 表级别
  • 列级别

InnoDB记录存储结构

InnoDB是一个将表中的数据存储到磁盘上的存储引擎,所以即使关机后重启我们的数据还是存在的。

而真正处理数据的过程是发生在内存中的,所以需要把磁盘中的数据加载到内存中,如果是处理写入或修改请求的话,还需要把内存中的内容刷新到磁盘上。而我们知道读写磁盘的速度非常慢,和内存读写差了几个数量级,所以当我们想从表中获取某些记录时,InnoDB存储引擎需要一条一条的把记录从磁盘上读出来么?不,那样会慢死,InnoDB采取的方式是:将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为 16 KB。也就是在一般情况下,一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。

页是MySQL中磁盘和内存交互的基本单位,也是MySQL是管理存储空间的基本单位。

小总结:即磁盘存储数据,在内存中处理数据,二者交互单位是页。

COMPACT行格式

mysql> USE xiaohaizi;
Database changed

mysql> CREATE TABLE record_format_demo (
    ->     c1 VARCHAR(10),
    ->     c2 VARCHAR(10) NOT NULL,
    ->     c3 CHAR(10),
    ->     c4 VARCHAR(10)
    -> ) CHARSET=ascii ROW_FORMAT=COMPACT;//这个表的行格式就是Compact
Query OK, 0 rows affected (0.03 sec)

《MySQL是怎么样运行的》读书笔记一 数据页+索引_第3张图片

 Redundant行格式

《MySQL是怎么样运行的》读书笔记一 数据页+索引_第4张图片

Dynamic和Compressed行格式 

一个页一般是16KB,当记录中的数据太多,当前页放不下的时候,会把多余的数据存储到其他页中,这种现象称为行溢出

InnoDB数据页结构

InnoDB为了不同的目的而设计了不同类型的页,我们把用于存放记录的页叫做数据页

【Mysql】InnoDB 引擎中的数据页结构 - 把苹果v咬哭 - 博客园

一个数据页划分为7个部分:

  • File Header,表示页的一些通用信息,占固定的38字节(记录各种页都通用的一些信息,如这个页的编号是多少,它的上一个页、下一个页是谁)。
  • Page Header,表示数据页专有的一些信息,占固定的56个字节(记录一个数据页中存储的记录的信息,如本页有多少个记录,第一个记录的地址是什么,页目录中的槽数量)。
  • Infimum + Supremum,两个虚拟的伪记录,分别表示页中的最小和最大记录,占固定的26个字节。
  • User Records真实存储我们插入的记录的部分,大小不固定。
  • Free Space:页中尚未使用的部分,大小不确定。
  • Page Directory:页中的某些记录相对位置,也就是各个槽在页面中的地址偏移量,大小不固定,插入的记录越多,这个部分占用的空间越多。
  • File Trailer:用于检验页是否完整的部分,占用固定的8个字节(记录该页是否完整,有无磁盘和内存交互时发生缺失)。
  • 《MySQL是怎么样运行的》读书笔记一 数据页+索引_第5张图片

在页的7个组成部分中,我们自己存储的记录会按照我们指定的行格式存储到User Records部分。。用户插入的记录会存储到User Records部分,从Free Space部分开辟空间生成User Records。当Free Space全部用完之后,代表该页用完了,去申请新页来插入记录。(User Records存储用户记录,从Free Space获得空间,用完了就开辟新页)

Page Directory(页目录)

单链表的产生:记录在页中按照主键值由小到大顺序串联成一个单链表。

页目录的制作过程:

1.将所有正常记录划分为几组。

2.每个组的最后一条记录的头信息中的n_owned属性表示该组内共有几条记录

3.将每个组的最后一条记录的地址偏移量(槽)单独提取出来按顺序存储到靠近的尾部的地方,这个地方就是所谓的Page Directory即页目录。页面目录中的这些地址偏移量被称为,所以这个页面目录就是由组成的。

页目录中使用二分法快速定位到对应的槽。

通过记录的next_record属性(类似于链表里面的指针)遍历该槽所在的组中的各个记录。

每个记录的头信息中都有一个next_record属性(即指针),从而使页中的所有记录串联成一个单链表

每个数据页的File Header部分都有上一个和下一个页的编号,所以所有的数据页会组成一个双链表。

【Mysql】InnoDB 引擎中的页目录 - 把苹果v咬哭 - 博客园

B+树索引

对上节的总结:

各个数据页可以组成一个双向链表,而每个数据页中的记录会按照主键值从小到大的顺序组成一个单向链表,每个数据页都会为存储在它里边儿的记录生成一个页目录,在通过主键查找某条记录的时候可以在页目录中使用二分法快速定位到对应的,然后再遍历该槽对应分组中的记录即可快速找到指定的记录。

《MySQL是怎么样运行的》读书笔记一 数据页+索引_第6张图片

在一个页中的查找:

以主键为搜索条件:在页目录中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可快速找到指定的记录。

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

在很多页中查找:

  1. 定位到记录所在的页(从第一个页沿着双向链表一直往下找)。
  2. 从所在的页内中进行在一个页中的查找。

 索引

《MySQL是怎么样运行的》读书笔记一 数据页+索引_第7张图片

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

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

《MySQL是怎么样运行的》读书笔记一 数据页+索引_第8张图片

 目录项记录

用来表示目录项的记录(实际也就是一个页)。目录项记录中只有两个列:主键还有页的编号

行格式中的头信息中的record_type属性就是来区分是普通用户记录还是目录项记录的。

  • 0:普通的用户记录
  • 1:目录项记录
  • 2:最小记录
  • 3:最大记录
  • 《MySQL是怎么样运行的》读书笔记一 数据页+索引_第9张图片

查找主键为20的记录的步骤

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

2.再到存储用户记录的页9中根据二分法快速定位到主键值为20的用户记录。

若表中的数据太多,以至于一个数据页不足以存放所有的目录项记录的解决方案:再多整一个存储目录项记录的页。

《MySQL是怎么样运行的》读书笔记一 数据页+索引_第10张图片

在含有多个目录项记录的页中查找主键值为20的步骤:

1.确定目录项记录

我们现在的存储目录项记录的页有两个,即页30页32,又因为页30表示的目录项的主键值的范围是[1, 320)页32表示的目录项的主键值不小于320,所以主键值为20的记录对应的目录项记录在页30中。

2.在页30中通过二分法快速定位到对应目录项,通过主键值12 < 20 < 209,可以定位到对应的记录所在的页就是页9

3.再到存储用户记录的页9中根据二分法快速定位到主键值为20的用户记录。

多级目录:

表中有很多存储目录项记录的页,生成更高的目录来管理这些页。

《MySQL是怎么样运行的》读书笔记一 数据页+索引_第11张图片

 目录层级逐渐增多以后,类似于树-----------B+树

《MySQL是怎么样运行的》读书笔记一 数据页+索引_第12张图片

 规定最下边的那层,也就是存放我们用户记录的那层为第0层。

我们用到的B+树都不会超过4层(查找3个目录项页和一个用户记录页)

聚簇索引

特点一:

1.页内的记录是按照主键的大小顺序排成一个单向链表。(在页内)

2.各个存放用户记录的页也是根据页中用户记录的主键大小顺序排成一个双向链表。(不同页之间)

3.存放目录项记录的页分为不同的层次,在同一层次中的页也是根据页中目录项记录的主键大小顺序排成一个双向链表。(在B+树中同层次的页之间)

特点二:

B+树的叶子节点存储的是完整的用户记录。

二级索引

聚簇索引只能在搜索条件是主键值时才能发挥作用,因为B+树中的数据都是按照主键进行排序的。

如果我们想以别的列作为搜索条件该咋办呢?难道只能从头到尾沿着链表依次遍历记录么?

解决方案:

我们可以多建几棵B+树,不同的B+树中的数据采用不同的排序规则。比方说我们用c2列(非主键)的大小作为数据页、页中记录的排序规则,再建一棵B+树。

这个B+树与上边介绍的聚簇索引有几处不同:

差别一:

  • 页内的记录是按照c2的大小顺序排成一个单向链表。

  • 各个存放用户记录的页也是根据页中记录的c2大小顺序排成一个双向链表。

  • 存放目录项记录的页分为不同的层次,在同一层次中的页也是根据页中目录项记录的c2大小顺序排成一个双向链表。

差别二:

聚簇索引B+树中叶子节点存储的是完整用户记录,二级索引B+树的叶子节点存储的并不是完整的用户记录,而只是c2列+主键(存主键是方便回表)这两个列的值。

差别三:

目录项记录中不再是主键+页号的搭配,而变成了c2列+页号的搭配。(主键被替换为了页号)

以查找c2列的值为4的记录为例:

《MySQL是怎么样运行的》读书笔记一 数据页+索引_第13张图片

  1. 确定目录项记录

    根据根页面,也就是页44,可以快速定位到目录项记录所在的页为页42(因为2 < 4 < 9)。

  2. 通过目录项记录页确定用户记录真实所在的页。

    页42中可以快速定位到实际存储用户记录的页,但是由于c2列并没有唯一性约束,所以c2列值为4的记录可能分布在多个数据页中,又因为2 < 4 ≤ 4,所以确定实际存储用户记录的页在页34页35中。

  3. 在真实存储用户记录的页中定位到具体的记录。

    页34页35中定位到具体的记录。

  4. 但是这个B+树的叶子节点中的记录只存储了c2c1(也就是主键)两个列,获取到主键值,再通过主键来获取完整的用户记录。所以我们必须再根据主键值去聚簇索引中再查找一遍完整的用户记录。

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

联合索引

同时为多个列建立索引,比方说我们想让B+树按照c2c3列的大小进行排序:

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

注意点:

  • 每条目录项记录都由c2c3页号这三个部分组成,各条记录先按照c2列的值进行排序,如果记录的c2列相同,则按照c3列的值进行排序。

  • B+树叶子节点处的用户记录由c2c3和主键c1列组成。

B+树的形成过程

  • 每当为某个表创建一个B+树索引(聚簇索引不是人为创建的,默认就有)的时候,都会为这个索引创建一个根节点页面。最开始表中没有数据的时候,每个B+树索引对应的根节点中既没有用户记录,也没有目录项记录。(创立之初,根节点无记录)

  • 随后向表中插入用户记录时,先把用户记录存储到这个根节点中。

  • 根节点中的可用空间用完时继续插入记录,此时会将根节点中的所有记录复制到一个新分配的页,比如页a中,然后对这个新页进行页分裂的操作,得到另一个新页,比如页b。这时新插入的记录根据键值(也就是聚簇索引中的主键值,二级索引中对应的索引列的值)的大小就会被分配到页a或者页b中,而根节点便升级为存储目录项记录的页。

B+树索引

上一章总结:

  • 每个索引都对应一棵B+树,B+树分为好多层,最下边一层是叶子节点,其余的是内节点。所有用户记录都存储在B+树的叶子节点,所有目录项记录都存储在内节点。

  • InnoDB存储引擎会自动为主键(如果没有它会自动帮我们添加)建立聚簇索引,聚簇索引的叶子节点包含完整的用户记录。(聚簇索引是根据主键排序的

  • 我们可以为自己感兴趣的列建立二级索引二级索引的叶子节点包含的用户记录由索引列 + 主键组成,所以如果想通过二级索引来查找完整的用户记录的话,需要通过回表操作,也就是在通过二级索引找到主键值之后再到聚簇索引中查找完整的用户记录。(叶子节点的记录只存放索引列和主键,防止臃肿。如果想知道完整的用户记录要通过主键来进行一个回表操作)

  • B+树中每层节点都是按照索引列值从小到大的顺序排序而组成了双向链表,而且每个页内的记录(不论是用户记录还是目录项记录)都是按照索引列的值从小到大的顺序而形成了一个单链表。如果是联合索引的话,则页面和记录先按照联合索引前边的列排序,如果该列值相同,再按照联合索引后边的列排序。

  • 通过索引查找记录是从B+树的根节点开始,一层一层向下搜索。由于每个页面都按照索引列的值建立了Page Directory(页目录),所以在这些页面中的查找非常快。

建索引的代价:时间上,空间上(建一个索引就会建立一个B+树)

CREATE TABLE person_info(
    id INT NOT NULL auto_increment,
    name VARCHAR(100) NOT NULL,
    birthday DATE NOT NULL,
    phone_number CHAR(11) NOT NULL,
    country varchar(100) NOT NULL,
    PRIMARY KEY (id),//id列为主键
    KEY idx_name_birthday_phone_number (name, birthday, phone_number)//通过这三列建立二级索引
);

用得上索引:

SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday = '1990-09-27';

用不上索引:

SELECT * FROM person_info WHERE birthday = '1990-09-27';

因为B+树的数据页和记录先是按照name列的值排序的,在name列的值相同的情况下才使用birthday列进行排序,也就是说name列的值不同的记录中birthday的值可能是无序的。而现在你跳过name列直接根据birthday的值去查找,臣妾做不到呀。

联合索引用于排序:

联合索引为:idx_name_birthday_phone_number

SELECT * FROM person_info WHERE name = 'Ashburn' AND birthday = '1980-01-01' AND phone_number > '15100000000';

这个查询的结果集需要先按照name值排序,如果记录的name值相同,则需要按照birthday来排序,如果birthday的值相同,则需要按照phone_number排序。

如果查询条件给出ORDER BY phone_number, birthday, name的顺序,那用不了B+树索引。

不可以使用索引进行排序的几种情况:

情况一:ASC、DESC混用。所以就规定使用联合索引的各个排序列的排序顺序必须是一致的。

情况二:排序列包含非同一个索引的列。即有时候用来排序的多个列不是一个索引里的,这种情况也不能使用索引进行排序。

SELECT * FROM person_info ORDER BY name, country LIMIT 10;

 namecountry并不属于一个联合索引中的列,所以无法使用索引进行排序

情况三:用于分组 

回表的代价

顺序I/O:将相连的内容读取出来。

随机I/O:将不相连的内容读取出来。

一般情况下,顺序I/O比随机I/O的性能高很多。

通过需要回表的记录数来决定是用二级索引 + 回表还是全表扫描。

覆盖索引

为了彻底告别回表操作带来的性能损耗,我们建议:最好在查询列表里只包含索引列

如何挑选索引

1.索引列的类型尽量小(因为数据类型越小,在查询时进行的比较操作越快,索引占用的存储空间就越少,在一个数据页内就可以放下更多的记录,从而减少磁盘I/O带来的性能损耗,也就意味着可以把更多的数据页缓存在内存中,从而加快读写效率。)

2.索引字符串值的前缀,只对列的前10个字符进行索引

CREATE TABLE person_info(
    name VARCHAR(100) NOT NULL,
    birthday DATE NOT NULL,
    phone_number CHAR(11) NOT NULL,
    country varchar(100) NOT NULL,
    KEY idx_name_birthday_phone_number (name(10), birthday, phone_number)
                                        //对name的前10个字符进行索引。
); 

3.让索引列在比较表达式中单独出现

  1. WHERE my_col * 2 < 4

  2. WHERE my_col < 4/2

第2个优于第一个。

不要重复和冗余索引,如:

CREATE TABLE person_info(
    id INT UNSIGNED NOT NULL AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    birthday DATE NOT NULL,
    phone_number CHAR(11) NOT NULL,
    country varchar(100) NOT NULL,
    PRIMARY KEY (id),
    KEY idx_name_birthday_phone_number (name(10), birthday, phone_number),
    KEY idx_name (name(10))
);    

 这就导致了维护索引的成本增加。

为了尽可能少的让聚簇索引发生页面分裂和记录移位的情况,建议让主键拥有AUTO_INCREMENT属性。

你可能感兴趣的:(mysql)