上一篇,后面留下两个问题,这里CHEN川 ^_^试着回答,让我们看看大佬的看法吧?
前言:
如果数据量非常大的情况下,根据业务选择了合适的字段,精心设计了表和索引,仔细检查了所有的SQL,确认没有什么问题了,但性能仍然不能满足要求,这该怎么办?接下来讨论一些常用的MySQL高级特性及背后的工作原理(注定是满满的干货),开始了:
正文:
分区表
合理使用索引可以提升mysql查询下性能,但如果单表数据量达到一定程度,索引也无法起到作用,因为数据量超大,除非覆盖索引,回表查询产生大量随机IO,库的响应时间可能会达到不可接受的程度,且索引维护(磁盘空间、IO操作待机大)
覆盖索引:
索引的叶子节点包含了要查询的数据,即索引包含或覆盖所有需要查询的字段的值,我们称这种索引为覆盖索引。【源】不错的博客,有例子,讲解也挺好的,这个索引主要是不用回表,减I/O;
因此单表数据量达到一定程度时(MySQL4.x时代,myisam业内公认的性能拐点是500W行,MySQL5.x时代性能拐点则为1KW~2KW行,具体情况具体测试),为提升性能,常用方法:分表;
分表策略可以是垂直拆分(不同订单状态的订单拆到不同表),水平拆分(按月将订单拆分到不同的表),总的来说,分表可以看作是从业务角度来解决大数据量问题,一定程度上提升性能,也提升编码复杂度(用过mycat的同学先稍安勿躁);
在业务层分表增加编码复杂程度,处理数据库的相关代码会大量散落在应用各处,维护困难;是否可以将分表的逻辑抽象出,同一处理,业务层专注业务即可,答案是可能的,目前非常多的数据库中间件都可以屏蔽分表后的细节,如果再将抽象的逻辑下移到数据库的服务层,就到了我们下面要说的分区表;
分区可以看作是从技术层面解决大数据问题的有效防范,简单理解:是mysql底层帮我们分表,分区表时一个独立的逻辑表,底层由多个物理子表组成,存储引擎管理分区的各个底层表和管理普通表一样(所有底层表必须使用相同的存储引擎),分区表的索引也是在各个底层表上各自加上一个完全相同的索引。从存储引擎的角度来看,底层表和普通表没有任何不同,存储引擎也无须知道。在执行查询时,优化器会根据分区的定义过滤那些没有我们需要数据的分区,这样查询就无需扫描所有分区,只需要查找包含需要数据的分区就可以了。
示例
一个订单表,数据量大概有10TB;因为数据量巨大,不能全表扫,使用索引,会发现数据不是按照想要的方式聚集,且产生大量碎片,导致查询产生大量的随机I/O,应用假死,so想要选择更粗粒度且消耗更少的方式来检索数据,如先根据索引找到一大块数据,然后再这块数据上顺序扫描。
这正是分区要做的事情,理解分区时还可以将其当作索引的最初形态,以代价非常小的方式定位到需要的数据在哪一片“区域”,在这片“区域”中,你可以顺序扫描,可以建索引,还可以将数据都缓存在内存中。因为分区无须额外的数据结构记录每个分区有哪些数据,所以其代价非常低。只需要一个简单的表达式就可以表达每个分区存放的是什么数据。
对表分区,可以在创建表时,使用如下语句
CREATE TABLE sales {
order_date DATETIME NOT NULL
-- other columns
} ENGINE=InnoDB PARTITION BY RANGE(YEAR(order_date)) (
PARTITION p_2014 VALUES LESS THAN (2014),
PARTITION p_2015 VALUES LESS THAN (2015)
PARTITION p_2016 VALUES LESS THAN (2016)
PARTITION p_2017 VALUES LESS THAN (2017)
PARTITION p_catchall VALUES LESS THAN MAXVALUE
)
分区子句中可以使用各种函数,但表达式的返回值必须是一个确定的整数,且不能是一个常数。MySQL还支持一些其他分区,比如键值、哈希、列表分区,但在生产环境中很少见到。在MySQL5.5以后可以使用RANGE COLUMNS类型分区,这样即使是基于时间分区,也无需再将其转化成一个整数。
接下来简单看下分区表上的各种操作逻辑:
SELECT
:当查询一个分区表时,分区层先打开并锁住所有的底层表,优化器先判断是否可以过滤部分分区,然后在调用对应的存储引擎接口访问各个分区的数据INSERT
:当插入一条记录时,分区层先打开并锁住所有的底层表,然后确定哪个分区接收这条记录,再将记录写入对应的底层表,DELETE
操作与其类似UPDATE
:当更新一条数据时,分区层先打开并锁住所有的底层表,然后确定数据对应的分区,然后取出数据并更新,再判断更新后的数据应该存放到哪个分区,最后对底层表进行写入操作,并对原数据所在的底层表进行删除操作
有些操作是支持条件过滤的。例如,当删除一条记录时,MySQL需要先找到这条记录,如果WHERE
条件恰好和分区表达式匹配,就可以将所有不包含这条记录的分区都过滤掉,这对UPDATE
语句同样有效。如果是INSERT
操作,本身就只命中一个分区,其他分区都会被过滤。
虽然每个操作都会 “先打开并锁住所有的底层表”,但这并不是说分区表在处理过程中是锁住全表的。如果存储引擎能够自己实现行级锁,例如InnoDB,则会在分区层释放对应表锁。这个加锁和解锁的操作过程与普通InnoDB上的查询类似。
在使用分区表时,为了保证大数据量的可扩展性,一般有两个策略:
- 全量扫描数据,不用索引。即只要能够根据WHERE条件将需要查询的数据限制在少数分区中,效率是不错的
- 索引数据,分离热点。如果数据有明显的“热点”,而且除了这部分数据,其他数据很少被访问到,那么可以将这部分热点数据单独存放在一个分区中,让这个分区的数据能够有机会都缓存在内存中。这样查询就可以值访问一个很小的分区表,能够使用索引,也能够有效的利用缓存。
分区表的优点是优化器可以根据分区函数来过滤一些分区,但很重要的一点是要在WHERE
条件中带入分区列,有时候即使看似多余的也要带上,这样就可以让优化器能够过滤掉无须访问的分区,如果没有这些条件,MySQL就需要让对应的存储引擎访问这个表的所有分区,如果表非常大的话,就可能会非常慢。
上面两个分区策略基于两个非常重要的前提:查询都能够过滤掉很多额外的分区、分区本身并不会带来很多额外的代价。而这两个前提在某些场景下是有问题的,比如:
1、NULL值会使分区过滤无效
假设按照PARTITION BY RANGE YEAR(order_date)
分区,那么所有order_date
为NULL或者非法值时,记录都会被存放到第一个分区。所以WHERE order_date BETWEEN '2017-05-01' AND ‘2017-05-31’
,这个查询会检查两个分区,而不是我们认为的2017年这个分区(会额外的检查第一个分区),是因为YEAR()
在接收非法值时会返回NULL。如果第一个分区的数据量非常大,而且使用全表扫描的策略时,代价会非常大。为了解决这个问题,我们可以创建一个无用的分区,比如:PARTITION p_null values less than (0)
。如果插入的数据都是有效的话,第一个分区就是空的。
在MySQL5.5以后就不需要这个技巧了,因为可以直接使用列本身而不是基于列的函数进行分区:
PARTITION BY RANGE COLUMNS(order_date)
。直接使用这个语法可避免这个问题。
2、分区列和索引列不匹配
可能会导致查询无法进行分区过滤,除非每个查询条件中都包含分区列。假设在列a上定义了索引,而在列b上进行分区。因为每个分区都有其独立的索引,所以在扫描列b上的索引就需要扫描每一个分区内对应的索引,当然这种速度不会太慢,但是能够跳过不匹配的分区肯定会更好。这个问题看起来很容易避免,但需要注意一种情况就是,关联查询。如果分区表是关联顺序的第2张表,并且关联使用的索引与分区条件并不匹配,那么关联时对第一张表中符合条件的每一行都需要访问并搜索第二张表的所有分区(关联查询原理,请参考前一篇文章)
3、选择分区的成本可能很高
分区有很多种类型,不同类型的分区实现方式也不同,所以它们的性能也不尽相同,尤其是范围分区,在确认这一行属于哪个分区时会扫描所有的分区定义,这样的线性扫描效率并不高,所以随着分区数的增长,成本会越来越高。特别是在批量插入数据时,由于每条记录在插入前,都需要确认其属于哪一个分区,如果分区数太大,会造成插入性能的急剧下降。因此有必要限制分区数量,但也不用太过担心,对于大多数系统,100个左右的分区是没有问题的。
4、打开并锁住所有底层表的成本在某些时候会很高
前面说过,打开并锁住所有底层表并不会对性能有太大的影响,但在某些情况下,比如只需要查询主键,那么锁住的成本相对于主键的查询来说,成本就略高。
5、维护分区的成本可能会很高
新增和删除分区的速度都很快,但是修改分区会造成数据的复制,这与ALTER TABLE
的原理类似,需要先创建一个历史分区,然后将数据复制到其中,最后删除原分区。因此,设计数据库时,考虑业务的增长需要,合理的创建分区表是一个非常好的习惯。在MySQL5.6以后的版本可以使用ALTER TABLE EXCHAGE PARTITION
语句来修改分区,其性能会有很大提升。
分区表还有一些其他限制,比如所有的底层表必须使用相同的存储引擎,某些存储引擎也不支持分区。分区一般应用于一台服务器上,但一台服务器的物理资源总是有限的,当数据达到这个极限时,即使分区,性能也可能会很低,所以这个时候分库是必须的。但不管是分区、分库还是分表,它们的思想都是一样的,大家可以好好体会下。
作者:CHEN川
链接:https://www.jianshu.com/p/01b9f028d9c7
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。