MySQL高级特性一:分区表

对用户来说,分区表时一个独立的罗技表,但是底层由多个无力字表组成。实现分区的代码实际上是对一组底层表的句柄对象的封装。对分区表的请求,都会通过句柄对象转化成对存储引擎的接口调用。所以分区对于SQL层来说是一个完全封装底层实现的黑盒子,对应用是透明的,但是从底层的文件系统来看就很容易发现,每一个分区表都有一个使用#分隔明明的表文件。

MySQL实现分区表的方式;对底层表的封装,意味着索引也是按照分区的子表定义的,而没有全局索引。这和Oracle不同,在Oracle中可以更加灵活的定义索引和表是否进行分区。

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

在下面的场景中,分区可以起到非常大的作用:

① 表非常大以至于无法全部都放在内存中,或者只是在表的最后部分有热点数据,其他均是历史数据。

② 分区表的数据更容易维护。例如,想批量删除大量数据可以使用清楚整个分区的方式。另外,还可以对一个独立分区进行优化、检查、修复等操作。

③ 分区表的数据可以分布在不同的无力设备上,从而高效的利用多个硬件设备。

④ 可以使用分区表来避免某些特殊的瓶颈,例如InnoDB的单个索引的互斥访问、ext3文件系统的inode锁竞争等、

⑤ 如果需要,还可以备份和恢复独立的分区,这在非常大的数据集的场景下效果非常好。

MySQL的分区实现非常复杂,这里不打算介绍实现的全部细节,主要专注于分区性能方面,如果想了解更多关于分区的基础知识,建议阅读MySQL光放手册中关于分区一节,其中介绍了很多分区的相关基础知识。另外可以阅读create table、show create table、alter table和information_schema.partitions、explain关于分区部分的介绍。分区特性使得create table和alter table命令更复杂。

分区表本身也有一些限制,下面是其中比较重要的几点:

① 一个表最多只能有1024个分区。

② 在MySQL5.1中,分区表达式必须是整数,或者是返回整数的表达式。在MySQL5.5中,某些场景可以直接使用列来进行分区

③ 如果分区字段中有主见或者唯一索引的列,那么所有主见列和唯一索引列都必须包含进来。

④ 分区表中无法使用外键约束。

1 分区表的原理

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

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

① select

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

② insert

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

③ delete

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

④ update

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

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

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

2 分区表的类型

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

create table sales(
    order_date DATETIME not null,
    -- Other columns omitted
)ENGINE=InnoDB PARTITION BY RANGE(YEAR(order_date))(
    PARTITION P_2017 VALUES LESS THAN(2017),
    PARTITION P_2018 VALUES LESS THAN(2018),
    PARTITION P_2019 VALUES LESS THAN(2019),
    PARTITION P_catchall VALUES LESS THAN MAXVALUE
);

partition分区子句中可以使用各种函数,但是表达式返回的值要是一个确定的整数,且不能是一个常数。

根据时间间隔进行分区,是一种很常见的分区方式。

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

在上面的例子中,如果近一年的数据被非常频繁的访问,这会导致大量的互斥量的竞争。使用哈希子分区可以将数据切成多个小片,大大降低互斥量的竞争问题。

一些其他的分区技术:

① 根据键值进行分区,来减少InnoDB的互斥量竞争。

② 使用数学模函数来进行分区,然后将数据轮训放入不同的分区。例如,可以对日期做模7的运算,或者更简单的使用返回周几的函数,如果只想保留最近几天的数据,这样分区很方便。

③ 假设表有一个自增的主键列id,希望根据时间将最近的热点数据集中存放。那么必须将时间戳包含在主键当中才行,而这和主键本身的意义想矛盾。这种情况下也可以使用这样的分区表达式来实现相同的目的:HASH(id DIV 1000000),这将为100W数据创建一个分区。这样一方面实现了当初的分区目的,另一方面比起使用时间范围分区还避免了一个超过一定阈值时新建分区的问题。

3 如何使用分区表

假设我们希望从一个非常大的表中查询出一段时间的记录,而这个表中包含了很多年的历史数据,数据是按照时间怕徐的,例如,希望查询最近几个月的数据,这大约有10亿条,假设使用的是2012年的硬件设备,而原表中有10TB的数据,这个数据量原大于内存,并且使用的是传统硬盘,不是山村。你打算如何查询这个表?如何才能更高效?

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

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

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

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

① 全量扫描数据,不要任何索引:

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

② 索引数据,并分离热点

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

仅仅知道这些还不够,MySQL的分区表实现还有很多坑,下面看看如何避免。

4 什么情况下回出问题

① NULL值会使分区过滤无效

关于分区表一个容易让人误解的地方就是分区的表达式的值可以是NULL:第一个分区时一个特殊分区。假设按照partition by range year(order_date)分区,那么所有order_date为NULL或者是一个非法值的时候,记录都会被存放到第一个分区。现在假设有一下查询

select * from sales where order_date between '2017-01-01' and '2018-01-31'

实际上,MySQL会检查两个分区。价差第一个分区时因为year()函数在接收非法值的时候可能会返回NULL值,那么这个范围的值可能会返回NULL而被存放到第一个分区了。这一点对于其他很多函数例如to_days()也一样。

如果第一个分区非常大,特别是当使用全量扫描数据,不要任何索引的策略时,代价会非常大。而且扫描两个分区来查找列也不是我们使用分区表的初衷。为了避免这种情况,可以创建一个无用的第一个分区,例如,上面的例子可以使用paritition p_nulls values less than(0)来创建第一个分区。如果插入表中的数据都是有效的,那么第一个分区就是空的,这样即使需要检测第一个分区,代价也会非常小。

在MySQL5.5中就不需要这个优化技巧了,因为可以直接使用列本身而不是基于列的函数进行分区:pratition by range columns(order_date)。所以这个案例最好的解决方法就是直接使用列来进行分区。

② 分区列和索引列不匹配

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

听起来避免这个问题很简单,不过有时候也会遇到一些意向不到的问题。例如,在一个关联查询中,分区表在关联顺序中是第二个表,并且关联使用的索引和分区条件并不匹配。那么关联时指针对第一个表符合条件的每一行,都需要访问并搜索第二个表的所有分区。

③ 选择分区的成本可能很高

如前所述分区有很多类型,不同类型分区的实现方式也不同,所以他们的性能也各不相同。尤其是范围分区,对于回答这一行属于哪个分区、这些符合查询条件的行在哪些分区这样的问题的成本可能会非常高,因为服务器需要扫描所有的分区定义的列表来找到正确的答案。类似这样的线性搜索的效率不高,所以随着分区数的曾昭,成本会越来越高。

我们所实际碰到的最糟糕的一次问题是按行写入大量数据的时候。没写入一行数据到范围分区的表时,都需要扫描分区定义列表来找到合适的目标分区。可以通过限制分区的数量来缓解此问题,根据实践惊险,对大多数系统来说,100个左右的分区时没有问题的。其他的分区类型,比如键分区和哈希分区,则没有这样的问题。

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

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

⑤ 维护分区的成本可能很高

某些分区维护操作的速度会非常快,例如新增或者删除分区。而有些操作,例如充足分区或类似alter语句的操作;这类操作需要赋值数据。重组分区的原理与alter类似,先创建一个临时的分区,然后将数据赋值到其中,然后在删除原分区。

下面是目前分区实现中的一些其他限制:

① 所有分区都必须使用相同的存储引擎。

② 分区函数中可以使用的函数和表达式也有一些限制。

③ 某些存储引擎不支持分区。

④ 对于MyISAM的分区表,不能在使用load index into cache操作。

⑤ 对于MyISAM表,使用分区表时需要打开更多的文件描述符。虽然看其阿里是一个表,实际上背后有很多独立的分区,每一个分区对于存储引擎来说都是一个独立的表。这样即使分区表只占用一个表缓存条目,文件描述符还是需要多个。因此,即使已经配置了合适的表缓存,以确保不会超过操作系统的单个进程可以打开的文件描述符的个数,但对于分区表而言 ,还是会出现超过文件描述符限制的问题。

5 查询优化

引入分区给查询带来了一些新的思路和bug。分区最大的优点就是优化器可以根据分区函数来过滤一些分区。根据粗粒度索引的优势,通过分区过滤通常可以让查询扫描更少的数据。

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

使用explain partitions select * from sales \G

这个查询将访问所有的分区。下面我们给where条件中加入一个时间限制条件

MySQL优化器已经很善于过滤分区。比如它能够将范围条件转化为离散的值列表,并根据列表中的每个值过滤分区。然而,优化器也不是万能的。下面的查询的where条件理论上可以裸露分区,实际上不行

MySQL只能在使用分区函数的列本身进行比较时才能过滤分区,而不能够根据表达式的值去过滤分区,即使这个表达式就是分区函数也不行。这就和查询中使用独立的列才能使用索引的道理是一样的。所以只需要把上面的穿等价的改写为:

这里写的where条件中带入的是分区列,而不是基于分区列的表达式,所以优化器能够利用这个条件过滤部分分区。一个很重要的原则是;几遍在创建分区时可以使用表达式,但在查询时却只能根据列来过滤分区。

优化器在处理查询的过程中总是尽可能聪明的去过滤分区。例如,若分区表时关联操作中的第二张表,且关联条件时分区间,MySQL就只会在对应的分区里匹配行。explain无法显示这种情况下的分区过滤,因为这是运行时的分区过滤,而不是在查询优化阶段的。

6 合并表

合并表是一种早期的、简单的分区实现,和分区表相比有一些不同的限制,并且缺乏优化。分区表严哥来说是一个所及上的概念,用户无法访问底层的各个分区,对用户来说分区时透明的。但是合并表允许用户单独访问各个字表。分区表和优化器的结合更紧密,这也是未来发展的趋势,而合并表则是一种将被淘汰的技术,在未来版本中有可能被删除。

和分区表类似的是,在MyISAM中各个字表可以被一个结构完全相同的逻辑表锁封装。可以简单的把这个表当做一个老大、早期的、功能有限的分区表,因为它自身的特性,甚至可以提供一些分区表没有的功能。

合并表相当于一个容器,里面包含了多个真实表。可以在create table中使用一种特别的union语法来指定包含哪些真实表:

合并表还有很多其他的限制和行为,下面有几个重点需要记住:

① 在使用create语句创建一个合并表的时候,并不会检查各个字表的兼容性。入股字表的定义稍有不同,那么MySQL就可能创建出一个后面无法使用的合并表。另外,如果在成功创建了合并表后在修改某个字表的定义,那么之后再使用合并表可能会报错:

Unable to open underlying table which is differently defined or of non-MyISAM type or doesn`t exitst.

② 根据合并表的特性,不能发现,在合并表上无法使用replace语法,无法使用自增字段。

③ 如果一个查询访问合并表,那么它需要访问所有字表。这会让根据键查找单行的查询速度变慢,如果能够值访问一个对应表,速度肯定更快。所以,限制合并表中的字表数量很重要,特别是当合并表是某个关联查询的一部分的时候,因为这时候访问一个表的记录数可能会将比较操作传递到关联的其他表中,这时减少记录的访问就是减少整个关联操作。当你打算使用合并表时还要记住以下几点:

【1】执行范文查询时,需要在每一个字表上各执行一次,这比直接访问单个表的性能要差很多,而且子表月多,性能越差。

【2】全表扫描和普通表的全表扫描速度相同。

【3】在合并表上做唯一键和主键查询时,一旦找到一行数据就会停止。所以一旦查询在合并表的某一个子表中找到一行数据,就会立刻返回,不会再访问任何其他的表。

【4】子表的读取顺序和create table语句中的顺序相同。

猜你喜欢

转载自blog.csdn.net/yongqi_wang/article/details/86576006
今日推荐