《MySQL技术内幕:InnoDB存储引擎》第2版笔记

第1章 MySQL体系结构和存储引擎

1.1 定义数据库和实例

  1. 在MySQL数据库中,数据库文件可以是fm、MYD、MYI、ibd结尾的文件。
  2. MySQL数据库由后台线程以及一个共享内存区组成。
  3. MySQL被设计为一个单进程多线程架构的数据库,这点与SQL Server比较类似,但与Oracle多进程的架构有所不同(Oracle的Windows版本也是单进程多线程架构的)。
  4. 在上述例子中使用了mysqld_safe命令来启动数据库,当然启动MySQL实例的方法还有很多,在各种平台下的方式可能又会有所不同。
  5. 而在MySQL数据库中,可以没有配置文件,在这种情况下,MySQL会按照编译时的默认参数设置启动实例。
  6. 可以看到,MySQL数据库是按
/etc/my.cnf -> /etc/mysql/my.cnf -> /usr/local/mysql/etc/my.cnf -> ~/.my.cnf

的顺序读取配置文件的。

1.2 MySQL体系结构

  1. 从概念上来说,数据库是文件的集合,是依照某种数据模型组织起来并存放于二级存储器中的数据集合;数据库实例是程序,是位于用户与操作系统之间的一层数据管理软件,用户对数据库数据的任何操作,包括数据库定义、数据查询、数据维护、数据库运行控制等都是在数据库实例下进行的,应用程序只有通过数据库实例才能和数据库打交道。
  2. MySQL数据库区别于其他数据库的最重要的一个特点就是其插件式的表存储引擎。
  3. 需要特别注意的是,存储引擎是基于表的,而不是数据库。
  4. MySQL由以下几个部分组成:
1. Connectors: Native C API, JDBC, ODBC, NET, PHP, Perl, Python, Ruby, Cobol 这行不是!!
2. Management Service & Utilities: Backup&Recovery, Security, Replication, Cluster, Administration, Configuration, Migration&Metadata
3. Connection Pool: Authentication, Thread Reuse, Connection Limits, Check Memory, Caches
4. SQL Interface: DML, DDL, Stored Procedures View, Triggers, etc.
5. Parser: Quary Translation Object Privilege
6. Optimizer: Access Paths, Statistics
7. Caches & Buffers: Global and Engine Specific Caches & Buffers
8. Pluggable Storage Engines: Memory, Index & Storage Management
9. 插件式存储引擎包括:MyISAM, InnoDB, NDB, Archive, Federated, Memory, Merge, Partner, Community, Custom
9. Files & Logs: Redo, Undo, Data, Index, Binary, Error, Query and Slow
10. 物理文件的文件系统包括:NTFS, ufs, ext2/3, NFS, SAN, NAS

1.3 MySQL存储引擎

有些第三方存储引擎很强大,如大名鼎鼎的InnoDB存储引擎(最早是第三方存储引擎,后被Oracle收购),其应用就极其广泛,甚至是MySQL数据库OLTP(Online Transaction Processing在线事务处理)应用中使用最广泛的存储引擎。

1.3.1 InnoDB存储引擎

  1. InnoDB存储引擎支持事务,其设计目标主要面向在线事务处理的应用。其特点是行锁设计、支持外键,并支持类似于Oracle的非锁定读,即默认读取操作不会产生锁。从MySQL数据库5.5.8版本开始,InnoDB存储引擎是默认的存储引擎。
  2. 从MySOL 4.1(包括4.1)版本开始,它可以将每个InnoDB存储引擎的表单独存放在一个独立的ibd文件中。此外,InnoDB存储引擎支持用裸设备(row disk)用来建立其表空间。
  3. InnoDB通过使用多版本并发控制(MVCC)来获得高并发性,并且实现了SQL标准的4中隔离级别,默认为REPEATABLE级别。同时,使用一种被称为next-key locking的策略来避免幻读(phantom)现象的产生。除此之外,InnoDB存储引擎还提供了插入缓冲(insert buffer)、二次写(double write)、自适应哈希索引(adaptive hash index)、预读(read ahead)等高性能和高可用的功能。
  4. 如果没有显式地在表定义时指定主键,InnoDB存储引擎会为每一行生成一个6字节的ROWID,并以此作为主键。

1.3.2 MyISAM存储引擎

  1. MyISAM存储引擎不支持事务、表锁设计,支持全文索引,主要面向一些OLAP数据库应用。
  2. 数据库系统和文件系统很大的一个不同之处在于对事务的支持,然而MyISAM存储引擎是不支持事务的。
  3. 此外,MyISAM存储引擎的另一个与众不同的地方是它的缓冲池只缓存(cache)索引文件,而不缓冲数据文件,这点和大多数的数据库都非常不同。
  4. MyISAM存储引擎表由MYD和MYI组成,MYD用来存放数据文件,MYI用来存放索引文件。

1.3.3 NDB存储引擎

  1. NDB存储引擎是一个集群存储引擎,类似于Oracle的RAC集群,不过与Oracle RAC share everything架构不同的是,其结构是share nothing的集群结构,因此能提供更高的可用性。NDB的特点是数据全部放在内存中(从MySQL 5.1版本开始,可以将非索引数据存放在磁盘上),因此主键查找(primary key lookups)的速度极快,并且通过添加NDB数据存储节点(Data Node)可以线性地提高数据库性能,是高可用、高性能的集群系统。
  2. 关于NDB存储引擎,有一个问题值得注意,那就是NDB存储引擎的连接操作(JOIN)是在MySQL数据库层完成的,而不是在存储引擎层完成的。这意味着,复杂的连接操作需要巨大的网络开销,因此查询速度很慢。如果解决了这个问题,NDB存储引擎的市场应该是非常巨大的。

1.3.4 Memory存储引擎

  1. Memory存储引擎(之前称HEAP存储引擎)将表中的数据存放在内存中,如果数据库重启或发生崩溃,表中的数据都将消失。**它非常适合用于存储临时数据的临时表,以及数据仓库中的纬度表。**Memory存储引擎默认使用哈希索引,而不是我们熟悉的B+树索引。
  2. 比如,只支持表锁,并发性能较差,并且不支持TEXT和BLOB列类型。
  3. MySQL数据库使用Memory存储引擎作为临时表来存放查询的中间结果集(intermediate result)。如果中间结果集大于Memory存储引擎表的容量设置,又或者中间结果包含有TEXT或BLOB列类型字段,则MySQL数据库会把其转换到MyISAM存储引擎表而存放到磁盘中。

1.3.5 Archive存储引擎

  1. Archive存储引擎只支持INSERT和SELECT操作,从MySQL 5.1开始支持索引。Archive存储引擎使用zlib算法将数据行(row)进行压缩后存储,压缩比一般可达1:10。**正如其名字所示,Archive存储引擎非常适合存储归档数据,如日志信息。**Archive存储引擎使用行锁来实现高并发的插入操作,但是其本身并不是事务安全的存储引擎,其设计目标主要是提供高速的插入和压缩功能。

1.3.6 Federated存储引擎

  1. Federated存储引擎表并不存放数据,它只是指向一台远程MySQL数据库服务器上的表。这非常类似于SQL Server的链接服务器和Oracle的透明网关,不同的是,当前Federated存储引擎只支持MySQL数据库表,不支持异构数据库表。

1.3.7 Maria存储引擎

  1. Maria存储引擎是新开发引擎,设计目标主要是用来取代原有的MyISAM存储引擎,从而成为MySQL的默认存储引擎。

1.3.8 其他存储引擎

  1. 为什么MySQL数据库不支持全文索引?不!MySQL支持,MyISAM、InnoDB(1.2版本)和Sphinx存储引擎都支持全文索引。
  2. MySQL数据库速度快是因为不支持事务?错!虽然MySQL的MyISAM存储引擎不支持事务,但是InnoDB支持。“快”是相对于不同应用来说的,对于ETL这种操作,MyISAM会有其优势,但是在OLTP环境中,InnoDB存储引擎的效率更好。

1.4 各存储引擎之间的比较

  1. InnoDB的comment: Supports transactions, row-level locking, and foreign keys
  2. MEMORY的comment: Hash based, stored in memory, useful for temporary tables
  3. 下面将通过MySQL提供的示例数据库来简单显示各存储引擎之间的不同。这里将分别运行一下语句,然后统计每次使用各存储引擎后表的大小。(下面的salaries就是示例数据库中的表咯)
mysql>CREATE TABLE mytest Engine=MyISAM
        ->AS SELECT * FROM salaries; 
mysql>ALTER TABLE mytest Engine=InnoDB;
mysql>ALTER TABLE mytest Engine=ARCHIVE;

通过每次的统计,可以发现当最初表使用MyISAM存储引擎时,表的大小为40.7MB,使用InnoDB存储引擎时表增大到了113.6MB,而使用Archive存储引擎时表的大小却只有20.2MB。

据我所知,知道MySQL示例数据库的人很少,可能是因为这个示例数据库没有在安装的时候提示用户是否安装(如Oracle和SQL Server)以及这个示例数据库的下载竟然和文档放在一起。

1.5 连接MySQL

  1. 需要理解的是,连接MySQL操作是一个连接进程和MySQL数据库实例进行通信。
  2. 如果对进程通信比较了解,可以知道常用的进程通信方式有管道、命名管道、命名字、TCP/IP套接字、UNIX域套接字。

1.5.1 TCP/IP

  1. 例如用户可以在Windows服务器下请求一台远程Linux服务器下的MySQL实例:
C:\>mysql -h192.168.0.101 -u david -p

这里需要注意的是,在通过TCP/IP连接到MySQL实例时,MySQL数据库会先检查一张权限视图,用来判断发起请求的客户端IP是否允许连接到MySQL实例。该视图在mysql架构下,表名为user:

mysql>USE mysql
mysql>SELECT host,user,password FROM user

从这张权限表中可以看到。MySQL允许david这个用户在任何IP段下连接该实例,并且不需要密码。

1.5.2 命名管道和共享内存

  1. 在Windows 2000、Windows XP、Windows2003和Windows Vista以及在此之上的平台上,如果两个需要进程通信的进程在同一台服务器上,那么可以使用命名管道,Microsoft SQL Server数据库默认安装后的本地连接也是使用命名管道。在MySQL数据库中须在配置文件中启用–enable-named-pipe选项。在MySQL 1.4之后的版本中,MySQL还提供了共享内存的连接方式,这是通过在配置文件中添加–shared-memory实现的。如果想使用共享内存的方式,在连接时,MySQL客户端还必须使用–protocol=memory选项。

1.5.3 UNIX域套接字

  1. UNIX域套接字其实不是一个网络协议,所以只能在MySQL客户端和数据库实例在一台服务器上的情况下使用。用户可以在配置文件中指定套接字文件的路径,如–socket=/tmp/mysql.sock。当数据库实例启动后,用户可以通过下列命令来进行UNIX域套接字文件的查找:
mysql>SHOW VARIABLES LIKE 'socket';

在知道了UNIX域套接字文件的路径后,就可以使用该方式进行连接了,如下所示:

[root@stargazer ~]# mysql -udavid -S /tmp/mysql.sock

(总结一下使用方法:1.用户需要在数据库中找到UNIX域套接字文件的路径 2.有了这个路径后,就可以用UNIX域套接字文件方式进行连接了)只能这么说,用户是程序员,所以能登陆MySQL,找到UNIX域套接字文件在哪儿,然后他要写程序啦,就在这个程序里用找到的UNIX域套接字路径来写代码,以实现连接MySQL数据库的功能。

第2章 InnoDB存储引擎

2.1 InnoDB存储引擎概述

  1. InnoDB存储引擎同MySQL数据库一样,在GNU GPL 2下发行。

2.2 InnoDB存储引擎的版本

  1. 所以在MySQL 5.1中,可以支持两个版本的InnoDB,一个是静态编译的InnoDB版本,可将其视为老版本的InnoDB;另一个是动态加载的InnoDB版本,官方称为InnoDB Plugin,可将其视为InnoDB 1.0.x版本。MySQL 5.5版本中又将InnoDB的版本升级到了1.1.x。
  2. 此外,由于不支持多回滚段,InnoDB Plugin支持的最大支持并发事务数量也被限制在1023。而且随着MySQL 5.5版本的发布,InnoDB Plugin也变成了一个历史产品。

2.3 InnoDB体系结构

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

2.3.1 后台线程

1. Master Thread
  1. Master Thread是一个非常核心的后台线程,主要负责将缓冲池的数据异步刷新到磁盘,保证数据的一致性,包括脏页的刷新、合并插入缓冲、UNDO页的回收等。
2. IO Thread
  1. 在InnoDB存储引擎中大量使用了AIO(Async IO)来处理写IO请求,这样可以极大提高数据库的性能。而IO Thread的工作主要是负责这些IO请求的回调处理。InnoDB 1.0版本之前共有4个IO Thread,分别是write、read、insert buffer和log IO Thread。
  2. 从InnoDB 1.0.x版本开始,read thread和write thread分别增大到了4个,并且不再使用innodb_file_io_threads参数,而是分别使用innodb_read_io_threads和innodb_write_io_threads参数进行设置。
3. Purge Thread
  1. 事务被提交后,其所使用的undolog可能不再需要,因此需要Purge Thread来回收已经使用并分配的undo页。在InnoDB 1.1版本之前,purge操作仅在InnoDB存储引擎的Master Thread中完成。而从InnoDB 1.1版本开始,purge操作可以独立到单独的线程中进行,以此来减轻Master Thread的工作,从而t提高CPU的使用率以及提升存储引擎的性能。用户可以在MySQL数据库的配置文件中添加如下命令来启用独立的Purge Thread:
[mysqld]
innodb_purge_threads=1

从InnoDB 1.2版本开始,InnoDB支持多个Purge Thread,这样做的目的是为了进一步加快undo页的回收。

4. Page Cleaner Thread
  1. Page Cleaner Thread是在InnoDB 1.2.x版本中引入的。其作用是将之前版本中脏页的刷新操作都放入到单独的线程中来完成。

2.3.2 内存

1. 缓冲池
  1. InnoDB存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理。因此可将其视为基于磁盘的数据库系统(Disk-base Database)。
  2. 这里需要注意的是,页从缓冲池刷新回磁盘的操作并不是在每次页发生更新时触发,而是通过一种称为Checkpoint的机制刷新回磁盘。
  3. 因此为了让数据库使用更多的内存,强烈建议数据库服务器都采用64位的操作系统。
  4. 对于InnoDB存储引擎而言,其缓冲池的配置通过参数innodb_buffer_pool_size来设置。
  5. 具体来看,缓冲池中缓存的数据页类型有:索引页、数据页、undo页、插入缓冲(insert buffer)、自适应哈希索引(adaptive hash index)、InnoDB存储的锁信息(lock info)、数据字典信息(data dictionary)等。
  6. 从InnoDB 1.0.x版本开始,允许有多个缓冲池实例。每个页根据哈希值平均分配到不同缓冲池实例中。可以通过参数innodb_buffer_pool_instances来进行配置,该值默认为1。
  7. 从MySQL 5.6版本开始,还可以通过information_schema架构下的表INNODB_BUFFER_POOL_STATS来观察缓冲的状态,如运行下列命令可以看到各个缓冲池的使用状态:
mysql> SELECT POOL_ID,POOL_SIZE,FREE_BUFFERS,DATABASE_PAGES
        -> FROM INNODB_BUFFER_POOL_STATS\G;
2. LRU List、Free List和Flush List
  1. 通常来说,数据库中的缓冲池是通过LRU(Latest Recent Used)算法来进行管理的。即最频繁使用的页在LRU列表的前端,而最少使用的页在LRU列表的尾端。
  2. 在InnoDB的存储引擎中,LRU列表中还加入了midpoint位置。新读取到的页,虽然是最新访问的页,但并不是直接放入到LRU列表的首部,而是放入到LRU列表的midpoint位置。
  3. 在默认配置下,该位置在LRU列表长度的5/8处。midpoint位置可由参数innodb_old_blocks_pct控制。
  4. 在InnoDB存储引擎中,把midpoint之后的列表称为old列表,之前的列表称为new列表。可以简单地理解为new列表中的页都是最为活跃的热点数据。
  5. 那为什么不采用朴素的LRU算法,直接将读取的页放入到LRU列表的首部呢?这是因为若直接将读取到的页放入到LRU的首部,那么某些SQL操作可能会使缓冲池中的页被刷新出,从而影响缓冲池的效率。常见的这类操作为索引或数据的扫描操作。这类操作需要访问表中的多个页,甚至是全部的页,而这些页通常来说又仅在这次查询操作中需要澳,并不是活跃的热点数据。如果页被放入LRU列表的首部,那么非常可能将所需要的热点数据页从LRU列表中移除,而在下一次需要读取该页时,InnoDB存储引擎需要再次访问磁盘。
  6. 为了解决这个问提,InnoDB存储殷勤引入了另一个参数来进一步管理LRU列表,这个参数是innodb_old_blocks_time,用于表示页读取到mid位置后需要等待多久才会被加入到LRU列表的热端。因此当需要执行上述所说的SQL操作时,可以通过下面的方法尽可能使LRU列表中热点数据不被刷出:
mysql> SET GLOBAL innodb_old_blocks_time=1000;

# data or index scan operation
...

mysql> SET GLOBAL innodb_old_blocks_time=0;

LRU列表用来管理已经读取的页,但当数据库刚启动时,LRU列表是空的,即没有任何的页。这时页都存放在Free列表中。

当页从LRU列表的old部分加入到new部分时,称此时发生的操作为page made young,而因为innodb_old_blocks_time的设置而导致页没有从old部分移动到new部分的操作称为page not made young。

可能的情况是Free buffers与Database pages的数量之和不等于Buffer pool size。因为缓冲池中的页还可能会被分配给自适应哈希索引、Lock信息、Insert Buffer等页,而这部分页不需要LRU算法进行维护,因此不存在于LRU列表中。

这里还有一个重要的观察变量——Buffer pool hit rate,表示缓冲池的命中率,这个例子中卫100%,说明缓冲池运行状态非常良好。通常该值不应该小于95%。若发生Buffer pool hit rate的值小于95%这种情况,用户需要观察是否是由于全表扫描引起的LRU列表被污染的问题。

执行命令SHOW ENGINE INNODB STATUS显示的不是当前的状态,而是过去某个时间范围内InnoDB存储引擎的状态。从上面的额例子可以发现,Per second averages calculated from the last 24 seconds代表的信息为过去24秒内的数据库状态。

从InnoDB 1.2版本开始,还可以通过表INNODB_BUFFER_POOL_STATS来观察缓冲池的运行状态,如:

mysql> SELECT POOL_ID,HIT_RATE,PAGES_MADE_YOUNG,PAGES_NOT_MADE_YOUNG
    ->FROM information_schema.INNODB_BUFFER_POOL_STATS\G;

此外,还可以通过表INNODB_BUFFER_PAGE_LRU来观察每个LRU列表中每个页的具体信息,例如通过下面的语句可以看到缓冲池LRU列表中SPACE为1的表的页类型:

mysql> SELECT TABLE_NAME,SPACE,PAGE_NUMBER,PAGE_TYPE
    -> FROM INNODB_BUFFER_PAGE_LRU WHERE SPACE = 1;

InnoDB存储引擎从1.0.x版本开始支持压缩页的功能,即将原本16KB的页压缩为1KB、2KB、4KB和8KB。对于非16KB的页,是通过unzip_LRU列表进行管理的。

可以看到LRU列表中一共有1539个页,而unzip_LRU列表中有156个页。这里需要注意的是,LRU中的页包含了unzip_LRU列表中的页。

首先,在unzip_LRU列表中队不同压缩页大小的页进行分别管理。其次,通过伙伴算法进行内存的分配。例如对需要从缓冲池中申请页为4KB的大小,其过程如下:

检查4KB的unzip_LRU列表,检查是否有可用的空闲页;
若有,则直接使用;
否则,检查8KB的unzip_LRU列表;
若能够得到空闲页,将页分成2个4KB页,存放到4KB的unzip_LRU列表;
若不能得到空闲页,从LRU列表中申请一个16KB的页,将页分成1个8KB的页、2个4KB的页,分别存放到对应的unzip_LRU列表中。

# 观察unzip_LRU列表中的页
mysql> SELECT TABLE_NAME,SPACE,PAGE_NUMBER,COMPRESSED_SIZE 
    -> FROM INNODB_BUFFER_PAGE_LRU
    -> WHERE COMPRESSED_SIZE <> 0;

这时数据库会通过CHECKPOINT机制将脏页刷新回磁盘,而Flush列表中的页即为脏页列表。需要注意的是,脏页既存在于LRU列表中,也存在于Flush列表中。

同LRU列表一样,Flush列表也可以通过命令SHOW_ENGINE_INNODB_STATUS来查看,前面例子中的Modified db pages 24673就显示了脏页的数量。

# 观察脏页
mysql> SELECT TABLE_NAME,SPACE,PAGE_NUMBER,PAGE_TYPE
    -> FROM INNODB_BUFFER_PAGE_LRU
    -> WHERE OLDEST_MODIFICATION>0;

TABLE_NAME为NULL表示该页属于系统表空间。

3. 重做日志缓冲
  1. 重做日志缓冲一般不需要设置得很大,因为一般情况下每一秒钟会将重做日志缓冲刷新到日志文件,因此用户只需要保证每秒产生的事务量在这个缓冲大小之内即可。该值可由配置参数innodb_log_buffer_size控制,默认为8MB。
  2. 在通常情况下,8MB的重做日志缓冲池足以满足绝大部分的应用,因为重做日志在下列三种情况下会将重做日志缓冲中的内容刷新到外部磁盘的重做日志文件中:
    Master Thread每一秒将重做日志缓冲刷新到重做日志文件
    每个事务提交时会将重做日志缓冲刷新到重做日志文件
    当重做日志缓冲池剩余空间小于1/2时,重做日志缓冲刷新到重做日志文件
4. 额外的内存池
  1. 在对一些数据结构的内存进行分配时,需要从额外的内存池中进行申请,当该区域的内存不够时,会从缓冲池中进行申请。例如,分配了缓冲池(innodb_buffer_pool),但是每个缓冲池中的帧缓冲(frame buffer)还有对应的缓冲控制对象(buffer control block),这些对象记录了一些诸如LRU、锁、等待等信息,而这个对象的内存需要从额外内存池中申请。

2.4 Checkpoint技术

  1. 为了避免发生数据丢失的问题,当前事务数据库系统普遍都采用了Write Ahead Log策略,即当事务提交时,先写重做日志,再修改页。当由于发生宕机而导致数据丢失时,通过重做日志来完成数据的恢复。
  2. 当数据库发生宕机时,数据库不需要重做所有的日志,因为Checkpoint之前的页都已经刷新回磁盘。故数据库只需对Checkpoint后的重做日志进行恢复。
  3. 此外,当缓冲池不够用时,根据LRU算法会溢出最近最少使用的页,若此页为脏页,那么需要强制执行Checkpoint,将脏页也就是页的新版本刷回磁盘。
  4. 重做日志可以被重用的部分是指这些重做日志已经不再需要,即当数据库发生宕机时,数据库恢复操作不需要这部分的重做日志,因此这部分就可以被覆盖重用。若此时重做日志还需要使用,那么必须强制产生Checkpoint,将缓冲池中的页至少刷新到当前重做日志的位置。
  5. 在InnoDB存储引擎内部,有两种Checkpoint,分别为:

    Sharp Checkpoint
    Fuzzy Checkpoint

Sharp Checkpoint发生在**数据库关闭**时将所有的脏页都刷新回磁盘,这是默认的工作方式,即参数innodb_fast_shutdown=1。 故在InnoDB存储引擎内部使用Fuzzy Checkpoint进行页的刷新,即只刷新一部分脏页,而不是刷新所有的脏页回磁盘。 在InnoDB存储引擎中可能发生如下几种情况的Fuzzy Checkpoint:

Master Thread Checkpoint
FLUSH_LRU_LIST Checkpoint
Async/Sync Flush Checkpoint
Dirty Page too much Checkpoint

FLUSH_LRU_LIST Checkpoint是因为InnoDB存储引擎需要保证LRU列表中需要有差不多100个空闲页可供使用。在InnoDB 1.1.x版本之前,需要检查LRU列表中是否有足够的可用空间操作发生在用户查询线程中,显然这会阻塞用户的查询操作。 而在MySQL 5.6版本,也就是InnoDB 1.2.x版本开始,这个检查被放在了一个单独的Page Cleaner线程中进行,并且用户可以通过参数innodb_lru_scan_depth控制LRU列表中可用页的数量,该值默认为1024。 Async/Sync Flush Checkpoint指的是重做日志文件不可用的情况,这时需要强制将一些页刷新回磁盘,而此时脏页是从脏页列表中选取的。若将已经写入重做日志的LSN即为redo_lsn,将已经刷新回磁盘最新页的LSN记为checkpoint_lsn,则可定义:
checkpoint_age = redo_lsn - checkpoint_lsn
再定义以下的变量:
async_water_mark = 75% * total_redo_log_file_size
sync_water_mark = 90% * total_redo_log_file_size
若每个重做日志文件的大小为1GB,并且定义了两个重做日志文件,则重做日志文件的总大小为2GB。那么async_water_mark=1.5GB,sync_water_mark=1.8GB。则

当checkpoint_age < async_water_mark时,不需要刷新任何脏页到磁盘;
当async_water_mark < checkpoint_age < sync_water_mark时触发Async Flush,从Flush列表中刷新足够的脏页回磁盘,使得刷新后满足checkpoint_age < async_water_mark;
checkpoint_age > sync_water_mark这种情况很少发生,除非设置的重做日志文件太小,并且在进行类似LOAD DATA的BULK INSERT操作。此时触发Sync Flush操作,从Flush列表中刷新足够的脏页回磁盘,使得刷新后满足checkpoint_age < async_water_mark。

可见,Async/Sync Flush Checkpoint是为了保证重做日志的循环使用的可用性。在InnoDB 1.2.x版本之前,Async Flush Checkpoint会阻塞发现问题的用户查询线程,而Sync Flush Checkpoint会阻塞所有的用户查询线程,并且等待脏页刷新完成。从InnoDB 1.2.x版本开始——也就是MySQL 5.6版本,这部分的刷新操作同样放入到了单独的Page Cleaner Thread中,故不会阻塞用户查询线程。 innodb_max_dirty_pages_pct值为75表示,当缓冲池中脏页的数量占据75%时,强制执行Checkpoint,刷新一部分的脏页到磁盘。

2.5 Master Thread工作方式

2.5.1 InnoDB 1.0.x版本之前的Master Thread

  1. Master Thread具有最高的线程优先级别。其内部由多个循环(loop)组成:主循环(loop)、后台循环(background loop)、刷新循环(flush loop)、暂停循环(suspend loop)。
  2. Loop被称为主循环,因为大多数的操作是在这个循环中,其中有两大部分的操作——每秒钟的操作和每10秒钟的操作。
  3. 每秒一次的操作包括:

    日志缓冲刷新到磁盘,即使这个事务还没有提交(总是)
    合并插入缓冲(可能)
    至多刷新100个InnoDB的缓冲池中的脏页到磁盘(可能)
    如果当前没有用户活动,则切换到background loop(可能)

**即使某个事务还没有提交,InnoDB存储引擎仍然每秒会将重做日志缓冲中的内容刷新到重做日志文件。这一点是必须要知道的,因为这很好地解释为什么再大的事务提交(commit)的时间也是很短的。** InnoDB存储引擎会判断当前一秒内发生的IO次数是否小于5次,如果小于5次,InnoDB认为当前的IO压力很小,可以执行合并插入缓冲的操作。 InnoDB存储引擎通过判断当前缓冲池中脏页的比例(buf_get_modified_ratio_pct)是否超过了配置文件中innodb_max_dirty_pages_pct这个参数(默认为90,代表90%),如果超过了这个阈值,InnoDB存储引擎认为需要做磁盘同步的操作,将100个脏页写入磁盘。 接着来看每10秒的操作,包括如下内容:

刷新100个脏页到磁盘(可能的情况下)
合并至多5个插入缓冲(总是)
将日志缓冲刷新到磁盘(总是)
删除无用的Undo页(总是)
刷新100个或者10个脏页到磁盘(总是)

在以上的过程中,InnoDB存储引擎会先判断过去10秒之内磁盘的IO操作是否小于200次,如果是,InnoDB存储引擎认为当前有足够的磁盘IO操作能力,因此将100个脏页刷新回磁盘。 **(从现在开始我将省略InnoDB后面的“存储引擎”四个字,珍爱生命)** **对表进行update、delete这类操作时,原先的行被标记为删除,但是因为一致性读(consistent read)的关系,需要保留这些行版本的信息。但是在full purge过程中,InnoDB会判断当前事务系统中已被删除的行是否可以删除,比如有时候可能还有查询操作需要读取之前版本的undo信息,如果可以删除,InnoDB会立即将其删除。**从源代码中可以发现,InnoDB存储引擎在执行full purge操作时,每次最多尝试回收20个undo页。 然后,InnoDB存储引擎会判断缓冲池中脏页的比例(buf_get_modified_ratio_pct),如果有超过70%的脏页,则刷新100个脏页到磁盘,如果脏页的比例小于70%,则只需刷新10%的脏页到磁盘。**(应该是10个吧)** 接着来看background loop,若当前没有用户活动(数据库空闲时)或者数据库关闭(shutdown),就会切换到这个循环。background loop会执行以下操作:

删除无用的Undo页(总是)
合并20个插入缓冲(总是)
跳回到主循环(总是)(应该不是总是吧?)
不断刷新100个页直到符合条件(可能,跳转到flush loop中完成)

若flush loop中也没有什么事情可以做了,InnoDB会切换到suspend loop,将Master Thread挂起,等待事件的发生。

2.5.2 InnoDB 1.2.x版本之前的Master Thread

  1. 即使磁盘能在1秒内处理多于100个页的写入和20个插入缓冲的合并,但是由于hard coding,Master Thread也只会选择刷新100个脏页和合并20个插入缓冲。同时,当发生宕机需要恢复时,由于很多数据还没有刷新回磁盘,会导致恢复的时间可能需要很久,尤其是对于insert buffer来说。
  2. 因此InnoDB Plugin(从InnoDB 1.0.x版本开始)提供了参数innodb_io_capacity,用来表示磁盘IO的吞吐量,默认值为200。对于刷新到磁盘页的数量,会按照innodb_io_capacity的百分比来进行控制。规则如下:

    在合并插入缓冲时,合并插入缓冲的数量为innodb_io_capacity值得5%
    在从缓冲区刷新脏页时,刷新脏页的数量为innodb_io_capacity


但是该值“太大”了,因为InnoDB在每秒刷新缓冲池和flush loop时会判断这个值,如果该值大于innodb_max_dirty_pages_pct,才刷新100个脏页,如果有很大的内存,或者数据库服务器的压力很大,这时刷新脏页的速度反而会降低。 Google在这个问题上进行了测试,证明20并不是一个最优值。而从InnoDB 1.0.x版本开始,innodb_max_dirty_pages_pct默认值变为了75,和Google测试的80比较接近。这样既可以加快刷新脏页的频率,又能保证了磁盘IO的负载。 随着innodb_adaptive_flushing参数的引入,InnoDB存储引擎会通过一个名为buf_flush_get_desired_flush_rate的函数来判断需要刷新脏页最合适的数量。粗略地翻阅源代码后发现buf_flush_get_desired_flush_rate通过判断产生重做日志的速度来决定最合适的刷新脏页数量。因此,当脏页的比例小于innodb_max_dirty_pages_pct时,也会刷新一定量的脏页。 还有一个改变是:之前每次进行full purge操作时,最多回收20个Undo页,从InnoDB 1.0.x版本开始引入了参数innodb_purge_batch_size,该参数可以控制每次full purge回收的Undo页的数量。 可以看到当前主循环运行了2188次,但是循环中的每秒挂起(sleep)的操作只运行了1537次。这是因为InnoDB对其内部进行了一些优化,当压力大时并不总是等待1秒。因此,并不能认为1_second和sleeps的值总是相等的。在某些情况下,可以通过两者之间的差值的比较来反映当前数据库的负载压力。

2.6 InnoDB关键特性

  1. InnoDB的关键特性包括:
    插入缓冲(Insert Buffer)
    两次写(Double Write)
    自适应哈希索引(Adaptive Hash Index)
    异步IO(Async IO)
    刷新邻接页(Flush Neighbor Page)

2.6.1 插入缓冲

1. Insert Buffer
  1. 其实不然,InnoDB缓冲池中有Insert Buffer信息固然不错,但是Insert Buffer和数据页一样,也是物理页的一个组成部分。
  2. 其中a列是自增长的,若对a列插入NULL值,则由于其具有AUTO_INCREMENT属性,其值会自动增长。同时页中的记录按a的值进行顺序存放。在一般情况下,不需要随机读取另一个页中的记录。因此,对于这类情况的插入操作,速度是非常快的。
  3. 并不是所有的主键插入都是顺序的。若主键类是UUID这样的类,那么插入和辅助索引一样,同样是随机的。即使主键是自增类型,但是插入的是指定的值,而不是NULL值,那么同样可能导致插入并非连续的情况。
  4. 但是不可能每张表上只有一个聚集索引,更多情况下,一张表上有多个非聚集的辅助索引(secondary index)。比如,用户需要按照b这个字段进行查找,并且b这个字段不是唯一的,即表是按如下的SQL语句定义的:
CREATE TABLE t {
    a INT AUTO_INCREMENT,
    b VARCHAR(30),
    PRIMARY KEY(a),
    key(b)
);

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

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

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

索引是辅助索引(secondary index)
索引不是唯一(unique)的

辅助索引不能是唯一的,因为在插入缓冲时,数据库并不去查找索引页来判断插入的记录的唯一性。

seg size显示了当前Insert Buffer的大小为11336 × 16KB,大约为177MB;free list len代表了空闲列表的长度;size代表了已经合并记录页的数量。

Inserts代表了插入的记录数;merged recs代表了合并的插入记录数量;merges代表合并的次数,也就是实际读取页的次数。merges:merged recs大约为1:3,代表了插入缓冲将对于非聚集索引页的离散IO逻辑请求大约降低了2/3。

正如前面所说的,目前Insert Buffer存在一个问题是:在写密集的情况下,插入缓冲会占用过多的缓冲池内存(innodb_buffer_pool),默认最大可以占用到1/2的缓冲池内存。

2. Change Buffer
  1. 从这个版本开始,InnoDB存储引擎可以对DML操作——INSERT、DELETE、UPDATE都进行缓冲,他们分别是:Insert Buffer、Delete Buffer、Purge Buffer。
  2. 当然和之前Insert Buffer一样,Change Buffer适用的对象依然是非唯一的辅助索引。
  3. 因为Delete Buffer对应UPDATE操作的第一个过程,即将记录标记为删除。Purge Buffer对应UPDATE操作的第二个过程,即将记录真正的删除。
  4. innodb_change_buffer_max_size值默认为25,表示最多使用1/4的缓冲池内存空间。而需要注意的是,该参数的最大有效值为50。
3. Insert Buffer的内部实现
  1. 可能令绝大部分用户感到吃惊的是,Insert Buffer的数据结构是一棵B+树。在MySQL 4.1之前的版本中每张表有一棵Insert Buffer B+树。而在现在的版本中,全局只有一棵Insert Buffer B+树,负责对所有的表的辅助索引进行Insert Buffer。
  2. 非页节点存放的是查询的search key(键值),其构造是:| space | marker | offset |。
  3. search key一共占用9个字节,**其中space表示待插入记录所在表的表空间id,在InnoDB存储引擎中,每个表有一个唯一的space id,可以通过space id查询得知是哪张表。**space占用4字节。marker占用1字节,它是用来兼容老版本的Insert Buffer。offset表示页所在的偏移量,占用4字节。
  4. 当一个辅助索引要插入到页(space, offset)时,如果这个页不在缓冲池中,那么InnoDB首先根据上述规则构造一个search key,接下来查询Insert Buffer这棵B+树,然后再将这条记录插入到Insert Buffer B+树的叶子节点中。
  5. IBUF_REC_OFFSET_COUNT是保存两个字节的整数,用来排序每个记录进入Insert Buffer的顺序。因为从InnoDB 1.0.x开始支持Change Buffer,所以这个值同样记录进入Insert Buffer**(应该是Change Buffer吧?)**的顺序。通过这个顺序回放(replay)才能得到记录的正确值。
  6. 从Insert Buffer叶子节点的第5列开始,就是实际插入记录的各个字段了。因此较之原插入记录,Insert Buffer B+树的叶子节点记录需要额外13字节的开销。
  7. 因为启用Insert Buffer索引后,辅助索引页(space,page_no)中的记录可能被插入到Insert Buffer B+树中,所以为了保证每次Merge Insert Buffer页必须成功,还需要有一个特殊的页用来标记每个辅助索引页(space,page_no)的可用空间。这个页的类型为Insert Buffer Bitmap。
  8. 每个Insert Buffer Bitmap页用来追踪16384个辅助索引页,也就是256个区(Extent)。每个Insert Buffer Bitmap页都在16384个页的第二个页中。每个辅助索引页在Insert Buffer Bitmap页中占用4位:
    IBUF_BITMAP_FREE 2 表示该辅助索引页中的可用空间数量
    IBUF_BITMAP_BUFFERED 1 1表示该辅助索引页有记录被缓存在Insert Buffer B+树中
    IBUF_BITMAP_IBUF 1 1表示该页为Insert Buffer B+树的索引页
4. Merge Insert Buffer
  1. 概括地说,Merge Insert Buffer的操作可能发生在以下几种情况下:

    辅助索引页被读取到缓冲池时
    Insert Buffer Bitmap页追踪到该辅助索引页已无空间可用时
    Master Thread

**第一种情况为当辅助索引页被读取到缓冲池中时,例如这在执行正常的SELECT查询操作,这时需要检查Insert Buffer Bitmap页,然后确认该辅助索引页是否有记录存放于Insert Buffer B+树中。若有,则将Insert Buffer B+树中该页的记录插入到该辅助索引页中。可以看到对该页多次的记录操作通过一次操作合并到了原有的辅助索引页中,因此性能会有大幅提高。** **Insert Buffer Bitmap页用来追踪每个辅助索引页的可用空间,并至少有1/32页的空间。若插入辅助索引记录时检测到插入记录后可用空间会小于1/32页,则会强制进行一次合并操作,即强制读取辅助索引页,将Insert Buffer B+树种该页的记录及待插入的记录插入到辅助索引页中。**

2.6.2 两次写

  1. doublewrite由两部分组成,一部分是内存中的doublewrite buffer,大小为2MB,另一部分是物理磁盘上共享表空间中连续的128个页,即2个区(extent),大小同样为2MB。在对缓冲池的脏页进行刷新时,并不直接写磁盘,而是会通过memcpy函数将脏页先复制到内存中的doublewrite buffer,之后通过doublewrite buffer再分两次,每次1MB顺序地写入共享表空间的物理磁盘上,然后马上调用fsync函数,同步磁盘,避免缓冲写带来的问题。在这个过程中,因为doublewrite页是连续的,因此这个过程是顺序写的,开销并不是很大。在完成doublewrite页的写入后,再将doublewrite buffer中的页写入各个表空间文件中,此时的写入则是离散的。
  2. 如果操作系统在将页写入磁盘的过程中发生了崩溃,在恢复过程中,InnoDB存储引擎可以从共享表空间中的doublewrite中找到该页的一个副本,将其复制到表空间文件,再应用重做日志。
  3. 有些文件系统本身就提供了部分写失效的防范机制,如ZFS文件系统。在这种情况下,用户就不要启用doublewrite了。

2.6.3 自适应哈希索引

  1. InnoDB存储引擎会自动根据访问的频率和模式来自动地为某些热点页建立哈希索引。
  2. 访问模式一样指的是查询的条件一样,若交替进行上述两种查询,那么InnoDB存储引擎不会对该页构造AHI。此外AHI还有如下的要求:
    以该模式访问了100次
    页通过该模式访问了N次,其中N=页中记录*1/16

2.6.4 异步IO

  1. 用户可以在发出一个IO请求后立即再发出另一个IO请求,当全部IO请求发送完毕后,等待所有IO操作的完成,这就是AIO。
  2. 而AIO会判断到这三个页是连续的(显然可以通过(space,page_no)得知)。因此AIO底层会发送一个IO请求,从(8,6)开始,读取48KB的页。
  3. 在InnoDB1.1.x之前,AIO的实现通过InnoDB存储引擎中的代码来模拟实现。而从InnoDB1.1.x开始(InnoDB Plugin不支持),提供了内核级别的AIO的支持,称为Native AIO。
  4. Windows系统和Linux系统都提供Native AIO支持,而Mac OSX系统则未提供。
  5. 参数innodb_use_native_aio用来控制是否启用Native AIO,在Linux操作系统下,默认值为ON。
  6. 官方的测试显示,启用Native AIO,恢复速度可以提供75%。
  7. 在InnoDB存储引擎中,read ahead方式的读取都是通过AIO完成,脏页的刷新,即磁盘的写入操作则全部由AIO完成。

2.6.5 刷新邻接页

  1. 其工作原理是:当刷新一个脏页时,InnoDB存储引擎会检测该页所在区(extent)的所有页,如果是脏页,那么一起进行刷新。这样做的好处显而易见,通过AIO可以将多个IO写入操作合并为一个IO操作,故该工作机制在传统机械磁盘下有着显著的优势。
  2. 对于传统机械硬盘建议启用该特性,而对于固态硬盘有着超高的IOPS性能的磁盘,则建议将该参数设置为0,即关闭此特性。

2.7 启动、关闭与恢复

  1. 在关闭时,参数innodb_fast_shutdown影响着表的存储引擎为InnoDB的行为。该参数可取值为0,1,2,默认值为1。

    0表示在MySQL数据库关闭时,InnoDB需要完成所有的full purge和merge insert buffer,并且将所有的脏页刷新回磁盘。这需要一些时间,有时甚至需要几个小时来完成。如果在进行InnoDB升级时,必须将这个参数设置为0,然后再关闭数据库。
    1是参数innodb_fast_shutdown的默认值,表示不需要完成上述的full purge和merge insert buffer操作,但是在缓冲池中的一些数据脏页还是会刷新回磁盘。
    2表示不完成full purge和merge insert buffer操作,也不将缓冲池中的数据脏页写回磁盘,而是将日志都写入日志文件。这样不会有任何事务的丢失,但是下次MySQL数据库启动时,会进行恢复操作(recovery)。

参数innodb_force_recovery影响了整个InnoDB存储引擎恢复的状况。该参数值默认为0,代表当发生需要恢复时,进行所有的恢复操作,当不能进行有效恢复时,如数据页发生了corruption,MySQL数据库可能发生宕机(crash),并把错误写入错误日志中去。 参数innodb_force_recovery还可以设置为6个非零值:1~6。大的数字表示包含了前面所有小数字表示的影响:

1(SRV_FORCE_IGNORE_CORRUPT):忽略检查到的corrupt页
2(SRV_FORCE_NO_BACKGROUND):阻止Master Thread线程的运行,如Master Thread线程需要进行full purge操作,而这会导致crash
3(SRV_FORCE_NO_TRX_UNDO):不进行事务的回滚操作
4(SRV_FORCE_NO_IBUF_MERGE):不进行插入缓冲的合并操作
5(SRV_FORCE_NO_UNDO_LOG_SCAN):不查看撤销日志(Undo Log),InnoDB存储引擎会将未提交的事务视为已提交。
6(SRV_FROCE_NO_LOG_REDO):不进行前滚的操作。

需要注意的是,在设置了参数innodb_force_recovery大于0后,用户可以对标进行select、create和drop操作,但insert、update和delete这类DML操作是不允许的。 可以看到,采用默认的策略,即将innodb_force_recovery设为0,InnoDB会在每次启动后对发生问题的表进行恢复操作。通过错误日志文件,可知这次回滚操作需要回滚8867280行记录,差不多总共进行了9分钟。 这里出现了“!!!”,InnoDB警告已经将innodb_force_recovery设置为3,不会进行回滚操作了,因此数据库很快启动完了。但是用户应该小心当前数据库的状态,并仔细确认是否不需要回滚事务的操作。

第3章 文件

  1. 参数文件:告诉MySQL实例启动时去哪里可以找到数据库文件,并且指定某些初始化参数,这些参数定义了某种内存结构的大小等设置,还会介绍各种参数的类型。
  2. 日志文件:用来记录MySQL实例对某种条件做出响应时写入的文件,如错误日志文件、二进制文件、慢查询日志文件、查询日志文件等。
  3. socket文件:当使用UNIX域套接字方式进行连接时需要的文件
  4. MySQL表结构文件:用来存放MySQL表结构定义文件
  5. 存储引擎文件:因为MySQL表存储引擎的关系,每个存储引擎都会有自己的文件来保存各种数据。这些存储引擎真正存储了记录和索引等数据。

3.1 参数文件

  1. MySQL稍微有所不同,MySQL实例可以不需要参数文件,这时所有的参数值取决于编译MySQL时指定的默认值和源代码中指定参数的默认值。但是,如果MySQL实例在默认的数据库目录下找不到mysql架构(应该是指mysql.host),则启动同样会失败。
  2. MySQL的mysql架构中记录了访问该实例的权限,当找不到这个架构时,MySQL实例不会成功启动。

3.1.1 什么是参数

  1. 可以通过命令SHOW_VARIABLES查看数据库中的所有参数,也可以通过LIKE来过滤参数名。从MySQL5.1版本开始,还可以通过information_schema架构下的GLOBAL_VARIABLES视图来进行查找:
mysql> SELECT * FROM GLOBAL_VARIABLES
    -> WHERE VARIABLE_NAME LIKE 'innodb_buffer%' \G;
mysql> SHOW VARIABLES LIKE 'innodb_buffer%' \G;

3.1.2 参数类型

  1. 动态参数意味着可以在MySQL实例运行中进行更改,静态参数说明在整个实例生命周期内都不得进行更改,就好像是只读(read only)的。
  2. 这里看到global和session关键字,它们表明该参数的修改是基于当前会话还是整个实例的生命周期。有些动态参数只能在会话中进行修改,如autocommit;而有些参数修改完后,在整个实例声明周期中都会生效,如binlog_cache_size;而有些参数既可以在会话中又可以在整个实例的生命周期内生效,如read_buffer_size。举例:
mysql> SET read_buffer_size=524288;
mysql> SELECT @@session.read_buffer_size\G;
# @@session.read_buffer_size: 524288
mysql> SELECT @@global.read_buffer_size\G;
# @@global.read_buffer_size: 2093056

用户同样可以直接使用SET@@global|@@session来更改。

mysql> SET @@global.read_buffer_size=1048576

这里需要注意的是,对变量的全局值进行了修改,在这次的实例生命周期内都有效,但MySQL实例本身并不会对参数文件中的值进行修改。

3.2 日志文件

3.2.1 错误日志

  1. 用户可以通过命令SHOW VARIABLES LIKE ‘log_error’来定位该文件。可以看到错误文件的路径和文件名,在默认情况下错误文件的文件名为服务器的主机名。如上面看到的,该主机名为stargazer,所以错误文件名为stargazer.err。

3.2.2 慢查询日志

  1. 3.2.1小节提到可以通过错误日志得到一些关于数据库优化的信息,而慢查询日志(slow log)可帮助DBA定位可能存在问题的SQL语句,从而进行SQL语句层面的优化。例如,可以在MySQL启动时设一个阈值,将运行时间超过该值的所有SQL语句都记录到慢查询日志文件中。DBA每天或每过一段时间对其进行检查,确认是否有SQL语句需要进行优化。该阈值可以通过参数long_query_time来设置,默认值为10,代表10秒。
  2. 在默认情况下,MySQL数据库并不启动慢查询日志,用户需要手工将这个参数设为ON。
  3. 另一个和慢查询日志有关的参数是log_queries_not_using_indexes,如果运行的SQL语句没有使用索引,则MySQL数据库同样会将这条SQL语句记录到慢查询日志文件。
  4. MySQL 5.6.5版本开始新增一个参数log_throttle_queries_not_using_indexes,用来表示每分钟允许记录到slow log的且未使用索引的SQL语句次数。该值默认为0,表示没有限制。在生产环境下,若没有使用索引,此类SQL语句会频繁地被记录到slow log,从而导致slow log文件的大小不断增加,故DBA可通过此参数进行配置。
  5. 如果用户希望得到执行时间最长的10条SQL语句,可以运行如下命令:
$ mysqldumpslow -s al -n 10 david.log

MySQL 5.1开始可以将慢查询的日志记录放入一张表中,这使得用户的查询更加方便和直观。慢查询表在mysql架构下,名为slow_log,其表结构定义如下:

mysql> SHOW CREATE TABLE mysql.slow_log\G;
# ...

参数log_output指定了慢查询输出的格式,默认为FILE,可以将它设为TABLE,然后就可以查询mysql架构下的slow_log表了。

查看slow_log表的定义发现该表使用的是CSV引擎,对大数据量下的查询效率可能不高。用户可以把slow_log表的引擎转换到MyISAM,并在start_time列上添加索引以进一步提高查询的效率。但是,如果已经启动了慢查询,将会提示错误。所以先关掉(SET GLOBAL slow_query_log=off),再改。

InnoSQL版本加强了对于SQL语句的捕获方式。在原版MySQL的基础上在slow log中增加了对于逻辑读取(logical reads)和物理读取(physical reads)的统计。这里的物理读取是指从磁盘进行IO读取的次数,逻辑读取包含所有的读取。

3.2.3 查询日志

  1. 查询日志记录了所有对MySQL数据库请求的信息,无论这些请求是否得到了正确的执行。默认文件名为:主机名.log。

3.2.4 二进制日志

  1. 二进制日志(binary log)记录了对MySQL数据库执行更改的所有操作,但是不包括SELECT和SHOW这类操作,因为这类操作对数据本身并没有修改。然而,若操作本身并没有导致数据库发生变化,那么该操作可能也会写入二进制文件。
  2. 从上述例子可以看到,MySQL数据库首先进行UPDATE操作,从返回的结果看到Changed为0,这意味着该操作并没有导致数据库的变化。但是通过命令SHOW BINLOG EVENT可以看出在二进制日志中的确进行了记录。
  3. 总的来说,二进制日志主要有以下几种作用:

    恢复(recovery):某些数据的恢复需要二进制日志,例如,在一个数据库全部文件恢复后,用户可以通过二进制日志进行point-in-time的恢复。
    复制(replication):其原理与恢复类似,通过复制和执行二进制日志使一台远程的MySQL数据库(一般称为slave或standby)与一台MySQL数据库(一般称为master或primary)进行实时同步。
    审计(audit):用户可以通过二进制日志中的信息来进行审计,判断是否有对数据库进行注入的攻击。

通过配置参数log_bin[=name]可以启动二进制日志。如果不指定name,则默认二进制日志文件名为主机名,后缀名为二进制日志的序列号,所在路径为数据库所在目录(datadir)。 bin_log.index为二进制的索引文件,用来存储过往产生的二进制日志序号,在通常情况下,不建议手动修改这个文件。 **根据MySQL官方手册中的测试表明,开启二进制日志会使性能下降1%。但考虑到可以使用复制(replication)和point-in-time的恢复,这些性能损失绝对是可以且应该被接受的。** 参数max_binlog_size指定了单个二进制文件的最大值,如果超过该值,则产生新的二进制文件,后缀名+1,并记录到.index文件。从MySQL 5.0开始的默认值为1 073 741 824,代表1G(在之前版本中max_binlog_size默认大小为1.1G)。 **当使用事务的表存储引擎(如InnoDB存储引擎)时,所有未提交的二进制日志会被记录到一个缓存中去,等该事务提交时直接将缓冲中的二进制日志写入二进制日志文件,而该缓冲的大小由binlog_cache_size决定,默认带下为32K。此外,binlog_cache_size是基于会话(session)的,也就是说,当一个线程开始一个事务时,MySQL会自动分配一个大小为binlog_cache_size的缓存,因此该值的设置需要相当小心,不能设置过大。当一个事务的记录大于设定的binlog_cache_size时,MySQL会把缓冲中的日志写入一个临时文件中,因此该值又不能设得太小。通过SHOW GLOBAL STATUS命令查看binlog_cache_use、binlog_cache_disk_use的状态,可以判断当前binlog_cache_size的设置是否合适。** **但是,即使将sync_binlog设为1,还是会有一种情况导致问题的发生。当使用InnoDB时,在一个事务发出COMMIT动作之前,由于sync_binlog为1,因此会将二进制日志立即写入磁盘。如果这时已经写入了二进制日志,但是提交还没有发生,并且此时发生了宕机,那么在MySQL数据库下次启动时,由于COMMIT操作并没有发生,这个事务会被回滚掉。但是二进制日志已经记录了该事务信息,不能被回滚。这个问题可以通过将参数innodb_support_xa设为1来解决,虽然innodb_support_xa与XA事务有关,但它同时也确保了二进制日志和InnoDB存储引擎数据文件的同步。** **bin_format参数十分重要,它影响了记录二进制日志的格式。在MySQL 5.1版本之前,没有这个参数。所有二进制文件的格式都是基于SQL语句(statement)级别的,因此基于这个格式的二进制日志文件的复制(Replication)和Oracle的逻辑Standby有点相似。同时,对于复制是有一定要求的。如在主服务器运行rand、uuid等函数,又或者使用触发器等操作,这些都可能会导致主从服务器上表中数据的不一致(not sync)。另一个影响是,会发现InnoDB存储引擎的默认事务隔离级别是REPEATABLE READ。这其实也是因为二进制日志文件格式的关系,如果使用READ COMMITED的事务隔离级别(大多数数据库,如Oracle,Microsoft SQL Server数据库的默认隔离级别),会出现类似丢失更新的现象,从而出现主从数据库上的数据不一致。** MySQL 5.1开始引入了binlog_format参数,该参数可设的值有STATEMENT、ROW和MIXED。

STATEMENT格式和之前的MySQL版本一样,二进制日志文件记录的是日志的逻辑SQL语句。
在ROW格式下,二进制日志记录的不再是简单的SQL语句,而是记录表的行更改情况。从MySQL 5.1版本开始,如果设置了binlog_format为ROW,可以将InnoDB的事务隔离基本设为READ COMMITED,以获得更好的并发性。
在MIXED格式下,MySQL默认采用STATEMENT格式进行二进制日志文件的记录,但是在一些情况下会使用ROW格式,可能情况有:

表的存储引擎为NDB,这时对表的DML操作都会以ROW格式记录
记录了UUID()、USER()、CURRENT_USER()、FOUND_ROWS()、ROW_COUNT()等不确定函数
使用了INSERT DELAY语句
使用了用户定义函数(UDF)
使用了临时表(temporary table)

**在通常情况下,我们将参数binlog_format设置为ROW,这可以为数据库的恢复和复制带来更好的可靠性。但是不能忽略的一点是,这会带来二进制文件大小的增加,有些语句下的ROW格式可能需要更大的容量。** 可以看到,在binlog_format格式为STATEMENT的情况下,执行UPDATE语句后二进制日志大小只增加了200字节(306-106)。同样的操作在ROW格式下竟然需要13 782 094字节,二进制日志文件的大小差不多增加了13MB,要知道t2表的大小也不超过17MB。这是因为这时MySQL数据库不再将逻辑的SQL操作记录到二进制日志中,而是记录对于每行的更改。 二进制日志文件的文件格式是二进制,不能像错误日志文件、慢查询日志文件那样用cat、head、tail等命令来查看。要查看二进制日志文件的内容,必须通过MySQL提供的工具mysqlbinlog。对于STATEMENT格式的二进制日志文件,在使用mysqlbinlog后,看到的就是执行的逻辑SQL语句。 但是如果这时使用ROW格式的记录方式,会发现mysqlbinlog的结果变得“不可读”。其实只要加上参数-v或-vv就能清楚地看到执行的具体信息了。-vv会比-v多显示出更新的类型。 可以看到,一句简单的update t2 set username=upper(username)where id=1语句记录了对于整个行更改的信息,这也解释了为什么前面更新了10W行的数据,在ROW格式下,二进制日志文件会增大13MB。

3.3 套接字文件

  1. 前面提到过,在UNIX系统下本地连接MySQL可以采用UNIX域套接字方式,这种方式需要一个套接字文件。套接字文件可由参数socket控制。一般在/tmp目录下,名为mysql.sock。

3.4 pid文件

  1. 当MySQL实例启动时,会将自己的进城ID写入一个文件中——该文件即为pid文件。该文件可由参数pid_file控制,默认位于数据库目录下,文件名为主机名.pid。

3.5 表结构定义文件

  1. 但不论表采用何种存储引擎,MySQL都有一个以frm为后缀名的文件,这个文件记录了该表的表结构定义。
  2. frm还用来存放视图的定义,如用户创建了一个v_a视图,那么对应地会产生一个v_a.frm文件,用来记录视图的定义,该文件是文本文件,可以直接使用cat命令进行查看。

3.6 InnoDB存储引擎文件

  1. 之前介绍的文件都是MySQL数据库本身的文件,和存储引擎无关。除了这些文件外,每个表存储引擎还有其自己独有的文件。本节将具体介绍与InnoDB存储引擎密切相关的文件,这些文件包括重做日志文件、表空间文件。

3.6.1 表空间文件

  1. InnoDB采用将存储的数据按表空间(tablespace)进行存放的设计。
  2. 用户可以通过多个文件组成一个表空间,同时制定文件的属性,如:
[mysqld]
innodb_data_file_path = /db/ibdata1:2000M;/dr2/db/ibdata2:2000M:autoextend

这里将/db/ibdata1和/dr2/db/ibdata2两个文件用来组成表空间。若这两个文件位于不同的磁盘上,磁盘的负载可能被平均,因此可以提高数据库的整体性能。同时,两个文件的文件名后都跟了属性,表示文件ibdata1的大小为2000MB,文件ibdata2的大小为2000MB,如果用完了这2000MB,该文件可以自动增长(autoextend)。

设置innodb_data_file_path参数后,所有基于InnoDB存储引擎的表的数据都会记录到该共享表空间中。若设置了参数innodb_file_per_table,则用户可以将每个基于InnoDB存储引擎的表产生一个独立表空间。独立表空间的命名规则为:表明.ibd。

需要注意的是,这些单独的表空间文件仅存储该表的数据、索引和插入缓冲BITMAP等信息,其余信息还是存放在默认的表空间中。

3.6.2 重做日志文件

  1. 在默认情况下,在InnoDB存储引擎的数据目录下会有两个名为ib_logfile0和ib_logfile1的文件。
  2. 为什么强调是重做日志文件呢?因为重做日志文件对于InnoDB存储引擎至关重要,它们记录了对于InnoDB存储引擎的事务日志。
  3. 每个InnoDB存储引擎至少有1个重做日志文件组(group),每个文件组下至少有2个重做日志文件,如默认的ib_logfile0和ib_logfile1。为了得到更高的可靠性,用户可以设置多个的镜像日志组(mirrored log groups),将不同的文件组放在不同的磁盘上,以此提高重做日志的高可用性。
  4. 参数innodb_log_file_size指定每个重做日志文件的大小。在InnoDB1.2.x版本之前,重做日志文件总的大小不得大于4GB,而1.2.x版本将该限制扩大为了512GB。
  5. 参数innodb_log_files_in_group指定了日志文件组中重做日志文件的数量,默认为2。参数innodb_mirrored_log_groups指定了日志镜像文件组的数量,默认为1,表示只有一个日志文件组,没有镜像。若磁盘本身已经做了高可用的方案,如磁盘阵列,那么可以不开启重做日志镜像的功能。
  6. 首先,二进制日志会记录所有与MySQL数据库有关的日志记录,包括InnoDB、MyISAM、Heap等其他存储引擎的日志。而InnoDB存储引擎的重做日志只记录有关该存储引擎本身的事务日志。
  7. 其次,记录的内容不同,无论用户将二进制日志文件记录的格式设为STATEMENT还是ROW,又或者是MIXED,其记录的都是关于一个事务的具体操作内容,即该日志是逻辑日志。而InnoDB存储引擎的重做日志文件记录的是关于每个页(Page)的更改的物理情况。
  8. 此外,写入的时间也不同,二进制日志文件仅在事务提交前进行提交,即只写磁盘一次,不论这时该事务多大。而在事务进行的过程中,却不断有重做日志条目(redo entry)被写入到重做日志文件中。
  9. 重做日志条目是由4个部分组成:

    redo_log_type占用1字节,表示重做日志的类型
    space表示表空间的ID,但采用压缩的方式,因此占用的空间可能小于4字节
    page_no表示页的偏移量,同样采用压缩的方式
    redo_log_body表示每个重做日志的数据部分,恢复时需要调用相应的函数进行解析

在第2章中已经提到,写入重做日志文件的操作不是直接写,而是先写入一个重做日志缓冲(redo log buffer)中,然后按照一定的条件顺序地写入日志文件。 从重做日志缓冲往磁盘写入时,是按512个字节,也就是一个扇区的大小进行写入。因为扇区是写入的最小单位,因此可以保证写入必定是成功的。因此在重做日志的写入过程中不需要有doublewrite。 第2章分析了主线程(master thread),知道在主线程中每秒会将重做日志缓冲写入磁盘的重做日志文件中,不论事务是否已经提交。另一个触发写磁盘的过程是由参数innodb_flush_log_at_trx_commit控制,表示在提交(commit)操作时,处理重做日志的方式。 **1和2不同的地方在于:1表示在执行commit时将重做日志缓冲同步写到磁盘,即伴有fsync的调用。2表示将重做日志异步写到磁盘,即写到文件系统的缓存中。** **因此为了保证事务的ACID中的持久性,必须将innodb_flush_log_at_trx_commit设置为1,也就是每当有事务提交时,就必须确保事务都已经写入重做日志文件。** **而将重做日志文件设为0或2,都有可能发生恢复时部分事务的丢失。不同之处在于,设置为2时,当MySQL数据库发生宕机而操作系统及服务器并没有发生宕机时,由于此时未写入磁盘的事务日志保存在文件系统缓存中,当恢复时同样能保证数据不丢失。**

第4章 表

4.1 索引组织表

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

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

**当表中有多个非空唯一索引时,InnoDB将选择建表时第一个定义的非空唯一索引为主键。这里需要注意的是,主键的选择根据的是定义索引的顺序,而不是建表时列的顺序。**:
mysql> CREATE TABLE z (
    -> a INT NOT NULL,
    -> b INT NULL,
    -> c INT NOT NULL,
    -> d INT NOT NULL,
    -> UNIQUE KEY (b),
    -> UNIQUE KEY (d), UNIQUE KEY (c)); # d是主键,b不是非空的,a不是唯一的,d排在c前边
_rowid可以显示表的主键,因此通过上述查询可以找到表z的主键。 另外需要注意的是,_rowid只能用于查看单个列为主键的情况,对于多列组成的键就显得无能为力了。

4.2 InnoDB逻辑存储结构

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

4.2.1 表空间

  1. 如果启用了innodb_file_per_table的参数,需要注意的是每张表的表空间内存放的只是数据、索引和插入缓冲Bitmap页,其他类的数据,如回滚(undo)信息,插入缓冲索引页、系统事务信息、二次写缓冲(Double write buffer)等还是存放在原来的共享表空间中。
  2. 接着执行会产生大量undo操作的语句update mytest set salary=0,完成后再观察共享表空间,会发现ibdata1已经增长到了114MB。(有更新操作,自然就要记录undo信息)这个例子虽然简单,但是足以说明共享表空间中还包含有undo信息。
  3. 很“可惜”,共享表空间的大小还是114MB,即InnoDB不会再执行rollback时去收缩这个表空间。虽然InnoDB不会回收这些空间,但是会自动判断这些undo信息是否还需要,如果不需要,则会将这些空间标记为可用空间,供下次undo使用。

4.2.2 段

  1. 表空间是由各个段组成的,常见的段有数据段、索引段、回滚段等。因为前面已经介绍过了InnoDB表是索引组织的(index organized),因此数据即索引,索引即数据。那么数据段即为B+树的叶子节点(Leaf node segment),索引段即为B+树的非索引节点(Non-leaf node segment)。

4.2.3 区

  1. 区是由连续页组成的空间,在任何情况下每个区的大小都为1MB。为了保证区中页的连续性,InnoDB一次从磁盘申请4~5个区。在默认情况下,InnoDB页的大小为16KB,即一个区中一共有64个连续的页。
  2. 但是,这里还有一个问题:在用户启用了参数innodb_file_per_table后,创建的表默认大小是96KB。区中是64个连续的页,创建的表的大小至少是1MB才对啊?其实这是因为在每个段开始时,先用32个页大小的碎片页(fragment page)来存放数据,在使用完这些页之后才是64个连续页的申请。做实验:

第一步:建表

mysql> CREATE TABLE t1 (
    -> col1 INT NOT NULL AUTO_INCREMENT,
    -> col2 VARCHAR(7000),
    -> PRIMARY KEY (col1))ENGINE=InnoDB;

上述的SQL语句创建了t1表,将col2字段设为VARCHAR(7000),这样能保证一个页最多可以存放2条记录。此时t1.ibd大小为96KB。

第二步:插入两条记录,t1.ibd仍然是96KB。因为当前所有记录都在一个页中,因此没有非叶节点。

第三步:再插入一条记录,产生一个非叶节点。现在可以看到page offset为3的页的page level由之前的0变为了1,这时虽然新插入的记录导致了B+树的分裂操作,但这个页的类型还是B-tree Node。

第四步:接着继续上述同样的操作,再插入60条记录,也就是说当前表t1中共有63条记录,32个页。为了导入的方便,在这之前先建立一个导入的存储过程:

mysql> DELIMITER//

mysql> CREATE PROCEDURE load_t1(count INT UNSIGNED)
    -> BEGIN
    -> DECLARE s INT UNSIGNED DEFAULT 1;
    -> DECLARE c VARCHAR(7000) DEFAULT('a',7000);
    -> WHILE s <= count DO
    -> INSERT INTO t1 SELECT NULL,c;
    -> SET s = s+1;
    -> END WHILE;
    -> END;
    -> //

mysql> DELIMITER ;

mysql> CALL load_t1(60);

此时t1.ibd大小为576KB。可以看到,在导入了63条数据后,表空间的大小还是小于1MB,即表示数据空间的申请还是通过碎片页,而不是通过64个连续页的区。

可以观察到B-tree Node页一共有33个,除去一个page level为1的非叶节点页,一共有32个page level为0的页,也就是说,对于数据段,已经有32个碎片页了。之后用户再申请空间,则表空间按连续64个页的大小开始增长了。

第五步,接着就这样来操作,插入一条数据,看之后表空间的大小:

mysql> CALL load_t1(1);

此时t1.ibd大小为2.0M。因为已经用完了32个碎片页,新的页会采用区的方式进行空间的申请,如果此时用户再通过py_innodb_page_info工具来看表空间文件t1.ibd,应该可以看到很多类型为Freshly Allocated Page的页。

4.2.4 页

  1. 同大多数数据库一样,InnoDB有页的概念,页是InnoDB磁盘管理的最小单位。在InnoDB中,默认每个页的带下为16KB。而从InnoDB 1.2.x版本开始,可以通过参数innodb_page_size将页的大小设置为4K、8K、16K。若设置完成,则所有表中页的大小都为innodb_page_size,不可以对其再次进行修改。除非通过mysqldump导入和导出操作来产生新的库。
  2. 在InnoDB存储引擎中,常见的页类型有:
    数据页(B-tree Node)
    undo页(undo Log Page)
    系统页(System Page)
    事务数据页(Transaction system Page)
    插入缓冲位图页(Insert Buffer Bitmap)
    插入缓冲空闲列表页(Insert Buffer Free List)
    未压缩的二进制大对象页(Uncompressed BLOB Page)
    压缩的二进制大对象页(compressed BLOB Page)

4.2.5 行

  1. InnoDB是面向行的(row-oriented),也就是说数据是按行进行存放的。每个页存放的行记录也是有硬性定义的,最多允许放16KB/2 -200行的记录,即7992行记录。这里提到了row-oriented的数据库,也就是说,存在有column-oriented的数据库。MySQL infobright存储引擎就是按列来存放数据的,这对于数据仓库下的分析类SQL语句的执行及数据压缩非常有帮助。类似的数据库还有Sybase IQ、Google Big Table。

4.3 InnoDB行记录格式

  1. 在MySQL 5.1版本中,默认设置为Compact行格式。用户可以通过命令SHOW TABLE STATUS LIKE ‘table_name’来查看当前表使用的行格式,其中row_format属性表示当前所使用的行记录结构类型。

4.3.1 Compact行记录格式

  1. 变长字段长度列表 | NULL标志位 | 记录头信息 | 列1数据 | 列2数据 | …
  2. Compact行记录格式的首部是一个非NULL变长字段长度列表,并且其实按照列的顺序逆序放置的,其长度为:

    若列的长度小于255字节,用1字节表示
    若大于255个字节,用2字节表示


变长字段的长度最大不可以超过2字节,这是因为在MySQL数据库中VARCHAR类型的最大长度限制为65535。 **(下面这个例子太清晰地解析Compact行记录格式了,后面Redundant行记录格式也用这个例子讲解得清清楚楚,赞!!!)** 第一步,建表,并插入两条记录:
mysql> CREATE TABLE mytest (
    -> t1 VARCHAR(10),
    -> t2 VARCHAR(10),
    -> t3 CHAR(10),
    -> t4 VARCHAR(10)
    ->) ENGINE=INNODB CHARSET=LATIN1 ROW_FORMAT=COMPACT;

mysql> INSERT INTO mytest
    -> VALUES ('a','bb','bb','ccc');

mysql> INSERT INTO mytest
    -> VALUES ('d',NULL,NULL,'fff');
第二步,查看mytest.ibd文件,转成hex看,找到第一条记录:
03 02 01 /*变长字段长度列表,逆序*/
00 /*NULL标志位,第一行没有NULL值*/
00 00 10 00 2c /*Record Header,固定5字节长度*/
00 00 00 2b 68 00 /*RowID InnoDB自动创建,6字节*/
00 00 00 00 06 05 /*TransactionID*/
80 00 00 00 32 01 10 /*Roll Pointer*/
61 /*列1数据'a'*/
62 62 /*列2数据'bb'*/
62 62 20 20 20 20 20 20 20 20 /*列3数据'bb'*/
63 63 63 /*列4数据'ccc'*/
需要注意的是,变长字段长度列表是逆序存放的,因此变长字段长度列表为03 02 01,而不是01 02 03。此外还需要注意InnoDB每行有隐藏列TransactionID和Roll Pointer。同时可以发现,固定长度CHAR字段在未能完全占用其长度空间时,会用0x20来进行填充。 接下来再分析Record Header的最后两个字节,这两个字节代表next_recorder,0x2c代表下一个记录的偏移量,即当前记录的位置加上偏移量0x2c就是下条记录的起始位置。所以InnoDB存储引擎在页内部是通过一种链表的结构来串连各个行记录的。 第三步,查看有NULL值得第三行:
03 01 /*变长字段长度列表,逆序*/
06 /*NULL标志位,第三行有NULL值*/
00 00 20 ff 98 /*Record Header*/
00 00 00 2b 68 02 /*RowID*/
00 00 00 00 06 07 /*TransactionID*/
80 00 00 00 32 01 10 /*Roll Pointer*/
64 /*列1数据'd'*/
66 66 66 /*列4数据'fff'*/
第三行有NULL值,因此NULL标志位不再是00而是06,转换成二进制位00000110,为1的值代表第2列和第3列的数据为NULL。在其后存储列数据的部分,用户会发现没有存储NULL列,而只存储了第1列和第4列非NULL的值。因此这个例子很好地说明了:不管是CHAR类型还是VARCHAR类型,在compact格式下NULL值都不占用任何空间。

4.3.2 Redundant行记录格式

  1. | 字段长度偏移列表 | 记录头信息 | 列1数据 | 列2数据 | 列3数据 | … |
  2. n_fields值代表一行中列的数量,占用10位。同时这也很好地解释了为什么MySQL数据库一行支持最多的列为1023。另一个需要注意的值为1byte_offs_flags,该值定义了偏移列表占用1字节还是2字节。
  3. 查看一行Redundant行记录格式的记录:
23 20 16 14 13 0c 06 /*长度偏移列表,逆序*/
00 00 10 0f 00 ba /*Record Header,固定6个字节*/
00 00 00 2b 68 0b /*RowID*/
00 00 00 00 06 53 /*TransactionID*/
80 00 00 00 32 01 10 /*Roll Pointer*/
61 /*列1数据'a'*/
62 62 /*列2数据'bb'*/
62 62 20 20 20 20 20 20 20 20 /*列3数据'bb' Char类型*/
63 63 63 /*列4数据 'ccc'*/

23 20 16 14 13 0c 06的逆序为06,0c,13,14,16,20,23,分别代表第一列长度6,第二列长度6(6+6=0x0C),第三列长度为7(6+6+7=0x13),第四列长度为1(6+6+7+1=0x14),第五列长度2(6+6+7+1+2=0x16),第六列长度10(6+6+7+1+2+10=0x20),第七列长度3(6+6+7+1+2+10+3=0x23)。(说明第一列是从RowID开始算起的)

在接下来的记录头信息中应该注意48位中的第22~32位,为0000000111,表示表共有7个列(包含了隐藏的3列),接下来的第33位为1,代表偏移列表为一个字节。

再看一行包含NULL值得行:

21 9e 94 14 13 0c 06 /*长度偏移列表,逆序*/
00 00 20 0f 00 74 /*Record Header,固定6个字节*/
00 00 00 2b 68 0d /*RowID*/
00 00 00 00 06 53 /*TransactionID*/
80 00 00 00 32 01 10 /*Roll Pointer*/
64 /*列1数据'd'*/
00 00 00 00 00 00 00 00 00 00 /*列3数据NULL*/
66 66 66 /*列4数据 'fff'*/

这里与之前Compact行记录格式有着很大的不同了,首先来看长度偏移列表,逆序排列后得到06 0c 13 14 94 9e 21,前4个值都很好理解,第5个NULL值变为了94,接着第6个CHAR类型的NULL值为9e(94+10=0x9e),之后的21代表(14+3=0x21)。可以看到对于VARCHAR类型的NULL值,Redundant行记录格式同样不占用任何存储空间,而CHAR类型的NULL值需要占用空间。(虽然从文件中发现VARCHAR的NULL确实不占空间,但是为啥是94呢?94-14=80,即10000000)

4.3.3 行溢出数据

  1. 这是因为还有别的开销,通过实际测试发现能存放VARCHAR类型的最大长度为65532。
  2. 需要注意的是,如果在执行上述示例的时候没有将SQL_MODE设为严格模式,或许可以建立表,但是MySQL数据库会抛出一个warning。warning信息提示了这次可以创建是因为MySQL数据库自动地将VARCHAR类型转换成了TEXT类型。
  3. 因此从这个例子中用户也应该理解VARCHAR(N)中的N指的是字符的长度。而文档中说明VARCHAR类型最大支持65535,单位是字节。
  4. 此外需要注意的是,MySQL官方手册中定义的65535长度是指所有VARCHAR列的长度总和,如果列的长度总和超出这个长度,依然无法创建。
  5. 因此,在一般情况下,InnoDB的数据都是存放在页类型为B-tree node中。但是当发生行溢出时,数据存放在页类型为Uncompressed BLOB页中。
  6. 通过工具可以观察到表空间中有一个数据页节点B-tree Node,另外有4个未压缩的二进制大对象页Uncompressed BLOB Page,在这些页中才真正存放了65532字节的数据。
  7. 可以看到,从0x0000c093到0x0000c392数据页面其实只保存了VARCHAR(65532)的前768字节的前缀数据,之后是偏移量,指向行溢出页,也就是前面用户看到的Uncompressed BLOB Page。
  8. 可以这样进行思考:InnoDB存储引擎表是索引组织的,即B+Tree的结构,这样每个页中至少应该有两条行记录(否则失去了B+Tree的意义,变成链表了)。因此,如果页中只能存放下一条记录,那么InnoDB存储引擎会自动将行数据存放到溢出页中。(如果页中只能放下一条记录,我就不放在B+Tree里了,没意义了啊,也就是不放在表里了)
  9. 但是,如果可以在一个页中至少放入两行数据,那VARCHAR类型的行数据就不会存放到BLOB页中去。经过多次试验测试,发现这个阈值的长度为8098。
  10. 上述例子建立含有BLOB类型列的表,然后插入4行数据长度为8000的记录。若通过py_innodb_page_info工具对表空间t.ibd进行查看,会发现其实数据并没有保存在BLOB页中。(毕竟虽然类型是BLOB,可大小没超过8098嘛)
  11. 当然既然用户使用了BLOB列类型,一般不可能存放长度这么小的数据。因此大多数的情况下BLOB的行数据还是会发生行溢出,实际数据保存在BLOB页中,数据页只保存数据的前768字节。

4.3.4 Compressed和Dynamic行记录格式

  1. 新的两种记录格式对于存放BLOB中的数据采用了完全的行溢出的方式,在数据页中只存放20个字节的指针,实际的数据都放在Off Page中,而之前的Compact和Redundant两种格式会存放768个前缀字节。
  2. Compressed行记录格式的另一个功能就是,存储在其中的行数据会以zlib的算法进行压缩,因此对于BLOB、TEXT、VARCHAR这类大长度类型的数据能够进行非常有效的存储。

4.3.5 CHAR的行结构存储

  1. 从MySQL 4.1版本开始,CHAR(N)中的N指的是字符的长度,而不是之前版本的字节长度。也就是说在不同的字符集下,CHAR类型列内部存储的可能不是定长的数据。
  2. 因此,对于多字节字符编码的CHAR数据类型的存储,InnoDB存储引擎在内部将其视为变长字符类型。这也就意味着在变长长度列表中会记录CHAR数据类型的长度。

4.4 InnoDB数据页结构

  1. 其中File Header、Page Header、File Trailer的大小是固定的,分别为38、56、8字节,这些空间用来标记该页的一些信息,如Checksum,数据页所在B+树索引的层数等。User Records、Free Space、Page Directory这些部分为实际的行记录存储空间,因此大小是动态的。

4.4.3 Infimum和Supremum Record

  1. 在InnoDB中,每个数据页中有两个虚拟的行记录,用来限定记录的边界。Infimum记录是比该页中任何主键值都要小的值,Supremum指比任何可能大的值还要大的值。这两个值在页创建时被建立,并且在任何情况下不会被删除。

4.4.4 User Record和Free Space

  1. User Record就是之前讨论过的部分,即实际存储行记录的内容。再次强调,InnoDB存储引擎表总是B+树索引组织的。

4.4.5 Page Directory

  1. 与其他数据库系统不同的是,在InnoDB中并不是每个记录拥有一个槽,InnoDB存储引擎的槽是一个稀疏目录(sparse directory),即一个槽中可能包含多个记录。
  2. 在Slots中记录按照索引键值顺序存放,这样可以利用二叉查找迅速找到记录的指针。
  3. 由于在InnoDB中Page Directory是稀疏目录,二叉查找的结果只是一个粗略的结果,因此InnoD必须通过recorder header中的next_record来继续查找相关记录。同时Page Directory很好地解释了recorder header中的n_owned值的含义,因为这些记录并不包括在Page Directory中。
  4. 需要牢记的是,B+树索引本身并不能找到具体的一条记录,能找到只是该记录所在的页。数据库把页载入内存,然后通过Page Directory再进行二叉查找。只不过二叉查找的时间复杂度很低,同时在内存中的查找很快,因此通常忽略这部分查找所用的时间。

4.4.6 File Trailer

  1. File Trailer只有一个FIL_PAGE_END_LSN部分,占用8字节。前4字节代表该页的checksum值,最后4字节和File Header中的FIL_PAGE_LSN相同。将这两个值与File Header中的FIL_PAGE_SPACE_OR_CHECKSUM和FIL_PAGE_LSN值进行比较,看是否一致(checksum的比较需要通过InnoDB的checksum函数来进行比较,不是简单的等值比较),以此来保证页的完整性(not corrupted)。
  2. MySQL 5.6.6版本开始新增了参数innodb_checksum_algorithm,该参数用来控制检测checksum函数的算法,默认值为crc32,可设置的值有:innodb、crc32、none、strict_innodb、strict_crc32、strict_none。
  3. 启用strict_crc32方式是最快的方式,因为其不再对innodb和crc32算法进行两次检测。故推荐使用该设置。若数据库从低版本升级而来,则需要进行mysql_upgrade操作。

4.4.7 InnoDB数据页结构示例分析

  1. (其实这一节我没有太仔细看)通过Recorder Header的最后两个字节记录的下一行记录的偏移量就可以得到该页中所有的行记录,通过Page Header的PAGE_PREV和PAGE_NEXT就可以知道上个页和下个页的位置,这样InnoDB存储引擎就能读到整张表所有的行记录数据。

4.6 约束

4.6.1 数据完整性

  1. 实体完整性保证表中有一个主键。用户还可以通过编写一个触发器来保证数据完整性。
  2. 域完整性保证数据每列的值满足特定的条件。
  3. 参照完整性保证两张表之间的关系。InnoDB存储引擎支持外键,因此允许用户定义外键以强制参照完整性,也可以通过编写触发器以强制执行。

4.6.2 约束的创建和查找

  1. Foreign Key约束似乎会有一个比较神秘的默认名称。
  2. 在上面的例子中,通过information_schema架构下的表TABLE_CONSTRAINTS来查看当前MySQL库下所有的约束信息。对于Foreign Key的约束的命名,用户还可以通过查看表REFERENTIAL_CONSTRAINTS,并且可以详细地了解外键的属性。
mysql> SELECT constraint_name,constraint_type
    -> FROM information_schema.TABLE_CONSTRAINTS
    -> WHERE table_schema='mytest' and table_name='p'\G;

mysql> SELECT * FROM information_schema.REFERENTIAL_CONSTRAINTS
    -> WHERE constraint_schema='mytest'\G;

4.6.3 约束和索引的区别

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

4.6.4 对错误数据的约束

  1. 在上述例子中,首先向NOT NULL的列插入了一个NULL值,同时向列date插入一个非法日期’2009-02-30’。“奇怪”的是MySQL数据库并没有报错,而是显示了警告。如果用户想通过约束对于数据库非法数据的插入或更新,即MySQL数据库提示报错而不是警告,那么用户必须设置参数sql_mode,用来严格审核输入的参数:
mysql> SET sql_mode='STRICT_TRANS_TABLES';

4.6.6 触发器与约束

  1. 触发器的作用是在执行INSERT、DELETE和UPDATE命令之前或之后自动调用SQL命令或存储过程。
  2. 创建触发器的命令是CREATE TRIGGER,只有具备Super权限的MySQL数据库用户才可以执行这条命令:
CREATE
[DEFINER = { user | CURRENT_USER }]
TRIGGER trigger_name BEFORE|AFTER INSERT|UPDATE|DELETE
ON tbl_name FOR EACH ROW trigger_stmt

(栗子来啦!!!)
因为消费总是意味着减去一个正值,而不是负值,所以这时要通过触发器来约束这个逻辑行为,可以进行如下设置:

mysql> CREATE TABLE usercash_err_log (
    -> userid INT NOT NULL,
    -> old_cash INT UNSIGNED NOT NULL,
    -> new_cash INT UNSIGNED NOT NULL,
    -> user VARCHAR(30),
    -> time DATETIME);

mysql> DELIMITER $$

mysql> CREATE TRIGGER tgr_usercash_update BEFORE UPDATE ON usercash
    -> FOR EACH ROW
    -> BEGIN
    -> IF new.cash - old.cash > 0 THEN
    -> INSERT INTO usercash_err_log
    -> SELECT old.userid,old.cash,new.cash,USER(),NOW();
    -> SET new.cash = old.cash;
    -> END IF;
    -> END;
    -> $$

mysql> DELIMITER $$

(关于上面的Trigger我只有一点疑问,就是old和new是Trigger默认的对更新前和更新后的表的命名吗?)

4.6.7 外键约束

  1. 外键的定义如下:
[CONSTRAINT [symbol]] FOREIGN KEY
[index_name] (index_col_name, ...)
REFERENCES tbl_name (index_col_name, ...)
[ON DELETE reference_option]
[ON UPDATE reference_option]
reference_option:
RESTRICT | CASCADE | SET NULL | NO ACTION

(看到吗,跟在FOREIGN KEY后面叫index_name,说明就算它没有被声明为索引(key),也会被悄悄设为索引。)

一个简单的外键的创建示例如下:

mysql> CREATE TABLE parent (
    -> id INT NOT NULL,
    -> PRIMARY KEY (id)
    -> )ENGINE=INNODB;

mysql> CREATE TABLE child (
    -> id INT, parent_id INT,
    -> FOREIGN KEY (parent_id) REFERENCES parent(id)
    -> )ENGINE=INNODB;

CASCADE表示当父表发生DELETE或UPDATE操作时,对相应的子表中的数据也进行DELETE或UPDATE操作。SET NULL表示当父表发生DELETE或UPDATE操作时,相应的子表中的数据被更新为NULL值,但是子表中相对应的列必须允许为NULL值。NO ACTION表示当父表发生DELETE或UPDATE操作时,抛出错误,不允许这类操作发生。RESTRICT表示当父表发生DELETE或UPDATE操作时,抛出错误,不允许这类操作发生。如果定义外键时没有指定ON DELETE或ON UPDATE,RESTRICT就是默认的外键设置。

在其他数据库中,如Oracle数据库,有一种称为延时检查(deferred check)的外键约束,即检查在SQL语句运行完成后再进行。而目前MySQL数据库的外键约束都是即时检查(immediate check),因此从上面的定义看出,在MySQL数据库中NO ACTION和RESTRICT的功能是相同的。

在Oracle数据库中,对于建立外键列,一定不要忘记给这个列加上一个索引。而InnoDB存储引擎在外键建立时会自动地对该列加一个索引,这和Microsoft SQL Server数据库的做法一样。因此可以很好地避免外键列上无索引而导致的死锁问题的产生。例如上述的例子中,表child创建时只定义了外键,并没有手动指定parent_id列为索引,但是通过命令SHOW CREATE TABLE可以发现InnoDB存储引擎自动为外键约束的列parent_id添加了索引:

mysql> SHOW CREATE TABLE child\G;
----------------------------------
Create Table: CREATE TABLE 'child' (
    'id' int(11) DEFAULT NULL,
    'parent_id' int(11) NOT NULL,
    KEY 'parent_id' ('parent_id"),
    CONSTRAINT 'child_ibfk_1' FOREIGN KEY ('parent_id') REFERENCES 'parent' ('id')
) ENGINE=InnoDB DEFAULT CHARSET=utf8

因为MySQL数据库的外键是即时检查的,所以对导入的每一行都会进行外键检查。但是用户可以在导入过程中忽视外键的检查,如:

mysql> SET foreign_key_checks = 0;
mysql> LOAD DATA ......
mysql> SET foreign_key_checks = 1;

4.7 视图

在MySQL数据库中,视图是一个命名的虚表,它由一个SQL查询来定义,可以当作表使用。

4.7.1 视图的作用

  1. 视图的主要用途之一是被用作一个抽象装置,特别是对于一些应用程序,程序本身不需要关心基表(base table)的结构,只需要按照视图定义来取数据或更新数据,因此视图同时在一定程度上起到一个安全层的作用。
  2. 创建视图的语法:
CREATE
[OR REPLACE]
[ALGORITHM = {UNDEFINED | MERGE | TEMPTABLE}]
[DEFINER = { user | CURRENT_USER }]
[SQL SECURITY { DEFINER | INVOKER }]
VIEW view_name [(column_list)]
AS select_statement
[WITH [CASCADED | LOCAL] CHECK OPTION]

虽然视图是基于基表的一个虚拟表,但是用户可以对某些视图进行更新操作,其本质就是通过视图的定义来更新基本表。一般称可以进行更新操作的视图为可更新视图(updatable view)。视图定义中的WITH CHECK OPTION就是针对可更新的视图的,即更新的值是否需要检查。

在上面的例子中,创建了一个id<10的视图v_t。但之后向视图里插入了id为20的值,插入操作并没有报错。但是用户查询视图还是没能查到数据。接着更改视图的定义,加上WITH CHECK OPTION选项。

mysql> ALTER VIEW v_t
    -> AS
    -> SELECT * FROM t WHERE id<10
    -> WITH CHECK OPTION;

mysql> INSERT INTO v_t SELECT 20;
ERROR 1369 (HY000): CHECK OPTION failed 'mytest.v_t'

这次MySQL数据库会对更新视图插入的数据进行检查,对于不满足视图定义条件的,将会抛出一个异常,不允许视图中数据更新。

MySQL数据库DBA的一个常用的命令是SHOW TABLES,该命令会显示出当前数据库下所有的表。但因为视图是虚表,同样被作为表显示出来。

若用户只想查看当前架构下的基表,可以通过information_schema架构下的TABLE表来查询,并搜索表类型为BASE TABLE的表:

mysql> SELECT * FROM information_schema.TABLES
    -> WHERE table_type='BASE TABLE'
    -> AND table_schema=database()\G;

要想查看视图的一些元数据,可以访问information_schema架构下的VIEWS表,该表给出了视图的详细信息,包括视图定义者(definer)、定义内容、是否是可更新视图、字符集等。

4.7.2 物化视图

  1. Oracle数据库支持物化视图——该视图不是基于基表的虚表,而是根据基表实际存在的实表,即物化视图的数据存储在非易失的存储设备上。物化视图可以用于预先计算并保存多表的链接(JOIN)或聚集(GROUP BY)等耗时较多的SQL操作结果。
  2. 在Microsoft SQL Server数据库中,称这种视图为索引视图。
  3. 查询重写是指当对无话视图的基表进行查询时,数据库会自动判断能否通过查询物化视图来直接得到最终的结果,如果可以,则避免了聚集或连接等这类较为复杂的SQL操作,直接从已经计算好的物化视图中得到所需的数据。
  4. 物化视图的刷新是指当基表发生了DML操作后,物化视图何时采用哪种方式和基表进行同步。ON DEMAND意味着物化视图在用户需要的时候进行刷新,ON COMMIT意味着物化视图在对基表的DML操作提交的同时进行刷新。FAST刷新采用增量刷新,只刷新自上次刷新以后进行的修改。
  5. MySQL数据库本身并不支持物化视图,换句话说,MySQL数据库中的视图总是虚拟的。但是用户可以通过一些机制实现物化视图的功能。例如要创建一个ON DEMAND的物化视图还是比较简单的,用户只需定时把数据导入到另一张表:
mysql> CREATE TABLE Orders_MV(
    -> product_name VARCHAR(30) NOT NULL,
    -> price_sum DECIMAL(8,2) NOT NULL,
    -> amount_sum INT NOT NULL,
    -> price_avg FLOAT NOT NULL,
    -> orders_cut INT NOT NULL,
    -> UNIQUE INDEX (product_name));

mysql> INSERT INTO Orders_MV
    -> SELECT product_name, SUM(price), SUM(amount), AVG(price), COUNT(*)
    -> FROM Orders
    -> GROUP BY product_name;

上面的例子中,把物化视图定义为一张表Orders_MV。如果是要实现ON DEMAND的物化视图,只需把表清空,重新导入数据即可。

但是,如果要实现ON COMMIT的物化视图,就不像上面这么简单了。在Oracle数据库中是通过物化视图日志来实现的,很显然在MySQL数据库没有这个日志,不过通过触发器同样可以达到这个目的,首先需要对表Orders建立一个触发器:(这个栗子太精彩啦!!!)

DELIMITER $$

CREATE TRIGGER tgr_Orders_insert
AFTER INSERT ON Orders
FOR EACH ROW
BEGIN
    SET @old_price_sum = 0;
    SET @old_amount_sum = 0;
    SET @old_price_avg = 0;
    SET @old_orders_cnt = 0;

    SELECT IFNULL(price_sum,0), IFNULL(amount_sum,0), IFNULL(price_avg,0), IFNULL(orders_cnt,0)
    FROM Orders_MV
    WHERE product_name = NEW.product_name
    INTO @old_price_sum, @old_amount_sum, @old_price_avg, @old_orders_cnt;

    SET @new_price_sum = @old_price_sum + NEW.price;
    SET @new_amount_sum = @old_amount_sum + NEW.amount;
    SET @new_orders_cnt = @old_orders_cnt + 1;
    SET @new_price_avg = @new_price_sum / @new_orders_cnt;

    REPLACE INTO Orders_MV
    VALUES(NEW.product_name, @new_price_sum, @new_amount_sum, @new_price_avg, @new_orders_cnt);

END;
$$

DELIMITER ;

上述代码创建了一个INSERT的触发器,每次INSERT操作都会重新统计表Orders_MV中的数据。

可以发现在插入两条新的记录后,直接查询Orders_MV表就能得到统计信息。而不像之前需要重新进行SQL语句的统计,这就实现了ON_COMMIT的物化视图功能。

4.8 分区表

4.8.1 分区概述

  1. 分区功能并不是在存储引擎层完成的,因此不是只有InnoDB存储引擎支持分区,常见的存储引擎MyISAM、NDB等都支持。但也并不是所有的存储引擎都支持,如CSV、FEDORATED、MERGE等就不支持。
  2. MySQL数据库支持的分区类型为水平分区(指将同一表中不同行的记录分配到不同的物理文件中),并不支持垂直分区。此外,MySQL数据库的分区是局部分区索引,一个分区中既存放了数据又存放了索引。而全局分区是指,数据存放在各个分区中,但是所有数据的索引放在一个对象中。目前,MySQL数据库还不支持全局分区。
  3. HASH分区:根据用户自定义的表达式的返回值来进行分区,返回值不能为负数。
  4. KEY分区:根据MySQL数据库提供的哈希函数来进行分区。
  5. 不论创建何种类型的分区,如果表中存在主键或唯一索引时,分区列必须是唯一索引的一个组成部分。
  6. 唯一索引可以是允许NULL值得,并且分区列只要是唯一索引的一个组成部分,不需要整个唯一索引列都是分区列。
mysql> CREATE TABLE t1 (
    -> col1 INT NULL,
    -> col2 DATE NULL,
    -> col3 INT NULL,
    -> col4 INT NULL,
    -> UNIQUE KEY (col1, col2, col3, col4))
    -> PARTITION BY HASH(col3)
    -> PARTITIONS 4;

如果建表时没有指定主键,唯一索引,可以指定任何一个列为分区列,因此下面两句创建分区的SQL语句都是可以正确运行的:

CREATE TABLE t1 (
    col1 INT NULL,
    col2 DATE NULL,
    col3 INT NULL,
    col4 INT NULL,
)engine=innodb
PARTITION BY HASH(col3)
PARTITIONS 4;

CREATE TABLE t1 (
    col1 INT NULL,
    col2 DATE NULL,
    col3 INT NULL,
    col4 INT NULL,
key (col4) #key不是唯一索引,所以还是可以任选一列做分区列
)engine=innodb
PARTITION BY HASH(col3)
PARTITIONS 4;

4.8.2 分区类型

1. RANGE分区
  1. 查看表在磁盘上的物理文件,启用分区之后,表不再由一个ibd文件组成了,而是由建立分区时的各个分区ibd文件组成,如下面的t#P#p0.ibd,t#P#p1.ibd。
  2. 对于上述问题,我们可以对分区添加一个MAXVALUE值的分区。MAXVALUE可以理解为正无穷,因此所有大于等于20且小于MAXVALUE的值被放入p2分区。
mysql> ALTER TABLE t
    -> ADD PARTITION(
    -> partition p2 values less than maxvalue );

RANGE分区主要用于日期列的分区,例如对于销售类的表,可以根据年来分区存放销售记录,如下面的分区表sales:

mysql> CREATE TABLE sales(
    -> money INT UNSIGNED NOT NULL,
    -> date DATETIME
    -> )ENGINE=INNODB
    -> PARTITION by RANGE (YEAR(date)) (
    -> PARTITION p2008 VALUE LESS THAN (2009),
    -> PARTITION p2009 VALUE LESS THAN (2010),
    -> PARTITION p2010 VALUE LESS THAN (2011));

通过EXPLAIN PARTITION命令我们可以发现,在上述语句中,SQL优化器只需要去搜索p2008这个分区,而不会去搜索所有的分区——称为Partition Pruning(分区修剪),故查询的速度得到了大幅度的提升。

这次条件改为date<’2009-01-01’而不是date<=’2008-12-31’时,优化器会选择搜索p2008和p2009两个分区,这是我们不希望看到的。因此对于启用分区,应该根据分区的特性来编写最优的SQL语句。

可以看到优化对分区p201001,p201002,p201003都进行了搜索。产生这个问题的主要原因是对于RANGE分区的查询,优化器只能对YEAR(),TO_DAYS(),TO_SECONDS(),UNIX_TIMESTAMP()这类函数进行优化选择,因此对于上述的要求,需要将分区函数改为TO_DAYS

mysql> CREATE TABLE sales (
    -> money INT UNSIGNED NOT NULL,
    -> date DATETIME
    -> )ENGINE=INNODB
    -> PARTITION by range (TO_DAYS(date)) (
    -> PARTITION p201001 VALUES LESS THAN (TO_DAYS('2010-02-01')),
    -> PARTITION p201002 VALUES LESS THAN (TO_DAYS('2010-03-01')),
    -> PARTITION p201003 VALUES LESS THAN (TO_DAYS('2010-04-01'))
    -> );
2. LIST分区
  1. 另外,在用INSERT插入多个行数据的过程中遇到分区未定义的值时,MyISAM和InnoDB存储引擎的处理完全不同。MyISAM引擎会将之前的行数据都插入,但之后的数据不会被插入。而InnoDB存储引擎将其视为一个事务,因此没有任何数据插入。
3. HASH分区
  1. 在RANGE和LIST分区中,必须明确指定一个给定的列值或列值集合应该保存在哪个分区中;而在HASH分区中,MySQL自动完成这些工作,用户所要做的只是基于将要进行哈希分区的列值指定一个列值或表达式,以及指定被分区的表格将要被分割成的分区数量。
  2. 要使用HASH分区来分割一个表,要在CREATE TABLE语句上添加一个“PARTITION BY HASH( expr )”子句,其中“ expr ”是一个返回一个整数的表达式。它可以仅仅是字段类型为MySQL整型的列名。
  3. 如果没有包括一个PARTITIONS子句,那么分区的数量将默认为1.
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中:

mysql> SELECT table_name,partition_name,table_rows
    -> FROM information_schema.PARTITIONS
    -> WHERE table_schema=DATABASE() AND table_name='t_hash'\G;

当然这个例子中也许并不能把数据均匀地分布到各个分区中去,因为分区是按照YEAR函数进行的,而这个值本身可是离散的。如果对于连续的值进行HASH分区,如自增长的主键,则可以较好地将数据进行平均分布。

CREATE TABLE t_linear_hash(
    a INT,
    b DATETIME
)ENGINE=INNODB
PARTITION BY LINEAR HASH (YEAR(b))
PARTITIONS 4;

同样插入’2010-04-01’的记录,这次MySQL数据库根据以下的方法来进行分区的判断:

取大于分区数量4的下一个2的幂值V, V=POWER(2,CEILING(LOG(2,num)))=4
所在分区N=YEAR('2010-04-01')&(V-1)=2

LINEAR HASH分区的优点在于,增加、删除、合并和拆分分区将变得更加快捷,这有利于处理含有大量数据的表。它的缺点在于,与使用HASH分区得到的数据分布相比,各个分区间数据的分布可能不大均衡。

4. KEY分区
  1. KEY分区和HASH分区相似,不同之处在于HASH分区使用用户定义的函数进行分区,KEY分区使用MySQL数据库提供的函数进行区分。对于NDB Cluster引擎,MySQL数据库使用MD5函数来分区;对于其他存储引擎,MySQL数据库使用其内部的哈希函数,这些函数基于与PASSWORD()一样的运算法则。如:
mysql> CREATE TABLE t_key(
    -> a INT,
    -> b DATETIME)ENGINE=INNODB
    -> PARTITION BY KEY (b)
    -> PARTITIONS 4;

在KEY分区中使用关键字LINEAR和在HASH分区中使用具有同样的效果,分区的编号是通过2的幂算法得到的,而不是通过模数算法。

5. COLUMNS分区
  1. 在前面介绍的RANGE、LIST、HASH和KEY这四种分区中,分区的条件是:数据必须是整型,如果不是整型,那应该需要通过函数将其转化为整型,如YEAR(),TO_DAYS(),MONTH()等函数。
  2. COLUMNS分区可以直接使用非整型的数据进行分区,分区根据类型直接比较而得,不需要转化为整型。此外,RANGE COLUMNS分区可以对多个列的值进行区分。
  3. COLUMNS分区支持一下的数据类型:
    所有的整型类型,如INT、SMALLINT、TINYINT、BIGINT。FLOAT和DECIMAL则不予支持。
    日期类型,如DATE和DATETIME。其余的日期类型不予支持。
    字符串类型,如CHAR、VARCHAR、BINARY和VARBINARY。BLOB和TEXT类型不予支持。
CREATE TABLE t_columns_range(
    a INT,
    b DATETIME
)ENGINE=INNODB
PARTITION BY RANGE COLUMNS (B) (
PARTITION p0 VALUES LESS THAN ('2009-01-01'),
PARTITION p1 VALUES LESS THAN ('2010-01-01')
);

CREATE TABLE customers_1 (
first_name VARCHAR(25),
last_name VARCHAR(25),
street_1 VARCHAR(30),
street_2 VARCHAR(30),
city VARCHAR(15),
renewal DATE
)
PARTITION BY LIST COLUMNS(city) (
PARTITION pRegion_1 VALUES IN ('Oskarshamn', 'Hogsby', 'Monsteras'),
PARTITION pRegion_2 VALUES IN ('Vimmerby', 'Hultsfred', 'Vastervik')
);

CREATE TABLE rcx (
    a INT,
    b INT,
    c CHAR(3),
    d INT
)ENGINE=INNODB
PARTITION BY RANGE COLUMNS(a,d,c) (
PARTITIONS p0 VALUES LESS THAN (5,10,'ggg'),
PARTITIONS p1 VALUES LESS THAN (10,20,'mmmm'),
PARTITIONS p2 VALUES LESS THAN (15,30,'sss'),
PARTITIONS p3 VALUES LESS THAN (MAXVALUE,MAXVALUE,MAXVALUE)
);

4.8.3 子分区

mysql> CREATE TABLE ts (a INT, b DATE)ENGINE=INNODB
    -> PARTITION BY RANGE (YEAR(b))
    -> SUBPARTITION BY HASH(TO_DAYS(b))
    -> SUBPARTITIONS 2 (
    -> PARTITION p0 VALUES LESS THAN (1990),
    -> PARTITION p1 VALUES LESS THAN (2000),
    -> PARTITION p2 VALUES LESS THAN MAXVALUE
    ->);
表ts先根据b列进行了RANGE分区,然后又进行了一次HASH分区,所以分区的数量应该为( 3×2=6 个,这通过查看物理磁盘上的文件也可以得到证实。我们也可以通过使用SUBPARTITION语法来显式地指出各个子分区的名字,例如对上述的ts表同样可以这样:
mysql> CREATE TABLE ts (a INT, b DATE)
    -> PARTITION BY RANGE (YEAR(b))
    -> SUBPARTITION BY HASH (TO_DAYS(b)) (
    -> PARTITION p0 VALUES LESS THAN (1990) (
    -> SUBPARTITIONS s0,
    -> SUBPARTITIONS s1
    -> ),
    -> PARTITION p1 VALUES LESS THAN (2000) (
    -> SUBPARTITION s2,
    -> SUBPARTITION s3
    -> ),
    -> PARTITION p2 VALUES LESS THAN MAXVALUE (
    -> SUBPARTITION s4,
    -> SUBPARTITION s5
    -> )
    -> );
子分区的建立需要注意以下几个问题:

每个子分区的数量必须相同。
要在一个分区表的任何分区上使用SUBPARTITION来明确定义任何子分区,就必须定义所有的子分区。
每个SUBPARTITION子句必须包括子分区的一个名字。
子分区的名字必须是唯一的。

子分区可以用于特别大的表,在多个磁盘间分别分配数据和索引。假设有6个磁盘,分别为/disk0、/disk1、/disk2等。现在考虑下面的例子:**(注意引擎是MyISAM!)**
mysql> CREATE TABLE ts (a INT, b DATE)ENGINE=MYISAM
    -> PARTITION BY RANGE (YEAR(b))
    -> SUBPARTITION BY HASH (TO_DAYS(b)) (
    -> PARTITION p0 VALUES LESS THAN (1990) (
    -> SUBPARTITIONS s0
    -> DATA DIRECTORY = '/disk0/data'
    -> INDEX DIRECTORY = '/disk0/idx',
    -> SUBPARTITIONS s1
    -> DATA DIRECTORY = '/disk1/data'
    -> INDEX DIRECTORY = '/disk1/idx'
    -> ),
    -> PARTITION p1 VALUES LESS THAN (2000) (
    -> SUBPARTITION s2
    -> DATA DIRECTORY = '/disk2/data'
    -> INDEX DIRECTORY = '/disk2/idx',
    -> SUBPARTITION s3
    -> DATA DIRECTORY = '/disk3/data'
    -> INDEX DIRECTORY = '/disk3/idx'
    -> ),
    -> PARTITION p2 VALUES LESS THAN MAXVALUE (
    -> SUBPARTITION s4
    -> DATA DIRECTORY = '/disk4/data'
    -> INDEX DIRECTORY = '/disk4/idx',
    -> SUBPARTITION s5
    -> DATA DIRECTORY = '/disk5/data'
    -> INDEX DIRECTORY = '/disk5/idx'
    -> )
    -> );
**由于InnoDB存储引擎使用表空间自动地进行数据和索引的管理,因此会忽略DATA DIRECTORY和INDEX DIRECTORY语法,因此上述的分区表的数据和索引文件分开放置对其是无效的。**

4.8.4 分区中的NULL值

  1. MySQL数据库的分区总是视NULL值小于任何的一个非NULL值,这和MySQL数据库中处理NULL值得ORDER BY操作是一样的。
  2. 对于RANGE分区,如果向分区列插入了NULL值,则MySQL数据库会将该值放入最左边的分区。
  3. 另外需要注意的是,如果删除p0这个分区,删除的将是小于10的记录,并且还有NULL值得记录,这点非常重要。
  4. 在LIST分区下要使用NULL值,则必须显式地指出哪个分区中放入NULL值,否则会报错。
  5. HASH和KEY分区对于NULL的处理方式和RANGE分区、LIST分区不一样。任何分区函数都会将含有NULL值得记录返回为0。

4.8.5 分区和性能

  1. 数据库的应用分为两类:一类是OLTP(在线事务处理),如Blog、电子商务、网络游戏等;另一类是OLAP(在线分析处理),如数据仓库、数据集市。在一个实际的应用环境中,可能既有OLTP的应用,也有OLAP的应用。如网络游戏中,玩家操作的游戏数据库应用就是OLTP的,但是游戏厂商可能需要对游戏产生的日志进行分析,通过分析得到的结果来更好地服务于游戏,预测玩家的行为等,而这却是OLAP的应用。
  2. 对于OLAP的应用,分区的确是可以很好地提高查询的性能,因为OLAP应用大多数查询需要频繁地扫描一张很大的表。假设有一张一亿行的表,其中有一个时间戳属性列。用户的查询需要从这张表中获取一年的数据。如果按时间戳进行分区,则只需要扫描相应的分区即可。这就是前面介绍的Partition Pruning技术。
  3. 然而对于OLTP的应用,分区应该非常小心。在这种应用下,通常不可能会获取一张大表中10%的数据,大部分都是通过索引返回几条记录即可。而根据B+树索引的原理可知,对于一张大表,一般的B+树需要2~3次的磁盘IO。因此B+树可以很好地完成操作,不需要分区的帮助,并且设计不好的分区会带来严重的性能问题。
  4. 但是有没有考虑过这样一种情况:100W和1000W行的数据本身构成的B+树的层次都是一样的,可能都是2层。那么上述走主键分区的索引并不会带来性能的提高。好的,如果1000W的B+树的高度是3,100W的B+树的高度是2,那么上述按主键分区的索引可以避免1次IO,从而提高查询效率。这没问题,但是这张表只有主键索引,没有任何其他的列需要查询的。如果还有类似如下的SQL语句:SELECT * FROM TABLE WHERE KEY = @key,这时对于KEY的查询需要扫描所有的10个分区,即使每个分区的查询开销为2次IO,则一共需要20次IO。而对于原来单表的设计,对于KEY的查询只需要2~3次IO。
  5. 即使是根据自增长主键进行的HASH分区也不能保证分区数据的均匀。因为插入的自增长ID并非总是连续的,如果该主键值因为某种原因被回滚了,则该值将不会再次被自动使用。
  6. 如果进行主键的查询,可以发现分区的确是有意义的。可以发现指寻找了p1分区,但是对于表Profile中nickname列索引的查询,EXPLAIN PARTITIONS则会得到如下结果。可以看到,MySQL数据库会搜索所有的分区,因此查询速度会慢很多。
  7. 上述简单的索引查找语句竟然需要1.05秒,这显然是因为查询需要遍历所有分区的关系,实际的IO执行了约20~30次。而在未分区的同样结构和大小的表上,执行上述同样的SQL语句只需要0.26秒。
  8. 因此对于使用InnoDB存储引擎作为OLTP应用的表在使用分区时应该十分小心,设计时确认数据的访问模式,否在在OLTP应用下分区可能不仅不会带来查询速度的提高,反而可能会使你的应用执行得更慢。

4.8.6 在表和分区间交换数据

  1. MySQL 5.6开始支持ALTER TABLE … EXCHANGE PARTITION语法。该语句允许分区或子分区中的数据与另一个非分区的表中的数据进行交换。
  2. 要交换的表需和分区表有着相同的表结构,但是表不能还有分区。
  3. 在非分区表中的数据必须在交换的分区定义内。
  4. 被交换的表中不能含有外键,或者其他的表含有对该表的外键引用
  5. 用户除了需要ALTER、INSERT和CREATE权限外,还需要DROP的权限
  6. 使用该语句时,不会触发交换表和被交换表上的触发器
  7. AUTO_INCREMENT列将被重置。
  8. 栗子!:
CREATE TABLE e (
    id INT NOT NULL,
    fname VARCHAR(30),
    lname VARCHAR(30)
)
PARTITION BY RANGE (id) (
    PARTITION p0 VALUES LESS THAN (50),
    PARTITION p1 VALUES LESS THAN (100),
    PARTITION p2 VALUES LESS THAN (150),
    PARTITION p3 VALUES LESS THAN (MAXVALUE)
);

INSERT INTO e VALUES
(1669, "Jim", "Smith"),
(337, "Mary", "Jones"),
(16, "Frank", "White"),
(2005, "Linda", "Black");

然后创建交换表e2。表e2的结构和表e一样,但需要注意的是表e2不能含有分区:

mysql> CREATE TABLE e2 LIKE e;

mysql> ALTER TABLE e2 REMOVE PARTITIONING;

因为表e2中没有数据,使用如下语句将表e的分区p0中的数据移动到表e2中:

mysql> ALTER TABLE e EXCHANGE PARTITION p0 WITH TABLE e2;

这时再观察表e中分区的数据,可以发现p0中的数据已经没有了。而这时可以在表e2中观察到被移动的数据。

第5章 索引与算法

  1. 一些开发人员总是事后才想起添加索引——我一直认为,这源于一种错误的开发模式。如果知道数据的使用,从一开始就应该在需要处添加索引。
  2. DBA往往不够了解业务的数据流,而添加索引需要通过监控大量的SQL语句进而从中找到问题,这个步骤所需的时间肯定是远大于初始添加索引所需要的时间,并且可能会遗漏一部分的索引。

5.1 InnoDB存储引擎索引概述

  1. 前面已经提到过,InnoDB存储引擎支持的哈希索引是自适应的,InnoDB会根据表的使用情况自动为表生成哈希索引,不能人为干预是否在一张表中生成哈希索引。
  2. B+树索引就是传统意义上的索引,这是目前关系型数据库系统中查找最为常用和最为有效的索引。
  3. 另一个常常被DBA忽视的问题是:B+树索引并不能找到一个给定键值的具体行。B+树索引能找到的只是被查找数据行所在的页。然后数据库通过把页读入到内存,再在内存中进行查找,最后得到要查找的数据。

5.2 数据结构与算法

5.2.2 二叉查找树和平衡二叉树

  1. 平衡二叉树的查找性能是比较高的,但不是最高的,只是接近最高性能。最好的性能需要建立一棵最优二叉树,但是最优二叉树的建立和维护需要大量的操作,因此,用户一般只需建立一棵平衡二叉树即可。

5.3 B+树

  1. B+树由B树和索引顺序访问方法(ISAM,是不是很熟悉?对,这也是MyISAM引擎最初参考的数据结构)演化而来,但是在现实使用中几乎已经没有使用B树的情况了。
  2. 这里,我来精简地对B+树做个介绍:B+树是为磁盘或其他直接存取辅助设备设计的一种平衡查找树。在B+树种,所有记录节点都是按键值的大小顺序存放在同一层的叶子节点上,由各叶子节点指针进行连接(双向链表)。先来看一个B+树,其高度为2,每页可存放4条记录,扇出(fan out)为5。

5.3.1 B+树的插入操作

Leaf Page满Index Page满操作
NoNo直接将记录插入到叶子节点
YesNo1. 拆分Leaf Page;2. 将中间的节点放入到Index Page中;3. 小于中间节点的记录放左边; 4. 大于或等于中间节点的记录放右边
YesYes1. 拆分Leaf Page;2. 小于中间节点的记录放左边;3. 大于或等于中间节点的记录放右边;4.拆分Index Page;5. 小于中间节点的记录放左边;6. 大于中间节点的记录放右边;7. 中间节点放入上一层Index Page
  1. 但是为了保持平衡对于新插入的键值可能需要做大量的拆分页(split)操作。因为B+树结构主要用于磁盘,页的拆分意味着磁盘的操作,所以应该在可能的情况下尽量减少页的拆分操作。因此,B+树同样提供了类似于平衡二叉树的旋转功能。
  2. 旋转发生在Leaf Page已经满,但是其的左右兄弟节点没有满的情况下。这时B+树并不会急于去做拆分页的操作,而是将记录移到所有页的兄弟节点上。在通常情况下,左兄弟会被首先检查用来做旋转操作。

5.3.2 B+树的删除操作

  1. B+树使用填充因子(fill factor)来控制树的删除变化,50%是填充因子可设的最小值。
叶子节点小于填充因子中间节点小于填充因子操作
NoNo直接将记录从叶子节点删除,如果该节点还是Index Page的节点,用该节点的右节点代替
YesNo合并叶子结点和它的兄弟节点,同时更新Index Page
YesYes1. 合并叶子节点和它的兄弟节点 2. 更新Index Page 3. 合并Index Page和它的兄弟节点

5.4 B+树索引

  1. 前面讨论过的都是B+树的数据结构及其一般操作,B+树索引的本质就是B+树在数据库中的实现。但是B+索引在数据库中有一个特点是高扇出性,因此在数据库中,B+树的高度一般都在2~4层,这也就是说查找某一键值的行记录时最多只需要2到4次IO,这倒不错。因为当前一般的机械磁盘每秒至少可以做100次IO,2~4次的IO意味着查询时间只需0.02~0.04秒。
  2. 数据库中的B+树索引可以分为聚集索引(clustered index)和辅助索引(secondary index),但是不管是聚集还是辅助的索引,其内部都是B+树的,即高度平衡的,叶子节点存放着所有的数据。聚集索引与辅助索引不同的是,叶子节点存放的是否是一整行的信息。

5.4.1 聚集索引

  1. 而聚集索引(clustered index)就是按照每张表的主键构造一棵B+树,同时叶子节点中存放的即为整张表的行记录数据,也将聚集索引的叶子结点称为数据页。聚集索引的这个特性决定了索引组织表中数据也是索引的一部分。
  2. 由于实际的数据页只能按照一棵B+树进行排序,因此每张表只能拥有一个聚集索引。
  3. 此外,由于定义了数据的逻辑顺序,聚集索引能够特别快地访问针对范围值的查询。
  4. 通过以上对非数据页节点的分析,可以发现数据页上存放的是完整的每行的记录,而在非数据页的索引页中,存放的仅仅是键值及指向数据页的偏移量,而不是一个完整的行记录。
  5. 所以,聚集索引的存储并不是物理上连续的,而是逻辑上连续的。这其中有两点:一是前面说过的页通过双向链表链接,页按照主键的顺序排序;另一个是每个页中的记录也是通过双向链表进行维护的,物理存储上可以同样不按照主键存储。
  6. 聚集索引的另一个好处是,它对于主键的排序查找和范围查找速度非常快。叶子节点的数据就是用户索要查询的数据。如用户需要查询一张注册用户的表,查询最后注册的10位用户,由于B+树索引是双向链表,用户可以快速找到最后一个数据页,并取出10条记录。若用命令EXPLAIN进行分析,可得:
mysql> EXPLAIN
    -> SELECT * FROM Profile ORDER BY id LIMIT 10\G;

可以看到虽然使用ORDER BY对行记录进行排序,但是在实际过程中并没有进行所谓的filesort操作,而这就是因为聚集索引的特点。

5.4.2 辅助索引

  1. 对于辅助索引,叶子节点并不包含行记录的全部数据。叶子节点除了包含键值以外,每个叶子节点中的索引行中还包含了一个书签(bookmark)。该书签用来告诉InnoDB存储引擎哪里可以找到与索引相对应的行数据。由于InnoDB存储引擎表是索引组织表,因此InnoDB存储引擎的辅助索引的书签就是相应行数据的聚集索引键。
  2. 辅助索引的存在并不影响数据在聚集索引中的组织,因此每张表上可以有多个辅助索引。当通过辅助索引来寻找数据时,InnoDB存储引擎会遍历辅助索引并通过叶级别的指针获得指向主键索引的主键,然后再通过主键索引来找到一个完整的行记录。举例来说,如果在一棵高度为3的辅助索引树种查找数据,那需要对这棵辅助索引树遍历3次找到指定主键,如果聚集索引树的高度同样为3,那么还需要对聚集索引树进行3次查找,最终找到一个完整的行数据所在的页,因此一共需要6次逻辑IO访问以得到最终的一个数据页。
  3. 对于其他的一些数据库,如Microsoft SQL Server数据库,其有一种称为堆表的表类型,即行数据的存储按照插入的顺序存放。
  4. 堆表的特性决定了堆表上的索引都是非聚集的,主键与非主键的区别只是是否唯一且非空(NOT NULL)。因此这时书签是一个行标识符(Row Identifier,RID),可以用如“文件号:页号:槽号”的格式来定位实际的行数据。
  5. 有的Microsoft SQL Server数据库DBA问我这样的问题,为什么在Microsoft SQL Server数据库上还要使用索引组织表?堆表的书签使非聚集查找可以比主键书签方式更快,并且非聚集可能在一张表中存在多个,我们需要对多个非聚集索引进行查找。而且对于非聚集索引的离散读取,索引组织表上的非聚集索引会比堆表上的聚集索引慢一些。
  6. 当然,在一些情况下,使用堆表的确会比索引组织表更快,但是我觉得大部分原因是由于存在OLAP(On-Line Analytical Processing,在线分析处理)的应用。其次就是前面提到的,表中数据是否需要更新,并且更新是否影响到物理地址的变更。此外另一个不能忽视的是对于排序和范围查找,索引组织表通过B+树的中间节点就可以找到要查找的所有页,然后进行读取,而堆表的特性决定了这对其是不能实现的。最后,非聚集索引的离散读,的确存在上述的情况,但是一般的数据库都通过实现预读(read ahead)技术来避免多次的离散读操作。因此具体是建堆表还是索引组织表,这取决于应用,不存在哪个更优的问题。
  7. 图5-16显示了表t中辅助索引idx_c和聚集索引的关系。可以看到辅助索引的叶子节点中包含了列c的值和主键的值。因为这里我特意将键值设为负值,所以会发现-1以7f ff ff ff的方式进行内部存储。7(0111)最高位为0,代表负值,实际的值应该取反后加1,即得-1。(第200页的图5-16辅助索引分析非常直观!!!)

5.4.3 B+树索引的分裂

  1. B+树索引页的分裂并不总是从页的中间记录开始,这样可能会导致页空间的浪费。例如下面的记录:1、2、3、4、5、6、7、8、9。
  2. 插入是根据自增顺序进行的,若这时插入10这条记录后需要进行页的分裂操作,那么根据5.3.1节介绍的分裂方法,会将记录5作为分裂点记录(split record),分裂后得到下面两个页:P1:1、2、3、4 P2:5、6、7、8、9、10。
  3. 然而由于插入是顺序的,P1这个页中将不会再有记录被插入,从而导致空间的浪费。而P2又会再次进行分裂。
  4. InnoDB的Page Header中有以下几个部分用来保存插入的顺序信息:PAGE_LAST_INSERT、PAGE_DIRECTION、PAGE_N_DIRECTION。
  5. 通过这些信息,InnoDB可以决定是向左还是向右进行分裂,同时决定将分裂点记录为哪一个。若插入是随机的,则取页的中间记录作为分裂点的记录,这和之前介绍的相同。若往同一方向进行插入的记录数量为5,并且目前已经定位(cursor)到的记录之后还有3条记录,则分裂点的记录为定位到的记录后的第三条记录,否则分裂点记录就是待插入的记录。**
  6. 对于图5-19的情况,分裂点就为插入记录本身,向右分裂后仅插入记录本身,这在自增插入时是普遍存在的一种情况。

5.4.4 B+树索引的管理

1. 索引管理
  1. 通过ALTER TABLE创建索引的语法为:
ALTER TABLE tbl_name
| ADD {INDEX|KEY} [index_name]
[index_type] (index_col_name,...) [index_option] ...

ALTER TABLE tbl_name
DROP PRIMARY KEY
| DROP {INDEX|KEY} index_name

CREATE/DROP INDEX的语法同样很简单:

CREATE [UNIQUE] INDEX index_name
[index_type]
ON tbl_name (index_col_name,...)

DROP INDEX index_name ON tbl_name

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

mysql> ALTER TABLE t
    -> ADD KEY idx_b (b(100));

通过命令SHOW INDEX FROM可以观察到表t上有4个索引,分别为主键索引、c列上的辅助索引、b列的前100字节构成的辅助索引,以及(a、c)的联合辅助索引。接着具体阐述命令SHOW INDEX展现结果中每列的含义。

Seq_in_index:索引中该列的位置,如果看联合索引idx_a_c就比较直观了。

Collation:列以什么方式存储在索引中。可以是A或NULL。B+树索引总是A,即排序的。如果使用了Heap存储引擎,并且建立了Hash索引,这里就会显示NULL了。因为Hash根据Hash桶存放索引数据,而不是对数据进行排序。

Cardinality:非常关键的值,表示索引中唯一值得数目的估计值。Cardinality表的行数应尽可能接近1,如果非常小,那么用户需要考虑是否可以删除此索引。

Sub_part:是否是列的部分被索引。如果看idx_b这个索引,这里显示100,表示只对列的前100字符进行索引。如果索引整个列,则该字段为NULL。

Cardinality值非常关键,优化器会根据这个值来判断是否使用这个索引。但是这个值并不是实时更新的,即并非每次索引的更新都会更新该值,因为这样代价太大了。因此这个值是不太准确的,只是一个大概的值。上面显示的结果主键的Cardinality为2,但是很显然我们的表中有4条记录,这个值应该是4。如果需要更新索引Cardinality的信息,可以使用ANALYZE TABLE命令。

Cardinality为NULL,在某些情况下可能会发生索引建立了却没有用到的情况。这时最好的解决办法就是做一次ANALYZE TABLE的操作。因此我建议在一个非高峰时间,对应用程序下的几张核心表做ANALYZE TABLE操作,这能使优化器和索引更好地为你工作。

2. Fast Index Creation


  1. MySQL 5.5版本之前存在的一个普遍被人诟病的问题是MySQL数据库对于索引的添加或者删除的这类DDL操作,MySQL数据库的操作过程为:

首先创建一张新的临时表,表结构为通过命令ALTER TABLE新定义的结构。
然后把原表中数据导入到临时表。
接着删除原表。
最后把临时表重名为原来的表名。

可以发现,若用户对于一张大表进行索引的添加和删除操作,那么这会需要很长的时间。更关键的是,若有大量事务需要访问正在被修改的表,这意味着数据库服务不可用。而这对于Microsoft SQL Server或Oracle数据库的DBA来说,MySQL数据库的索引维护始终让他们感觉非常痛苦。

InnoDB存储引擎从InnoDB 1.0.x版本开始支持一种称为Fast Index Creation(快速索引创建)的索引创建方式——简称FIC。

对于辅助索引的创建,InnoDB存储引擎会对创建索引的表加上一个S锁。在创建的过程中,不需要重建表,因此速度较之前提高很多,并且数据库的可用性也得到了提高。删除辅助索引操作就更简单了,InnoDB存储引擎只需更新内部视图,并将辅助索引的空间标记为可用,同时删除MySQL数据库内部视图上对该表的索引定义即可。

由于FIC在索引的创建的过程中对表加上了S锁,因此在创建的过程中只能对该表进行读操作,若有大量的事务需要对目标表进行写操作,那么数据库的服务同样不可用。此外,FIC方式只限定于辅助索引,对于主键的创建和删除同样需要重建一张表。

3. Online Schema Change


  1. 所谓“在线”是指在事务的创建过程中,可以有读写事务对表进行操作,这提高了原有MySQL数据库在DDL操作时的并发性。
  2. Facebook采用PHP脚本来实现OSC,而并不是通过修改InnoDB存储引擎源码的方式。
  3. 实现OSC步骤如下:

init,即初始化阶段,会对创建的表做一些验证工作,如检查表是否有主键,是否存在触发器或者外键等。
createCopyTable,创建和原始表结构一样的新表
alterCopyTable,对创建的新表进行ALTER TABLE操作,如添加索引或列等
createDeltasTable,创建deltas表,该表的作用是为下一步创建的触发器所使用。之后对原表的所有DML操作会被记录到createDeltasTable中
createTriggers,对原表创建INSERT、UPDATE、DELETE操作的触发器。触发操作产生的记录被写入到deltas表。
startSnpshotXact,开始OSC操作的事务。
selectTableInfoOutfile,将原表中的数据写入到新表。为了减少对原表的锁定时间,这里通过分片(chunked)将数据输出到多个外部文件,然后将外部文件的数据导入到copy表中。分片的大小可以指定,默认值是500 000。
dropNCIndexs,在导入到新表前,删除新表中所有的辅助索引。
loadCopyTable,将导出的分片文件导入到新表。
replayChanges,将OSC过程中原表DML操作的记录应用到新表中,这些记录被保存在deltas表中。
recreateNCIndexs,重新创建辅助索引。
replayChanges,再次进行DML日志的回放操作,这些日志是在上述创建辅助索引过程中新产生的日志。
swapTables,将原表和新表交换名字,整个操作需要锁定2张表,不允许新的数据产生。由于改名是一个很快的操作,因此阻塞的时间非常短。

上述只是简单介绍了OSC的实现过程,实际脚本非常复杂,仅OSC的PHP核心代码就有2200多行,用到的MySQL InnoDB的知识点非常多,建议DBA和数据库开发人员尝试进行阅读,这有助于更好地理解InnoDB存储引擎的使用。

4. Online DDL


  1. MySQL 5.6版本开始支持Online DDL操作,其允许辅助索引创建的同时,还允许其他诸如INSERT、UPDATE、DELETE这类DML操作,这极大地提高了MySQL数据库在生产环境中的可用性。
  2. 此外,不仅是辅助索引,以下这几类DDL操作都可以通过“在线”的方式进行操作:

辅助索引的创建与删除
改变自增长值
添加或删除外键约束
列的重命名

通过新的ALTER TABLE语法,用户可以选择索引的创建方式:

ALTER TABLE tbl_name
| ADD {INDEX|KEY} [index_name]
[index_type] (index_col_name,...) [index_option] ...
ALGORITHM [=] {DEFAULT|INPLACE|COPY}
LOCK [=] {DEFAULT|NONE|SHARED|EXCLUSIVE}

ALGORITHM指定了创建或删除索引的算法,COPY表示按照MySQL 5.1版本之前的工作模式,即创建临时表的方式。INPLACE表示索引创建或删除操作不需要创建临时表。

在EXCLUSIVE模式下,执行索引创建或删除操作时,对目标表加上一个X锁。读写事务都不能进行,因此会阻塞所有的线程,这和COPY方式运行得到的状态相似,但是不需要像COPY方式那样创建一张临时表。

DEFAULT模式首先会判断当前操作是否可以使用NONE模式,若不能,则判断是否可以使用SHARE模式,最后判断是否可以使用EXCLUSIVE模式。也就是说DEFAULT会通过判断事务的最大并发性来判断执行DDL的模式。

InnoDB实现Online DDL的原理是在执行创建或者删除操作的同时,将INSERT、UPDATE、DELETE这类DML操作日志写入到一个缓存中。待完成索引创建后再将重做应用到表上,以此达到数据的一致性。这个缓存的大小由参数innodb_online_alter_log_max_size控制,默认的大小为128MB。

需要注意的是,由于Online DDL在创建索引完成后再通过重做日志达到数据库的最终一致性,这意味着在索引创建过程中,SQL优化器不会选择正在创建中的索引。

5.5 Cardinality值

5.5.1 什么事Cardinality

  1. 对于什么时候添加B+树索引,一般的经验是,在访问表中很少一部分时使用B+树索引才有意义。对于性别字段、地区字段、类型字段,它们可取值的范围很小,称为低选择性。
  2. 相反,如果某个字段的取值范围很广,几乎没有重复,即属于高选择性,则此时使用B+树索引是最适合的。例如,对于姓名字段,基本上在一个应用中不允许重名的出现。
  3. 怎样查看索引是否是高选择性的呢?可以通过SHOW INDEX结果中的列Cardinality来观察。Cardinality值非常关键,表示索引中不重复记录数量的预估值。同时需要注意的是,Cardinality是一个预估值,而不是一个准确值,基本上用户也不可能得到一个准确的值。在实际应用中,Cardinality/n_rows_in_table应尽可能地接近1.如果非常小,那么用户需要考虑是否还有必要创建这个索引。

5.5.2 InnoDB存储引擎的Cardinality统计


  1. 因为MySQL数据库中有各种不同的存储引擎,而每种存储引擎对于B+树索引的实现又各不相同,所以对Cardinality的统计是放在存储引擎层进行的。
  2. 因此,数据库对于Cardinality的统计都是通过采样(Sample)的方法来完成的。
  3. 因此,InnoDB存储引擎内部对更新Cardinality信息的策略为:

表中1/16的数据已发生过变化。
stat_modified_counter>2 000 000 000

第二种情况考虑的是,如果对表中某一行数据频繁地进行更新操作,这时表中的数据实际并没有增加,实际发生变化还是这一行数据,则第一种更新策略就无法适用这种情况。故在InnoDB内部有一个计数器stat_modified_counter,用来表示发生变化的次数,当stat_modified_counter大于2 000 000 000时,则同样需要更新Cardinality信息。

默认InnoDB对8个叶子节点(Leaf Page)进行采样。采样的过程如下:

取得B+树索引页中叶子节点的数量,记为A
随机取得B+树索引中的8个叶子节点。统计每个页不同记录的个数,即为P1, P2, …, P8.
根据采样信息给出Cardinality的预估值:Cardinality=(P1+P2+…+P8) * A/8

可以看到,第二次运行SHOW INDEX FROM语句时,表OrderDetails中索引的Cardinality值都发生了变化,虽然表OrderDetails本身并没有发生任何的变化,但是,由于Cardinality是对随机取8个叶子节点进行分析,所以即使表没有发生变化,用户观察到的索引Cardinality值还是会发生变化,这本身并不是InnoDB存储引擎的Bug,只是随机采样而导致的结果。

例如某页中索引记录为NULL、NULL、1、2、2、3、3、3,在参数innodb_stats_method的默认设置下,该页的Cardinality为4;若参数innodb_stats_method为nulls_unequal,则该页的Cardinality为5;若参数innodb_stats_method为nulls_ignored,则Cardinality为3。

当执行SQL语句ANALYZE TABLE、SHOW TABLE STATUS、SHOW INDEX以及访问INFORMATION_SCHEMA架构下的表TABLES和STATISTICS时会导致InnoDB存储引擎去重新计算索引的Cardinality值。若表中的数据量非常大,并且表中存在多个辅助索引时,执行上述这些操作可能会非常慢。虽然用户可能并不希望去更新Cardinality值。

  • 0
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值