目录
为什么要分表和分区?
我们的数据库数据越来越大,随之而来的是单个表中数据太多。以至于查询速度变慢,而且由于表的锁机制导致应用操作也搜到严重影响,出现了数据库性能瓶颈。
mysql 中有一种机制是表锁定和行锁定,是为了保证数据的完整性。表锁定表示你们都不能对这张表进行操作,必须等我对表操作完才行。行锁定也一样,别的sql 必须等我对这条数据操作完了,才能对这条数据进行操作。当出现这种情况时,我们可以考虑分表或分区。
分表
分表是将一个大表按照一定的规则分解成多张具有独立存储空间的实体表,每个表都对应三个文件, MYD 数据文件,.MYI 索引文件, .frm 表结构文件。这些表可以分布在同一块磁盘上,也可以在不同的机器上。 app 读写的时候根据事先定义好的规则得到对应的表名,然后去操作它。
将单个数据库表进行拆分,拆分成多个数据表,然后用户访问的时候,根据一定的算法(如用 hash 的方式,也可以用求余(取模)的方式),让用户访问不同的表,这样数据分散到多个数据表中,减少了单个数据表的访问压力。提升了数据库访问性能。分表的目的就在于此,减小数据库的负担,缩短查询时间
Mysql分表分为垂直切分和水平切分
垂直切分是指数据表列的拆分,把一张列比较多的表拆分为多张表 通常我们按以下原则进行垂直拆分 : 把不常用的字段单独放在一张表; 把 text , blob ( binary large object ,二进制大对象)等大字段拆分出来放在附表中 ;经常组合查询的列放在一张表中; 垂直拆分更多时候就应该在数据表设计之初就执行的步骤,然后查询的时候用join关键起来即可
分表的几种方式:
mysql集群
它并不是分表,但起到了和分表相同的作用。集群可分担数据库的操作次数,将任务分担到多台数据库上。集群可以读写分离,减少读写压力。从而提升数据库性能。
预先估计会出现大数据量并且访问频繁的表,将其分为若干个表
根据一定的算法(如用 hash 的方式,也可以用求余(取模)的方式)让用户访问不同的表。 例如论坛里面发表帖子的表,时间长了这张表肯定很大,几十万,几百万都有可能。聊天室里面信息表,几十个人在一起一聊一个晚上,时间长了,这张表的数据肯定很大。像这样的情况很多。所以这种能预估出来的大数据量表,我们就事先分出个N 个表,这个 N 是多少,根据实际情况而定。以聊天信息表为例:我们事先建 100 个这样的表, message_00,message_01,message_02..........message_98,message_99.然后根据用户的 ID 来判断这个用户的聊天信息放到哪张表里面,可以用hash 的方式来获得,也可以用求余的方式来获得,方法很多。 或者可以设计每张表容纳的数据量是N条,那么如何判断某张表的数据是否容量已满呢?可以在程序段对于要新增数据的表,在插入前先做统计表记录数量的操作,当<N条数据,就直接插入,当已经到达阀值,可以在程序段新创建数据库表(或者已经事先创建好),再执行插入操作)。
利用merge存储引擎来实现分表
如果要把已有的大数据量表分开比较痛苦,最痛苦的事就是改代码,因为程序里面的 sql 语句已经写好了,用merge存储引擎来实现分表 , 这种方法比较适合。 merge 分表,分为主表和子表,主表类似于一个壳子,逻辑上封装了子表,实际上数据都是存储在子表中的。 我们可以通过主表插入和查询数据,如果清楚分表规律,也可以直接操作子表。
下面我们来实现一个简单的利用merge 存储引擎来实现分表的演示: 创建一个完整表存储着
所有的成员信息(表名为 member )
bash
mysql> drop database IF EXISTS test;
mysql> use test;
create table member(
id bigint auto_increment primary key,
name varchar(20),
sex tinyint not nulldefault '0'
)engine=myisam default charset=utf8 auto_increment=1;
加入点数据:
bash
mysql> insert into member(name,sex) values('tom1',1);
mysql> insert into member(name,sex) select name,sex from member;
第二条语句多执行几次就有了很多数据
bash
mysql> select * from member;
+----+------+-----+
| id | name | sex |
+----+------+-----+
| 1 | tom1 | 1 |
| 2 | tom1 | 1 |
| 3 | tom1 | 1 |
| 4 | tom1 | 1 |
| 5 | tom1 | 1 |
| 6 | tom1 | 1 |
| 7 | tom1 | 1 |
| 8 | tom1 | 1 |
| 9 | tom1 | 1 |
| 10 | tom1 | 1 |
| 11 | tom1 | 1 |
| 12 | tom1 | 1 |
| 13 | tom1 | 1 |
| 14 | tom1 | 1 |
| 15 | tom1 | 1 |
| 16 | tom1 | 1 |
+----+------+-----+
下面我们进行分表,这里我们把member分两个表tb_member1,tb_member2
bash
mysql> use test;
DROP table IF EXISTS tb_member1;
create table tb_member1(
id bigint primary key ,
name varchar(20),
sex tinyint not null default '0'
)ENGINE=MyISAM DEFAULT CHARSET=utf8 ;
DROP table IF EXISTS tb_member2;
create table tb_member2(
id bigint primary key,
name varchar(20),
sex tinyint not null default '0'
)ENGINE=MyISAM DEFAULT CHARSET=utf8;
//创建tb_member2也可以用下面的语句 create table tb_member2 like tb_member1;
创建主表tb_member
DROP table IF EXISTS tb_member;
create table tb_member(
id bigint primary key ,
name varchar(20),
sex tinyint not null default '0'
) ENGINE=MERGE UNION=(tb_member1,tb_member2) INSERT_METHOD=LAST CHARSET=utf8 ;
注:THOD = LAST表示插入到最后的一张表里面。INSERT_METHOD = first表示插入到第一张表里面。 查看一下tb_member表的结构::INSERT_METHOD,此参数INSERT_METHOD = NO 表示该表不能做任何写入操作只作为查询使用,INSERT_ME
bash
mysql> desc tb_member;
mysql> desc tb_member;
+-------+-------------+------+-----+---------+------------------------------------
-----+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+------------------------------------
-----+
| id | bigint(20) | NO | PRI | NULL | auto_increment |
| name | varchar(20) | YES | | NULL | |
| sex | tinyint(4) | NO | | 0 | |
+-------+-------------+------+-----+---------+------------------------------------
------+
3 rows in set (0.00 sec)
注:查看子表与主表的字段定义要一致
接下来,我们把数据分到两个分表中去:
bash
mysql> insert into tb_member1(id,name,sex) select id,name,sex from member where
id%2=0;
mysql> insert into tb_member2(id,name,sex) select id,name,sex from member where
id%2=1;
查看两个子表的数据:
bash
mysql> select * from tb_member1;
+----+------+-----+
| id | name | sex |
+----+------+-----+
| 16 | tom1 | 1 |
| 14 | tom1 | 1 |
| 12 | tom1 | 1 |
| 10 | tom1 | 1 |
| 8 | tom1 | 1 |
| 6 | tom1 | 1 |
| 4 | tom1 | 1 |
| 2 | tom1 | 1 |
+----+------+-----+
8 rows in set (0.00 sec)
mysql> select * from tb_member2;
+----+------+-----+
| id | name | sex |
+----+------+-----+
| 3 | tom1 | 1 |
| 1 | tom1 | 1 |
| 5 | tom1 | 1 |
| 7 | tom1 | 1 |
| 9 | tom1 | 1 |
| 11 | tom1 | 1 |
| 13 | tom1 | 1 |
| 15 | tom1 | 1 |
+----+------+-----+
8 rows in set (0.00 sec)
查看一下主表的数据:
bash
mysql> select * from tb_member;
+----+------+-----+
| id | name | sex |
+----+------+-----+
| 16 | tom1 | 1 |
| 14 | tom1 | 1 |
| 12 | tom1 | 1 |
| 10 | tom1 | 1 |
| 8 | tom1 | 1 |
| 6 | tom1 | 1 |
| 4 | tom1 | 1 |
| 2 | tom1 | 1 |
| 15 | tom1 | 1 |
| 13 | tom1 | 1 |
| 11 | tom1 | 1 |
| 9 | tom1 | 1 |
| 7 | tom1 | 1 |
| 5 | tom1 | 1 |
| 3 | tom1 | 1 |
| 1 | tom1 | 1 |
+----+------+-----+
16 rows in set (0.00 sec)
mysql> select * from tb_member where id=3;
+----+------+-----+
| id | name | sex |
+----+------+-----+
| 3 | tom1 | 1 |
+----+------+-----+
1 row in set (0.00 sec)
注意:总表只是一个外壳,存取数据发生在一个一个的子表里面。 注意:每个子表都有自已独立的相关表文 件,而主表只是一个壳,并没有完整的相关表文件
bash
[root@localhost ~]# ls -l /usr/local/mysql/data/test/tb_member*
-rw-r-----. 1 mysql mysql 8614 Sep 15 21:49
/usr/local/mysql/data/test/tb_member1.frm
-rw-r-----. 1 mysql mysql 320 Sep 16 00:02
/usr/local/mysql/data/test/tb_member1.MYD
-rw-r-----. 1 mysql mysql 2048 Sep 16 00:43
/usr/local/mysql/data/test/tb_member1.MYI
-rw-r-----. 1 mysql mysql 8614 Sep 15 21:50
/usr/local/mysql/data/test/tb_member2.frm
-rw-r-----. 1 mysql mysql 180 Sep 16 00:02
/usr/local/mysql/data/test/tb_member2.MYD
-rw-r-----. 1 mysql mysql 2048 Sep 16 00:43
/usr/local/mysql/data/test/tb_member2.MYI
-rw-r-----. 1 mysql mysql 8614 Sep 16 21:12
/usr/local/mysql/data/test/tb_member3.frm
-rw-r-----. 1 mysql mysql 0 Sep 16 21:12
/usr/local/mysql/data/test/tb_member3.MYD
-rw-r-----. 1 mysql mysql 1024 Sep 16 21:12
/usr/local/mysql/data/test/tb_member3.MYI
-rw-r-----. 1 mysql mysql 8614 Sep 16 21:14
/usr/local/mysql/data/test/tb_member.frm
-rw-r-----. 1 mysql mysql 53 Sep 16 21:14
/usr/local/mysql/data/test/tb_member.MRG
分区
什么是分区?
分区可以按照不同的标准进行划分,比如按照范围、列表、哈希或者键值等方式进行分区。每个分区都可以存储部分数据,这样可以减少查询的数据量,从而提高查询的速度
分区的分类
范围分区(Range Partitioning):按照某个范围的值划分分区,例如按照日期、年龄、价格等范围划分。可以使用
VALUES LESS THAN
或VALUES IN
来定义分区的范围。列表分区(List Partitioning):根据列值匹配来划分分区,可以根据具体的列值来划分,例如按照国家、地区等列表信息进行分区。可以使用
VALUES IN
来定义分区的列表。哈希分区(Hash Partitioning):根据列值的哈希值进行划分分区,确保数据分布均匀,适用于无法使用范围或列表进行明确划分的情况。
键值分区(Key Partitioning):根据列的哈希值或者键值的哈希值进行划分,类似于哈希分区,但可以指定列作为分区的键。
子分区(Subpartitioning):在分区内部再进行子分区,可以进行更细粒度的数据划分。子分区可以按照范围、列表、哈希或键值等方式进行划分。
分区的优点
提高查询性能:分区可以将数据划分为多个逻辑部分,可以仅查询特定分区中的数据,而不需要扫描整个表。这样可以大大提高查询的速度,特别是对于大型表和频繁查询的情况。
提高维护效率:通过分区,可以对某个特定分区执行维护操作,如备份、恢复和优化等。这样可以减少对整个表的操作,提高维护的效率,减少维护所需的时间和资源。
改善数据管理:通过分区,可以将不同的数据存储在不同的分区中,从而更好地管理数据。可以根据数据的特性、生命周期等因素设置不同的存储策略,比如将热数据和冷数据分开存储,根据需要进行灵活的存储扩展。
增强容错性:分区可以将数据分布在不同的物理位置上,可以在出现故障或灾难恢复时提供更好的容错性。例如,在某个存储设备发生故障时,其他分区的数据仍然可用,可以快速进行故障切换或恢复。
简化数据删除:通过分区,可以更容易地删除或清理过期的数据。可以只清理某些分区中的数据,而不需要对整个表进行操作,从而简化了数据清理的过程。
下面我们先演示一个按照范围 (range) 方式的表分区 创建 range 分区表
bash
mysql> use test2;
mysql> create table if not exist user (
-> id int not null auto_increment,
-> name varchar(30) not null default '',
-> sex int(1) not null default '0',
-> primary key(id)
-> )default charset=utf8 auto_increment=1
-> partition by range(id) (
-> partition p0 values less than (3),
-> partition p1 values less than (6),
-> partition p2 values less than (9),
-> partition p3 values less than (12),
-> partition p4 values less than maxvalue
-> );
插入些数据
bash
mysql> insert into test2.user(name,sex)values ('tom1','0');
mysql> insert into test2.user(name,sex)values ('tom2','1');
mysql> insert into test2.user(name,sex)values ('tom3','1');
mysql> insert into test2.user(name,sex)values ('tom4','0');
mysql> insert into test2.user(name,sex)values ('tom5','0');
mysql> insert into test2.user(name,sex)values ('tom6','1');
mysql> insert into test2.user(name,sex)values ('tom7','1');
mysql> insert into test2.user(name,sex)values ('tom8','1');
mysql> insert into test2.user(name,sex)values ('tom9','1');
mysql> insert into test2.user(name,sex)values ('tom10','1');
mysql> insert into test2.user(name,sex)values ('tom11','1');
mysql> insert into test2.user(name,sex)values ('tom12','1');
mysql> insert into test2.user(name,sex)values ('tom13','1');
mysql> insert into test2.user(name,sex)values ('tom14','1');
到存放数据库表文件的地方看一下
bash
[root@localhost ~]# ls -l /usr/local/mysql/data/test2/user*
-rw-r-----. 1 mysql mysql 8614 Sep 16 21:46 /usr/local/mysql/data/test2/user.frm
-rw-r-----. 1 mysql mysql 98304 Sep 16 21:48
/usr/local/mysql/data/test2/user#P#p0.ibd
-rw-r-----. 1 mysql mysql 98304 Sep 16 21:48
/usr/local/mysql/data/test2/user#P#p1.ibd
-rw-r-----. 1 mysql mysql 98304 Sep 16 21:49
/usr/local/mysql/data/test2/user#P#p2.ibd
-rw-r-----. 1 mysql mysql 98304 Sep 16 21:49
/usr/local/mysql/data/test2/user#P#p3.ibd
-rw-r-----. 1 mysql mysql 98304 Sep 16 21:49
/usr/local/mysql/data/test2/user#P#p4.ibd
mysql> select count(id) as count from user;// 给查询结果指定列名。
+-------+
| count |
+-------+
| 14 |
+-------+
1 row in set (0.00 sec)
从 information_schema 系统库中的 partitions 表中查看分区信息
bash
mysql> select * from information_schema.partitions where table_schema='test2' and
table_name='user'\G;
从某个分区中查询数据
bash
mysql> select * from test2.user partition(p0);
新增分区
bash
mysql> alter table test2.user add partition (partition partionname values less
than (n));
删除分区 当删除了一个分区,也同时删除了该分区中所有的数据
bash
ALTER TABLE test2.user DROP PARTITION p3;
分区的合并 下面的 SQL ,将 p1 -- p3 合并为 2 个分区 p01-- p02
bash
mysql> alter table test2.user
-> reorganize partition p1,p2,p3 into
-> (partition p01 values less than (8),
-> partition p02 values less than (12)
-> );
[root@localhost ~]# ls -l /usr/local/mysql/data/test2/user*
-rw-r-----. 1 mysql mysql 8614 Sep 16 22:06 /usr/local/mysql/data/test2/user.frm
-rw-r-----. 1 mysql mysql 98304 Sep 16 22:06
/usr/local/mysql/data/test2/user#P#p01.ibd
-rw-r-----. 1 mysql mysql 98304 Sep 16 22:06
/usr/local/mysql/data/test2/user#P#p02.ibd
-rw-r-----. 1 mysql mysql 98304 Sep 16 21:48
/usr/local/mysql/data/test2/user#P#p0.ibd
-rw-r-----. 1 mysql mysql 98304 Sep 16 21:49
/usr/local/mysql/data/test2/user#P#p4.ibd
mysql> select * from test2.user partition(p01);
+----+------+-----+
| id | name | sex |
+----+------+-----+
| 3 | tom3 | 1 |
| 4 | tom4 | 0 |
| 5 | tom5 | 0 |
| 6 | tom6 | 1 |
| 7 | tom7 | 1 |
+----+------+-----+
5 rows in set (0.00 sec)
未分区表和分区表性能测试 创建一个未分区的表
bash
mysql> create table test2.tab1(c1 int,c2 varchar(30),c3 date);
创建分区表,按日期的年份拆分
mysql> CREATE TABLE test2.tab2 ( c1 int, c2 varchar(30) , c3 date )
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 );
注意:最后一行,考虑到可能的最大值 通过存储过程插入100万条测试数据 创建存储过程:
bash
mysql> delimiter $$//指定存储过程结束符
mysql>CREATE PROCEDURE load_part_tab()
begin
declare v int default 0;
while v < 2000000
do
insert into test2.tab1
values (v,'testing partitions',adddate('1995-01-01',
(rand(v)*36520) mod 3652));
set v = v + 1;
end while;
end
$$
注:RAND()函数在0和1之间产生一个随机数,如果一个整数参数N被指定,它被用作种子值。每个种子产生的随机数序列是不同的。
执行存储过程 load_part_tab 向 test2.tab1 表插入数据
bash
mysql> delimiter ; // 注意有空格
mysql> call load_part_tab();
向test2.tab2表中插入数据
mysql> insert into test2.tab2 select * from test2.tab1;
测试SQL性能
mysql> select count(*) from test2.tab1 where c3 > '1995-01-01' and c3 < '1995-12-
31';
+----------+
| count(*) |
+----------+
| 219642 |
+----------+
1 row in set (0.84 sec)
mysql> select count(*) from test2.tab2 where c3 > '1995-01-01' and c3 < '1995-12-
31';
+----------+
| count(*) |
+----------+
| 219642 |
+----------+
1 row in set (0.09 sec)
结果表明分区表比未分区表的执行时间少很多。 通过 explain 语句来分析执行情况
bash
mysql> flush tables;
mysql> explain select count(*) from test2.tab1 where c3 > '1995-01-01' and c3 <
'1995-12-31'\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: tab1
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 2001552
filtered: 11.11
Extra: Using where
1 row in set, 1 warning (0.00 sec)
mysql> explain select count(*) from test2.tab2 where c3 > '1995-01-01' and c3 <
'1995-12-31'\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: tab2
partitions: p1
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 220206
filtered: 11.11
Extra: Using where
1 row in set, 1 warning (0.00 sec)
explain 语句显示了 SQL 查询要处理的记录数目可以看出分区表比未分区表的明显扫描的记录要少很多。 创建索引后情况测试
bash
mysql> create index idx_of_c3 on test2.tab1(c3);
Query OK, 0 rows affected (5.07 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> create index idx_of_c3 on test2.tab2(c3);
Query OK, 0 rows affected (4.87 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> flush tables;
mysql> select count(*) from test2.tab1 where c3 > '1996-01-01' and c3 < '1996-12-
31';
+----------+
| count(*) |
+----------+
| 220264 |
+----------+
1 row in set (0.12 sec)
重启 mysqld 服务
bash
mysql> select count(*) from test2.tab2 where c3 > '1996-01-01' and c3 < '1996-12-
31';
+----------+
| count(*) |
+----------+
| 220264 |
+----------+
1 row in set (0.11 sec)
: