MySQL
MySQL组成
通常意义上来讲,MySQL是由三部分组成:
- MySQL client - 我们使用的mysql工具。在此工具中连接server,输入sql语句,输出查询结果。
- MySQL server - MySQL服务器端,用于编译sql语句 ,优化sql,生成执行计划,执行,使用CPU在内存中对结果进行计算(比如排序),对结果进行缓存,最终将结果传输给客户端。
- 数据存储引擎 - MySQL服务器是一个计算器,数据库真正的数据是存储于存储引擎中。MySQL服务器使用存储引擎存储,扫描,获取数据。常用的存储引擎有InnoDB, MyISAM, Memory。
借助《高性能MySQL》书中的一张图片,展示MySQL查询的工作流程:
存储引擎
最常用的存储引擎为InnoDB,MyISAM和Memory。还有其它引擎以及第三方提供的引擎。选择合适的。
90%的案例应该使用InnoDB。除非你需要特殊的功能而InnoDB无法满足。InnoDB是一个支持事务,行级锁,崩溃后可自行恢复的存储引擎。
MyISAM是MySQL最早使用的存储引擎,它不支持事务,使用表级锁,崩溃无法恢复。因为MyISAM可以压缩表,所以读取速度快。
Memory引擎要求数据都存放于内存中,只有表结构存放于硬盘,一旦重启,数据清空。它支持hash index,所以它的取速度极快。但它不支持Blob和Text。并且它要求column类型必须是定长,varchar会自动转为char。它也只支持表锁,所以并发写性能不好。
InnoDB存储结构
InnoDB数据存储结构由大到小分Tablespace, segment, extend, page(block), row。借用网上一张图片:
每一个table对应一个tablespace,在tablespace中,index,数据,trasaction信息又各自存放于不同的segment中。其中数据比较特殊,innoDB的数据是跟cluster index一起的,cluster index会在后面详述。
每一个segment又分64个连续page,每个page是固定大小16K。存储引擎是按叶读取的,它会把需要的内容的所在页一并load到内存中。一个page上可以存储的是index,也可以是row,还可以是其它信息。
从开发者角度看
编写SQL
会设计Schema,编写SQL语句,是一个后台程序员必须的技能。create database, create table, create index, create trigger, create event, grant, alter, insert, select, update, delete, join, union,subquery, 这些都是必须掌握的基础。
其中最复杂的部分还是index,join,union与subquery,这经常牵扯到查询性能。
事务
无论使用任何事务型数据库都应该了解事物的4大隔离级别: Serializable, Repeatible read, read commited, read uncommited.
事物的隔离是依靠锁来实现的。MySQL支持读锁,写锁,MVCC。MVCC称为多版本并发控制,使用快照和时间戳来维护同一时间一条数据所产生的多个版本。不同的事务可以读取不同的快照。
Seriablizable隔离级别需要MySQL严格使用读写锁,而后面三种级别则不使用读锁,而是用写锁+MVCC来实现。
临时表和内存表
临时表存在两类:
- mysql内部临时表。这种表是由系统产生,用于保存中间结果的临时表。这种表会使用memory存储引擎,但如果中间结果太多,超出限制,则会转为MyISAM存储。如果表中含有text,blob,也会转为MyISAM。
- 用户使用create temporary table创建的临时表,这种表可以使用任何存储引擎。且在用户连接断开的时候销毁。
以上两类临时表不要混淆。
用户还会使用Momery存储引擎创建内存表。内存表支持hash index,随机访问的速度占优,适合做缓存。内存表在服务器重启以后,数据清空。内存表不支持blob和text类型。
create table table_name (definition) engine=memory;
create table table_name (definition) engine=memory;
索引index
常用的MySQL存储引擎支持B+树索引和Hash索引。InnoDB和MyISAM支持B+树索引,而Memory引擎支持B+树和Hash索引。在创建index的时候,可以声明使用哪种算法:
create index ix1 on table_name(columns) using {BTREE|HASH}
create index ix1 on table_name(columns) using {BTREE|HASH}
B+树的算法建议大家还是去稍微了解一下,明白其原理。
如果罗列MySQL innoDB的索引种类的话,我会将其分为如下:
- Primary key, 系统会自动将primary key创建为cluster index(聚簇索引)。
- Foreign key.
- 唯一索引 Unique index
- 二级索引 Secondary index
- 组合索引或多列索引
- 覆盖索引
- 聚簇索引 cluster index
这有些混乱,让我们一个一个的看。
聚簇索引
首先看聚簇索引,它和其它索引不一样,聚簇索引本身就是一种数据存储方式。聚簇索引和数据在物理磁盘上是存放在一起,索引的叶子节点本身就是数据。这使得插入的数据会按照聚簇索引的顺序排列。一张表只能有一个聚簇索引。InnoDB会使用primary key做聚簇索引,如果没有primary key,则选择一个unique index做聚簇索引,如果没有unique index, 则自动生成row id,使用row id做聚簇索引。
如果primary key是连续增长的整数,那么插入的效率会高些,因为数据存放的物理位置永远是一个最末端的page。但写都集中在最末端的page,锁的激烈竞争导致写的并发性下降。
如果primary key是一个非连续的uuid的话,那么插入会导致随机IO,并可能会发生分裂page,产生碎片。在并发情况下,写的page比较分散,不会导致锁的激烈竞争。
由于聚簇索引的数据是按照索引在物理位置上排列的,创建好的聚簇索引,可以使相关的数据在物理上存放在一起,在查询的时候,连续几个page的读取便能获取所有相关的数据。比如在论坛里面,一个topic页面总是伴随着所有的评论。
二级索引
二级索引与聚簇索引相对。二级索引的叶子节点指向的并不是数据地址,而是数据所对应的聚簇索引的key。所以一个二级索引,默认的包含了primary key在最后面。使用二级索引查找数据,需要访问两次索引。
使用二级索引查找数据,首先从二级索引中得到叶子节点所指向的key,然后再通过聚簇索引找到key指向的数据。
组合索引和覆盖索引
组合索引和覆盖索引在语法上没有任何区别,它们都是在一个索引中包含了多个列。它们只是在使用意义上有所不同。
组合索引是为了where右边的条件所做的优化,可以快速的定位符合条件的数据。组合索引必须从索引的左边开始计算,如果左边缺失,或者左边是一个range,那么右边全部失效。(这句写的实在是烂,但懂得人自然懂。)
覆盖索引是为了消除回表扫描而将索引范围扩大到查询语句中所有涉及到的列。这样,查询语句中所有用到的列均可以在覆盖索引中得到,而不用再到表中获取。
优化查询性能
- 使用缓存。像Hibernate,MyBatis都支持二级缓存。MySQL也支持查询缓存。
- 创建合适的index。这是最常用的方法。
- 使用分区,后面会谈。
- 使用explain,查看执行计划,了解MySQL的执行计划可以帮助我们调整index,SQL结构来优化查询效率。
- 尽量定义长度短的列,没用的列不要出现在select中。size太大的列占用内存和传输带宽。SQL在执行过程中,会将中间结果存放在内存临时表中,如果中间结果占用的内存超过限制,会把临时表转存到硬盘上,操作效率会变低。
- 使用not null,用特殊值替代null。允许null值,会使得column后面需要额外的空间来控制是否为null。此外,null值无法使用索引。
- 尽量避免子查询。MySQL对子查询的优化非常糟。可以使用Join来代替子查询。
- in()比 or 的速度快,MySQL会对in中的值先排序,再访问,这样可以顺序IO访问。
- MySQL不会将外层条件推入Union中,在使用union时,可以在union里面使用充足的条件,来减小结果集。
- 对大偏移量的分页,可以使用延迟关联,避免对表中大量数据的扫描。
对以上的最后一点给一个例子:
create table test (
id int auto_increment,
name varchar(20) not null,
description varchar(255) not null,
primary key(id)
);
create index ix on test(name);
select * from test order by name limit 10000, 20;
create table test (
id int auto_increment,
name varchar(20) not null,
description varchar(255) not null,
primary key(id)
);
create index ix on test(name);
select * from test order by name limit 10000, 20;
MySQL优化器并没有聪明到像人脑,上面的查询会先将test按照name排序,从表中取出前10020名的数据,然后丢掉前10000名,返回后20名。因为description并不在ix的index中,所以需要回表扫面。使用延迟关联可以优化查询:
select t.* from test t
inner join (
select id from test order by name limit 10000, 20
) as s on t.id=s.id;
select t.* from test t
inner join (
select id from test order by name limit 10000, 20
) as s on t.id=s.id;
上面的子查询会先执行,子查询中使用二级索引ix,ix中包含name和id,所以不需要扫描原表,只需要再index上找出排名10000到10020之间的name的id。然后在使用这20个id与原表去join,只需要回表扫描20条数据就足够了。
Explain
Explain是一个后端开发人员最常用的查询优化工具。Explain可以给出MySQL优化器优化过后的执行计划,通过执行计划我们可以了解一条SQL在数据库服务器中是如何被执行的。MySQL中的多表查询都是使用NLP(嵌套循环查询),所以每一层嵌套,均是对一个table的查询。当使用explain工具时,MySQL会列出一张表,表中每一行均是对一个table的访问计划。而嵌套的顺序则可以通过id,select_type以及table这三列判断出来。
除去前三列,后面几个列则关系到查询的性能:
type | 表示查询表中的行时,使用什么访问方法: ALL, index, range, ref, eq_ref, const, NULL | ALL代表对整个表的扫描.这类访问应该避免. 当出现all时,需要添加index. |
possible_keys | 此访问可以使用到的潜在索引 | 潜在索引太多,会使优化器变慢. 优化器需要计算各种index带来的性能,然后选择最好的方案. |
key | 执行计划选定的索引 | |
key_len | 索引中key的字节数 | 过长的key会占内存. |
ref | 当type为ref, eq_ref或const的时候,ref里面才会有内容.ref是访问此表的时候,使用的条件值. | |
rows | MySQL找到结果数据估计需要读取的总行数, 数值越小, 代表NLP中循环的次数越小. | 此数目是根据数据的statistic计算出来的, 只能是一个估计值. 有时候与实际值差别很大. |
Extra | 额外信息会给出执行计划里面一些额外说明. Using index - 表明使用了覆盖索引 Using where - 表明存储引擎在检索时,给出的结果还需要MySQL服务器在内存中使用where来过滤. Using temporary - 表明对查询结果使用了临时表存储. Using filesort - 表明对结果集进行了排序.排序可能发生在内存里或文件中. Impossible - 类似的信息表明优化器提前发现查询语句不会返回任何结果,于是终结查询,直接返回empty. |
案例分析
以上面的两条limit查询语句为例。一条是未优化前的语句:
执行计划给出,查询的类型是ALL,会做全表扫描,然后使用排序。
而改进后的sql如下:
解释一下这段执行计划。最外层两个相互做join的表,一个为<derived2>,另一个为t。MySQL都是使用嵌套循环查询(NLP),<derived2>在前面,则<derived2>为循环最外层。
<derived2>是一张派生表,这张派生表是有id=2的子查询产生的,通过名字<derived2>可以判断的出。id=2的查询便是
#id=2
select id from test order by name limit 10000, 20;
#id=2
select id from test order by name limit 10000, 20;
这段子查询没有使用全表扫描,完全使用index扫描,索引ix上存有name和id的值,所以可以快速的拿到按照name顺序排列的id,而不用filesort。子查询会只返回20条数据。
执行计划中第一行对<derived2>中rows的描述不正确,这是MySQL的一个不足。<derived2>的rows数量应该为20.
使用NLP,最外层只需循环为20次,内层根据外层传下来的id,获取t中的数据行,可以通过primary key index快速定位,然后回表查询。
优化后的sql只需要回表查询20次即可。对比优化前的回表查询上万次,速度会有很多提升。
使用以下命令,可以得到MySQL优化引擎所产生的执行伪代码:
mysql> explain extended select...
mysql> show warnings;
warnings中会显示伪代码,上面优化后的查询伪代码是:
/* select#1 */
select `test`.`t`.`id` AS `id`,`test`.`t`.`name` AS `name`,`test`.`t`.`description` AS `description` from `test`.`test` `t`
join (/* select#2 */ select `test`.`test`.`id` AS `id` from `test`.`test` order by `test`.`test`.`name` limit 10000,20) `s`
where (`test`.`t`.`id` = `s`.`id`)
/* select#1 */
select `test`.`t`.`id` AS `id`,`test`.`t`.`name` AS `name`,`test`.`t`.`description` AS `description` from `test`.`test` `t`
join (/* select#2 */ select `test`.`test`.`id` AS `id` from `test`.`test` order by `test`.`test`.`name` limit 10000,20) `s`
where (`test`.`t`.`id` = `s`.`id`)
Trigger
Trigger是一个很常见的功能,众多数据库都支持trigger. MySQL的trigger有几个地方需要注意.
Trigger语法如下:
mysql>delimiter |
mysql>create trigger trigger_name before insert on table_name for each row
> begin
> <trigger body>
> end|
mysql>delimiter ;
mysql>delimiter |
mysql>create trigger trigger_name before insert on table_name for each row
> begin
> <trigger body>
> end|
mysql>delimiter ;
在trigger body中,既可以使用sql语句,也可以调用stored procedure, 还可以调用MySQL的UDF. 由于MySQL的UDF可以做任何事情, 这使得trigger可以调用MySQL以外的系统资源.
在MySQL主从复制的环境中,trigger的工作方式需要注意. Trigger在主从节点中均存在. 如果replication是statement based, 主服务器的变动出发了trigger, 从服务器也会触发trigger. 如果replication是row based, 则从服务器不会触发trigger. 但主服务器trigger更新的内容被被同步到从服务器.
一个面试题
我曾被人问及, 如果数据库中的数据发生了变化, 如何能实时的通知应用服务器. Oracle的JDBC中有一个feature,叫做Database Change Notification. 但MySQL却并没有这种功能. MySQL可以使用trigger来做到这一点.
第三方提供了很多MySQL的UDF, 比如mysql-udf-http, 可以直接在sql语句中调用其提供的方法,如 get, post等. 我们也可以自己开发UDF. 如果使用mysql-udf-http, 当trigger被触发时,我们可以调用post方法,向应用服务器发送一条更新记录. HTTP性能不好,还可以使用其它的udf,使mysql和应用服务器之间的逆向通信更快速.
Event
Event提供了scheduler的功能. MySQL可以创建一个一次性固定时间点的scheduler, 也可以创建一个周期性的scheduler. 周期可以是自己定义的, 也可以是MySQL提供的Hour, Day, Week, Month, Year等等等等.
event的语法类似于
create event e_name
on schedule every 1 day
do
begin
body
end;
create event e_name
on schedule every 1 day
do
begin
body
end;
body中既可以是简单的sql,也可以是复杂的stored procudure.
分区
分区是一个非常重要的优化方法. MySQL只支持水平分区,不支持垂直分区. 为什么分区? 当一个表里的数据量变得无比庞大的时候, index B-Tree的深度会过深, 导致读写的性能变的都非常差. 并且过多的数据存放在一个storage上, 过度的并发也使得storage成为瓶颈. 如果使用分区, 把相关的数据分在一个区, 则可以客服上面的困难.
分区和分片不同,分区是对单节点内的数据做查询优化,它不能增加单服务器的写容量。 为了增加MySQL的写容量,则需要使用分片(Sharding)。分片会在下一章详述。
举一个例子, 大众点评网上的店家数据, 可以按照城市进行分区. 因为点评上还从没见过跨城市综合查询或排序的. 再一个例子,比如logEvents, 我们可以按照时间段进行分区.
MySQL分区有4中方式:
- by range - 根据某一个字段的取值范围进行分区, 比如 createDate, 每年分为一个区.
- by list - 跟range类似, 根据某一个字段的取值list进行分区, 比如city in ('shanghai', 'hangzhou', 'nanjing')为一个长三角分区, city in ('beijing', 'tianjing', 'tanshan')为一个京津唐分区.
- by hash - 根据某一个字段的hash值进行分区, 这种分区方法可以使数据分布比较均匀.
- by key - 跟hash类似,根据某个地段进行分区. 此字段会被MySQL默认算法进行hash.
range分区方法还可以接受多个column进行分段.
range和list分区计算的时候只接受整形, 但如果在分区的时候, 声明columns, 它们则可以支持string, date, datetime.
每个range和list分区,还支持子分区, 子分区必须为hash或key分区方式.
每个分区就相当于一张表, 所有的分区合在一起则可以展现一张完整的表. 每个分区拥有自己的index和数据, index并不跨越所有分区. 在创建分区的时候,用户可以自己指定每个分区的数据和index所存储的位置.
例子:
create table test (id int, created date)
partition by range(YEAR(created)) (
partition p0 values less than (2000)
data directory = '/disk0/data'
index directory = '/disk0/index',
partition p1 less than MAXVALUE
data directory = '/disk1/data'
index directory = '/disk1/index'
);
create table test (id int, created date)
partition by range(YEAR(created)) (
partition p0 values less than (2000)
data directory = '/disk0/data'
index directory = '/disk0/index',
partition p1 less than MAXVALUE
data directory = '/disk1/data'
index directory = '/disk1/index'
);
分区减轻了IO的并发负担, 把大表分成小表, 优化了查询速度.
然而,当跨区排序的时候,则会是一种灾难. 所以要避免大数据量的跨区排序.
主从读写分离
借网上一张图,说明MySQL主从备份的实现方式.
上文先写到这里吧, 主从备份将在下文详述.