经常听到有人说“数据表太大了,需要分表”,“xxxx了,要分表”的言论,那么,到底为什么要分表?
难道数据量大就要分表?
mysql数据量对索引的影响
本人mysql版本为5.7
新增数据测试
为了测试mysql索引查询是否和数据量有关,本人做了以下的测试准备:
新建4个表article1,article2,article3,article4,article5 每个表分别插入20万,50万,100万,200万,1500万的数据,数据都是随机生成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
create
table
test.article1(
id
int
auto_increment comment
'id'
primary
key
,
user_id
int
not
null
comment
'用户id'
,
title
varchar
(64)
not
null
comment
'标题'
,
add_time datetime
null
comment
'新增时间'
,
update_time
int
null
comment
'更新时间'
,
description
varchar
(255)
null
comment
'简介'
,
status tinyint(1)
null
comment
'状态 1正常 0隐藏'
)
charset = utf8;
create
index
article_title_index
on
test.article1 (title);
|
生成数据脚本,使用easyswoole,多协程插入:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
<?php
include
"./vendor/autoload.php"
;
\EasySwoole\EasySwoole\Core::getInstance()->initialize();
for
(
$i
= 0;
$i
<= 2000;
$i
++) {
//协程最多3000,创建1000个协程
go(
function
()
use
(
$i
) {
\App\Utility\Pool\MysqlPool::invoke(
function
(\App\Utility\Pool\MysqlPoolObject
$mysqlObject
)
use
(
$i
) {
for
(
$y
= 0;
$y
<= 1000;
$y
++) {
//每个协程插入100条数据
$data
= [
'user_id'
=> mt_rand(1, 2500),
'title'
=> \EasySwoole\Utility\Random::character(32),
//随机生成32位字母的标题
'add_time'
=>
date
(
'Y-m-d H:i:s'
, mt_rand(
strtotime
(
'2018-01-01'
),
strtotime
(
'2019-01-01'
))),
//随机生成日期
'update_time'
=> mt_rand(
strtotime
(
'2018-01-01'
),
strtotime
(
'2019-01-01'
)),
//随机生成日期
'description'
=> getChar(mt_rand(8, 64)),
//随机生成8-64位汉字,
'status'
=> mt_rand(0, 1),
];
$mysqlObject
->insert(
'article2'
,
$data
);
}
echo
"协程$i 插入完成\n"
;
}, -1);
});
}
function
getChar(
$num
)
// $num为生成汉字的数量
{
$b
=
''
;
for
(
$i
= 0;
$i
<
$num
;
$i
++) {
// 使用chr()函数拼接双字节汉字,前一个chr()为高位字节,后一个为低位字节
$a
=
chr
(mt_rand(0xB0, 0xD0)) .
chr
(mt_rand(0xA1, 0xF0));
// 转码
$b
.= iconv(
'GB2312'
,
'UTF-8'
,
$a
);
}
return
$b
;
}
|
生成的数据如图:
数据库总条数预览:
1
|
select
(
select
count
(1)
from
article1)
as
"1"
, (
select
count
(1)
from
article2)
as
"2"
, (
select
count
(1)
from
article3)
as
"3"
, (
select
count
(1)
from
article4)
as
"4"
, (
select
count
(1)
from
article5)
as
"5"
;
|
查询时间测试
查询脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
<?php
/**
* Created by PhpStorm.
* User: tioncico
* Date: 19-5-11
* Time: 下午7:20
*/
include
"./vendor/autoload.php"
;
\EasySwoole\EasySwoole\Core::getInstance()->initialize();
go(
function
() {
/**
* @var $db \App\Utility\Pool\MysqlPoolObject
*/
$db
= \App\Utility\Pool\MysqlPool::defer();
$startTime
= microtimeFloat();
//查询1000次
for
(
$i
= 0;
$i
< 10000;
$i
++) {
$str
=\EasySwoole\Utility\Random::character(32);
//随机生成字符串,用于查询
$data
=
$db
->where(
'title'
,
$str
)->getOne(
'article1'
);
}
echo
"1耗时"
. (microtimeFloat() -
$startTime
) .
'秒'
.PHP_EOL;
$startTime
= microtimeFloat();
//查询1000次
for
(
$i
= 0;
$i
< 10000;
$i
++) {
$str
=\EasySwoole\Utility\Random::character(32);
//随机生成字符串,用于查询
$data
=
$db
->where(
'title'
,
$str
)->getOne(
'article2'
);
}
echo
"2耗时"
. (microtimeFloat() -
$startTime
) .
'秒'
.PHP_EOL;
$startTime
= microtimeFloat();
//查询1000次
for
(
$i
= 0;
$i
< 10000;
$i
++) {
$str
=\EasySwoole\Utility\Random::character(32);
//随机生成字符串,用于查询
$data
=
$db
->where(
'title'
,
$str
)->getOne(
'article3'
);
}
echo
"3耗时"
. (microtimeFloat() -
$startTime
) .
'秒'
.PHP_EOL;
$startTime
= microtimeFloat();
//查询1000次
for
(
$i
= 0;
$i
< 10000;
$i
++) {
$str
=\EasySwoole\Utility\Random::character(32);
//随机生成字符串,用于查询
$data
=
$db
->where(
'title'
,
$str
)->getOne(
'article4'
);
}
echo
"4耗时"
. (microtimeFloat() -
$startTime
) .
'秒'
.PHP_EOL;
$startTime
= microtimeFloat();
//查询1000次
for
(
$i
= 0;
$i
< 10000;
$i
++) {
$str
=\EasySwoole\Utility\Random::character(32);
//随机生成字符串,用于查询
$data
=
$db
->where(
'title'
,
$str
)->getOne(
'article5'
);
}
echo
"5耗时"
. (microtimeFloat() -
$startTime
) .
'秒'
.PHP_EOL;
});
function
microtimeFloat()
{
list(
$usec
,
$sec
) =
explode
(
" "
, microtime());
return
((float)
$usec
+ (float)
$sec
);
}
|
该脚本是一个实例脚本,在后面的其他测试中依旧使用该脚本,修改下字段和逻辑
title全索引查询一条时间情况:(为了准确,本人运行了多次)
可以看出,数据量在200万以下时,查询时间几乎没有差别,只是在数据量1400万时,查询1万次的时间增加了1秒
注:本人在之前测试,和之后测试时,查询article5时时间大概是2.1-2.5秒左右,可能mysql有其他知识点本人未掌握,所以没法详细解释
title全索引查询不限制条数时间情况:(为了准确,本人运行了多次)
可以看出,在200万数据之前 查询时间并没有太大的差距,1400万有一点点的差距
title like 左前缀 索引查询不限制条数时间情况:(为了准确,本人运行了多次)
根据这次测试,我们可以发现
1:mysql的查询和数据量的大小关系并不大(微乎其微)
2:mysql只要是命中索引,不管数据量有多大,都会非常快(快的一批,由于本人比较懒,并且本人之前也测试过单表1.5亿速度一样很快,就懒得继续新增2亿测试数据了,太累)
什么情况需要分表
从上面的章节可以发现,数据量的多少和查询速度其实关系不是很大,那么为什么要分表呢?原因有以下几种:
1: 单表 不涉及索引的操作太多,无法直接命中索引的
2:模糊查找范围过大,无法直接命中索引的,例如日志表查时间区间
3:单表数据量过大,操作繁忙的
4:数据量过大,有大部分数据很少访问的(冷热数据)
5:装逼,需要用分表装逼的
分表优缺点
在上面,我们已经知道了为什么要分表,分表该怎么分呢?
首先,我们需要先搞懂分表的意义
数据分表有着以下好处:
1:分散表压力,使其响应速度提高
2:数据降维,提升查询速度
3:分冷热数据,更好管理,备份
4:支持分布式部署数据库,将压力分担到其他服务器中
同时,缺点如下:
1:分表之后较难管理多表
2:join表时可能需要join多个
3:查询模糊数据时需要全部的表一起查
所以,数据量不大时候,不建议分表。
水平分表
根据数据的不同规则作为一个分表条件,区分数据以数据之间的分表叫做水平分表
水平分表是比较常见的分表方法,也是解决数据量大时候的分表方法,在水平分表中,也根据场景的不同而分表方法不同
取模分表
假设有个用户表(1000w用户)需要分表,那么我们可以根据该用户表的唯一标识(id ,用户账号)进行取模分表
重新新建n个表。例如5个, user1,user2,user3....uesr5
取出所有用户,根据 用户账号进行取模,例如:
1
2
3
4
5
6
|
<?php
$userAccount
=
'tioncico'
;
$num
= (crc32(
$userAccount
)%5);
$tableName
=
'user'
.(
$num
+1);
echo
"{$userAccount}应该存储到{$tableName}表"
;
//tioncico应该存储到user3表
|
不建议使用id分表,因为一般情况下,我们是使用账号,或者其他唯一标识 来进行区分某个人的,如果你表设计像qq号一样,那完全可以将id命名为其他的字段,用于区分,自增id同样需要
取模分表法会使数据尽量的均衡分布,压力均衡,非常适合于需要通过特定标识字段查找数据的表(会员表)
冷热数据分表
冷热数据大多数体现在跟时间有关的 日志表,订单表上面
在冷热数据分表时,我们应该遵循以下几种分表规则
1:数据冷热分表,需要注意冷热数据的界限
例如,商城订单表,每天增加100万的订单,一年就会增加到3.6亿的订单数,而大多数情况下,用户只会查询近1-3个月的数据,我们可以
通过订单时间进行分表,只需要按照月份进行分表即可
2:通过取模分表,需要注意取模字段,
垂直分表
区分一条数据的不同字段,叫做垂直分表
垂直分表其实我们在设计数据库时,可能已经是用到了的,比如会员金额表,关联会员表的userId,这个时候,其实就可以叫做是垂直分表
把会员金额的字段分到了其他的表中(会员金额表)
垂直分表较为简单,有以下几种分法:
1:字段意义和表其他字段意义不同,可以尝试分表
2:字段占用空间太大,不常用或只在特定情况使用,可以尝试分表
3:字段与其他字段更新时间不同,可以尝试分表