深入理解MySQL——分区表

一、分区表的原理

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

SELECT查询

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

INSERT 操作

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

DELETE 操作

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

UPDATE 操作

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

有些操作是支持过滤的。例如,当删除一条记录时,MySQL 需要先找到这条记录,如果 WHERE条件恰好和分区表达式匹配,就可以将所有不包含这条记录的分区都过滤掉。这对UPDATE语句同样有效。如果是 INSERT操作,则本身就是只命中一个分区,其他分区都会被过滤掉。MySOL 先确定这条记录属于哪个分区,再将记录写入对应的底层分区表,无须对任何其他分区进行操作。

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

后面我们会通过一些例子来看看,当访问一个分区表的时候,打开和锁住所有底层表的代价及其带来的后果。

二、分区表的类型

MySQL 支持多种分区表。我们看到最多的是根据范围进行分区,每个分区存储落在某个范围的记录,分区表达式可以是列,也可以是包含列的表达式。例如,下表就可以将每一年的销售额存放在不同的分区里;

CREATE TABLE sales(
order_date DATETIME NOT NULL, --Other columns omitted
)ENGINE=InoDB PARTITION BY RANGE(YEAR(order_date))(
PARTITION p_2010 VALUES LESS THAN(2010), 
PARTITION p_2011 VALUES LESS THAN(2011), 
PARTITION p_2012 VALUES LESS THAN(2012),
PARTITION p_catchall VALUES LESS THAN MAXVALUE);

PARTITION分区子句中可以使用各种函数。但有一个要求,表达式返回的值要是一个确定的整数,且不能是一个常数。这里我们使用函数 YEAR(),也可以使用任何其他的函数,如 TO DAYS()。根据时间间隔进行分区,是一种很常见的分区方式,后面我们还会再回过头来看这个例子,看看如何优化这个例子来避免一些问题。

MySQL还支持键值、哈希和列表分区,这其中有些还支持子分区,不过我们在生产环境中很少见到。在 MySQL 5.5中,还可以使用RANGE COLUMNS类型的分区,这样即使是基于时间的分区也无须再将其转化成一个整数。

在我们看过的一个子分区的案例中,对一个类似于前面我们设计的按时间分区的InnoDB 表,系统通过子分区可降低索引的互斥访问的竞争。最近一年的分区的数据会被非常频繁地访问,这会导致大量的互斥量的竞争。使用哈希子分区可以将数据切成多个小片,大大降低互斥量的竟争问题。
我们还看到的一些其他的分区技术包括;

  • 根据键值进行分区,来减少InnoDB的互斥量竞争。
  • 使用数学模函数来进行分区,然后将数据轮询放入不同的分区。例如,可以对日期做模7的运算,或者更简单地使用返回周几的函数,如果只想保留最近几天的数据,这样分区很方便。
  • 假设表有一个自增的主键列id,希望根据时间将最近的热点数据集中存放。那么必须将时间戳包含在主键当中才行,而这和主键本身的意义相矛盾。这种情况下也可以使用这样的分区表达式来实现相同的目的; HASH(id DIV 1000000),这将为100 万数据建立一个分区。这样一方面实现了当初的分区目的,另一方面比起使用时间范围分区还避免了一个问题,就是当超过一定阈值时,如果使用时间范围分区就必须新增分区。

三、如何使用分区表

假设我们希望从一个非常大的表中查询出一段时间的记录,而这个表中包含了很多年的历史数据,数据是按照时间排序的,例如,希望查询最近几个月的数据,这大约有10亿条记录。可能过些年本书会过时,不过我们还是假设使用的是 2012年的硬件设备,而原表中有10TB的数据,这个数据量远大于内存,并且使用的是传统硬盘,不是闪存(多数 SSD也没有这么大的空间)。
你打算如何查询这个表?如何才能更高效?

首先很肯定;因为数据量巨大,肯定不能在每次查询的时候都扫描全表。考虑到索引在空间和维护上的消耗,也不希望使用索引。即使真的使用索引,你会发现数据并不是按照想要的方式聚集的,而且会有大量的碎片产生,最终会导致一个查询产生成千上万的随机 I/O,应用程序也随之僵死。情况好一点的时候,也许可以通过一两个索引解决一些问题。不过多数情况下,索引不会有任何作用。这时候只有两条路可选;让所有的查询都只在数据表上做顺序扫描,或者将数据表和索引全部都缓存在内存里。

这里需要再陈述一遍;在数据量超大的时候,B-Tree索引就无法起作用了。除非是索引覆盖查询,否则数据库服务器需要根据索引扫描的结果回表,查询所有符合条件的记录,如果数据量巨大,这将产生大量随机 I/O,随之,数据库的响应时间将大到不可接受的程度。另外,索引维护(磁盘空间、I/O操作)的代价也非常高。有些系统,如Infobright,意识到这一点,于是就完全放弃使用 B-Tree 索引,而选择了一些更粗粒度的但消耗更少的方式检索数据,例如在大量数据上只索引对应的一小块元数据。

这正是分区要做的事情。理解分区时还可以将其当作索引的最初形态。以代价非常小的方式定位到需要的数据在哪一片"区域"。在这片"区域"中,你可以做顺序扫描,可以建索引,还可以将数据都缓存到内存,等等。因为分区无须额外的数据结构记录每个分区有哪些数据-—分区不需要精确定位每条数据的位置,也就无须额外的数据结构——所以其代价非常低。只需要一个简单的表达式就可以表达每个分区存放的是什么数据。

为了保证大数据量的可扩展性,一般有下面两个策略∶

1. 全量扫描数据,不要任何索引

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

2. 索引 数据,并分离热点

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

你可能感兴趣的:(深入理解MySQL,MySQL,数据库,InnoDB,分区表,MySQL分区表)