分区:就是把一个数据表的文件和索引,按某种方式,分散存储在不同的物理文件中。
通俗的讲表分区是将一个表,根据条件分割成若干块,存储在不同的文件中。
支持Mysql表分区需要MYSQL版本为5.1以上,含5.1~
表分区的作用:增加MYSQL的执行效率,可以在以下几点分析表分区是如何增加MYSQL的执行效率的。
1、与单个磁盘或文件系统分区相比,可以存储更多的数据。
2、对于那些已经失去保存意义的数据,通常可以通过删除与那些数据有关的分区,很容易地删除那些数据。
3、一些查询可以得到极大的优化,这主要是借助于满足一个给定WHERE语句的数据可以只保存在一个或多个分区内,这样在查找时就不用查找其他剩余的分区。
4、涉及到例如SUM()和COUNT()这样聚合函数的查询,可以很容易地进行并行处理。这种查询的一个简单例子如 “SELECT salesperson_id, COUNT (orders) as order_total FROM sales GROUP BY salesperson_id;”。通过“并行”,这意味着该查询可以在每个分区上同时进行,最终结果只需通过总计所有分区得到的结果。
5、通过跨多个磁盘来分散数据查询,来获得更大的查询吞吐量。
mysql支持的分区类型,包括Range、List、Hash、Key,其中Range比较常用。
- Range分区:基于一个连续区间的列值,把多行分配给分区。
- List分区:基于一个离散值集合的列值,什么值匹配什么分区;类似于按Range分区
- Hash分区:基于用户自定义的表达式的返回值,来选择分区;该表达式使用将要插入到表中的这些行的列值进行计算。这个函数可以包含MYSQL中有效的、产生非负整数值的任何表达式。
- Key分区:类似于Hash分区,区别在于Key分区只支持计算一列或多行,且MYSQL服务器提供自身的哈希函数。必须有一列或多列包含整数值。
示例,以Range方式分区:
1.在创建表时,同时创建分区
.建立一个user表, 以id进行分区, id小于10的在user_1分区,id小于20的在user_2分区
create table user(
id int not null auto_increment,
username varchar(10),
primary key(id)
)engine=Innodb charset=utf8
partition by range (id)(
partition user_1 values less than (10),
partition user_2 values less than (20)
);
2.在建立表后,再添加分区:
maxvalue:表示最大值,只能用于最后一个分区。
这样大于等于30的id,都出存储在user_3分区
alter table user add partition(
partition user_3 values less than (30),
partition user_4 values less than maxvalue
);
3.删除分区:
alter table user drop partition user_4;
4.查看分区文件:
在数据库mydb中,user的文件:
可以看见多了user#P#user_1.ibd 、user#P#user_2.ibd 和user#P#user_3.ibd, 这三个文件
如果表使用的存储引擎是MyISAM类型,就是:
user#P#user_1.MYD,user#P#user_1.MYI...........
由此可见,mysql通过分区把数据保存到不同的文件里,同时索引也是分区的。相对于未分区的表来说,分区后单独的数据库文件索引文件的大小都明显降低,效率则明显的提示了。
可以插入一条数据然后分析查询语句验证一下。
5.验证
insert into user values(null,'测试');
explain partitions select * from user where id =1;
explain后,显示:
可以看见仅仅在user_1分区执行了这条查询。
具体分区的效率是多少还需要看数据量。
6.在分区时,同时将分区存放在不同的磁盘上
在分区时可以通过 DATA DIRECTORY 和 INDEX DIRECTORY 选项,把不同的分区放到不同的磁盘上进一步提高系统的I/O吞吐量。
7.分区类型选择
分区类型的选择,通常使用Range类型,不过有些情况,比如主从结构中,主服务器很少使用‘select’查询,在主服务器上使用 Range类型分区通常没有太大的意义,此时使用Hash类型分区更好例如:
partition by hash(id) partitions 10;
当插入数据时,根据id把数据平均散到各个分区上,由于文件小,效率高,更新操作变得更快。
在分区时使用的字段,通常情况下按时间字段分区,具体情况以需求而定。
划分应用的方式有很多种,比如按时间或用户,哪种用的多,就选择哪种分区。
如果使用主从结构可能就更加灵活,有的从服务器使用时间,有的使用用户。
不过如此一来当执行查询时,程序应该负责选择真确的服务器查询,写个mysql proxy脚本应该可以透明的实现。
8.分区的限制:
1.主键或者唯一索引必须包含分区字段,如primary key (id,username),不过innoDB的大组建性能不好。
2.很多时候,使用分区就不要在使用主键了,否则可能影响性能。
3.只能通过int类型的字段或者返回int类型的表达式来分区,通常使用year或者to_days等函数(mysql 5.6 对限制开始放开了)。
4.每个表最多1024个分区,而且多分区会大量消耗内存。
5.分区的表不支持外键,相关的逻辑约束需要使用程序来实现。
6.分区后,可能会造成索引失效,需要验证分区可行性。
9.分区模式详解:
* Range(范围) – 这种模式允许DBA将数据划分不同范围。例如DBA可以将一个表通过年份划分成三个分区,80年代(1980's)的数据,90年代(1990's)的数据以及任何在2000年(包括2000年)后的数据。
示例,将用户表分成4个分区,以每300万条记录为界限,每个分区都有自己独立的数据、索引文件的存放目录。:
CREATE TABLE users (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
usersname VARCHAR(30) NOT NULL DEFAULT '',
email VARCHAR(30) NOT NULL DEFAULT ''
)
ENGINE=InnoDB CHARSET=UTF8
PARTITION BY RANGE (id) (
PARTITION p0 VALUES LESS THAN (3000000),
PARTITION p1 VALUES LESS THAN (6000000),
PARTITION p2 VALUES LESS THAN (9000000),
PARTITION p3 VALUES LESS THAN MAXVALUE
);
还可以将这些分区所在的物理磁盘分开完全独立,可以提高磁盘IO吞吐量。
DROP TABLE IF EXISTS users;
CREATE TABLE users (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
usersname VARCHAR(30) NOT NULL DEFAULT '',
email VARCHAR(30) NOT NULL DEFAULT ''
)
ENGINE=InnoDB CHARSET=UTF8 COMMENT='出入库明细表'
PARTITION BY RANGE (id) (
PARTITION p0 VALUES LESS THAN (3000000)
DATA DIRECTORY = '/data0/data'
INDEX DIRECTORY = '/data0/index',
PARTITION p1 VALUES LESS THAN (6000000)
DATA DIRECTORY = '/data1/data'
INDEX DIRECTORY = '/data1/index',
PARTITION p2 VALUES LESS THAN (9000000)
DATA DIRECTORY = '/data2/data'
INDEX DIRECTORY = '/data2/index',
PARTITION p3 VALUES LESS THAN MAXVALUE
DATA DIRECTORY = '/data3/data'
INDEX DIRECTORY = '/data3/index'
);
* List(预定义列表) – 这种模式允许系统通过DBA定义的列表的值所对应的行数据进行分割。例如:DBA根据用户的类型进行分区。
CREATE TABLE users (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(30) NOT NULL DEFAULT '' ,
user_type int not null
)
PARTITION BY LIST (user_type ) (
PARTITION p0 VALUES IN (0,4,8,12) ,
PARTITION p1 VALUES IN (1,5,9,13) ,
PARTITION p2 VALUES IN (2,6,10,14),
PARTITION p3 VALUES IN (3,7,11,15)
);
分成4个区,同样可以将分区设置的独立的磁盘中。
* Key(键值) – 上面Hash模式的一种延伸,这里的Hash Key是MySQL系统产生的。
CREATE TABLE users (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(30) NOT NULL DEFAULT '',
email VARCHAR(30) NOT NULL DEFAULT ''
)
PARTITION BY KEY (id) PARTITIONS 4 (
PARTITION p0,
PARTITION p1,
PARTITION p2,
PARTITION p3
);
* Hash(哈希) – 这中模式允许DBA通过对表的一个或多个列的Hash Key进行计算,最后通过这个Hash码不同数值对应的数据区域进行分区,。例如DBA可以建立一个对表主键进行分区的表。
CREATE TABLE users (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(30) NOT NULL DEFAULT '',
email VARCHAR(30) NOT NULL DEFAULT ''
)
PARTITION BY HASH (id) PARTITIONS 4 (
PARTITION p0 ,
PARTITION p1,
PARTITION p2,
PARTITION p3
);
分成4个区,同样可以将分区设置的独立的磁盘中。
10.分区管理
删除分区:
ALTER TABLE users DROP PARTITION p0;
重建分区:条件,分区前后的分区类型必须相同。
1).RANGE 分区重建:将原来的 p0,p1 分区合并起来,放到新的 p0 分区中。
ALTER TABLE users REORGANIZE PARTITION p0,p1 INTO
(PARTITION p0 VALUES LESS THAN (6000000));
2).LIST 分区重建:将原来的 p0,p1 分区合并起来,放到新的 p0 分区中。
ALTER TABLE users REORGANIZE PARTITION p0,p1 INTO (PARTITION p0 VALUES IN(0,1,4,5,8,9,12,13));
用 REORGANIZE 方式重建分区的数量变成2,在这里数量只能减少不能增加。想要增加可以用 ADD PARTITION 方法。
新增分区
新增 RANGE 分区
alter table user add partition(partition user_3 values less than maxvalue);
新增 LIST 分区
ALTER TABLE users ADD PARTITION (PARTITION p4 VALUES IN (16,17,18,19));
新增 HASH/KEY 分区:将分区总数扩展到8个。
ALTER TABLE users ADD PARTITION PARTITIONS 8;
给已有的表加上分区
alter table users partition by RANGE (month(ttime))
(PARTITION p0 VALUES LESS THAN (1),
PARTITION p1 VALUES LESS THAN (2) , PARTITION p2 VALUES LESS THAN (3) ,
PARTITION p3 VALUES LESS THAN (4) , PARTITION p4 VALUES LESS THAN (5) ,
PARTITION p5 VALUES LESS THAN (6) , PARTITION p6 VALUES LESS THAN (7) ,
PARTITION p7 VALUES LESS THAN (8) , PARTITION p8 VALUES LESS THAN (9) ,
PARTITION p9 VALUES LESS THAN (10) , PARTITION p10 VALUES LESS THAN (11),
PARTITION p11 VALUES LESS THAN (12),
PARTITION P12 VALUES LESS THAN (13) );
示例,比较分区表和不分区表的查询效率:
创建含分区表,RANGE分区
CREATE TABLE part_tab(
c1 int default NULL,
c2 varchar(30) default NULL,
c3 date default NULL) engine=myisam
PARTITION BY RANGE (year(c3))
(PARTITION p0 VALUES LESS THAN (1995),
PARTITION p1 VALUES LESS THAN (1996) ,
PARTITION p2 VALUES LESS THAN (1997) ,
PARTITION p3 VALUES LESS THAN (1998) ,
PARTITION p4 VALUES LESS THAN (1999) ,
PARTITION p5 VALUES LESS THAN (2000) ,
PARTITION p6 VALUES LESS THAN (2001) ,
PARTITION p7 VALUES LESS THAN (2002) ,
PARTITION p8 VALUES LESS THAN (2003) ,
PARTITION p9 VALUES LESS THAN (2004) ,
PARTITION p10 VALUES LESS THAN (2010),
PARTITION p11 VALUES LESS THAN MAXVALUE);
创建不含分区表
CREATE TABLE no_part_tab( c1 int default NULL,
c2 varchar(30) default NULL,
c3 date default NULL) engine=myisam
创建存储过程,在表中插入800,0000条数据以做测试
CREATE PROCEDURE load_part_tab()
begin
declare v int default 0;
while v < 8000000
do
insert into part_tab
values (v,'testing partitions',adddate('1995-01-01',(rand(v)*36520) mod 3652));
set v = v + 1;
end while;
end
执行存储过程,将数据写入分区的表中:
call load_part_tab;
再写入未分区表中
insert into no_part_tab select * from part_tab;
好了,所有表数据准备完毕,下面我们来做一下测试
select count(*) from part_tab where c3 >date('1995-01-01') and c3 < date('1995-12-31');
select count(*) from no_part_tab where c3 >date('1995-01-01') and c3 <date('1995-12-31');
分别用时: 0.4秒和2.7秒
很清楚的看到了,读取同样的数据,分区与未分区的两个表相差的不是同一个级别的数据~,接下来就可以修改自己大数据的表了
给未分区的表,创建分区。
ALTER TABLE no_part_tab PARTITION by range (c1)(
PARTITION s2009 VALUES LESS THAN (1000000),
PARTITION s2010 VALUES LESS THAN (2000000),
PARTITION s2010 VALUES LESS THAN MAXVALUE
);
分表:
分表和分区类似,区别是,分区是把一个逻辑表文件分成几个物理文件后进行存储,而分表则是把原先的一个表分成几个表。进行分表查询时可以通过union或者视图。
分表又分垂直分割和水平分割,其中水平分割最为常用。
水平分割通常是指切分到另外一个数据库或表中。
例如对于一个会员表,按3的模进行分割: table = id%3
如果id%3 = 0 则将用户数据放入到user_0表中,如id%3=1就放入user_1表中,依次类推。
在这里有个问题,这个id应该是所有会员按序增长的,可他是怎么得到的呢?使用auto_increment是不行的,这样就用到序列了。
对于一些流量统计系统,其数据量比较大,并且对过往数据的关注度不高,这时按年、月、日进行分表,将每日统计信息放到一个以日期命名的表中;或者按照增量进行分表,如每个表100万数据,超过100万就放入第二个表。还可以按Hash进行分表,但是按日期和取模余数分表最为常见,也容易扩展。
分表后可能会遇到新的问题,那就是查询,分页和统计。
通用的方法是在程序中进行处理,辅助视图。
使用分表案例:
案例1:
对会员数据对5取模,放在5个表中,如何查询会员数据:
1.已知id查询会员数据,代码如下:
<?php
//查询单个会员数据
$customer_table = 'customer'.$id%5;
$sql = 'select * from '.$customer_table.' where customer_id = '.$id;
//查询全部会员数据
$sql = '';
$tbale = ['customer0','customer1','customer2','customer3','customer4'];
foreach($table as $v){
$sql .='select * from '.$v.' union';
}
$sql = substr($sql,0,-5);
?>
这样就可以查询某一个会员的数据或者全部会员的数据了。
同理,分页的话在这个大集合中使用limit 就可以了。
但是这样做又会有一个疑问,把所有的表连起来查询和不分表没有什么区别。
其实在实际的应用中,不可能查看所有的会员资料,一次查看20个然后分页。
完全没有必要做union,仅查询一个表就可以了,唯一需要考虑的是在分页零界点时的衔接。
其实,这个衔接是否那么重要?即使偶尔出现几条数据的差异,也不会对业务有任何的影响。
2.和其它表进行关联和1类似。
3.根据会员姓名搜索用户信息。在这种需求下,需要搜索所有的表,并对结果进行汇总。虽然这样做产生了多次的查询,但并不代表效率低。好的sql语句执行10次也比差的sql语句执行一次快。
案例2:
在一个流量监控系统中,由于网络流量巨大,统计数据很庞大,需要按天分表。先要得到任意日,周,月的数据。
1.需要任意一天的数据。直接查询当天的数据表即可。
2.需要几天的数据。分爱查询这几天的数据,然后进行汇总。
3.需要查询一周的数据。对一周的数据定期汇总到一个week表,从这个表里面查询。这个汇总过程可以由一个外部程序完成,也可以由定期的脚本完成。
4.查询一个月的数据。汇总本月所有的数据到month表,在此表查询。
5.查询5个月内的详细数据。不支持。仅支持最多3个月的详细数据。数据没3个月已归档一次。在大数据的处理中,必须做出一些牺牲。对于超出3个月的数据,仅提供统计数据,详细数据需要查看归档。90天或者180天,给数据保存设个界限,也是大部分这类系统的常规做法,超出90天的数据就不再提供数据详单了。比如,移动的通话记录最多保存半年,即180天,超过这个范围的数据不在提供查询。如果你实在需要,可能就要联系移动的工程师了。
分表前应该尽量按照实际业务来分表,参考依据就是哪些字段在查询中起到作用,那就这些字段来分表,并且需要在分表前就估算好规模,也就是先确定好规则在分表。
对于分表后的操作,依然是联合查询,视图等基本操作,或者使用merge引擎合并数据并在此表中查询。复杂一些操作需要借助存储过程来完成,借助外部工具实现对分表的管理。
对于比较庞大的数据,不论是否进行分表,都必须考虑功能和效率的平衡性,并在功能上做出让步。我们不能事事迁就用户,而应该对某些影响效率的功能做出限制。例如移动公司的180天限制、论坛禁止对老帖进行回复等。