Skip to content

Latest commit

 

History

History
767 lines (408 loc) · 55.2 KB

MySQL技术内幕.md

File metadata and controls

767 lines (408 loc) · 55.2 KB

MySQL技术内幕

1、MySQL体系结构和储存引擎

MySQL被设计为一个单进程多线程架构的数据库。

实例

MySQL数据库由后台线程以及一个共享内存区组成。共享内存可以被运行的后台线程所共享

当启动数据库实例时,MySQL会去读取配置文件,根据配置文件的参数来启动数据库实例。这与Oracle的参数文件相似,不同的是,Oracle中如果没有参数文件,在启动实例时会提示找不到该参数文件,数据库启动失败。而MySQL数据库中,可以没有配置文件,在这种情况下,MySQL会按照编译时的默认参数设置启动实例。

以下命令可以查看当MySQL数据库实例启动时,会在哪些位置查找配置文件。

mysql --help | grep my.cnf

MySQL体系结构

从概念上来说,数据库是文件的集合,是依照每种数据模型组织起来并存放于二级存储器中的数据集合;数据库实例是程序,是位于用户与操作系统之间的一层数据库管理软件,用户对数据库数据的任何操作,包括数据库定义、数据查询、数据维护、数据库运行控制等都是在数据库实例下进行的,应用程序只有通过数据库实例才能和数据库打交道。

换一种更直白的方式来解释:数据库是由一个个文件组成(一般来说都是二进制文件)的,要对这些文件执行诸如SELECT、INSERT、UPDATE和DELETE之类的数据库操作是不能通过简单的操作文件来更改数据库内容的,需要通过数据库实例来完成数据库的操作

需要特别注意的是,储存引擎是基于表的,而不是数据库

MySQL储存引擎

InnoDB储存引擎

InnoDB储存引擎支持事务,其设计目标主要面向在线事务处理的应用。其特点是行锁设计、支持外键,并支持类似于Oracale的非锁定读(即默认读取操作不会产生锁),同时被设计用来最有效地利用以及使用内存和CPU。从MySQL数据库5.5.8版本开始,InnoDB储存引擎是默认的储存引擎。

InnoDB通过使用多版本并发控制(MVCC)来获得高并发性,并且实现了SQL标准的4种隔离级别,默认为REPEATABLE级别。

对于表中数据的储存,InnoDB储存引擎采用了聚集的方式,因此每张表的储存都是按照主键的顺序进行存放如果没有显示地在表定义时指定主键,InnoDB储存引擎会为每一行生成一个6字节的ROWID,并以此作为主键

MyISAM储存引擎

MyISAM储存引擎不支持事务、表锁设计,支持全文索引,主要面向一些OLAP数据库应用。数据库系统与文件系统很大的一个不同之处在于对事务的支持(数据库与传统文件系统的最大区别在于数据库是支持事务的,而MySQL数据库的设计者在开发时却认为可能不是所有的应用都需要事务,所以存在不支持事务的储存引擎),然而MyISAM储存引擎是不支持事务的。究其根本,这也不是很难理解。试想用户是否在所有的应用都需要事务呢?在数据仓库中,如果没有ETL这些操作,只是简单的报表查询是否还需要事务的支持呢?此外,MyISAM储存引擎的另一个与众不同的地方是它的缓冲池只缓存索引文件,而不缓冲数据文件,这点和大多数的数据库都非常不同。

MyISAM储存引擎表由MYD和MYI组成,MYD(D指的是data)用来存放数据文件,MYI(I指的是Index)用来存放索引文件

注意:

对于MyISAM储存引擎表,MySQL数据库只缓存其索引文件,数据文件的缓存交由操作系统本身来完成,这与其他使用LRU算法缓存数据的大部分数据库大不相同

各储存引擎之间的比较

连接MySQL

命名管道和共享内存

SQL Server数据库默认安装后的本地数据库连接是使用命名管道。在MySQL数据库中须在配置文件中启用 —enable-named-pipe选项。在MySQL4.1之后的版本中,MySQL还提供了共享内存的连接方式

Unix域套接字

在Linux和Unix环境下,还可以使用UNIX域套接字。UNIX域套接字其实不是一个网络协议,所以只能在MySQL客户端和数据库实例在一台服务器上的情况下使用。用户可以在配置文件中指定套接字文件的路径。

2、InnoDB储存引擎

InnoDB体系架构

InnoDB储存引擎有多个内存块,可以认为这些内存块组成了一个大的内存池,负责如下工作:

  • 维护所有进程/线程需要的多个内部数据结构
  • 缓存磁盘上的数据,方便快速地读取,同时在对磁盘文件的数据修改之前在这里缓存
  • 重做日志(redo log)缓冲
  • …...

后台线程的主要作用是负责刷新内存池中的数据,保证缓冲池中的内存缓存的是最近的数据。此外将已修改的数据文件刷新到磁盘文件,同时保证在数据库发生异常的情况下InnoDB能恢复到正常运行状态。

后台线程

Master Thread

这是一个非常核心的后台线程,主要负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性,包括脏页的刷新、合并插入缓冲等。

IO Thread

InnoDB储存引擎中大量使用了AIO(Async IO)来处理写IO请求,这样可以极大提高数据库的性能。而IO Thread的工作主要是负责这些IO请求的回调(call back)处理

Purge Thread

事务被提交后,其所使用的undolog可能不再需要,因此需要Purge Thread来回收已经使用并分配的undo页。

内存

缓冲池

InnoDB储存引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理。由于CPU速度与磁盘速度之间的鸿沟,基于磁盘的数据库系统通常使用缓冲池技术来提高数据库的整体性能。

缓冲池简单来说就是一块内存区域,通过内存的速度来弥补磁盘速度较慢对数据库性能的影响。在数据库中进行读取页的操作,首先将从磁盘读到的页存放在缓冲池中,这个过程称为将页“FIX”在缓冲池中。下次再读相同的页时,首先判断该页是否在缓冲池中。若在缓冲池中,称该页在缓冲池中被命中,直接读取该页。否则,读取磁盘上的页

对于数据库中页的修改操作,则首先修改在缓冲池中的页,然后再以一定的频率刷新到磁盘上。这里需要注意的是,页从缓冲池刷新回磁盘的操作并不是在每次页发生更新时触发,而是通过一种称为Checkpoint的机制刷新回磁盘。

具体来看,缓冲池中缓冲的数据页类型有:索引页、数据页、undo页、插入缓冲、自适应哈希索引、InnoDB储存的锁信息、数据字典信息等。

从InnoDB 1.0x版本开始,允许有多个缓冲池实例

那么InnoDB储存引擎是怎么对怎么大的内存区域进行管理的呢?

通常来说,数据库中的缓冲池是通过LRU(最近最少使用)算法来进行管理的。即最频繁使用的页在LRU列表的前端,而最少使用的页在LRU列表的尾端。当缓冲池不能存放新读取的页时,将首先释放LRU列表尾端的页

在InnoDB储存引擎中,对传统的LRU算法做了一些优化。在InnoDB储存引擎中,LRU列表中还加入了midpoint位置。新读取到的页,虽然是最新访问的页,但并不是最直接放入到LRU列表的首部,而是放入到LRU列表的midpoint位置。在默认配置下,该位置在LRU列表长度的5/8处。

那为什么不采用朴素的LRU算法,直接将新读取的页放入到LRU列表的首部呢?这是因为若直接将读取到的页放入到LRU的首部,那么某些SQL操作可能会使缓冲池中的页被刷新出,从而影响缓冲池的效率。常见的这类操作为索引或数据的扫描操作。这类操作需要访问表中的许多页,甚至是全部的页,而这些页通常来说又仅在这次查询操作中需要,并不是活跃的热点数据。如果页被放入LRU列表的首部,那么非常可能将所需要的热点数据页从LRU列表中移除,而在下一次需要读取该页时,InnoDB储存引擎需要再次访问磁盘

InnoDB关键特性

插入缓冲

在InnoDB储存引擎中,主键是行唯一的标识符。通常应用程序中行记录的插入顺序(指的是插入缓冲区)是按照主键递增(要给主键一个自增的约束)的顺序进行插入的。因此,插入聚集索引(Primary Key)一般是顺序的,不需要磁盘的随机读取。比如按照下列SQL定义表:

CREATE TABLE t (
    a INT AUTO_INCREMENT,
    b VARCHAR(30),
    PRIMARY KEY(a)
);

其中a列是自增长的,若对a列插入NULL值,则由于其具有AUTO_INCREMENT属性,其值会自动增长。同时页中的行记录按照a的值进行顺序存放在一般情况下,不需要随机读取另一个页中的记录。因此,对于这类情况下的插入操作,速度是非常快的。

但是不可能每张表只存在聚集索引,更多情况下,一张表上有多个非聚集的辅助索引。比如,用户需要按照b这个字段进行查找,并且b这个字段不是唯一的,即表是按照如下的SQL语句定义的:

CREATE TABLE t (
    a INT AUTO_INCREMENT,
    b VARCHAR(30),
    PRIMARY KEY(a),
    key(b)
);

在这样的情况下产生了一个非聚集的且不是唯一的索引。在进行插入操作时,数据页的存放还是按照主键a进行顺序存放的,但是对于非聚集索引叶子节点(非聚集索引的叶子节点存放的不是数据)的插入不再是顺序的了(因为为b字段创建的非聚集索引它不是递增的),这是就需要离散地访问非聚集索引页,由于随机读取的存在而导致了插入操作性能下降。当然这并不是这个b字段上索引的错误,而是因为B+树的特性决定了非聚集索引插入的离散性

InnoDB储存引擎开创性地设计了Insert Buffer,对于非聚集索引的插入或更新操作,不是每一次直接插入到索引页中,而是先判断插入的非聚集索引页是否在缓冲池中,若在,则直接插入;若不在,则先放入到一个Insert Buffer对象中,好似欺骗。数据库这个非聚集的索引已经插到叶子节点,而实际并没有,只是存放在另一个位置。然后再以一定的频率和情况进行Insert Buffer和辅助索引页子节点的merge(合并)操作,这时通常能将多个插入合并到一个操作中(因为在一个索引页中),这就大大提高了对于非聚集索引插入的性能。

然而Insert Buffer的使用需要同时满足以下两个条件:

  • 索引是辅助索引
  • 索引不是唯一的
Insert Buffer的内部实现

Insert Buffer的数据结构是一棵B+树。全局只有一棵Insert Buffer B+树,负责对所有的表的辅助索引进行Insert Buffer。而这棵B+树存放在共享表空间中。

Insert Buffer是一棵B+树,因此其也由叶节点和非叶节点组成。非叶节点存放的是查询的search key(键值),其构造如图所示:

search key 一共占用9个字节,其中space表示待插入记录所在表的表空间id,在InnoDB储存引擎中,每个表有一个唯一的space id,可以通过space id查询得知是哪张表。space占4字节。marker占用1字节,他是用来兼容老版本的Insert Buffer。offset表示页所在的偏移量,占用4字节。

当一个辅助索引要插入到页(space,offset)时,如果这个页不在缓冲池中,那么InnoDB储存引擎首先根据上述规则构造一个search key,接下来查询Insert Buffer这棵B+树,然后再将这条记录插入到Insert Buffer B+树的叶子节点中。

两次写

如果说Insert Buffer带给InnoDB储存引擎的是性能上的提升,那么doublewrite(两次写)带给InnoDB储存引擎的是数据页的可靠性。

当发生数据库宕机时,可能InnoDB储存引擎正在写入每个页到表中,而这个页只写了一部分,比如16KB的页,只写了前4KB,之后就发生了宕机,这种情况被称为部分写失效。在InnoDB储存引擎未使用doublewrite技术前,曾经出现过因为部分写失效而导致数据丢失的情况。

有经验的DBA也许会想,如果发生写失效,可以通过重做日志进行恢复。这是一个办法。但是必须清楚地认识到,重做日志中记录的是对页的物理操作,如果偏移量800,写‘aaaa’记录。如果这个页本身已经发生了损坏,再对其进行重做是没有意义的(因为数据也是损坏的)。这就是说,在应用重做日志前,用户需要一个页的副本,当写入失效发生时,先通过页的副本来还原页,再进行重做,这就是doublewrite。在InnoDB储存引擎中,doublewrite的体系架构如图:

自适应哈希索引

哈希是一种非常快的查找方法,在一般情况下这种查找的时间复杂度为O(1),即一般仅需要一次查找就能定位数据。而B+树的查找次数,取决于B+树的高度,在生产环境中,B+树的高度一般为3~4层,故需要3~4次的查询

InnoDB储存引擎会监控对表上各索引页的查询。如果观察到建立哈希索引可以带来速度提升,则建立哈希索引,称之为自适应哈希索引自适应哈希索引是通过缓冲池的B+树页构造而来,因此建立的速度很快,而且不需要对整张表构建哈希索引。InnoDB储存引擎会自动根据访问的频率和模式来自动地为某些热点页建立哈希索引

异步IO

为了提高磁盘操作性能,当前的数据库系统都采用异步IO(AIO)的方式来处理磁盘操作,InnoDB储存引擎亦是如此。

与AIO对应的是Sync IO,即每次进行一次IO操作,需要等待此次操作结束才能继续接下来的操作。但是如果用户发出的是一条索引扫描的查询,那么这条SQL查询语句可能需要**扫描多个索引页,也就是需要进行多次的IO操作。**在扫描一个页等待其完成后再进行下一次的扫描,这是没有必要的。用户可以在发出一个IO请求后立即再发出另一个IO请求,当全部IO请求发送完毕后,等待所有IO操作的完成,这就是AIO。

刷新邻接页

InnoDB储存引擎还提供了Flush Neighbor Page(刷新邻接页)的特性。其工作原理为:当刷新一个脏页时,InnoDB储存引擎会检测该页所在区(extent)的所有页,如果是脏页,那么一起进行刷新。这样做的好处是通过AIO可以将多个IO写入操作合并为一个IO操作。

4、表

索引组织表

即,表是根据索引来顺序组织存放的。

在InnoDB储存引擎中,表都是根据主键顺序组织存放的,这种储存方式的表称为索引组织表。在InnoDB储存引擎表中,每张表都有一个主键(Primary Key),如果在创建表时没有显示地定义主键,则InnoDB储存引擎会按如下方式选择或创建主键:

  • 首先判断表中是否有非空的唯一索引(Unique NOT NULL),如果有,则该列即为主键
  • 如果不符合上述条件,InnoDB储存引擎自动创建一个6字节大小的指针。

当表中有多个非空唯一索引时,InnoDB储存引擎将选择建表时第一个定义的非空唯一索引为主键。

InnoDB逻辑储存结构

从InnoDB储存引擎的逻辑储存结构看,所有数据都被逻辑地存放在一个空间中,称为表空间(tablespace)。表空间又由段(segment)、区(extent)、页(page)组成。页在一些文档中有时也称为块(block),InnoDB储存引擎的逻辑储存结构大致如图所示:

表空间

表空间可以看做是InnoDB储存引擎逻辑结构的最高层,所有的数据都存放在表空间中。在默认情况下InnoDB储存引擎有一个共享表空间ibdata1,即所有数据都存放在这个表空间内。如果用户启用了参数innodb_file_per_table,则每张表内的数据可以单独放到一个表空间内。

如果启用了innodb_file_per_table的参数,需要注意的是每张表的表空间内存放的只是数据、索引和插入缓冲Bitmap页,而那些回滚信息,插入缓冲索引页、系统事务信息、二次写缓冲(Double write buffer)等还是存放在原来的共享表空间内。这说明了一个问题:即使在启用了参数innodb_file_per_table之后,共享表空间还是会不断地增加其大小

表空间是由各个段组成的,常见的段有数据段、索引段、回滚段等。因为InnoDB储存引擎表是索引组织的,因此数据即索引,索引即数据。那么数据段即为B+树的叶子节点,索引段即为B+树的非叶子节点。

区是由连续页组成的空间,在任何情况下每个区的大小都为1MB。为了保证区中页的连续性,InnoDB储存引擎一次从磁盘申请4~5个区。在默认情况下,InnoDB储存引擎页的大小为16KB,即一个区中一共有64个连续的页。

同大多数数据库一样,InnoDB有页(Page)的概念(也可以称为块),页是InnoDB磁盘管理的最小单位。

InnoDB储存引擎是面向行(row-oriented)的,也就是说数据是按行存放的。每个页存放的行记录也是有硬性定义的,最多允许存放16KB / 2 - 200行的记录,即7992行记录。

约束

数据完整性

关系型数据库系统和文件系统的一个不同点是,关系数据库本身能保证储存数据的完整性,不需要应用程序的控制,而文件系统一般需要在程序端进行控制。当前几乎所有的关系型数据库都提供了约束机制,该机制提供了一条强大而简易的途径来保证数据库中数据的完整性。一般来说,数据完整性有以下三种形式:

实体完整性保证表中只有一个主键。在InnoDB储存引擎表中,用户可以通过定义Primary Key或Unique Key约束来保证实体的完整性。用户还可以通过编写一个触发器来保证数据完整性

域完整性保证数据每列的值满足特定的条件。在InnoDB储存引擎表中,域完整性可以通过以下几种途径来保证:

  • 选择合适的数据类型确保一个数据值满足特定条件
  • 外键约束
  • 编写触发器

参照完整性保证两张表直接的关系。InnoDB储存引擎支持外键,因此允许用户定义外键以强制参照完整性,也可以通过编写触发器以强制执行。

对于InnoDB储存引擎本身而言,提供了以下几种约束:

  • Primary Key
  • Unique Key
  • Foreign Key
  • Default
  • NOT NULL

约束和索引的区别

前面看到Primary Key 和Unique Key的约束,有人不禁会问:这不就是通常创建索引的方法吗?那约束和索引有什么区别呢?

的确,当用户创建了一个唯一索引就创建了一个唯一的约束。但是约束和索引的概念还是有所不同的,约束更是一个逻辑的概念,用来保证数据的完整性,而索引是一个数据结构,既有逻辑上的概念,在数据库中还代表物理储存的方式

ENUM和SET约束

MySQL数据库不支持传统的CHECK约束,但是通过ENUM和SET类型可以解决部分这样的约束需求。例如表上有一个性别类型,规定域的范围只能是male或female,在这种情况下用户可以通过ENUM类型来进行约束。

外键约束

外键原来保证参照完整性,MySQL数据库的MyISAM储存引擎本身并不支持外键,对于外键的定义只是起到了一个注释的作用。而InnoDB储存引擎则完整支持外键约束

在Oracle数据库中,对于建立外键的列,一定不要忘记给这个列加上一个索引。而InnoDB储存引擎在外键建立时会自动地给该列加一个索引。因此可以很好地避免外键列上无索引而导致的死锁问题的产生。

对于参照完整性约束,外键能起到一个非常好的作用。但是对于数据的导入操作时,外键往往导致在外键约束的检查上花费大量时间。因为MySQL数据库的外键是即时检查的,所以对于导入的每一行都会进行外键检查。但是用户可以在导入过程中忽视外键的检查

视图

在MySQL数据库中,视图(View)是一个命名的虚表,它由一个SQL查询来定义,可以当作表使用。与持久表不同的是,视图中的数据没有实际的物理储存。

视图的作用

视图在数据库中发挥着重要的作用。视图的主要用途之一是被用做一个抽象装置,特别是对于一些应用程序,程序本身不需要关心基表的结构,只需要按照视图定义来取数据或更新数据,因此,视图同时在一定程度上起到了一个安全层的作用

虽然视图是基于基表的一个虚拟表,但是用户可以对某些视图进行更新操作,其本质就是通过视图的定义来更新基本表。

物化视图

Oracle数据库支持物化视图—该视图不是基于基表的虚表,而是根据基表实际存在的实表,即物化视图的数据存储在非易失的储存设备上。物化视图可以用于预先计算并保存多表的链接(JOIN)或聚集(GROUP BY)等耗时较多的SQL操作结果这样,在执行复杂查询时,就可以避免进行这些耗时的操作,从而快速得到结果。(实际上是用空间换时间

物化视图的好处是对于一些复杂的统计类查询能直接查出结果。

物化视图的刷新是指当基表发生了DML操作后,物化视图何时采用哪种方式和基表进行同步。刷新的模式有两种:

  • ON DEMAND
  • ON COMMIT

ON DEMAND意味着物化视图在用户需要的时候进行刷新,ON COMMIT意味着物化视图在对基表的DML操作提交的同时进行刷新。

MySQL数据库本身并不支持物化视图,换句话说,MySQL数据库中的视图总是虚拟的。但是用户可以通过一些机制来模拟物化视图的功能。例如要创建一个ON DEMAND的物化视图还是比较简单的,用户只需要定时把数据导入到另一张表

分区表

分区的过程是将一个表或索引分解为多个更小、更可管理的部分。就访问数据库的应用而言,从逻辑上讲,只有一个表或一个索引,但是在物理上这个表或索引可能由数十个分区组成。每个分区都是独立的对象,可以独自处理,也可以作为一个更大对象的一部分进行处理

MySQL数据库支持的分区类型为水平分区(即将同一表中不同行的记录分配到不同的物理对象文件中),并不支持垂直分区(即将同一表中不同列的记录分配到不同的物理文件中)。

分区类型

RANGE分区

最常用的一种分区类型。下面的CREATE TABLE语句创建了一个id列的区间分区表。当id小于10时,数据插入p0分区。当id大于等于10小于20时,数据插入p1分区。

CREATE TABLE t (
    id INT
)ENGINE = INNODB
PARTITION BY RANGE (id) (
    PARTITION p0 VAlUES LESS THAN (10),
    PARTITION p1 VALUES LESS THAN (20)
);

查看表在磁盘上的物理文件,启用分区之后,表不再由一个ibd文件组成了,而是由建立分区时的各个分区ibd文件组成

因为表t根据列id进行分区,所以数据是根据列id的值的范围存放在不同的物理文件中的

LIST分区

LIST分区和RANGE分区非常相似,只是分区列的值是离散的,而非连续的。例如:

CREATE TABLE t (
    a INT,
    b INT
)ENGINE = INNODB
PARTITION BY LIST(b) (
    PARTITION p0 VALUES IN (1,3,5,7,9),
    PARTITION p1 VALUES IN (0,2,4,6,8)
);

在用INSERT插入多行数据的过程中遇到分区未定义的值时,MyISAM和InnoDB储存引擎的处理完全不同。MyISAM储存引擎会将之前的行数据都插入,但之后的数据不会被插入。而InnoDB储存引擎将其视为一个事务,因此没有任何数据插入

HASH分区

HASH分区的目的是将数据均匀地分布到预先定义的各个分区中,保证各分区的数据数量大致都是一样的。在RANGE和LIST分区中,必须明确指定一个给的的列值或列值集合应该保存在哪个分区中;而在HASH分区中,MySQL自动完成这些工作,用户所要做的只是基于要进行哈希分区的列值指定一个列值或表达式,以及指定被分区的表将要被分割成的分区数量。

下面创建一个HASH分区的表t,分区按日期列b进行:

CREATE TABLE t_hash (
    a INT,
    b DATETIME
)ENGINE = InnoDB
PARTITION BY HASH (YEAR(b))
PARTITIONs 4;

如果插入一个列b为2010-04-01的记录到表t_hash中,那么保存该条记录的分区如下:

MOD(YEAR('2010-04-01'), 4)
= MOD(2010, 4)
= 2

因此记录会被放入分区p2中。

分区和性能

我们常听到开发人员说‘对表做个分区’,然后数据库的查询就会快了。这是真的吗?实际上可能根本感觉不到查询速度的提升,甚至会发现查询速度急剧下降。因此,在合理使用分区之前,必须理解分区的使用环境。

数据库的应用分为两类:一类是OLTP(在线事务处理),如Blog、电子商务、网络游戏等;另一类是OLAP(在线分析处理),如数据仓库、数据集市。在一个实际的应用环境,可能既有OLTP的应用,也有OLAP的应用。如网络游戏中,玩家操作的游戏数据库应用就是OLTP的,但是游戏厂商可能需要对游戏产生的日志进行分析,通过分析得到的结果来更好的服务于游戏,预测玩家的行为等,而这却是OLAP的应用。

对于OLAP的应用,分区的确是可以很好地提高查询的性能,因为OLAP应用大多数查询需要频繁地扫描一张很大的表。假设有一张1亿行的表,其中有一个时间戳属性列。用户的查询需要从这张表中获取一年的数据。如果按时间分区,则只需要扫描相应的分区即可。这就是前面介绍的Partition Pruning技术。

然而对于OLTP的应用,分区应该非常小心。在这种应用下,通常不可能会获取一张大表中10%的数据,大部分都是通过索引返回几条记录即可。而根据B+树索引的原理可知,对于一张大表,一般的B+树需要2~3次的磁盘IO。因此B+树可以很好地完成操作,不需要分区的帮助,并且设计不好的分区会带来严重的性能问题。

很多开发团队会认为含有1000W行的表是一张非常巨大的表,所以它们往往会选择采用分区,如对主键做10个HASH的分区,这样每个分区就只有100W的数据了,因此查询应该变得更快了,如:

SELECT * FROM TABLE WHERE PK=@pk

但是有没有考虑到这样一种情况:100W和1000W行的数据本身构成的B+树的层次都是一样的,可能都是2层。那么上述走主键分区的索引并不会带来性能的提高。好的,如果1000W的B+树的高度是3,100W的B+树的高度是2,那么上述按主键分区的索引可以避免一次IO,从而提高查询的效率。这没问题,但是这张表只有主键索引,没有任何其他的列需要查询的。如果还有类似如下的SQL语句:

SELECT * FROM TABLE WHERE KEY=@key

这时对于KEY的查询需要扫描所有的10个分区,即使每个分区的查询开销为2次IO,则一共需要20次IO。而对于原来单表的设计,对于KEY的查询只需要2~3次IO。

5、索引与算法

InnoDB储存引擎索引概述

InnoDB储存引擎支持以下几种常见的索引:

  • B+树索引
  • 全文索引
  • 哈希索引

前面已经提到过,InnoDB储存引擎支持的哈希索引是自适应的,InnoDB储存引擎会根据表的使用情况自动为表生成哈希索引,不能人为干预是否在一张表中生成哈希索引

B+树索引就是传统意义上的索引,这是目前关系型数据库系统中查找最为常用和最为有效的索引。B+树索引的构造类似于二叉树,根据键值(Key Value)快速找到数据。

B+树索引并不能找到一个给定键值的具体行。B+树索引能找到的只是被查找数据行所在的页。然后数据库通过把页读入到内存,再在内存中进行查找,最后得到要查找的数据。(所以用于范围查找)

注意:

B+树中的B不是代表二叉,而是代表平衡,因为B+树是从最早的平衡二叉树演化而来,而B+树不是一个二叉树

数据结构与算法

二分查找法

每页Page Directory中的槽是按照主键的顺序存放的,对于某一条具体记录的查询是通过对Page Directory进行二分查找得到的。

二叉查找树和平衡二叉树

B+树是通过二叉查找树,再由平衡二叉树,B树演化而来的。

给定一组数据,二叉查找树可以任意地构造(当然,也可以构造成线性的,此时查询效率和顺序查找差不多)。因此,若想最大性能地构造一棵二叉查找树,需要这棵二叉查找树是平衡的,从而引出了新的定义--平衡二叉树,或称为AVL树。

平衡二叉树的定义如下:首先符合二叉查找树的定义,其次必须满足任何节点的两个子树的高度最大差为1。平衡二叉树的查找性能是比较高的,但不是最高的,只是接近最高性能。最好的性能需要建立一棵最优二叉树,但是最优二叉树的建立和维护需要大量的操作,因此,用户一般只需要建立一棵平衡二叉树即可。

平衡二叉树的查询速度的确很快,但是维护一棵平衡二叉树的代价是非常大的。通常来说,需要1次或多次左旋和右旋来得到插入或更新后树的平衡性。因此对一棵平衡树的维护是有一定开销的,不过平衡二叉树多用于内存结构对象中,因此维护的开销相对较小。

B+树

B+树由B树和索引顺序访问方法(ISAM,是不是很熟悉?对,这也是MyISAM引擎最初参考的数据结构)演化而来,但是在现实使用过程中几乎已经没有使用B树的情况了。

B+树是为了磁盘或其他直接存取辅助设备设计的一种平衡查找树。在B+树中,所有记录节点都是按键值对的大小顺序存放在同一层的叶子节点上,由各叶子节点指针进行连接。先来看一个B+树,其高度为2,每页可存放4条记录,扇出为5,如图所示:

Index Page的左指针指向的这一页的节点都小于Index Page的这个节点,Index Page的右指针指向的这一页的节点都大于Index Page的这个节点。

从图中可以看出,所有记录都在叶子节点上,并且是顺序存放的,如果用户从最左边的叶子节点开始顺序遍历,可以得到所有键值的顺序排序:5、10、15、20、25、30、50、55、60、65、75、80、90。

B+树的插入操作

B+树的插入操作必须保证插入后叶子节点中的记录依然排序。

分别举例:

若用户插入28这个键值,发现Leaf Page 和 Index Page都没有满,直接进行插入即可,之后如图:

接着再插入70这个键值,这时原先的Leaf Page已经满了,但是Index Page还没有满,符合表5-1的第二种情况,这时插入Leaf Page后的情况为50、55、60、65、70,并根据中间的值60来拆分叶子节点,可得图:

最后插入键值95,这时符合第三种情况,即Leaf Page 和 Index Page都满了,这时需要做两次拆分,如图:

可以看出,不管怎么变化,B+树总是会保存平衡。但是为了保持平衡对于新插入的键值可能需要做大量的拆分页操作。因此B+树结构主要用于磁盘,页的拆分意味着磁盘的操作,所以应该在可能的情况下尽量减少页的拆分查找。因此,B+树同样提供了类似于平衡二叉树的旋转功能。

旋转发生在Leaf Page已经满,但是其左右兄弟节点没有满的情况下。这时B+树并不会急于去做拆分页的操作,而是将记录移到所在页的兄弟节点上。因此,再来看图5-7,若插入键值70,其实B+树并不会急于去拆分叶子节点,而是去做旋转操作,得到下图:

可以看到,采用旋转操作使B+树减少了一次页的拆分查找,同时这棵B+树的高度依然还是2。

B+树索引

B+树索引的本质就是B+树在数据库中的实现。但是B+树索引在数据库中有一个特点是高扇出性因此在数据库中,B+树的高度一般都在2~4层,也就是说查找某一键值的行记录时最多只需要2到4次IO,这倒不错。因为当前一般的机械磁盘每秒至少可以做100次IO,2~4次的IO意味着查询时间只需0.02~0.04秒。

数据库中的B+树索引可以分为聚集索引和辅助索引,但是不管是聚集索引还是辅助索引,其内部都是B+树,即高度平衡的,叶子节点存放着所有的数据。聚集索引与辅助索引不同的是,叶子节点存放的是否是一整行的信息即在数据页上存放的是否是完整的每行记录)。

聚集索引

聚集索引决定数据在物理磁盘上的物理排序,一个表只能有一个聚集索引,如果定义了主键,那么InnoDB会通过主键来聚集数据,如果没有定义主键,InnoDB会选择一个唯一的非空索引代替,如果没有唯一的非空索引,InnoDB会隐式定义一个主键来作为聚集索引。

聚集索引可以很大程度的提高访问速度,因为聚集索引将索引和行数据保存在了同一个B-Tree中,所以找到了索引也就相应的找到了对应的行数据,但在使用聚集索引的时候需注意避免随机的聚集索引(一般指主键值不连续,且分布范围不均匀),如使用UUID来作为聚集索引性能会很差,因为UUID值的不连续会导致增加很多的索引碎片和随机I/O,最终导致查询的性能急剧下降。

之前介绍过,InnoDB储存引擎表是索引组织表,即表中数据按照主键顺序存放。而聚集索引就是按照每张表的主键构造一棵B+树,同时叶子节点中存放的即为整张表的行记录数据,也将聚集索引的叶子节点称为数据页。同B+树数据结构一样,每个数据页都是通过一个双向链表来进行连接的。

由于实际的数据页只能按照一棵B+树进行排序,因此每张表只能拥有一个聚集索引(也就是说,聚集索引本质上就是一棵B+树)。因为聚集索引能够在B+树索引的叶子节点上直接找到数据,此外,由于定义了数据的逻辑顺序,聚集索引能够特别快地访问针对范围值的查询。查询优化器能够快速发现某一段范围的数据页需要扫描。

在数据页上存放的是完整的每行的记录,而在非数据页的索引页中,存放的仅仅是键值及指向数据页的偏移量,而不是一个完整的行记录

许多数据库的文档会告诉读者:聚集索引按照顺序物理地储存数据。但是试想一下,如果聚集索引必须按照特定顺序存放物理记录,则维护成本显得非常之高。所以,聚集索引的储存并不是物理上连续的,而是逻辑上连续的。这其中有两点:一是前面说过的页通过双向链表连接,页按照主键的顺序排序;另一点是每个页中的记录也是通过双向链表进行维护的物理储存上可以同样不按照主键储存

聚集索引的一个好处是:它对于主键的排序查找和范围查找速度非常快。叶子节点的数据就是用户所要查询的数据。如用户需要查询一张注册用户的表,查询最后注册的10位用户,由于B+树索引中的叶子节点是双向链表,用户可以快速找到最后一个数据页,并取出10条记录。

辅助索引(也称为非聚集索引)

对于辅助索引,叶子节点并不包含行记录的全部数据。叶子节点除了包含键值以外,每个叶子节点中的索引行还包含了一个书签。该书签用来告诉InnoDB储存引擎哪里可以找到与索引相对应的行数据。由于InnoDB储存引擎表是索引组织表,因此InnoDB储存引擎的辅助索引的书签就是相应行数据的聚集索引键。下图展示了辅助索引和聚集索引中间的关系(上面的是辅助索引,下面的是聚集索引):

辅助索引的存在并不影响数据在聚集索引中的组织,因此每张表可以有多个辅助索引(上图有两个辅助索引)。当通过辅助索引来寻找数据时,InnoDB储存引擎会遍历辅助索引并通过叶级别的指针获得指向主键索引的主键,然后再通过主键索引来找到一个完整的行记录

举例来说,如果在一棵高度位3的辅助索引树中查找数据,那需要对这棵辅助索引树遍历3次找到指定主键,如果聚集索引的高度同样为3,那么还需要对聚集索引树进行3次查找,最终找到一个完整的行数据所在的页,因此一共需要6次逻辑IO访问以得到最终的一个数据页。

B+树索引的管理
索引管理

用户可以设置对整个列的数据进行索引,也可以只索引一个列的开头部分数据,例如创建表t,列b为varchar(8000),但是用户可以只索引前100个字段。

Cardinality值

什么是Cardinality

并不是在所有的查询条件中出现的列都需要添加索引。对于什么时候添加B+树索引一般经验是,在访问表中很少一部分时(即得到的行记录数很少)使用B+树索引才有意义。对于性别字段、地区字段、类型字段,它们可能取值的范围很小。例如,按照性别进行查询时,可取值的范围一般只有‘M’、‘F’。因此以性别作为查询条件得到的结果可能是该表50%的数据(假设男女比例1:1),这时添加B+树索引是完全没有必要的。相反,如果某个字段的取值范围很广,几乎没有重复,则使用B+树索引是最适合的。

B+树索引的使用

联合索引

联合索引是指对表上的多个列进行索引。例如,一下代码创建了一张t表,并且索引idx_a_b是联合索引,联合的列为(a, b)。

CREATE TABLE t (
    a INT,
    b INT,
    PRIMARY KEY (a),
    KEY idx_a_b (a, b)
) ENGINE = InnoDB;

从本质上来说,联合索引也是一棵B+树,不同的是联合索引的键值的数量不是1,而是大于等于2。接着来讨论两个整形列组成的联合索引,假定两个键值的名称分别为a,b,如图:

可以看出,和之前讨论的单个键值的B+数并没有什么不同,键值都是排序的,通过叶子节点可以逻辑上顺序地读出所有数据,就上面的例子来说,即(1,1)、(1,2)、(2,1)、(2,4)、(3,1)、(3,2)。数据按(a,b)的顺序进行了存放(即先比a再比b)。

因此,对于查询

SELECT * FROM TABLE WHERE a = xxx AND b = xxx;

显然是可以使用(a,b)这个联合索引的。对于单个的a列查询:

SELECT * FROM TABLE WHERE a = xxx;

也是可以使用这个(a,b)索引。但是对于b列的查询:

SELECT * FROM TABLE WHERE b = xxx; 

则不可以使用这棵B+树索引。因为我们可以看到叶子节点上的b值为1、2、1、4、1、2,显然是不排序的,因此对于b列的查询使用不到(a,b)的索引。

联合索引的第二个好处是已经对第二个键值进行了排序处理。例如,在很多情况下应用程序都需要查询某个用户的购物情况,并按照时间进行排序,最后取出最近三次的购买记录,这时使用联合索引可以避免多一次的排序操作,因为索引本身在叶子节点已经排序了。(例如查找某个用户最近三次的购买记录,那么可以添加一个(user_id, buy_time)联合索引)

覆盖索引

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

对于InnoDB储存引擎的辅助索引而言,由于其包含了主键信息,因此其叶子节点存放的数据为(primary key1,primary key2,…,key1,key2)。

覆盖索引的另一个好处是对于某些统计问题而言。例如:

SELECT COUNT(*) FROM buy_log;

InnoDB储存引擎并不会选择通过查询聚集索引来进行统计。由于buy_log表上还有辅助索引,而辅助索引远小于聚集索引,选择辅助索引可以减少IO操作。

此外,在通常情况下,诸如(a, b)的联合索引,一般是不可以选择列b中所谓的查询条件。但是如果是统计操作,并且是覆盖索引的,则优化器会进行选择。

优化器选择不使用索引的情况

在某些情况,当执行EXPLAIN命令进行SQL语句的分析时,会发现优化器并没有选择索引去查找数据,而是通过扫描聚集索引,也就是直接进行全表扫描来得到数据。这种情况多发生于范围查找、JOIN连接操作等情况。

哈希算法

哈希算法是一种常见算法,时间复杂度为O(1),且不只存在于索引中,每个数据库应用中都存在该数据库结构。设想一个问题,当前服务器的内存为128GB时,用户怎么从内存中得到某一个被缓存的页呢?虽然内存中查询速度很快,但是也不可能每次都要遍历所有内存来进行查找。这时对于字典操作只需O(1)的哈希算法就有了很好的用武之地。

哈希表

哈希表也称散列表,由直接寻址表改进而来。我们先来看看直接寻址表。当关键字的全域U比较小时,直接寻址是一种简单而有效的技术。假设某应用要用到一个动态集合,其中每个元素都有一个取自全域U={0, 1, …, m - 1}的关键字。同时假设没有两个元素具有相同的关键字。

用一个数组(即直接寻址表)T[0…m-1]表示动态集合,其中每个位置(或称槽或称桶)对应全域U中的一个关键字。图5-38说明了这个方法,槽k(即数组中的第k个元素,表示为T[k])指向集合中一个关键字为k的元素。如果该集合中没有关键字为k的元素,则T[k] = NULL。

可以看出,全域中一共有9个数字,所以右边的数组(直接寻址表)也有9个槽。但是左边实际的关键字不一定有9个(图中只有4个),所以这种直接寻址表就浪费了5个槽。即浪费了一定的内存。(可以看出,集合中的k指向了第k个槽,所以叫做直接寻址表)

所以,直接寻址技术存在一个很明显的问题,如果域U很大,在一台典型计算机的可用容量的限制下,要在机器中储存大小为U的一张表T就有点不实际,甚至是不可能的。假设可以储存下这张很大的表T,但是实际要储存的关键字集合K相对于U来说很小,那么分配给T的大部分空间都要浪费掉。

因此,哈希表出现了。在哈希方式下,该数据处于h(k)中,即利用哈希函数h,根据关键字k计算出槽的位置函数h将关键字域U映射到哈希表T[0…m-1]的槽位上。如图:

(可以看出,算法的思想和直接地址表的相反,直接地址表是从表(槽的位置)推到关键字集合;而哈希表是利用哈希函数从关键字集合推到槽的位置)

哈希技术很好地解决了直接寻址遇到的问题(内存浪费),但是这样做有一个小问题,如图5-39所示的两个关键字可能映射到同一个槽上。一般将这种情况称之为发生了碰撞。在数据库中一般采用最简单的碰撞技术,这种技术称为链接法(chaining)。

在链接法中,把散列到同一槽中的所有元素都放在一个链表中,如图:

槽j中有一个指针,它指向由所有散列到j的元素构成的链表的头;如果不存在这样的元素(即没有发生碰撞),则j中为NULL。

最后要考虑的是哈希函数。哈希函数h必须是可以很好地进行散列。最好的情况是能避免碰撞的发生。即使不能避免,也应该使碰撞在最小程度下产生。一般来说,都将关键字转换成自然数,然后通过除法散列、乘法散列、或全域散列来实现。数据库中一般采用除法散列的方法。

在哈希函数的除法散列中,通过取k除以m的余数,将关键字k映射到m个槽的每一个去,即哈希函数为:

h(k) = k mod m
InnoDB储存引擎中的哈希算法

InnoDB储存引擎使用哈希算法来对字典进行查找,其冲突机制采用链表方式,哈希函数采用除法散列方式。对于缓冲池页的哈希表来说,在缓冲池中的Page页都有一个chain指针。它们指向相同哈希函数值的页。而对于除法散列,m的取值为略大于2倍的缓冲池页数量的质数。例如:当前参数innodb_buffer_pool_size的大小为10M,则共有640个16KB的页。对于缓冲池页内存的哈希表来说,需要分配640*2=128个槽,但是由于1280不是质数,所以需要取比1280略大的一个质数,应该是1399,所以在启动时会分配1399个槽的哈希表,用来哈希查询所在缓冲池中的页。

自适应哈希索引

自适应哈希索引采用之前讨论的哈希表的方式实现。不同的是,这仅是数据库自身创建并使用的,DBA本身并不能对其进行干预。自适应哈希索引经哈希函数映射到一个哈希表中,因此对于字典类型的查找非常快速,如:

SELECT * FROM tb WHERE index_col = 'xxx';

但是对于范围查找就无能为力了。需要注意的是,哈希索引只能用来搜索等值的查询

全文检索

根据B+树索引的特点,可以通过索引字段的前缀(prefix)进行查找。例如,对于下面的查询B+树索引是支持的:

SELECT * FROM blog WHERE content LIKE 'xxx%';

但是在更多的情况下,用户需要查询的是内容中包含单词xxx的文章,即:

SELECT * FROM blog WHERE content LIKE ''%xxx%;

根据B+树索引的特性,上述SQL语句即便添加了B+树索引也是需要进行索引的扫描来得到结果。

全文检索是将储存于数据库中的整本书或整篇文章中的任意内容信息查找出来的技术。它可以根据需要获得全文中有关章、节、段、句、词等信息,也可以进行各种统计和分析。

6、锁

开发多用户、数据库驱动的应用时,最大的一个难点是:一方面要最大程度地利用数据库的并发访问,另一方面还要确保每个用户能以一致的方式读取和修改数据。为此就有了锁的机制,同时这也是数据库系统区别于文件系统的一个关键特性。InnoDB储存引擎较之MySQL数据库的其他储存引擎在这方面技高一筹,其实现方式非常类似于Oracle数据库。

人们认为行级锁总会增加开销。实际上,只有当实现本身会增加开销时,行级锁才会增加开销InnoDB储存引擎不需要锁升级,因为一个锁和多个锁的开销是相同的

什么是锁

锁是数据库系统区别于文件系统的一个关键特性。锁机制用于管理对共享资源的并发访问。InnoDB储存引擎会在行级别上对表数据上锁,这固然不错。不过InnoDB储存引擎也会在数据库内部其他多个地方使用锁,从而允许多种不同资源提供并发访问。例如,操作缓冲池中的LRU列表,删除、添加、移动LRU列表中的元素,为了保证一致性,必须有锁的介入。(注意:不同的数据库,锁机制的实现可能不同。)

对于MyISAM引擎,其锁是表锁设计。并发情况下读没有问题,但是并发插入时的性能可能就要差一些了,若插入是在”底部“,MyISAM储存引擎还是可以有一定的并发写入操作。

lock与latch

在数据库中,lock与latch都可以被称为“锁”。但是两者有着截然不同的含义。

latch一般称为闩(读作shuan)锁(轻量级的锁),因为其要求锁定的时间非常短。若持续的时间长,则应用的性能会非常差。在InnoDB储存引擎中,latch又可分为mutex(互斥量)和rwlock(读写锁)。其目的是用来保证并发线程操作临界资源的正确性,并且通常没有死锁检测的机制。

lock的对象是事务,原来锁定的是数据库中的对象,如表、页、行。并且一般lock的对象仅在事务commit或rollback后进行释放(不同食物隔离级别释放的时间可能不同)。此外,lock正如大多数数据库一样,有死锁检测机制

InnoDB储存引擎中的锁

InnoDB储存引擎实现了如下两种标准的行级锁:

  • 共享锁(S Lock),允许事务读一行时间;
  • 排它锁(X Lock),允许事务删除或更新一行数据。

如果一个事务T1已经获得了行r的共享锁,那么另外的事务T2可以立即获得行r的共享锁,因为读取并没有改变行r的数据,称这种情况为锁兼容。但若有其他的事务T3想获得行r的排它锁,则其必须等待事务T1、T2释放行r上的共享锁--这种情况称为锁不兼容。

此外,InnoDB储存引擎支持多粒度锁定,这种锁定允许事务在行级上的锁和表级上的锁同时存在。为了支持在不同粒度上进行加锁操作,InnoDB储存引擎支持一种额外的锁方式,称之为意向锁意向锁是将锁定的对象分为多个层次意向锁意味着事务希望在更细粒度上进行加锁。如图:

若将上锁的对象看成一棵树,那么对下层的对象上锁,也就是对最细粒度的对象进行上锁,那么首先需要对粗粒度的对象上锁。例如图6-3,如果需要对页上的记录r进行上X锁,那么分别需要对数据库A、表、页上意向锁IX,最后对记录r上X锁。若其中任何一个部分导致等待,那么该操作需要等待粗粒度锁的完成。举例来说,在对记录r加X锁之前,已经有事务对表1进行了S表锁,那么表1上已经存在S锁,之后事务需要对记录r在表1上加上IX锁,由于不兼容,所以该事务需要等待表锁操作的完成

InnoDB储存引擎支持的意向锁设计比较简练,其意向锁即为表级别的锁。设计目的主要是为了在一个事务中揭示下一行被请求的锁类型。其支持的两种意向锁:

  • 意向共享锁(IS Lock),事务想要获得一张表中某几行的共享锁
  • 意向排它锁(IX Lock),事务想要获得一张表中某几行的排它锁

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

7、事务

事务是数据库区别于文件系统的主要特性之一。在文件系统中,如果正在写文件,但是操作系统突然崩溃了,这个文件就很可能被破坏

这正是数据库系统引入事务的主要目的:事务会把数据库从一种一致状态转换为另一种一致状态在数据库提交工作前,可以确保要么所有修改都已经保存,要么所有修改都不保存

InnoDB储存引擎中的事务完全符合ACID的特性:

  • 原子性(atomicity)
  • 一致性(consistency)
  • 隔离性(isolation)
  • 持久性(durability)

原子性指的是整个数据库事务是不可分割的工作单位。只有使事务中所有的数据库操作都执行成功,才算整个事务成功。事务中任何一个SQL语句执行失败,已经执行成功的SQL语句也必须撤销,数据库状态应该退回到执行事务前的状态。

一致性是指事务将数据库从一种状态转变为下一种一致的状态。在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。

隔离性还有其他称呼,如并发控制、可串行化、锁等。事务的隔离性要求每个读写事务的对象对其他事务的操作对象能相互分离,即该事务提交前对其他事务都不可见,通常这使用锁来实现

持久性指的是事务一旦提交,其结果就是永久性的。即使发生宕机等故障,数据库也能将数据恢复。需要注意的是,只能从事务本身的角度来保证结果的永久性。

(从上面的介绍可以看出,原子性和隔离性的区别是原子性是针对某个事务,而隔离性是针对多个事务)

事务的隔离级别

SQL标准定义的四个隔离级别:

  • READ UNCOMMITTED
  • READ COMMITTED
  • REPEATABLE READ
  • SERIALIZABLE

READ UNCOMMITTED称为浏览访问,仅仅针对事务而言的。READ COMMITTED称为游标稳定。REPEATABLE READ是2.9999度的隔离,没有幻读保护。SERIALIZABLE称为隔离,或3度隔离。

InnoDB储存引擎默认支持的隔离级别是REPEATABLE READ,但是与标准的SQL不同的是,InnoDBS储存引擎在REPEATABLE READ事务的隔离级别下,使用Next-Key Lock锁的算法,因此避免幻读的产生

隔离级别越低,事务请求的锁越少或保持锁的时间就越短。这也是为什么大多数数据库系统默认的事务隔离级别是READ COMMITTED。