高性能mysql1-5章
mysql架构与历史
mysql会解析查询,并创建内部数据结构(解析树),然后对其进行各种优化,包括重写查询,觉得表的读取顺序,以及选择合适的索引等。
读写锁:也叫做共享锁和排它锁。读锁是共享的,写锁是互斥的,同一时间内只有一个线程可以占用写锁来写数据,其他线程会阻塞。
锁粒度越小则并发程度越高,因为锁很消耗资源。根据粒度锁可以分为表锁和行锁。
事务:ACID.原子性,一致性,隔离性,持久性。
隔离级别:
- 未提交读:事务中的数据修改未提交就能被别的事务可见。这种现象叫做脏读。
- 提交读/不可重复读:大部分数据库默认的隔离级别。一个事务从开始直到提交,所做的任何修改对别的事务都不可见,但是可以看见别的已提交的事务修改的数据,导致在本事务内部重复读取不一致。这种现象叫不可重复读。
- 可重复读:保证了同个事务内部重复读取某些记录是一致的。但是可能会发生读取了别的事务新增的新纪录。这种现象叫做幻读。mysql通过MVCC解决幻读。
- 序列化:事务串行执行,不会出现任何并发问题。
死锁:多个事务以不同的顺序锁定资源时,可能会发生死锁,或者多个事务同时锁定了同一个资源,也会死锁。
Innodb处理死锁的方式:将持有最少行级排它锁的事务进行回滚。
mysql采用autocommit,如果不设置的话默认每一个查询都是一个事务。
innodb显式加锁语句:
select ... lock in share mode;
select ... for update;
MVCC:多版本并发控制。通过MVCC实现了非阻塞的读操作,写操作也只要锁定必要的行。
- MVCC实现的机制是通过保存石距在某个时间点的快照。不管执行多久,事物内部看到的数据都是一致的,因为是快照。根据事务开始的时间不同,每个事物对于同样的记录可能看到数据是不一致的。
- innodb的MVCC是通过在每行记录后面保存两个隐藏的列实现的。这两个列是行的创建版本号和行的过期版本号。没开始一个事务,系统的版本号会自增。
- select语句只查找版本号早于当前事务的行,这样确保事务读取的行要么是在事务开始前就存在,要么是当前事务自己插入或者修改的;并且行的删除版本要么未定义,要么大于当前事务版本,确保事务读取的行在事务开始之前还没有被删除。
- insert语句为新插入每一行保存当前系统版本号
- delete语句为删除的每一行保存当前系统版本号
- update会插入一条新语句,保存当前系统版本号,并且将原有的行设置删除版本号。
Inoodb:采用MVCC支持高并发,默认隔离级别是可重复读。基于聚簇索引,对主键查询具有很高的性能。二级索引必须包含主键列。innodb做了很多优化,包括磁盘可预测与读取,并且能自动在内存中创建自适应哈希索引以及能加速插入操作的插入缓冲等。
MyISAM:不支持事务和行级锁。
服务器性能解析
捕获查询到日志文件中
show profile:查看语句的剖析结果和详细信息。
show status:返回各种计数器。
explain
使用慢查询日志
schema与数据类型优化
基本原则:通常情况下选择尽可能小的数据类型;尽量避免null
float和double是不精确的,decimal是精确的
varchar是可变长字符串,节省了存储空间,但是update时可能使行变得太长引起页分裂,会导致很多操作。innodb会把很长的varchar自动转换为blob类型
char是定长的,一般适合存储md5值
blob和text都是存储长文本的,但是blob是二进制方式,text是字符方式
datetime格式是YYYYMMDDHHMMSS,精度是秒,范围很大,是可排序的。
timestamp是unix时间戳,存储范围小,可以自动配置为当前时间,用于更新时间和插入时间。
通常都应该使用timestamp,效率高。
主键选择:
- 整数:效率高,可以使用auto_increment
- enum或者set:不考虑
- 字符串:消耗空间,比数字慢,有其是随机字符串,因为分布空间很大,insert会很慢,因为insert会随机写索引的不同位置,所以会慢,而且可能导致页分裂,磁盘的随机io和聚簇索引碎片等;select语句也可能变慢,因为逻辑上相邻的行在物理上不连续了,无法通过局部性原理预读取了。
mysql大部分修改表结构的操作都是用新的结构创建表,然后迁移数据,最后删除旧表,会很慢。
创建高性能的索引
索引的类型:
B-tree索引:MyISAM采用前缀压缩技术使得索引更小,innodb按照原数据格式存储索引。MyISAM通过数据的物理位置引用被索引的行,二innodb根据主键引用被索引的行。B-tree索引意味着数据都是按照顺序存储的。从索引的根节点开始搜索,通过比较节点页的值和待查找的值可以找到合适的指针进入下一层,叶子节点指向的是被索引的数据。由于B-tree索引是顺序组织的,所以非常适合范围查找。
索引对于多个值进行排序的依据是create table中定义索引的顺序一致。
B-tree索引适用于全键值、键值范围或者键前缀的查找。
- 全值匹配是指和索引中的所有列进行匹配
- 匹配最左前缀,这时只使用索引的第一列(多列复合索引的话)
- 匹配列前缀,匹配索引第一列的开头部分
- 匹配范围值,也只适用于第一个列
- 精确匹配某一列并范围匹配另一列,前面的列可以精确,紧跟着的那一列可以范围,中间不能断
索引还能用于order by,如果B-tree索引能根据这个索引查询,那么也能根据这个索引排序。
B-tree的限制:
- 如果不是从最左列开始则无效
- 不能跳过索引中的列
- 如果查询中有某个列的范围查询,那么这一列之后的所有列都不能用索引。
哈希索引:基于哈希表实现,只能通过精确匹配才能有效。
哈希索引的限制:
- 只包含hash值和行指针,并不实际存储值,所以多一次内存访问。
- 并不是按照索引值顺序的,所以无法用于排序。
- 也不支持部分索引列,因为hash函数是通过所有列的值来计算的。
- 只支持等值比较,不支持范围查询。
- 会出现hash冲突
innodb是自适应hash索引,当注意到某些索引值被适用的非常频繁时会自动在内存创建一个hash索引,用户无法干预,是自动适应的。
全文索引:查找的是文本中的关键字,类似于搜索引擎。
高性能的索引策略:
- 索引列不能使表达式的一部分,也不能是函数的参数。如where id + 1 = 5;
- 索引很长的字符串时,可以索引开始的部分字符,节约空间。需要选择合适的长度,保证选择性高同时空间节约。前缀索引的缺点是无法使用前缀索引做order by 和group by,也无法使用前缀索引做覆盖扫描(覆盖扫描是指select的数据列从索引中就能取得,就不用再去查全表,即查询列要被所使用的索引覆盖)
- 多列索引:
- 常见的错误是把每个where中使用的列都单独创建索引,新版本引入了索引合并,之前的话并不能使用到单列索引做多个索引上面的or或者and操作,要使用单列索引查询数据然后用union all来进行操作才能使用到单列索引。
- 当对多个索引列进行and操作时,意味着需要一个包含所有相关列的索引,
- 当对多个索引列or时,会耗费大量资源进行排序合并等
- 索引列顺序:首先从最左列开始匹配,然后依次往右。通常将选择性最高的列放最左
聚簇索引:并不是一种单独的索引类型,而是一种数据存储方式。innodb的聚簇索引实际上在同一个结构中保存了B-tree索引和数据行。
- 数据行实际存放在索引的叶子页中,一张表只有一个聚簇索引,一般都是主键,如果没有主键则会选择一个唯一非空索引,没有的话会自己隐式创建一个。
- 叶子页包含所有的数据,节点页只有索引列,作为指针比较。
- 聚簇索引的优点:
- 把相关数据保存在一起,比如使用用户id来聚集,则查询某个id下的数据则在相邻的数据页中,减少磁盘io次数
- 数据访问更快,因为索引和数据保存在一起,所以通过聚簇索引查询数据会少一次io
- 使用覆盖索引扫描的查询可以直接使用叶节点中的主键值
- 聚簇索引的缺点:
- 插入速度严重依赖插入顺序
- 更新聚簇索引列的代价很大,因为需要移动很多数据
- 插入数据时可能引起页分裂操作
- 可能导致全表扫描变慢,尤其是数据比较稀疏的情况下
- 二级索引因为必须包含主键列,所以会很大,因为二级索引叶节点保存的不是行的物理位置,而是主键值
innodb和myisam引擎的数据分布对比:
- myiasm按照行插入的顺序存储在磁盘上,按照行号排列。这种方式很适合创建索引,叶子节点的结构为主键值→行号的结构,根据主键值可以很快找到行号去磁盘中查找;非主键索引和主键索引其实一样,只不过结构式列值→行号,也是找到行号再去读磁盘。
- innodb支持聚簇索引,聚簇索引保存了所有的数据,不需要向myisam一样用独立的行存储,聚簇索引的每个叶子结点包含了主键值,事务id,MVCC和事务的回滚指针,以及所有剩余列的数据。
- 二级索引和聚簇索引不同,二级索引的叶节点保存的不是行号,而是主键值,这样可以减少行发生变动时的维护工作;但是这样会让二级索引占用空间增大。
避免使用随机的(不连续而且分布范围很大)聚簇索引,这样插入的效率会特别慢,而且索引占空间更大,原因是因为页分裂和碎片引起的。如果主键是随机的,新行的主键不一定比之前的大,所以无法把新行就插在最后,而是寻找一个合适的位置,通常是已有数据的中间,可能会导致页分裂。所以尽量使用自增连续主键。用随机主键的缺点:
- 写入的目标页可能已经不再内存中,则插入又要读磁盘,慢
- 写入是乱序的,所以会页分裂,导致大量移动数据,慢
- 频繁的页分裂也会导致页稀疏,并且会有碎片。
覆盖索引:如果一个索引包含索要查询的所有字段的值,则成为覆盖索引。
使用索引来排序:
- Mysql有两种方式可以生成有序的结果:通过排序操作;或者按索引顺序扫描。
- 扫描索引是很快的,只需要从一条索引记录移动到相邻的下一条记录。但是如果索引不能覆盖查询的所有列,则必须扫描一条索引然后回表查询一次对应的行,这就会慢,所以按索引顺序读取数据的速度会比全表顺序扫描慢。
- 只有当order by的列顺序和索引的列顺序完全一致,且所有列的排序方向都一样时,Mysql才能用索引来对结果进行排序。
- 如果查询需要关联多张表,则只有order by的列都是第一张表的时候,才能使用索引做排序。
- 对于多列索引,可以用第一列来查询,第二列来排序,但是必须最左,即中间不能隔一列。
冗余和重复索引:如果创建了索引(A,B),那么在创建索引(A)就是冗余,但是创建索引(B,A)就不是冗余。或者索引(B,id),由于主键本来就包含在二级索引中,所以这也是一种冗余。
索引和锁:
- innodb只有在访问行的时候才会加锁,而使用索引能够减少访问的行数。
- 如果不能使用索引查找和锁定行的话,mysql可能会做全表扫描并且锁住所有行。
索引案例学习:
- 假设一个用户信息表包括国家、地区、城市、性别、眼睛颜色等属性
- 首先需要看哪些列的取值很多,哪些列在where字句中出现的很频繁。国家的选择性不高,但是查询都会用的到,性别也是一样,所以考虑到使用的频率,应该把(sex,country)作为前缀。这样即使不查sex,也能使用技巧,在查询中新增 and sex in (’m’,’f’)来解决最左前缀原则。
- age一般要放在索引的最后一列,因为age一般都是进行范围查找,范围查找尽量放在右边,因为范围查找右边的列都不能索引了。
- limit往后越来越慢的原因:随着偏移量的增加,mysql需要花费大量的时间来扫描需要丢弃的数据。