目录
分区表的原理
分区表的类型
如何使用分区表
什么情况下会出问题
NULL值会使分区过滤无效
分区列和索引列不匹配
选择分区的成本可能很高
打开并锁住所有底层表的成本可能很高
维护分区的成本可能很高
查询优化
合并表
对用户来说,分区表是一个独立的逻辑表,但是底层由多个物理子表组成。实现分区的 代码实际上是对一组底层表的句柄对象(Handler Object)的封装。对分区表的请求,都 会通过句柄对象转化成对存储引擎的接口调用。所以分区对于SQL层来说是一个完全封 装底层实现的黑盒子,对应用是透明的,但是从底层的文件系统来看就很容易发现,每 一个分区表都有一个使用#分隔命名的表文件。
MySQL实现分区表的方式一对底层表的封装一一意味着索引也是按照分区的子表定 义的,而没有全局索引。这和Oracle不同,在Oracle中可以更加灵活地定义索引和表是 否进行分区。
MySQL在创建表时使用PARTITION BY子句定义每个分区存放的数据。在执行查询的时 候,优化器会根据分区定义过滤那些没有我们需要数据的分区,这样查询就无须扫描所 有分区一一只需要查找包含需要数据的分区就可以了。
分区的一个主要目的是将数据按照一个较粗的粒度分在不同的表中。这样做可以将相关 的数据存放在一起,另外,如果想一次批量删除整个分区的数据也会变得很方便。
在下面的场景中,分区可以起到非常大的作用:
表非常大以至于无法全部都放在内存中,或者只在表的最后部分有热点数据,其他 均是历史数据。
分区表的数据更容易维护。例如,想批量删除大量数据可以使用清除整个分区的方式。
另外,还可以对一个独立分区进行优化、检查、修复等操作。
分区表的数据可以分布在不同的物理设备上,从而高效地利用多个硬件设备。
可以使用分区表来避免某些特殊的瓶颈,例如InnoDB的单个索引的互斥访问、ext3文件系统的inode锁竞争等。
如果需要,还可以备份和恢复独立的分区,这在非常大的数据集的场景下效果非常好。
分区表本身也有一些限制,下面是其中比较重要的几点:
一个表最多只能有1024个分区。
在MySQL 5.1中,分区表达式必须是整数,或者是返回整数的表达式。在MySQL 5.5 中,某些场景中可以直接使用列来进行分区。
如果分区字段中有主键或者唯一索引的列,那么所有主键列和唯一索引列都必须包 含进来。
分区表中无法使用外键约束。
如前所述,分区表由多个相关的底层表实现,这些底层表也是由句柄对象(Handler object)表示,所以我们也可以直接访问各个分区。存储引擎管理分区的各个底层表和 管理普通表一样(所有的底层表都必须使用相同的存储引擎),分区表的索引只是在各 个底层表上各自加上一个完全相同的索引。从存储引擎的角度来看,底层表和一个普通 表没有任何不同,存储引擎也无须知道这是一个普通表还是一个分区表的一部分。
分区表上的操作按照下面的操作逻辑进行:
SELECT查询
当查询一个分区表的时候,分区层先打开并锁住所有的底层表,优化器先判断是否 可以过滤部分分区,然后再调用对应的存储引擎接口访问各个分区的数据。
INSERT操作
当写入一条记录时,分区层先打开并锁住所有的底层表,然后确定哪个分区接收这 条记录,再将记录写入对应底层表。
DELETE操作
当删除一条记录时,分区层先打开并锁住所有的底层表,然后确定数据对应的分区, 最后对相应底层表进行删除操作。
UPDATE操作
当更新一条记录时,分区层先打开并锁住所有的底层表,MySQL先确定需要更新 的记录在哪个分区,然后取出数据并更新,再判断更新后的数据应该放在哪个分区, 最后对底层表进行写入操作,并对原数据所在的底层表进行删除操作。
有些操作是支持过滤的。例如,当删除一条记录时,MySQL需要先找到这条记录,如 果WHERE条件恰好和分区表达式匹配,就可以将所有不包含这条记录的分区都过滤掉。 这对UPDATE语句同样有效。如果是INSERT操作,则本身就是只命中一个分区,其他分 区都会被过滤掉。MySQL先确定这条记录属于哪个分区,再将记录写人对应的底层分 区表,无须对任何其他分区进行操作。
虽然每个操作都会“先打开并锁住所有的底层表”,但这井不是说分区表在处理过程中 是锁住全表的。如果存储引擎能够自己实现行级锁,例如InnoDB, 则会在分区层释放 对应表锁。这个加锁和解锁过程与普通InnoDB上的查询类似。
MySQL支持多种分区表。我们看到最多的是根据范围进行分区,每个分区存储落在某 个范围的记录,分区表达式可以是列,也可以是包含列的表达式。例如,下表就可以将 每一年的销售额存放在不同的分区里:
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索引,而选择了一些更粗粒度 的但消耗更少的方式检索数据,例如在大量数据上只索引对应的一小块元数据。
这正是分区要做的事情。理解分区时还可以将其当作索引的最初形态,以代价非常小的 方式定位到需要的数据在哪一片”区域”。在这片”区域”中,你可以做顺序扫描,可以 建索引,还可以将数据都缓存到内存,等等。因为分区无须额外的数据结构记录每个分 区有哪些数据一分区不需要精确定位每条数据的位置,也就无须额外的数据结构—— 所以其代价非常低。只需要一个简单的表达式就可以表达每个分区存放的是什么数据。
为了保证大数据量的可扩展性,一般有下面两个策略:
全量扫描数据,不要任何索引。
可以使用简单的分区方式存放表,不要任何索引,根据分区的规则大致定位需要的 数据位置。只要能够使用WHERE条件,将需要的数据限制在少数分区中,则效率是 很高的。当然,也需要做一些简单的运算保证查询的响应时间能够满足需求。使用 该策略假设不用将数据完全放入到内存中,同时还假设需要的数据全都在磁盘上, 因为内存相对很小,数据很快会被挤出内存,所以缓存起不了任何作用。这个策略 适用于以正常的方式访问大量数据的时候。警告:后面我们会详细解释,必须将查 询需要扫描的分区个数限制在一个很小的数量。
索引数据,并分离热点。
如果数据有明显的“热点”,而且除了这部分数据,其他数据很少被访问到,那么可 以将这部分热点数据单独放在一个分区中,让这个分区的数据能够有机会都缓存在 内存中。这样查询就可以只访问一个很小的分区表,能够使用索引,也能够有效地 使用缓存。
仅仅知道这些还不够,MySQL的分区表实现还有很多陷阱。下面我们看看都有哪些,以及如何避免。
上面我们介绍的两个分区策略都基于两个非常重要的假设:查询都能够过滤(prunning) 掉很多额外的分区、分区本身并不会带来很多额外的代价。而事实证明,这两个假设在某些场景下会有问题。下面介绍一些可能会遇到的问题。
关于分区表一个容易让人误解的地方就是分区的表达式的值可以是NULL: 第一个 分区是一个特殊分区。
假设按照PARTITION BY RANGE YEAR(order_date)分区,那 么所有order_date为NULL或者是一个非法值的时候,记录都会被存放到第一个 分区。
现在假设有下面的查询: WHERE order_date BETWEEN'2012-01-01'AND '2012-01-31'。实际上,MySQL会检查两个分区,而不是之前猜想的一个:它会检 查2012年这个分区,同时它还会检查这个表的第一个分区。检查第一个分区是因为 YEAR()函数在接收非法值的时候可能会返回NULL值,那么这个范围的值可能会返 回NULL而被存放到第一个分区了。这一点对于其他很多函数,例如TO_DAYS()也一 样。
如果第一个分区非常大,特别是当使用“全量扫描数据,不要任何索引"的策略时, 代价会非常大。而且扫描两个分区来查找列也不是我们使用分区表的初衷。为了避 免这种情况,可以创建一个“无用”的第一个分区,例如,上面的例子中可以使用 PARTITION p_nulls VALUES LESS THAN (0)来创建第一个分区。如果插入表中的数 据都是有效的,那么第一个分区就是空的,这样即使需要检测第一个分区,代价也 会非常小。
在MySQL 5.5中就不需要这个优化技巧了,因为可以直接使用列本身而不是基于列 的函数进行分区: PARTITION BY RANGE COLUMNS(order_date)。所以这个案例最好 的解决方法是能够直接使用MySQL 5.5的这个语法。
如果定义的索引列和分区列不匹配,会导致查询无法进行分区过滤。假设在列a上 定义了索引,而在列b上进行分区。因为每个分区都有其独立的索引,所以扫描列 b上的索引就需要扫描每一个分区内对应的索引。如果每个分区内对应索引的非叶 子节点都在内存中,那么扫描的速度还可以接受,但如果能跳过某些分区索引当然会更好。要避免这个问题,应该避免建立和分区列不匹配的索引,除非查询中还同 时包含了可以过滤分区的条件。
听起来避免这个问题很简单,不过有时候也会遇到一些意想不到的间题。例如,在 一个关联查询中,分区表在关联顺序中是第二个表,并且关联使用的索引和分区条 件井不匹配。那么关联时针对第一个表符合条件的每一行,都需要访问并搜索第二 个表的所有分区。
如前所述分区有很多类型,不同类型分区的实现方式也不同,所以它们的性能也各 不相同。尤其是范围分区,对于回答“这一行属于哪个分区”、“这些符合查询条件 的行在哪些分区”这样的问题的成本可能会非常高,因为服务器需要扫描所有的分 区定义的列表来找到正确的答案。类似这样的线性搜索的效率不高,所以随着分区 数的增长,成本会越来越高。
我们所实际碰到的类似这样的最糟糕的一次问题是按行写入大量数据的时候。每写 入一行数据到范围分区的表时,都需要扫描分区定义列表来找到合适的目标分区。 可以通过限制分区的数量来缓解此问题,根据实践经验,对大多数系统来说,100 个左右的分区是没有问题的。
其他的分区类型,比如键分区和哈希分区,则没有这样的问题。
当查询访问分区表的时候,MySQL需要打开并锁住所有的底层表,这是分区表的另 一个开销。这个操作在分区过滤之前发生,所以无法通过分区过滤降低此开销,并 且该开销也和分区类型无关,会影响所有的查询。这一点对一些本身操作非常快的 查询,比如根据主键查找单行,会带来明显的额外开销。可以用批量操作的方式来 降低单个操作的此类开销,例如使用批量插入或者LOAD DATA INFILE、一次删除多 行数据,等等。当然同时还是需要限制分区的个数。
某些分区维护操作的速度会非常快,例如新增或者删除分区(当删除一个大分区可 能会很慢,不过这是另一回事)。而有些操作,例如重组分区或者类似ALTER语句的 操作:这类操作需要复制数据。重组分区的原理与ALTER类似,先创建一个临时的 分区,然后将数据复制到其中,最后再删除原分区。
如上所述,分区表不是什么“银弹”。
下面是目前分区实现中的一些其他限制:
所有分区都必须使用相同的存储引擎。
分区函数中可以使用的函数和表达式也有一些限制。
某些存储引擎不支持分区。
对于MyISAM的分区表,不能再使用LOAD INDEX INTO CACHE操作。
对于MyISAM表,使用分区表时需要打开更多的文件描述符。虽然看起来是一个表, 其实背后有很多独立的分区,每一个分区对于存储引擎来说都是一个独立的表。这 样即使分区表只占用一个表缓存条目,文件描述符还是需要多个。因此,即使已经 配置了合适的表缓存,以确保不会超过操作系统的单个进程可以打开的文件描述符 的个数,但对于分区表而言,还是会出现超过文件描述符限制的问题。
最后,需要指出的是较老版本的MySQL问题会更多些。所有的软件都是有bug的。分区表在MySQL 5.1中引入,在后面的5.1.40和5.1.50之后修复了很多分区表的bug。在 MySQL 5.5中,分区表又做了很多改进,这才使得分区表可以逐步考虑用在生产环境了。 在即将发布的MySQL 5.6版本中,分区表做了更多的增强,例如新引入的ALTER TABLE EXCHANGE PARTITION。
引入分区给查询优化带来了一些新的思路(同时也带来新的bug)。分区最大的优点就是 优化器可以根据分区函数来过滤一些分区。根据粗粒度索引的优势,通过分区过滤通常 可以让查询扫描更少的数据(在某些场景下)。
所以,对于访问分区表来说,很重要的一点是要在WHERE条件中带人分区列,有时候即 使看似多余的也要带上,这样就可以让优化器能够过滤掉无须访问的分区。如果没有这 些条件,MySQL就需要让对应存储引擎访问这个表的所有分区,如果表非常大的话, 就可能会非常慢。
使用EXPLAIN PARTITION可以观察优化器是否执行了分区过滤,下面是一个示例:
正如你所看到的,这个查询将访问所有的分区。下面我们在WHERE条件中再加入一个时 间限制条件:
MySQL优化器已经很善于过滤分区。比如它能够将范围条件转化为离散的值列表,井 根据列表中的每个值过滤分区。然而,优化器也不是万能的。下面查询的WHERE条件理 论上可以过滤分区,但实际上却不行:
MySQL只能在使用分区函数的列本身进行比较时才能过滤分区,而不能根据表达式的 值去过滤分区,即使这个表达式就是分区函数也不行。这就和查询中使用独立的列才能 使用索引的道理是一样的。所以只需要把上面的查询等价地 改写为如下形式即可:
这里写的WHERE条件中带入的是分区列,而不是基于分区列的表达式,所以优化器能够 利用这个条件过滤部分分区。一个很重要的原则是:即便在创建分区时可以使用表达式, 但在查询时却只能根据列来过滤分区。
优化器在处理查询的过程中总是尽可能聪明地去过滤分区。例如,若分区表是关联操作 中的第二张表,且关联条件是分区键,MySQL就只会在对应的分区里匹配行。(EXPLAIN 无法显示这种情况下的分区过滤,因为这是运行时的分区过滤,而不是查询优化阶段 的。)
合并表(Merge tahle)是一种早期的、简单的分区实现,和分区表相比有一些不同的限 制,并且缺乏优化。分区表严格来说是一个逻辑上的概念,用户无法访问底层的各个分区, 对用户来说分区是透明的。但是合并表允许用户单独访问各个子表。分区表和优化器的 结合更紧密,这也是未来发展的趋势,而合并表则是一种将被淘汰的技术,在未来的版 本中可能被删除。
和分区表类似的是,在MyISAM中各个子表可以被一个结构完全相同的逻辑表所封装。
可以简单地把这个表当作一个“老的、早期的、功能有限的"的分区表,因为它自身的特性,甚至可以提供一些分区表没有的功能。
合井表相当于一个容器,里面包含了多个真实表。可以在CREATE TABLE 中使用一种特别 的UNION语法来指定包含哪些真实表。下面是一个创建合并表的例子:
注意到,这里最后建立的合并表和前面的各个真实表字段完全相同,在合并表中有的 索引各个真实子表也有,这是创建合并表的前提条件。另外还注意到,各个子表在对 应列上都有主键限制,但是最终的合并表中仍然出现了重复值,这是合并表的另一个 不足:合并表中的每一个子表行为和表定义都是相同,但是合并表在全局上并不受这 些条件限制。
这里的语法INSERT_METHOD=LAST告诉MySQL, 将所有的INSERT语句都发送给最后一 个表。指定FIRST或者LAST关键字是唯一可以控制行插入到合井表的哪一个子表的方式 (当然,还是可以直接在SQL中明确地操作任何一个子表)。而分区表则有更多的方式可 以控制数据写入到哪一个子表中。
INSERT语句的执行结果可以在最终的合并表中看到,也可以在对应的子表中看到:
合并表还有些有趣的限制和特性,例如,在删除合并表或者删除一个子表的时候会怎样? 荨删除一个合并表,它的子表不会受任何影响,而如果直接删除其中一个子表则可能会有 不同的后果,这要视操作系统而定。例如在GNU/Linux上,如果子表的文件描述还是被 打开的状态,那么这个表还存在,但是只能通过合并表才能访问到:
合并表还有很多其他的限制和行为,下面列举的这几点需要在使用的时候时刻记住。
在使用CREATE语句创建一个合并表的时候,并不会检查各个子表的兼容性。如果子 表的定义稍有不同,那么MySQL就可能创建出一个后面无法使用的合并表。另外, 如果在成功创建了合并表后再修改某个子表的定义,那么之后再使用合并表可能会 看到这样的报错: ERROR 1168 (HYOOO): Unable to open underlying table which is differently defined or ofnon-MyISAM type or doesn't exist。
根据合并表的特性,不难发现,在合并表上无法使用REPLACE语法,无法使用自增 字段。更多的细节请参阅MySQL官方手册。
如果一个查询访问合并表,那么它需要访问所有子表。这会让根据键查找单行的查 询速度变慢,如果能够只访问一个对应表,速度肯定将更快。所以,限制合并表中 的子表数量很重要,特别是当合井表是某个关联查询的一部分的时候,因为这时访 问一个表的记录数可能会将比较操作传递到关联的其他表中,这时减少记录的访问 就是减少整个关联操作。当你打算使用合并表的时候,还需要记住以下几点:
执行范围查询时,需要在每一个子表上各执行一次,这比直接访问单个表的性 能要差很多,而且子表越多,性能越糟。
全表扫描和普通表的全表扫描速度相同。
在合并表上做唯一键和主键查询时,一且找到一行数据就会停止。所以一且查 询在合并表的某一个子表中找到一行数据,就会立刻返回,不会再访问任何其 他的表。
子表的读取顺序和CREATE TABLE语句中的顺序相同。如果需要频繁地按照某个 特定顺序访问表,那么可以通过这个特性来让合并排序操作更高效。
因为合并表的各个子表可以直接被访问,所以它还具有一些MySQL 5.5分区所不能提供的特性:
一个MyISAM表可以是多个合并表的子表。
可以通过直接复制,f rm,MYl,MYD文件,来实现在不同的服务器之间复制各个子表。
在合并表中可以很容易地添加新的子表:直接修改合并表的定义就可以了。
可以创建一个合并表,让它只包含需要的数据,例如只包含某个时间段的数据,而 在分区表中是做不到这一点的。
如果想对某个子表做备份、恢复、修改、修复或者别的操作时,可以先将其从合并 表中删除,操作结束后再将其加回去。
可以使用myisampack来压缩所有的子表。
相反,分区表的子表都是被MySQL隐藏的,只能通过分区表去访问子表。