MYSQL中的分区维护
PARTITION Maintenance in MySQL by Rick James
摘要
- 使用分区的适用条件
- 如何维护一个按时间序列分区表
- 自增长字段的秘诀
首先,对分区的建议
取自 Rick’s RoTs - Rules of Thumb for MySQL (需要翻墙)
- 在你不清楚分区为为什么以及如何提高性能之前,请不要使用。
- 如果你的行数不打于100w行,不要使用分区
- 不要让一个表的分区大于50个(打开/显示表状态等操作耗时,5.6.6修复了这个问题?5.7才更好的修复了这个问题)
- 按RANG分区是唯一有用的分区方法
- SUBPARTITIONs(子分区)是无用的
- 分区的字段不能是任何主键的第一个字段
AUTO_INCREMENT 字段可以作为混合主键的第一部分,或者non-UNIQUE索引的一部分
使用分区解决性能问题是如此的诱人,但是很多情况下实事并非如此.
PARTITIONing 拆分一个表中的数据到若干个小的分区表中.但是引起性能问题的原因不是表的大小通常,而是I/O时间和索引.
一个经常的错误认识:“分区将使得许多查询更快”.我不这么认为.仔细想一下,精确的查找需要消耗什么?
没有分区,但是有合适的索引的情况下,BTree索引可以直接命被中查找的数据行.对一百万行数据来说,可能只需要5层深度的BTree.
使用分区,第一步分区选择命中然后打开,然后一个小的BTree(4层树深)直接命中.好吧,更浅的树的代价是需要打开分区.
类似的,如果你查看需要访问的缓存磁盘块,你会意识到两种情况几乎访问了相同数据量的磁盘快.
总所周知磁盘稍描是一个查询的主要消耗,因此分区不能获取任何性能改善(至少在这个中情况下).
下面的二维情况,会得出来的一个矛盾的结论.
分区的适用场景
Case#1 – 时间序列
或许分区在数据库中最通常有效的适用场景就是需要周期性的从表中删除‘旧’的数据.
按天分区(或者其他uint格式的时间)可以让你用瞬间删除或者添加分区来替代慢的多的DELETE,这片blog主要讨论这种情况.
这种适用场景也在Big DELETEs讨论过
对于这个场景DROP PARTITION比DELETEing 大量数据块的多的多
Case#2 – 二维索引
索引一半来说都是一维的,如果你在你的where条件中需要两个范围,请尝试将其中一个迁移到PARTITIONing中
查找地图最近的10个匹萨店就需要一个2维的索引,排序修建过的分区可以作为第二个维度.参见Latitude/Longitude Indexing
上面的例子使用了PARTITION BY RANGE(latitude)和 PRIMARY KEY(longitude, …)
对于这个场景将大大减少稍描的行数
Case#3 – 热点数据
这个场景解释起来有点复杂,设想下下面几种情况
- 一张表的索引太大不能加载到缓存中,但是其中一个分区的是可以缓存的,并且
- 表的索引的随机访问的,并且
- 数据吞吐的主要限制是更新索引的I/O
分区可以让所有的‘热’索引始终在内存中,这样可以节约大量的I/O
对于这个场景通过提升缓存适用效率来减少I/O,提升操作性能
Case#4 – 可以移动的表空间
使用EXPORT/IMPORT partition 归档或者导入数据.(导入可能需要一些 partition key 的技巧).
参见Transportable Tablespaces for InnoDB Partitions
这个连接针对5.7版本,但是有一个章节“5.6版本怎么做?”
参考(See also FLUSH TABLES … FOR EXPORT), 直到5.6.17版本,分区InnoDB表仍然是不支持的
对于这个场景可以快速的移动分区在不同的服务器和数据库之间
Case#5 – 我还没有发现适用的第5个场景
注意到现在为止,各种场景中我们只使用了RANGE partitioning,其他的并没有被使用.
AUTO_INCREMENT(自增id) 在分区中的使用
- 对AUTO_INCREMENT字段来说,它必须是第一个索引中的第一个字段,除此之外索引它就没有其他要求了.
- AUTO_INCREMENT字段作为一些索引中的第一个字段,使得引擎在操作表时能够找到下一个值.
- AUTO_INCREMENT字段不能使UNIQUE(唯一),为此你将不能阻止插入重复的id值(和少情况下会需要这个限制)
例子(id 是 AUTO_INCREMENT)
- PRIMARY KEY (…), INDEX(id)
- PRIMARY KEY (…), UNIQUE(id, partition_key) – not useful
- INDEX(id), INDEX(…) (不是UNIQUE键值)
- PRIMARY KEY(id), … – 只有在id左右分区key时可用(并不十分有用)
分区维护时间序列数据
让我们把注意力放在管理情景#1的情况
假设你有一张大的数据表,在一张表上添加新数据,并定期的归档整理到其他表中,例如新闻,日志,以及其他的有时效性消息.分区是一种极好的解决方案
- DROP PARTITION要比使用DELETE快的多.这也是个使用分区解决这个问题的主要原因.
- 查找请求通常是限定了最近的数据,因而可以在裁减后的分区上快速执行
根据数据类型和多久数据过期,你可以按天,按周或者按小时等分区
没有简单的sql语句,来‘删除最近30天的分区’ 或者 ‘添加一个明天的分区’,手工做这件事情将是非常麻烦的.
示例代码
ALTER TABLE tbl
DROP PARTITION from20120314;
ALTER TABLE tbl
REORGANIZE PARTITION future INTO
from20120415 VALUES LESS THAN (TO_DAYS('2012-04-16')),
future VALUES LESS THAN MAXVALUE;
After which you have...
CREATE TABLE tbl (
dt DATETIME NOT NULL, -- or DATE
...
PRIMARY KEY (..., dt),
UNIQUE KEY (..., dt),
...
)
PARTITION BY RANGE (TO_DAYS(dt)) (
start VALUES LESS THAN (0),
from20120315 VALUES LESS THAN (TO_DAYS('2012-03-16')),
from20120316 VALUES LESS THAN (TO_DAYS('2012-03-17')),
...
from20120414 VALUES LESS THAN (TO_DAYS('2012-04-15')),
from20120415 VALUES LESS THAN (TO_DAYS('2012-04-16')),
future VALUES LESS THAN MAXVALUE
);
为什么?
或许你已经注意到例子中一些趣的东西,下面解释下.
- from20120415 … 04-16: 注意这个LESS THAN是下一天的日期
- “start”分区:见下文
- “future”分区: 通常情况下这个分区是空的,但是以后可以捕捉到越界的数据.
- 这个range key(dt)必须包含在任何一个PRIMARY或者UNIQUE字段中
- 这个range key(dt)应该作为任何主键的最后一个字段,因为你已经使用它进行过数据裁减,所以它在主键中几乎是没用的,尤其是开始的时候
- DATETIME,等 – 我选择这个数据类型是因为它是时间序列数据的特征,新的MYSQL版本允许使用TIMESTAMP或INT类型.
范围中多了一天(03/16-04/16): 最后一天是部分完整的.
为什么使用一个伪造的”start”分区?如果使用了一个非法的日期(3月31号),那么在转换成datetime的时候结果将是null,而null值是被放到第一个分区上的因为所有的select可能都包含了不可用的日期(关于这一点可以展开来说明),导致分区选择的结果中始终包含第一个分区,然后查找第一个个分区,因此如果select肯定会稍描第一个分区,那么保持第一个分区为空将会更为的高效.
更多的讨论参见The Data Charmer
5.5版本中使用新的语法避免了使用伪造分区 PARTITION BY RANGE COLUMNS(dt) (PARTITION day_20100226 VALUES LESS THAN (‘2010-02-27’), …下一个关于”future”分区,早晚有一天用来添加新分区的cron/EVENT可能执行过程中发生了错误,最坏的结果就是丢失了一天的数据.预防丢失数据的最简的方法就是有个分区来捕捉这样的数据,虽然通常情况下这个分区是空的.
有一个”future”分区是的添加分的脚本变得复杂了些,它需要处理讲明天的数据移从”future”动到新分区中.这是用到了REORAGNIZE命令.通常情况下,是没有数据要移动的,所以基本上不会花费时间.
什么情况下执行ALTER命令?
- DROP 如果最老的分区实在太老了.
- Add 在今天的结尾添加’tomorrow’分区,但是不要试图重复添加.
- 不要计算分区的数量 –我们使用了额外的两个分区. 使用分区名字或者information_schema.PARTITIONS.PARTITION_DESCRIPTION.
- DROP/Add 在脚本中只作一次操作,如果需要多个就执行多次脚本
- 比需要的更频繁的执行脚本. 对于一日的分区, 至少一天内要执行两次,甚至一小时一次,为什么?可以自动修复错误.
其他用法
就像我反复提到的,在许多方面,BY RANGE可能是仅有的用法,并且时间序列数据对分区来说是最常用的.
- DATETIME/DATE with TO_DAYS()
- DATETIME/DATE with TO_DAYS(), but with 7-day intervals
- TIMESTAMP with TO_DAYS(). (version 5.1.43 or later)
- PARTITION BY RANGE COLUMNS(DATETIME) (5.5.0)
- PARTITION BY RANGE(TIMESTAMP) (version 5.5.15 / 5.6.3)
- PARTITION BY RANGE(TO_SECONDS()) (5.6.0)
- INT UNSIGNED with constants computed as unix timestamps.
- INT UNSIGNED with constants for some non-time-based series.
- MEDIUMINT UNSIGNED containing an “hour id”: FLOOR(FROM_UNIXTIME(timestamp) / 3600)
Months, Quarters, etc: Concoct a notation that works.
使用多少个分区?
- Under, say, 5 partitions – you get very little of the benefits.
- Over, say, 50 partitions, and you hit inefficiencies elsewhere.
- Certain operations (SHOW TABLE STATUS, opening the table, etc) open every partition.
- MyISAM, before version 5.6.6, would lock all partitions before pruning!
- Partition pruning does not happen on INSERTs (until Version 5.6.7), so INSERT needs to open all the partitions.
- A possible 2-partition use case: http://forums.mysql.com/read.php?24,633179,633179
- 8192 partitions is a hard limit (1024 before 5.6.7).
- Before “native partitions” (5.7.6), each partition consumed a chunk of memory.
例子代码
Reference implementation, in Perl, with demo of daily partitions
代码中最复杂的部分在发现PARTITION名称,尤其是最老的和下一个分区.
执行这个例子需要
- Install Perl and DBIx::DWIW (from CPAN).
- copy the txt file (link above) to demo_part_maint.pl
execute perl demo_part_maint.pl to get the rest of the instructions
这段代码将生成并执行(需要的时候)下面两个中的一个
ALTER TABLE tbl REORGANIZE PARTITION
future
INTO (
PARTITION from20150606 VALUES LESS THAN (736121),
ARTITION future VALUES LESS THAN MAXVALUE
)
ALTER TABLE tbl DROP PARTITION from20150603
更新日志
- Original writing – Oct, 2012;
- Use cases added: Oct, 2014; -
- Refreshed: June, 2015; 8.0: Sep, 2016
PARTITIONing requires at least MySQL 5.1
The tips in this document apply to MySQL, MariaDB, and Percona.
More on PARTITIONing
LinkedIn discussion
Future (as envisioned in 2016):
- 5.7.6 has “native partitioning for InnoDB”.
- FOREIGN KEY support, perhaps in a later 8.0.xx.
- “GLOBAL INDEX” – this would avoid the need for putting the partition key in every unique index, but make DROP PARTITION costly. This will be farther into the future.
MySQL 8.0, released Sep, 2016:
- Only InnoDB tables can be partitioned – MariaDB is likely to continue maintaining Partitioning on non-InnoDB tables, but Oracle is clearly not.
Native partitioning will give:
- This will improve performance slightly by combining two “handlers” into one.
- Decreased memory usage, especially when using a large number of partitions.
Contact me by posting a question at MySQL Forums :: Partitions
- - Rick James