跳至主要內容

表设计及测试

pptg大约 5 分钟

1. 需求

项目需要存储对接的第三方接口数据。对应每个用户每天会产生50个分类的日、周、月、年三种统计,也就是每个用户每天会产生200条记录。如果是1w用户的话,那么每天就是150w条

项目原表如下,已经过脱敏处理:

create table record
(
    id                  bigint auto_increment primary key,
    user_id             varchar(100)     not null comment '用户id',
    type                varchar(30)      not null comment '分类的标签',
    cycle               int(8)           not null comment '统计周期日、周、月',
    value               int(8)           not null comment '统计值',
    -- 其他字段(创建、修改时间等)
)

在上述结构中:

  • type:即50个分类,会有50个不重复的标签
  • cycle:即日、周、月、年的统计,其中1代表日,2代表周,3代表月,4代表年
  • value:统计值,范围是(0-9),即表示:“某人某天在某个分类上的统计值”

2. 优化

我们的项目组之前是接触不到这么大的数据的,隔壁的项目组经理总是说500w就是上限,虽然在我的认知上远远不止如此,但毕竟talk is cheap

对于原来的表结构,如果是1w用户的话,每天会在数据库中插入150w的数据,以先前部门的经验来说表超过500万速度就很慢了(这里其实并不是绝对的,后面的测试环节有提及)

这里我提出的优化方法是将type、cycle和value压缩成矩阵:

create table record
(
    id                  bigint auto_increment primary key,
    user_id             varchar(100)     not null comment '用户id',
    data                char(200)        not null comment '每日统计' 
    -- 其他字段(创建、修改时间等)
)

比如说,原表某用户某天的150条数据如下(省略了id和user_id):

(type,  cycle, value) 
(分类1 , 1    , 2    )
(分类2 , 2    , 4    )
(分类3 , 3    , 1    )
(分类3 , 4    , 8    )
...
(分类50, 1    , 3    )
(分类50, 2    , 2    )
(分类50, 3    , 8    )
(分类50, 4    , 7    )

在新的表中存储如下:

(data)
(2418...3287)

其中分类i的日、周、月、年统计值分别在data(i-1)*4+1(i-1)*4+2(i-1)*4+3(i-1)*4+4

这样不但让记录的总数缩小了200倍;而且在单行的字节上,即使额外添加一些字段也不会超过0.5kb

3. 测试

那么,对于这样的结构,mysql的表究竟能存储到多少才导致性能下降呢?

为了考虑到未来可能出现的更多的冗余数据,所以这里的数据量以单条1kb测试

-- 测试环境
-- CentOS 7、32G、24核、x86
-- MySQL 8.0.33 Docker部署 默认配置

-- 创建表如下
create table record(
     id             bigint not null auto_increment primary key ,
     tenant_id      bigint,
     user_id        bigint,
     data           char(200),
     test_1         char(200),
     test_2         char(200),
     test_3         char(200),
     create_time    datetime,
     update_time    datetime
);

这里直接给测试的冗余字段用的char(因为varchar没有实际插入的话,还是不会占空间的),分别测试了:

  • count:select count(1) from record;
  • 主键索引查询:select count(1) from record where id = 'xxxxxxx';
  • 全表遍历:select count(1) from record where record.data = 'xxxxxx';, 这里record.data不是索引,所以会触发全表遍历
  • 单条末尾插入

理论预估存储性能瓶颈:

  • 非叶子节点存储索引约1280条
  • 叶子节点存储数据页16KB,去除页目录等数据1KB,实际能存储15行
  • 若三层B+树即:(1280)^2*15=24576000,即约24.5M

执行时间(ms)结果如下:

数据量(M)count主键索引查询全表遍历单条末尾插入
111781532595221
220071354652178
328751136892189
437491008637208
5446712311424128
7614310715125135
9837311220074145
121068913725010159
14128249632317167
161435813334445139
191841714443603149
222012812348467155
252301014555727141
304883115288283156
35101735166129489163

这里主要的性能差距在count和全表遍历上,对其画图如下:

count
count
全表遍历
全表遍历

可以看出在超过25M时,数据库的性能确实受到了较大影响,但在25M以下的时候,仍然是随着数据的增长而线性增长的。

4. 性能下降的原因

  1. B+树从3层索引变为4层索引

当B+树从3层变成4层时,理论上是会影响索引的效率,从原来的3次索引IO变成了4次索引IO,最后再进行一次页内查找。但我觉得没道理会影响到count和全表遍历的效率

  1. Buffer Pool导致内存不足

MySQL会将查找的数据缓存到Buffer Pool中,也就是从磁盘IO变成了内存读取。如果内存空间不足以继续缓存数据的话,则会导致后续的查找都会去磁盘中进行IO,进而降低速度。

这里发现MySQL已经将内存的buff/cache全部吃满了,所以导致了效率的降低

# docker stats
CONTAINER ID   NAME    CPU %    MEM USAGE / LIMIT     MEM %     NET I/O           BLOCK I/O        PIDS
c34923bad950   mysql   0.83%    14.1GiB / 31.26GiB    45.11%    78.6kB / 1.38MB   162GB / 82.3GB   44

# free -h
              total        used        free      shared  buff/cache   available
Mem:            31G        3.7G        202M         85M         27G         27G
Swap:           15G        295M         15G

最后的话,我认为如果支撑MySQL的内存条足够大,是可以进一步提升数据量的。当然,这属于是力大飞砖了。还是该分库分库,该Redis缓存就缓存吧。