一、垂直和水平分表
中大型项目中,一旦遇到数据量比较大,小伙伴应该都知道就应该对数据进行拆分了。有垂直和水平两种。
在数据库设计和优化中,垂直切分和水平切分是两种常用的数据分片技术。
它们用于处理大规模数据集,以提高性能、可扩展性和管理效率。
以下是对这两种切分策略的详细解释:
1.1、垂直拆分
(Vertical Partitioning)
定义:垂直切分是将一个表的不同列分成多个表的过程。每个新表只包含原表的一部分列,但所有新表共享相同的主键。这样可以根据实际访问需求来优化数据访问性能。
描述:垂直分表是将一个表中的不同列拆分到多个表中,每个表包含部分列。这样做的目的是将表的不同功能分开,以减少表的宽度(列数),提高查询效率,并减少磁盘I/O。
垂直拆分比较简单,也就是本来一个数据库,数据量大之后,从业务角度进行拆分多个库。如下图,独立的拆分出订单库和用户库。

使用场景:
表中列的访问模式不同,比如有些列访问频繁,有些列访问较少。
将大的表按功能划分成更小的表,以提高查询效率和管理性。
应用场景:
数据访问优化:当某些查询只访问表中的部分列时,可以通过垂直切分减少不必要的数据读取,从而提高查询性能。
列的访问模式:当某些列的访问频率与其他列有显著差异时,可以将这些列分开,以提高缓存效率和 I/O 性能。
表的宽度优化:减少表的宽度可以减轻缓存和 I/O 压力,特别是当表包含很多列时。
示例: 假设有一个用户表 users,包含用户的基本信息和账户信息。可以将其垂直分表为:
users_basic:包含用户 ID、用户名、密码等基本信息。
users_account:包含用户 ID、账户余额、账户状态等账户信息。
示例:假设有一个用户信息表 User:
CREATE TABLE User (
UserID INT PRIMARY KEY,
Name VARCHAR(100),
Email VARCHAR(100),
PasswordHash CHAR(64),
Address TEXT,
PhoneNumber VARCHAR(20)
);
可以将其垂直切分为:
CREATE TABLE UserBasicInfo (
UserID INT PRIMARY KEY,
Name VARCHAR(100),
Email VARCHAR(100)
);
CREATE TABLE UserSecurity (
UserID INT PRIMARY KEY,
PasswordHash CHAR(64)
);
CREATE TABLE UserContact (
UserID INT PRIMARY KEY,
Address TEXT,
PhoneNumber VARCHAR(20)
);
这样,频繁访问基本信息的查询只涉及 UserBasicInfo 表,从而减少了不必要的 I/O。
1.2、水平拆分
(Horizontal Partitioning)
定义:水平切分是将一个表的不同数据行分成多个表的过程。每个新表包含原表的一部分行,而表的结构(列)保持一致。常用于数据量过大时,通过将数据分布到不同的物理存储中来提高性能和管理效率。
描述:水平分表是将表中的行按某种规则(例如ID范围)拆分到多个表中。每个表包含表的所有列,但只包含一部分的行。水平分表的目的是将大表拆分成多个小表,以分散负载和提高性能。
水平拆分的概念,是同一个业务数据量大之后,进行水平拆分。

上图中订单数据达到了 4000 万,我们也知道 mysql 单表存储量推荐是百万级,如果不进行处理,mysql 单表数据太大,会导致性能变慢。
使用方案可以参考数据进行水平拆分。把 4000 万数据拆分 4 张表或者更多。当然也可以分库,再分表;把压力从数据库层级分开。
使用场景:
数据量非常大,单表性能无法满足需求。
数据访问量大,单表的负载过高。
应用场景:
数据量管理:当表的数据量达到一定规模,导致性能下降时,可以通过水平切分将数据分布到多个表中,提高查询和维护效率。
负载均衡:将数据分布到不同的数据库实例或服务器上,以实现负载均衡和高可用性。
归档和历史数据管理:可以将历史数据和近期数据存储在不同的表中,方便归档和历史数据管理。
示例: 假设有一个订单表 orders,可以按订单 ID 范围将数据分表为:
orders_1:存储订单 ID 在 1 到 10000 之间的记录。
orders_2:存储订单 ID 在 10001 到 20000 之间的记录。
orders_3:存储订单 ID 在 20001 到 30000 之间的记录。
示例:假设有一个订单表 Orders:
CREATE TABLE Orders (
OrderID INT PRIMARY KEY,
UserID INT,
OrderDate DATE,
Amount DECIMAL(10, 2)
);
可以将其水平切分为:
CREATE TABLE Orders_2021 (
OrderID INT PRIMARY KEY,
UserID INT,
OrderDate DATE,
Amount DECIMAL(10, 2)
);
CREATE TABLE Orders_2022 (
OrderID INT PRIMARY KEY,
UserID INT,
OrderDate DATE,
Amount DECIMAL(10, 2)
);
在实际操作中,可以使用范围切分(如按年份分表)或哈希切分(如按用户 ID 的哈希值分表)来决定数据如何分配到不同的表中。
1.3、垂直切分 vs 水平切分
垂直切分:
优点
减少不必要的列访问,提高查询效率,优化缓存和 I/O 性能。
性能优化:减少了单表的列数,使得查询时只需访问相关列,减少了数据的加载和处理时间。
维护方便:将不同功能的字段分开,便于对特定功能进行优化和维护。
缺点
增加了表之间的关联复杂性,可能需要更多的连接操作,增加了数据的维护复杂度。
查询复杂性:涉及到需要联合查询的操作时,需要通过 JOIN 操作将多个表的数据合并,这可能导致性能下降。
设计复杂:需要仔细设计分表策略,以确保表之间的数据一致性和完整性。
水平切分:
优点
处理大规模数据时提高性能,支持更好的数据分布和负载均衡,方便数据的归档和管理。
负载均衡:将数据分散到多个表中,可以减轻单表的负担,提高性能。
扩展性:可以方便地增加新表,以处理更多的数据和请求。
缺点
查询可能涉及多个表,增加了查询的复杂性和开销,需要处理数据的分片和分布问题。
管理复杂:需要处理分表的逻辑,如数据路由、数据一致性等。
跨表查询:执行涉及多个分表的查询时,需要额外的处理逻辑。
垂直切分适用于优化列访问模式和减少数据宽度的场景。
水平切分适用于处理大数据量和负载均衡的场景。
在设计和实施这些切分策略时,需要根据具体的业务需求和数据库系统的特性进行选择和调整。通过合理的切分策略,可以显著提高数据库的性能和可扩展性。
1.4、分表的跨表查询
跨表查询是指在多个分表中进行查询、合并和处理数据。这在使用水平分表时尤其常见。
问题:
性能影响:跨表查询可能会涉及到多个表的连接、聚合和过滤操作,这可能会对性能产生影响。
复杂性:需要额外的逻辑来处理数据的聚合和合并。
解决方案:
分表策略设计:合理设计分表规则和数据分布,减少跨表查询的频率和复杂度。
中间件支持:使用分布式数据库中间件来处理跨表查询,自动处理分表的逻辑和性能优化。
数据冗余:在一些情况下,可以使用数据冗余来减少跨表查询的需要,例如在不同表中存储相关的数据以便于更高效的查询。
示例: 假设有订单表 orders 和订单明细表 order_items,需要查询某个用户的所有订单及其明细。可以通过以下步骤实现跨表查询:
在 orders表 中查询该用户的所有订单。
根据订单 ID 在 order_items 表中查询相关的订单明细。
将查询结果进行合并,返回给用户。
在实际应用中,处理跨表查询时需要考虑性能和复杂性,尽量减少跨表查询的频率,并优化查询效率。
二、hash 取模和 range 范围
分库分表方案中有常用的方案,hash 取模和 range 范围方案;分库分表方案最主要就是路由算法,把路由的 key 按照指定的算法进行路由存放。
下边来介绍一下两个方案的特点。
2.1、hash 取模方案

在我们设计系统之前,可以先预估一下大概这几年的订单量,如:4000 万。每张表我们可以容纳 1000 万,也我们可以设计 4 张表进行存储。
那具体如何路由存储的呢?hash 的方案就是对指定的路由key(如:id)对分表总数进行取模,上图中,id=12 的订单,对 4 进行取模,也就是会得到 0,那此订单会放到 0 表中。id=13 的订单,取模得到为 1,就会放到 1 表中。为什么对 4 取模,是因为分表总数是 4。
优点:订单数据可以均匀的放到那 4 张表中,这样此订单进行操作时,就不会有热点问题。
热点的含义:热点的意思就是对订单进行操作集中到 1 个表中,其他表的操作很少。
订单有个特点就是时间属性,一般用户操作订单数据,都会集中到这段时间产生的订单。如果这段时间产生的订单都在同一张订单表中,那就会形成热点,那张表的压力会比较大。
缺点:将来的数据迁移和扩容,会很难。如:业务发展很好,订单量很大,超出了 4000 万的量,那我们就需要增加分表数。如果我们增加 4 个表,一旦我们增加了分表的总数,取模的基数就会变成 8,以前 id=12 的订单按照此方案就会到 4 表中查询,但之前的此订单时在 0 表的,这样就导致了数据查不到。就是因为取模的基数产生了变化。
遇到这个情况,我们小伙伴想到的方案就是做数据迁移,把之前的 4000 万数据,重新做一个 hash 方案,放到新的规划分表中。也就是我们要做数据迁移。这个是很痛苦的事情。有些小公司可以接受晚上停机迁移,但大公司是不允许停机做数据迁移的。
当然做数据迁移可以结合自己的公司的业务,做一个工具进行,不过也带来了很多工作量,每次扩容都要做数据迁移。
那有没有不需要做数据迁移的方案呢,我们看下面的方案。
2.2、range 范围方案
range 方案也就是以范围进行拆分数据。

range 方案比较简单,就是把一定范围内的订单,存放到一个表中;如上图 id=12 放到 0 表中,id=1300 万的放到 1 表中。
设计这个方案时就是前期把表的范围设计好。通过 id 进行路由存放。
优点: 我们小伙伴们想一下,此方案是不是有利于将来的扩容,不需要做数据迁移。即使再增加 4 张表,之前的 4 张表的范围不需要改变,id=12 的还是在 0 表,id=1300 万的还是在 1 表,新增的 4 张表他们的范围肯定是 大于 4000 万之后的范围划分的。
缺点: 有热点问题,我们想一下,因为 id 的值会一直递增变大,那这段时间的订单是不是会一直在某一张表中,如 id=1000万 ~ id=2000 万之间,这段时间产生的订单是不是都会集中到此张表中,这个就导致 1 表过热,压力过大,而其他的表没有什么压力。
总结:
hash 取模方案:没有热点问题,但扩容迁移数据痛苦
range 方案:不需要迁移数据,但有热点问题。
2.3、混合方案
那有什么方案可以做到两者的优点结合呢?即不需要迁移数据,又能解决数据热点的问题呢?
其实还有一个现实需求,能否根据服务器的性能以及存储高低,适当均匀调整存储呢?
hash 是可以解决数据均匀的问题,range 可以解决数据迁移问题,那我们可以不可以两者相结合呢?利用这两者的特性呢?
我们考虑一下数据的扩容代表着,路由 key(如 id)的值变大了,这个是一定的,那我们先保证数据变大的时候,首先用 range 方案让数据落地到一个范围里面。这样以后id 再变大,那以前的数据是不需要迁移的。
但又要考虑到数据均匀,那是不是可以在一定的范围内数据均匀的呢?因为我们每次的扩容肯定会事先设计好这次扩容的范围大小,我们只要保证这次的范围内的数据均匀是不是就 ok 了。
我们先定义一个 group 组概念,这组里面包含了一些分库以及分表,如下图

上图有几个关键点:
id=0~4000 万肯定落到 group01 组中
group01 组有 3 个 DB,那一个 id 如何路由到哪个 DB?
根据 hash 取模定位 DB,那模数为多少?模数要为所有此 group 组 DB 中的表数,上图总表数为 10。为什么要去表的总数?而不是 DB 总数 3 呢?
如 id=12,id%10=2;那值为 2,落到哪个 DB 库呢?这是设计是前期设定好的,那怎么设定的呢?
一旦设计定位哪个 DB 后,就需要确定落到 DB 中的哪张表呢?

按照上面的流程,我们就可以根据此规则,定位一个 id,我们看看有没有避免热点问题。
我们看一下,id 在【0,1000万】范围内的,根据上面的流程设计,1000 万以内的 id 都均匀的分配到 DB_0,DB_1,DB_2 三个数据库中的 Table_0 表中,为什么可以均匀,因为我们用了 hash 的方案,对 10 进行取模。
上面我们也提了疑问,为什么对表的总数 10 取模,而不是 DB 的总数 3 进行取模?我们看一下为什么 DB_0 是 4 张表,其他两个 DB_1 是 3 张表?
在我们安排服务器时,有些服务器的性能高,存储高,就可以安排多存放些数据,有些性能低的就少放点数据。如果我们取模是按照 DB 总数 3,进行取模,那就代表着【0,4000万】的数据是平均分配到 3 个 DB 中的,那就不能够实现按照服务器能力适当分配了。
按照 Table 总数 10 就能够达到,看如何达到

上图中我们对 10 进行取模,如果值为【0,1,2,3】就路由到 DB_0,【4,5,6】路由到 DB_1,【7,8,9】路由到 DB_2。现在小伙伴们有没有理解,这样的设计就可以把多一点的数据放到 DB_0 中,其他 2 个 DB 数据量就可以少一点。DB_0 承担了 4/10 的数据量,DB_1 承担了 3/10 的数据量,DB_2 也承担了 3/10 的数据量。整个Group01 承担了【0,4000万】的数据量。
注意:小伙伴千万不要被 DB_1 或 DB_2 中 table 的范围也是 0~4000 万疑惑了,这个是范围区间,也就是 id 在哪些范围内,落地到哪个表而已。
上面一大段的介绍,就解决了热点的问题,以及可以按照服务器指标,设计数据量的分配。
其实上面设计思路理解了,扩容就已经出来了;那就是扩容的时候再设计一个 group02 组,定义好此 group 的数据范围就 ok 了。

因为是新增的一个 group01 组,所以就没有什么数据迁移概念,完全是新增的 group 组,而且这个 group 组照样就防止了热点,也就是【4000 万,5500 万】的数据,都均匀分配到三个 DB 的 table_0 表中,【5500万~7000 万】数据均匀分配到 table_1 表中。
思路确定了,设计是比较简单的,就 3 张表,把 group,DB,table 之间建立好关联关系就行了。
group 和 DB 的关系

table 和 db 的关系

mysql 第6.5章 性能优化-大表拆分