/etc/my.cnf -> /etc/mysql/my.cnf -> /usr/local/mysql/etc/my.cnf -> ~/.my.cnf
的顺序读取配置文件的。
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
有些第三方存储引擎很强大,如大名鼎鼎的InnoDB存储引擎(最早是第三方存储引擎,后被Oracle收购),其应用就极其广泛,甚至是MySQL数据库OLTP(Online Transaction Processing在线事务处理)应用中使用最广泛的存储引擎。
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)以及这个示例数据库的下载竟然和文档放在一起。
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段下连接该实例,并且不需要密码。
mysql>SHOW VARIABLES LIKE 'socket';
在知道了UNIX域套接字文件的路径后,就可以使用该方式进行连接了,如下所示:
[root@stargazer ~]# mysql -udavid -S /tmp/mysql.sock
(总结一下使用方法:1.用户需要在数据库中找到UNIX域套接字文件的路径 2.有了这个路径后,就可以用UNIX域套接字文件方式进行连接了)只能这么说,用户是程序员,所以能登陆MySQL,找到UNIX域套接字文件在哪儿,然后他要写程序啦,就在这个程序里用找到的UNIX域套接字路径来写代码,以实现连接MySQL数据库的功能。
[mysqld]
innodb_purge_threads=1
从InnoDB 1.2版本开始,InnoDB支持多个Purge Thread,这样做的目的是为了进一步加快undo页的回收。
mysql> SELECT POOL_ID,POOL_SIZE,FREE_BUFFERS,DATABASE_PAGES
-> FROM INNODB_BUFFER_POOL_STATS\G;
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表示该页属于系统表空间。
Master Thread每一秒将重做日志缓冲刷新到重做日志文件
每个事务提交时会将重做日志缓冲刷新到重做日志文件
当重做日志缓冲池剩余空间小于1/2时,重做日志缓冲刷新到重做日志文件
Sharp Checkpoint
Fuzzy 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,则可定义:Master Thread Checkpoint
FLUSH_LRU_LIST Checkpoint
Async/Sync Flush Checkpoint
Dirty Page too much Checkpoint
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。则
可见,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,刷新一部分的脏页到磁盘。当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。
日志缓冲刷新到磁盘,即使这个事务还没有提交(总是)
合并插入缓冲(可能)
至多刷新100个InnoDB的缓冲池中的脏页到磁盘(可能)
如果当前没有用户活动,则切换到background loop(可能)
在以上的过程中,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会执行以下操作:刷新100个脏页到磁盘(可能的情况下)
合并至多5个插入缓冲(总是)
将日志缓冲刷新到磁盘(总是)
删除无用的Undo页(总是)
刷新100个或者10个脏页到磁盘(总是)
若flush loop中也没有什么事情可以做了,InnoDB会切换到suspend loop,将Master Thread挂起,等待事件的发生。删除无用的Undo页(总是)
合并20个插入缓冲(总是)
跳回到主循环(总是)(应该不是总是吧?)
不断刷新100个页直到符合条件(可能,跳转到flush loop中完成)
在合并插入缓冲时,合并插入缓冲的数量为innodb_io_capacity值得5%
在从缓冲区刷新脏页时,刷新脏页的数量为innodb_io_capacity
插入缓冲(Insert Buffer)
两次写(Double Write)
自适应哈希索引(Adaptive Hash Index)
异步IO(Async IO)
刷新邻接页(Flush Neighbor Page)
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的缓冲池内存。
IBUF_BITMAP_FREE 2 表示该辅助索引页中的可用空间数量
IBUF_BITMAP_BUFFERED 1 1表示该辅助索引页有记录被缓存在Insert Buffer B+树中
IBUF_BITMAP_IBUF 1 1表示该页为Insert Buffer B+树的索引页
辅助索引页被读取到缓冲池时
Insert Buffer Bitmap页追踪到该辅助索引页已无空间可用时
Master Thread
以该模式访问了100次
页通过该模式访问了N次,其中N=页中记录*1/16
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大于0后,用户可以对标进行select、create和drop操作,但insert、update和delete这类DML操作是不允许的。 可以看到,采用默认的策略,即将innodb_force_recovery设为0,InnoDB会在每次启动后对发生问题的表进行恢复操作。通过错误日志文件,可知这次回滚操作需要回滚8867280行记录,差不多总共进行了9分钟。 这里出现了“!!!”,InnoDB警告已经将innodb_force_recovery设置为3,不会进行回滚操作了,因此数据库很快启动完了。但是用户应该小心当前数据库的状态,并仔细确认是否不需要回滚事务的操作。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):不进行前滚的操作。
mysql> SELECT * FROM GLOBAL_VARIABLES
-> WHERE VARIABLE_NAME LIKE 'innodb_buffer%' \G;
mysql> SHOW VARIABLES LIKE 'innodb_buffer%' \G;
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实例本身并不会对参数文件中的值进行修改。
$ 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读取的次数,逻辑读取包含所有的读取。
恢复(recovery):某些数据的恢复需要二进制日志,例如,在一个数据库全部文件恢复后,用户可以通过二进制日志进行point-in-time的恢复。
复制(replication):其原理与恢复类似,通过复制和执行二进制日志使一台远程的MySQL数据库(一般称为slave或standby)与一台MySQL数据库(一般称为master或primary)进行实时同步。
审计(audit):用户可以通过二进制日志中的信息来进行审计,判断是否有对数据库进行注入的攻击。
**在通常情况下,我们将参数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。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)
[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等信息,其余信息还是存放在默认的表空间中。
redo_log_type占用1字节,表示重做日志的类型
space表示表空间的ID,但采用压缩的方式,因此占用的空间可能小于4字节
page_no表示页的偏移量,同样采用压缩的方式
redo_log_body表示每个重做日志的数据部分,恢复时需要调用相应的函数进行解析
首先判断表中是否有非空的唯一索引(Unique NOT NULL),如果有,则该列即为主键。
如果不符合上述条件,InnoDB存储引擎自动创建一个6字节大小的指针。
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只能用于查看单个列为主键的情况,对于多列组成的键就显得无能为力了。
第一步:建表
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的页。
数据页(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)
若列的长度小于255字节,用1字节表示
若大于255个字节,用2字节表示
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值都不占用任何空间。
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)
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;
mysql> SET sql_mode='STRICT_TRANS_TABLES';
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默认的对更新前和更新后的表的命名吗?)
[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;
在MySQL数据库中,视图是一个命名的虚表,它由一个SQL查询来定义,可以当作表使用。
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)、定义内容、是否是可更新视图、字符集等。
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的物化视图功能。
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;
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'))
-> );
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分区得到的数据分布相比,各个分区间数据的分布可能不大均衡。
mysql> CREATE TABLE t_key(
-> a INT,
-> b DATETIME)ENGINE=INNODB
-> PARTITION BY KEY (b)
-> PARTITIONS 4;
在KEY分区中使用关键字LINEAR和在HASH分区中使用具有同样的效果,分区的编号是通过2的幂算法得到的,而不是通过模数算法。
所有的整型类型,如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)
);
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
-> )
-> );
子分区的建立需要注意以下几个问题:
子分区可以用于特别大的表,在多个磁盘间分别分配数据和索引。假设有6个磁盘,分别为/disk0、/disk1、/disk2等。现在考虑下面的例子:**(注意引擎是MyISAM!)**每个子分区的数量必须相同。
要在一个分区表的任何分区上使用SUBPARTITION来明确定义任何子分区,就必须定义所有的子分区。
每个SUBPARTITION子句必须包括子分区的一个名字。
子分区的名字必须是唯一的。
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语法,因此上述的分区表的数据和索引文件分开放置对其是无效的。**
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中观察到被移动的数据。
Leaf Page满 | Index Page满 | 操作 |
No | No | 直接将记录插入到叶子节点 |
Yes | No | 1. 拆分Leaf Page;2. 将中间的节点放入到Index Page中;3. 小于中间节点的记录放左边; 4. 大于或等于中间节点的记录放右边 |
Yes | Yes | 1. 拆分Leaf Page;2. 小于中间节点的记录放左边;3. 大于或等于中间节点的记录放右边;4.拆分Index Page;5. 小于中间节点的记录放左边;6. 大于中间节点的记录放右边;7. 中间节点放入上一层Index Page |
叶子节点小于填充因子 | 中间节点小于填充因子 | 操作 |
No | No | 直接将记录从叶子节点删除,如果该节点还是Index Page的节点,用该节点的右节点代替 |
Yes | No | 合并叶子结点和它的兄弟节点,同时更新Index Page |
Yes | Yes | 1. 合并叶子节点和它的兄弟节点 2. 更新Index Page 3. 合并Index Page和它的兄弟节点 |
mysql> EXPLAIN
-> SELECT * FROM Profile ORDER BY id LIMIT 10\G;
可以看到虽然使用ORDER BY对行记录进行排序,但是在实际过程中并没有进行所谓的filesort操作,而这就是因为聚集索引的特点。
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操作,这能使优化器和索引更好地为你工作。
首先创建一张新的临时表,表结构为通过命令ALTER TABLE新定义的结构。
然后把原表中数据导入到临时表。
接着删除原表。
最后把临时表重名为原来的表名。
可以发现,若用户对于一张大表进行索引的添加和删除操作,那么这会需要很长的时间。更关键的是,若有大量事务需要访问正在被修改的表,这意味着数据库服务不可用。而这对于Microsoft SQL Server或Oracle数据库的DBA来说,MySQL数据库的索引维护始终让他们感觉非常痛苦。
InnoDB存储引擎从InnoDB 1.0.x版本开始支持一种称为Fast Index Creation(快速索引创建)的索引创建方式——简称FIC。
对于辅助索引的创建,InnoDB存储引擎会对创建索引的表加上一个S锁。在创建的过程中,不需要重建表,因此速度较之前提高很多,并且数据库的可用性也得到了提高。删除辅助索引操作就更简单了,InnoDB存储引擎只需更新内部视图,并将辅助索引的空间标记为可用,同时删除MySQL数据库内部视图上对该表的索引定义即可。
由于FIC在索引的创建的过程中对表加上了S锁,因此在创建的过程中只能对该表进行读操作,若有大量的事务需要对目标表进行写操作,那么数据库的服务同样不可用。此外,FIC方式只限定于辅助索引,对于主键的创建和删除同样需要重建一张表。
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存储引擎的使用。
辅助索引的创建与删除
改变自增长值
添加或删除外键约束
列的重命名
通过新的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优化器不会选择正在创建中的索引。
表中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值。