高性能 MySQL 学习笔记
高性能MySQL读书笔记。。。
MySQL架构
并发控制
读写锁
在处理并发读或者写时,通过实现一个由两种类型的锁组成的锁系统来解决问题。
- 两种锁的类型为:
- 共享锁(读锁):是共享的,或者说是互补阻塞的。多个客户在同意时刻可以同事读取同一个资源。
- 排他锁(写锁):排他的,一个写锁会阻塞其他写锁和读锁。
- 锁粒度
2.1 表锁 是最基本的锁策略,开销最小的策略。
- 当一个用户获得写锁时,会阻塞其他用户对该表进行的所有读写操作。
- 读锁相互不阻塞。
2.2 行级锁 可以最大成都的支持并发处理(最大的锁开销).
事物
事物就是一组原子性的sql,或者说是一个独立的工作单元。
ACID的概念:
- 原子性
- 一致性
- 隔离性
- 持久性
隔离级别
在sql标准中定义了四种隔离级别
- READ UNCOMMITTED (未提交读) 事物中的修改,即使没有提交,对其他事物也都是可见的。事物可以读取未提交的数据,成为脏读。
- READ COMMITED(提交读) 大多数数据库系统的默认隔离级别都是提交读(但MySQL不是)。该隔离级别定义为:一个事物开始时,只能看见已经提交的事务所做的修改。这个级别有时候也叫做不可重复读。
- REPEATABLE READ (可重复读) 该级别保证在同一个事务中多次读取同样的记录结果是一直的。该级别不能解决幻读的问题。幻读就是在某个事务读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,就会产生幻行。
- SERIALIZABLE (可串行化) 是最高的隔离级别,通过强制事务穿行执行,避免了前面所说的幻读的问题。简单来说,该级别会在读取的每一行的数据上都加锁,所以可能导致大量的超时和锁征用的问题。
MySQL的存储引擎
默认引擎为InnoDB,一般没有特殊需求直接使用即可。
Schema与数据类型优化
选择优化的数据类型
- 更小的通常更好
- 简单就好
- 尽量避免null
MySQL schema设计中的陷阱
- 太多的列
- 太多的关联
- 全能的枚举
- 变相的枚举
- 非此发明的null
范式和反范式
范式的优缺点
优点:
- 范式的更新操作通常更快
- 有较少的重复数据,在修改时,只要修改更少的数据
- 表通常更小,更好的放在内存中,执行操作会更快
- 检索列表数据时更少需要DISTINCT或者GROUP BY语句
缺点
- 通常需要关联,多表关联的代价昂贵,也可能使得一些索引策略无效
反范式的优缺点
优点:
- 反范式很好的避免关联
- 能使用更有效的索引策略
缺点
- 造成较多的数据冗余
混用范式和反范式
完全的范式和反范式是不存在的,在实际操作中建议混用这两种策略,可能使用部分范式化的schema、缓存表、以及其他技巧。
缓存表和汇总表
- 用缓存表表示存储那些可以比较简单的从schema其他表获取(但是每次获取的数据比较慢)数据的表(逻辑上的沉余的数据)
- 用汇总表表示使用GROUP BY语句聚合数据的表(数据不是路逻辑上沉余的)
物化视图
实际上是预计计算并且存储在磁盘上的表,可以通过各种各样的策略刷新和更新
计数器表
如果应用在表中保存计算器,则在更新计数器时可能碰到并发问题,创建一个独立的表存储计数器通常是个好主意,这样可以使计数器表小且快,使用独立的表可以帮助避免查询缓存失效。
加快ALTER TABLE操作的数据
myql执行大部分修改表结构的操作方法是用一个新的结构创建一个空表,从旧表查询中所有数据插入新表,然后删除旧表
- 一般来说,ALTER TABLE操作将导致myql服务中断,对常见的场景,能使用的技巧只有两种:
- 先在一台不提供服务的机器上执行ALTER TABLE,然后和提供服务的主库进行切换
- 影子拷贝,意思是要用要求的表结构创建一张和源表无关的新表,然后通过重命名和删表操作交换两张表
不是所有的ALTER TABLE操作都会引起表重建,有两种方式:
只修改.frm文件
快速创建myisam索引
创建高性能的索引
索引基础
索引基础
B-Tree索引
如果没有特别说明,一般说的都是B-Tree索引,它使用B-Tree数据结构来存储数据。
- 该索引对如下类型的查询有效:
- 全值匹配(和索引中所有列进行匹配)
- 匹配最左前缀(只使用索引的第一列)
- 匹配范围值
- 匹配列前缀(匹配某一列的值的开头部分)
- 精确匹配某一列并范围匹配另外一列
- 只访问索引的查询(只需要访问索引,无需访问数据行)
- 该索引的一些限制
- 如果不是按照索引的最左列开始查找,则无法使用索引
- 不能跳过索引中的列
- 如果查询中有某个列的范围查询,则右边的所有列都无法使用索引优化查找
哈希索引
基于哈希表实现,只有精确匹配索引所有列才有效。 在mysql中只有Memory引擎显示支持哈希索引
- 哈希索引的限制
- 只包含哈希值和行指针,而不包含字段值
- 不是按照索引值的顺序存储的,无法用于排序
- 不支持部分索引列匹配查找
- 只支持等值比较查询
- 访问哈希索引的数据非常快,除非有很多哈希冲突
- 如果冲突多,一些索引维护操作的代价会很高
空间数据索引
MyISAM表支持空间索引,可以用作地理数据存储,和b-tree索引不同,这类索引无须前缀查询,空间索引会从所有维度来所有数据。
全文索引
查找的是文本中的关键词,而不是直接比较索引中的值
索引的优点
索引可以让服务器快速定位到表的指定位置,但这不是索引的唯一作用。
索引的三个优点
- 大大减少服务器需要扫描的数据量
- 帮助服务器避免排序和临时表
- 将随机io变为顺序io
高性能的索引策略
独立的列
如果查询中的列不是独立的,则mysql就不会使用索引
前缀索引和索引选择性
- 索引前缀:有时候索引很长的字符列,这会让索引变的大且慢。解决这个问题有两种方案。
- 使用哈希索引
- 通常可以索引开始的部分字符,这样可以大大减少索引空间,从而提高索引效率
- 索引选择性:不重复的索引值(也称之为基数)和数据表的记录总数的比值,索引的选择性越高则查询效率越高
多列索引
一个常见的错误就是,为每个列创建独立的索引
在多个列上建立索引大部分情况下并不能提高mysql的查询性能,mysql5.0之后的版本引入了一种“索引合并”的策略,一定程度上可以使用表上的多个单列索引来定位指定的行。
索引合并有时候是一种优化的结构,但实际上更多时候说明了表上的索引建的很糟糕:
- 当出现服务器对多个索引做香蕉操作时,(通常有多个and条件),通常意味着需要一个包含所有相关列的多列索引,而不是多个独立的单列索引。
- 当服务器需要对多个索引做联合操作时(通常有多个or条件),通常需要耗费大量cou和和内存上在算法的的缓存、排序和合并操作上。
- 优化器不会吧这些计算到“查询成本”中,优化器只关心随机页面读取。
选择合适的索引列顺序
正确的顺序依赖于使用该索引的查询,并且需要同事考虑如何更好的满足排序和分组的需要。
在一个多列的B-Tree索引中,索引列的顺序意味着索引首先按照从左到右进行排序,所以索引可以按照升序或者降序进行扫描,以满足精确符合列顺序的ORDER BY、GROUP BY和DISTINCT等子句的查询需求。
当不考虑排序和分组时,将选择性最高的列防在前面通常是很好的。然而性能不只是依赖于所有索引列的选择性,也和查询条件的具体值有关,也就是和值的分布有关。
聚簇索引
聚簇索引并不是一种单独的索引类型,而是一种数据存储方式,InnoDB的聚簇索引实际上在同一个结构中保存了B-Tree索引和数据行。
当表有聚簇索引时,数据行实际上存放在索引的叶子页中。InnoDB将通过主键聚集数据,如果没有定义主键,会选择一个唯一的非空索引来替代,如果没有这样的索引,会隐式定义一个主键来作为聚簇索引。
优点:
- 可以把相关数据保存在一起
- 数据访问更快
- 使用覆盖索引扫描的查询可以直接使用页节点中的主键值
缺点:
- 最大限度的提高了io密集型应用的性能,如果数据全部放在内存中,则访问的顺序就不重要了
- 插入速度严重依赖于插入顺序
- 更新聚簇索引列的代价很高
- 居于聚簇索引的表在插入新航,或者主键呗更新导致需要移动行的时候,可能面临”页分裂”的问题
- 可能导致全表扫描变慢,尤其行比较稀疏,或者由于页分裂导致数据存储不连续
- 二级索可能比想象的更大,因为二级索引的叶子节点包含了引用行的主键列。
- 二级索引访问需要两次索引查找,而不是一次
InnoDB MyISAM的数据分布对比
- MyISAM的数据分组非常简单,按照数据插入的顺序存储在磁盘上。
- InnoDB的数据分布使用非常不同的方式存储同样的数据,聚簇索引“就是”表,不需要独立的行存储
- 使用InnoDB时应该尽可能的按主键顺序插入数据,并且尽可能使用单调增加的聚簇键的值来插入新行
覆盖索引
如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为”覆盖索引”
如果查询只需要扫描索引而无须回表,能带来许多好处
- 索引条目通常远小鱼数据行的大小,所以如果只需要读取索引,那mysql就会极大的减少数据访问量
- 因为索引是按照列值顺序存储的,所以对于io密集型的范围查询会比随机从磁盘读取每一行数据的io要少的多
- 一些存储引擎(如MyISAM)在内存中只缓存索引数据,数据则依赖操作系统来缓存,因此访问数据需要一次系统调用
- 由于InnoDB的聚簇索引,覆盖索引对InnoDB表特别有用
使用索引扫描来做排序
MySQL有两种方式可以生成有序的结果,通过排序操作,或者按照索引顺序扫描;如果EXPLAIN出来的type列的值为“index”,则说明使用了索引扫描来做排序
MySQL可以使用同一个索引既满足排序又用于查找行,所以,如果可能,设计索引时应该尽可能的满足这两种任务
只有当索引列顺序和ORDER BY子句的顺序完全一致,并且所有列的排序方向都一样时,才能使用索引来对结构做排序。如果需要关联多表,则当只有ORDER BY子句引用的字段全部为第一个表时,才能使用。
压缩(前缀压缩)索引
MyISAM使用前缀压缩来减少索引的大小,从而让更多的索引可以防到内存中,在某些情况下可以极大提高性能。
查询性能优化
慢查询基础:优化数据访问
有效的分析方法:
- 确认应用是否存在检索大量超过需要的数据
- 确认服务器是否存在大量超过需要的数据行
是否向数据可请求了不需要的数据
- 查询不需要的记录
- 多表关联时返回全部列
- 总是取出全部列
- 重复查询相同的数据
MySQL是否扫描额外的记录
在确定查询只返回需要的数据后,接下来应该看看为了返回结果是否扫描了过多的数据
对于MySQL,简单的衡量查询开销的指标如下:
- 响应时间
- 扫描的行数
- 返回的行数
响应时间
响应时间=服务时间+排队时间
扫描的行数和返回的行数
理想情况下扫描的行数和返回的行数应该时相同的,但实际上基本不可能。扫描的行数对返回的行数的比率通常很小,一般在1:1和10:1之间。
扫描的行数和访问类型
在评估查询开销的时候,需要考虑一下从表中找到某一行的数据的成本。
在EXPLAIN语句中的type列反应了访问类型,如果查询没有办法找到合适的访问类型,那么最好的解决办法通常就是增加一个合适的索引
一般MySQL能够使用如下方式应用where条件,从好到坏依次为:
- 在索引中使用where条件来过滤不匹配的记录
- 使用索引覆盖扫描来返回记录,直接从索引中过滤不需要的记录并返回命中的结果。
- 从数据表中返回数据,然后过滤不满足条件的记录
重构查询方式
在优化有问题的查询时,目标应该是找到一个更优的方式获得实际需要的结果,而不是一定总是需要从MySQL获取一样的结果集。
一个复杂查询还是多个简单查询
设计查询的时候一个需要考虑的问题,是否需要将一个复杂的查询分成多个简单的查询。
在MySQL从设计上让连接和断开都很轻量,在返回一个小的查询结果方面很高效。
在其他条件都相同的时候,使用尽可能少的查询当然时好的,但是有时候,将一个大查询分解为多个小查询时很有必要的。在设计应用时,如果也过查询能够胜任时还写成多个独立的查询时不明智的。
切分查询
有时候我们需要对大查询分而治之,将大查询切分为小查询,每个查询功能完全一样,只完成一小部分,每次只返回一小部分查询结果
删除旧数据就是一个很好的例子,定期清除大量数据时如果用一个大的语句一次性完成的话,则可能需要一次锁住很多数据,占满整个失误日志,耗尽系统资源,阻塞很多小的但是重要的查询。
分解关联查询
对关联查询进行分解。简单的,可以对每一个表进行一次单表查询,然后将结果在应用程序中进行关联。
通过分解关联查询的方式有如下的好处:
- 让缓存的效率更高
- 将查询分解后,执行单个查询可以减少锁的竞争
- 查询本身效率也可能会有所提升
- 可以减少沉余记录的查询
- 相当于在应用中实现了哈希关联,而不是使用MySQL的嵌套循环关联
优化特定类型的查询
优化count()查询
有两种不同的作用:
- 统计某个列的数量
- 统计行数
简单的优化
count(*)全表非常快,可以来加速一些特定条件的count查询
比如查询所有id大于5的城市
select count(*) from city where id >5
通过反转可以将扫描行数减少到5行之内
select (select count(*) form city) - count(*) from city where id <=5
使用近似值
有些业务场景并不要求完全精确count值,此时可以用近似值来代替,EXPLAIN出来的优化器估算的行数就是一个不粗的近似值,执行explain并不需要真正的去执行查询
更复杂的优化
在mysql层面还能做的就只有覆盖索引扫描,如果还不够,就需要考虑修改应用的架构,可以增加汇总表,或者增加外部缓存系统
优化关联查询
- 确保on或者using字句中的列上有索引
- 确保任何的group by和order by中的表达式只设计到一个表中的列
- 当升级mysql时需要注意关联语法、运算符优先级其他可能会发生变化的地方
优化子查询
子查询尽可能使用关联查询代替
优化group by和distinct
使用索引优化是最有效的优化方法
在mysql中,当无法使用索引的时候,group by使用两种策略来完成:使用临时表或者文件排序来左分组
如果需要对关联查询左分组,并且是按照查找表中的某个列进行分组,那么通常采用查找表的标识列分组的效率会比其他列更好
优化group by with rollup
分组查询的一个变种就是要求mysql对返回的分组结果在做一次超级聚合,很多时候建议在应用中实现超级聚合
优化limit分页
优化分页查询的一个最简单的办法就是尽可能的使用索引覆盖扫描,而不是查询所有的列,然后根据需要左一次关联操作在返回所需的列,对于偏移量很大的时候,这样做的效率提升非常大
比如
select file_id,description from file order by title limit 50,5
最好改写成以下语句
select file_id,description
from file
inner join (
select file_id
from file
order by title
limit 50,5
)as lim using(file_id)
这里的延迟关联将会大大提升查询效率,它让mysql扫描尽可能少的页面,获取需要访问的记录了后在根据关联列在回到原表查询需要的所有列
优化sql_calc_found_rows
分页的时候,另一个常用的技巧时在limit语句中加上sql_calc_found_rows提示(hint),这样就可以获得去掉limit以后的满足条件的行数,因此可以作为分页的总数
优化union查询
mysql总是通过创建并填充临时表的方式来执行union查询,因此很多优化策略在union查询中都无法使用。
除非确实需要服务器消除重复的行,否则就一定要使用union all,如果没有all关键字,mysql会给临时表加上distinct选项,这回导致对整个临时表的数据做唯一性检查
使用用户自定义变量
在以下场景不能使用用户自定义变量
- 使用自定义变量的查询,无法使用查询缓存
- 不能在使用常量或者标识符的地方使用自定义变量,
- 用户自定义变量的生命周期是在一个连接中游戏哦啊,所以不能用他们来左连接中的通信
- 如果使用连接池或者使用持久化连接,自定义变脸可能让看起来毫无关系的代码发生交互
- 不能显示的声明自定义变量的类型
- mysql优化器在某些场景下可能会将这些变量优化掉
- 赋值的顺序和赋值的时间点不总是固定的
- 赋值符号:=的优先级非常低
- 使用未定义变量不会产生任何语法错误
注:
- 上述测试在ubuntu16.04 lts jdk1.8 spring boot 1.5.6.RELEASE中成功
- 上述文字皆为个人看法,如有错误或建议请及时联系我
上一篇: MySQL 默认事务隔离级别_sqlserver 事务隔离级别
下一篇: 四级数据库事务隔离