MySQL分区

MySQL分区

MySQL在创建表时使用PARTITION BY子句定义每个分区存放的数据。在执行查询的时候,优化器会根据分区定义过滤那些没有我们需要数据的分区,这样只需要查找包含需要数据的分区就可以了。

分区的一个主要目的就是将数据按照一个较粗的粒度分在不同的表中,这样做可以将相关的数据存放在一起。另外,如果想一次批量删除整个分区的数据也会变得很方便。

对用户来说,分区表是一个独立的逻辑表,但是底层由多个物理子表组成。实现分区的代码实际上是对一组底层表的句柄对象(Handler Object)的封装,对分区表的请求,都会通过句柄对象转化为对存储引擎的接口调用,所以分区对于SQL层来说是一个完全封装底层实现的黑盒子,对应用是透明的,但是从底层的文件系统来看就很容易防线,每一个分区表都有一个使用#分隔命名的表文件。这意味着索引也是按照分区的字表定义的,而没有全局索引。

原理

分区表由多个相关的底层表实现,这些底层表也是由句柄对象表示,所以我们也可以直接访问各个分区。存储引擎管理分区的各个底层表和管理普通表一样(所有底层表都必须使用相同的存储引擎),分区表的索引只是在各个底层表上各自加上一个完全相同的索引。从存储引擎的角度上看,底层表和一个普通表没有任何不同,存储引擎也无须知道这是一个普通表还是一个分区表的一部分。

分区表上的操作按照下面的操作逻辑进行:

SELECT查询

当查询一个分区表的时候,分区层先打开并锁住所有的底层表,优化器先判断是否可以过滤部分分区,然后再调用对应的存储迎亲接口访问各个分区的数据。

INSERT操作

当写入一条记录时,分区层先打开并锁住所有的底层表。然后确定哪个分区接受这条记录,再将记录写入对应底层表。

DELETE操作

当删除一条记录时,分区层先打开并锁住所有的底层表,然后确定数据对应的分区,最后对相应底层表进行删除操作。

UPDATE操作

当更新一条记录时,分区层先打开并锁住所有的底层表,MySQL先确定需要更新的记录在哪个分区,然后取出数据并更新,并判断更新后的数据应该放在哪个分区,最后对底层表进行写入操作,并对元数据所在的底层表进行删除操作。

虽然每个操作都会“先打开并锁住所有的底层表”,但这并不是说分区表在处理过程中是锁住全表的。如果存储引擎能够自己实现行级锁,例如InnoDB,则会在分区层释放对应表锁。

类型

MySQL支持多种分区表。范围、键值、哈希和列表分区

限制

  • 一个表最多只能由1024个分区
  • 在MySQL5.1中,分区表达式必须是整数,或者是返回整数的表达式。在MySQL5.5中,某些场景中可以直接使用列来进行分区
  • 如果分区字段中有主见或者唯一索引的列,那么所有主键列和唯一索引列都必须包含进来
  • 分区表中无法使用外键约束
  • 所有的分区必须使用相同的存储引擎
  • 分区函数中可以使用的函数和表达式也有一些限制
  • 某些存储引擎不支持分区
  • 对于MyISAM的分区表,不能再使用LOAD INDEX INTO CACHE操作
  • 对于MyISAM表,使用分区表时需要打开更多的文件描述符。虽然看起来是一个表,起始背后有很多独立的分区,每一个分区对于存储引擎来书都是一个独立的表。这样即使分区表只占用一个表缓存条目,文件描述符还是需要多个。因此,即使已经配置了合适的表缓存,以确保不会超过操作系统的单个进程可以打开的文件描述符的个数,对于分区表而言,还是会出现超过文件描述符限制的问题。

如何使用

全量扫描数据,不要使用任何索引

可以使用简单的分区方式存放表,不要任何索引,根据分区的规则大致定位需要的数据位置。只要能够使用WHERE条件,将需要的数据限制在少数分区中,则效率是很高的。当然,也需要做一些简单的运算保证查询的响应时间能够满足需求。使用该策略假设不用将数据完全放入到内存中,同时还假设需要的额数据全部在磁盘上,因为内存相对很小,数据很快会被挤出内存,所以缓存起不了任何作用。这个策略适用于以正常的方式访问大量数据的时候。

但必须将查询需要扫描的分区个数限制在一个很小的数量。

索引数据,并分离热点

如果数据有明显的“热点”,而且除了这部分数据,其他数据很少被放文档,那么可以将这部分热点数据单独放在一个分区中,让这个分区的数据能够有机会都缓存在内存中。这样查询就可以只访问一个很小的分区表,能够使用索引,也能够有效地使用缓存。

可能会出现哪些问题

NULL值会使分区过滤无效

分区表达式的值可以是NULL:第一个分区是一个特殊分区。假设按照PARTITION BY RANGE YEAR(order_date)分区,那么所有order_date为NULL或者为非法制的时候,记录都会被放到第一个分区。因此,如果查询2020年分区的记录,MySQL实际会扫描2个分区,而不是仅仅2020这个分区。检查第一个分区是因为YEAR函数在接受非法值的时候可能会返回NULL值,那么这个访问的值可能会返回NULL而被存放到第一个分区了。

为了避免这种情况,可以创建一个“无用”的第一个分区,例如,上面的例子中可以使用PARTITION p_nulls VALUES LESS THAN (0)来创建第一个分区。如果插入表中的数据都是有效的,那么第一个分区就是空的,这样即使需要检测第一个分区,代价也会非常小。

分区列和索引列不匹配

如果定义的索引列和分区列不匹配,会导致查询无法进行分区过滤。假设在列a上定义了索引,而在列b上进行分区。因为每个分区都有其独立的索引,所以扫描列b上的索引就需要扫描每一个分区内对应的索引。如果每个分区内对应索引的非叶子节点都在内存中,那么扫描速度还可以解说,但如果能跳过某些分区索引当然会更好,要避免这个问题,应该避免建立和分区列不匹配的索引,除非查询中还同时包含了可以过滤分区的条件。

选择分区的成本可能很高

对于范围分区,回答“这一行属于哪个分区”、“这些符合查询条件的行在那些分区”这样的问题的成本可能会非常高,因为服务器需要扫描所有的分区定义的列表来找到正确的答案。类似这样的线性搜索的效率并不高,所以随着分区数的增长,成本会越来越高。

根据实践经验,对大多数系统来说,100个左右的分区是没有问题的。

其他类型的分区,比如键分区和哈希分区,则没有这样的问题。

打开并锁住所有底层表的成本可能很高

当查询访问分区表的时候,MySQL需要打开并锁住所有的底层表,这时分区表的另一个开销。这个操作在分区过滤之前发生,所以无法通过分区过滤降低次开销,并且该开销也和分区类型无关,会影响所有的查询。这一点对一些本身操作非常快的查询,比如根据主键查找单行,会带来明显的额外开销。可以用批量操作的方式来降低单个操作的此类开销,例如使用批量插入或者LOAD DATA INFILE、一次删除多行数据等等。当然同时还是需要限制分区的个数。

维护分区的成本可能很高

某些分区维护操作的速度会非常快,例如新增或者删除分区(当删除一个大分区可能会很慢,不过这是另一回事)。而有些操作,例如重组分区或者类似ALTER语句的操作:这类操作需要复制数据。重组分区的原理和ALTER类似,县创建一个临时的分区,然后将数据复制到其中,最后在删除原分区。

查询优化

对于访问分区表来说,很重要的一点是要在WHERE条件中带入分区列,有时候即使看似多余也要带上,这样就可以让优化器能够过滤到无需访问的分区,如果没有这些条件,MySQL就需要让对应存储引擎访问这个表的所有分区,如果表非常大的话,就可能会非常慢。

参考资料

高性能MySQL

0%