想象你管理的不是数据库,而是一座藏书过亿的图书馆仓库。书被随意堆在一起,读者报出书名,管理员只能从头翻到尾,找一本花两小时,整个仓库怨声载道。后来馆长给仓库砌起隔断:按年代分房间、按类别分房间,或者用盖章机把新书均匀摊到几间房里。读者再来找书,直奔对应房间,翻找时间从两小时变成两分钟。数据库分区表干的就是这件事:当一张表的数据量从百万冲到千万、上亿,全表扫描就像翻遍整个仓库;分区表把大表按规则拆成多个物理分区,逻辑上还是同一张表,查询却能精准落在少数分区上。这篇文章就用这座“图书馆仓库”当主线,带你搞懂范围分区、列表分区、哈希分区怎么选,分区键怎么设计,裁剪如何生效,以及日常怎么维护。读完你也能为海量数据表设计出又快又省心的分区方案。
分区表是海量数据下数据库优化的核心武器,掌握它能让查询提速数倍到数十倍,是后端开发工程师与 DBA 进阶的必备技能。
可视化演示
本节配有交互动效(进入视口后自动加载,约 3KB)。
三种分区策略的数据分布对比 范围分区按连续值区间划分数据,适合时间序列等有序数据的高效范围查询 列表分区按离散枚举值分组,适合按地区或类别等维度组织数据的场景 哈希分区通过哈希函数均匀分散数据,有效缓解数据倾斜并支持并行查询 hash(key) mod N = partition_id 选择与查询模式匹配的分区策略是分区表设计的关键
分区表概述
市图书馆的仓库里堆着几千万本书,管理员找一本《数据库之美》要从入口翻到尽头,两小时过去书没找到,读者先崩溃了。仓库越大,翻找越慢。一张数据表从百万行涨到千万、上亿行时,数据库的处境一模一样——全表扫描的成本随行数线性膨胀,索引层变深、内存命中率下降,一次简单查询都能拖到秒级。
单表数据量过大会带来三重难题:查询慢、管理难、备份难。想删百万行历史数据要逐条 DELETE,想加索引要重建整个表,备份文件膨胀到难以处理。面对这些问题,数据库工程师发现解决办法不是堆更强的机器,而是像馆长那样给仓库“砌隔断”——这就是分区表诞生的背景。
分区表在逻辑上仍是一张表,应用端的 SQL 几乎无需改动;物理上,数据按分区规则被拆成若干独立存储的片段,每个片段叫作一个分区(Partition)。常见规则有三种:范围分区、列表分区、哈希分区。查询时如果条件命中了分区规则,优化器就只访问相关分区,扫描量成倍缩减。
仓库里所有书架加起来还是那几千万本书,但隔断让“找书”从线性翻找变成“先定位房间、再找书架”。分区的价值同理:它不是减少数据,而是减少“无意义翻看的数据”。带着这个直觉,我们去看三种最常见的隔断方式。
- 数据总量$D_{\text{total}}$ 代表整张表参与扫描的总行数,对应仓库里所有书的数量。
- 分区个数$n_{\text{parts}}$ 表示把表切成了几个物理分区,对应仓库被隔成了几间房。
- 裁剪后扫描量$D_{\text{pruned}}$ 表示只查一个分区时需要扫描的行数,约为总量的 $1/n_{\text{parts}}$。
范围分区
馆长按出版年代把仓库分成一间间房:「1980 年代房」「1990 年代房」「2000 年代房」「2010 年代房」。新书一到,看出版年份放对应房间;读者要找 2015 年的书,只进 2010 年代房。这就是范围分区最朴素的样子——按一个连续的区间来决定每一行的归属。
在数据库里,范围分区(RANGE Partitioning)按照分区键的值落在哪个区间,决定行进入哪个分区。它是最常用的一种分区方式,特别适合订单、日志、流水这类天然带时间属性的数据:记录按时间持续生成,区间划分清晰,查询也常带时间范围条件,天然能对上裁剪。
以 MySQL 为例,范围分区用 VALUES LESS THAN 定义边界,每个分区是一个“左闭右开”的区间,例如 p2021 存放 2020 年(含)到 2021 年(不含)的数据。设计时要为边界留好余量,并配上最后一个 MAXVALUE 兜底分区,否则超出边界的新数据会因无处安放而直接报错。
年代房间的灵感来自“给数据排序”。数据是历史的堆积品,时间是最自然的隔断;当读者问“最近十年有什么新书”,你立刻知道该去哪个房间,而不是把建馆以来的书全翻一遍。范围分区正是把这种直觉变成了数据库规则。
- 第 i 个分区$\text{P}_{i}$ 表示第 i 个范围分区,相当于第 i 个年代房间,总共有 $n$ 个。
- 成员条件集合中的记录 $r$ 必须满足分区键 $\text{key}(r)$ 的值落入某个区间,才会被放进这个分区。
- 左闭右开下界可以取到、上界取不到,保证相邻两个分区之间没有任何一行会同时属于两边。
列表分区
另一家书店不问年代,而是按类型隔房间:科幻房、历史房、艺术房、文学房。每本书贴上唯一的类型标签,进门就放进对应房间。读者只关心科幻小说,就直奔科幻房——这是列表分区的日常。
列表分区(LIST Partitioning)按分区键的离散取值来划分配置:每个分区绑定一个取值集合,例如 VALUES IN ('华东','华北')。它适合地区、业务类型、状态码这类枚举值场景。比如全国订单按「华东、华北、华南、西部」四个大区划分,一张大表就变成四间“地区房”。
列表分区要求不同分区的取值集合互不重叠,且每个取值必须落入某个分区;设计时通常加一个 DEFAULT 兜底分区,承接将来可能出现的未知值。相比范围分区,列表分区更适合“按类别查”而非“按区间查”——例如按城市查门店、按渠道查订单转化,分区数量等于枚举类别数,直观可控。
类别房间的标签是有限的几类,新书到时先看类别再看房间,管理效率极高。但前提是类别体系得稳定——一旦标签体系频繁重构,房间隔断也要跟着大动干戈。所以列表分区适合那些“枚举值固定、分类清楚”的业务。
- 取值集合$L_j$ 是第 j 个列表分区绑定的所有合法取值,相当于房间门牌上贴的类别清单。
- 互不重叠任意两个分区的取值集合交集为空,一行数据只可能属于一个分区,不会左右横跳。
- 全集覆盖所有取值集合的并集等于全部合法取值的集合 $U$,没被任何分区接住的取值要交给兜底分区。
哈希分区
新书仓库没有明显的时间或类别特征,书号连续,新书上架时总是扎堆堆在入口。馆长想出一个办法:在门口放一台“盖章机”,按书号对 8 取余,余数为 0 进 0 号房,余数为 1 进 1 号房……每本书进门就被均匀分流到 8 个房间,再热门的批次也不会挤在同一间屋里。
这就是哈希分区(HASH Partitioning):先对分区键做哈希运算,再对分区总数取模,得到分区编号。它不关心数据的自然分布,用计算换均匀。适合那些分区键没有明显区间或类别规律、但写入量巨大且要求打散的场景,例如按用户 ID 分布的用户行为表。
哈希分区的分区数在创建时就固定下来,后续扩缩容通常需要重新映射全部数据,代价较高。它的弱点是无法高效做范围裁剪:查询指定某个用户 ID 时能直接定位分区,但查询“某个时间区间”就必须遍历所有分区,因为时间根本不是分布依据。
盖章机让“随机入库”变成“均匀入库”。哈希分区用同样的思路,把热点写入均匀摊到每个房间。但房间数量像固定摊位,将来想增加摊位,要把所有书重新盖一遍章——所以选它之前,必须对未来规模心里有数。
- 分区键$\text{key}$ 是参与哈希计算的列,例如 user_id、order_id,对应书上盖的号码。
- 哈希函数$\text{hash}(\cdot)$ 把任意长度的键值映射成一个整数,相当于盖章机吐出的随机数。
- 取模对分区总数 $n$ 取余数,把整数映射到 $[0, n-1]$ 的范围内。
- 分区编号$p$ 是余数,决定这行进哪个分区,取值范围从 0 号到 $n-1$ 号。
分区键设计
不管房间怎么隔,最怕的是隔错了。馆长发现读者大多按「书名」找书,仓库却按「封皮颜色」分区——结果每本书都得翻遍所有房间才能找到。分区键就是决定“隔法”的那个核心标尺,选错了一切白搭。
分区键设计的第一原则:选择高频查询中稳定出现的过滤列。对订单表而言,绝大多数查询带时间条件,于是选 order_time;对用户维度查询多的表,则要围绕用户标识设计。其次,要避免单次查询跨越多个分区——一次查询涉及的分区越多,裁剪收益越小,性能越接近全表扫描。
如果查询条件里根本没有分区键,优化器就无法裁剪,只能全分区扫描,分区形同虚设;如果分区键基数太低(比如性别),数据会严重倾斜到少数几个分区。另外,分区键的值原则上应不可修改——一旦某行的键值变更,这一行就要在物理上“搬家”,从一个分区挪到另一个分区,代价极高。
好的分类要问读者怎么找书。分区键不是“数据库需要什么”,而是“业务怎么查”。把查询模式当作读者动线,分区键才能让每间房都精准承接对应的需求——这也是分区设计里最考验经验的一步。
- 涉及分区集合$S$ 是一次查询需要访问的分区编号集合,理想情况下它只包含 1 个元素。
- 分区行数$|\text{P}_i|$ 表示第 i 个分区里的数据行数,相当于每个房间的藏书量。
- 总代价累加把涉及分区的行数全部加起来,就是这次查询要付出的扫描代价。
分区裁剪
现在仓库入口有位值班管理员,他手上有张“房间地图”。读者一报需求,他立刻判断“这本书在 2010 年代房”,然后刷卡开门带你直奔目标;其他房间连门都不用开。这个动作,就是数据库里的分区裁剪。
分区裁剪(Partition Pruning)是查询优化器的一项优化:执行 SQL 时,优化器根据 WHERE 条件里的分区键,提前把不可能命中的分区排除掉,只扫描真正有数据的分区。裁剪发生在执行计划生成阶段,对应用完全透明——你写的是普通 SQL,优化器偷偷帮你做了“只进一间房”的判断。
判断裁剪是否生效,最直接的方法是看执行计划:MySQL 的 EXPLAIN 会列出实际访问的分区列表。如果结果显示全部分区都在列表中,说明裁剪失败,要检查 WHERE 条件是否包含分区键、分区键是否被函数包裹导致无法推导。另外注意:裁剪解决“进哪个房间”,房间内的精确定位仍要靠普通索引,两者是叠加关系。
值班管理员的价值不在跑得快,而在“少跑”。裁剪让查询从“翻遍整个仓库”变成“只进一间房”,扫描成本从 O(总行数) 降到 O(单分区行数)——这正是分区表存在的全部理由:不是让硬盘更快,而是让数据库少干活。
- 全表扫描行数$\text{Scan}_{\text{full}}$ 是不做分区时将要读取的总行数,即翻遍整个仓库的工作量。
- 除以分区数$\frac{\text{Scan}_{\text{full}}}{n}$ 是理想情况下只扫一个分区的工作量。
- 近似相等约等于号表示真实值还受数据倾斜、索引回表等因素影响,但量级上已大幅缩小。
维护策略
仓库不是建好就一劳永逸:每年要为新书加一间「2026 年代房」,过时的旧书要整体搬去冷库,读者变少的相邻房间要合并。分区维护,就是这套日常运营。
对范围分区来说,时间驱动的数据管理是最大红利:新增分区只需 ADD PARTITION 加一个未来边界;清理三年前的历史数据,直接 DROP PARTITION 即可,不必逐行 DELETE。相比普通表几百万行的 DELETE 要写日志、要锁表,分区删除是近乎瞬间的元数据操作。
维护还包括 REORGANIZE 拆分过大的分区(例如把全年分区拆成季度分区)、合并过小的分区、监控各分区大小与增长趋势,以及定期检查执行计划确认查询仍在走裁剪。冷热数据借此分层:热分区放高性能存储,冷分区归档或直接删除,让仓库永远只装该装的书。
会运营的馆长才能让仓库永远干净高效:及时加新房、果断搬旧书、定期盘点房间大小。分区表不是一次性设计,而是与数据增长节奏同步的生命周期工程——设计定生死,维护定长久。
- 分区行数$\text{rows}(\text{P}_i)$ 是被清理的第 i 个分区中的数据行数,相当于冷库房间的藏书量。
- 平均行宽$\text{row\_size}$ 是一行数据平均占用的存储字节数,对应每本书的平均厚度。
- 总释放空间把每个被清理分区的行数与行宽相乘再求和,就是这次维护释放的总存储空间。
AI 应用场景
场景 1:电商订单表的按月范围分区
阶段:数据建模与存储优化
解决的问题:电商订单表突破亿行后,全表扫描延迟数秒,历史备份耗时,清理旧数据要逐行 DELETE。按订单时间做范围分区后,查询“近 30 天订单”自动裁剪到 1-2 个分区;清理 3 年前数据直接 DROP PARTITION。
具体案例:某电商平台订单表 2 亿行,按月分区后按时间范围查询从平均 3.2 秒降到 120 毫秒;月度整理历史数据从每小时删十万行,变成秒级删除整个分区。
场景 2:用户行为日志的哈希分区
阶段:数据写入与负载均衡优化
解决的问题:用户行为日志没有天然时间聚类,若按范围分区会导致部分时段写入扎堆。按 user_id 哈希分区,把写入均匀打散到 16 个分区,消除单分区写热点,同时支持按用户 ID 快速定位轨迹。
具体案例:某 App 每日写入 5 亿条行为日志,哈希分区后写入吞吐提升 3 倍,按 user_id 查询用户行为轨迹从全表扫描降为单分区检索。
场景 3:多租户 SaaS 的列表分区
阶段:数据隔离与合规管理
解决的问题:多租户系统按行业或区域分区,既隔离了不同业务线的数据,又让区域维度查询只扫本区数据;配合合规要求按区域定期清理,操作清晰可控。
具体案例:某 SaaS 平台按租户行业做列表分区,行业报表查询裁剪到本行业分区,季度合规导出与清除时间从按天计缩短到按分钟计。
Python 演示
import randomimport time def make_orders(n): """生成 n 条模拟订单,年份分布在 2018-2025""" orders = [] for _ in range(n): orders.append({'id': random.randint(1, 10**8), 'year': random.randint(2018, 2025), 'amount': random.randint(1, 1000)}) return orders def partition_key(year, boundaries): """分区键函数:返回 year 所属分区编号,按边界 [b1, b2, ...) 切分""" for i, b in enumerate(boundaries): if year < b: return i return len(boundaries) def build_partitions(orders, boundaries): """构建分区:把每行数据放入对应分区的列表""" parts = {} for r in orders: p = partition_key(r['year'], boundaries) parts.setdefault(p, []).append(r) return parts def query_full_scan(orders, lo, hi): """全表扫描:遍历所有行,统计年份落在 [lo, hi) 的订单""" return sum(1 for r in orders if lo <= r['year'] < hi) def query_with_pruning(parts, boundaries, lo, hi): """分区裁剪:只扫描与 [lo, hi) 有交集的分区""" count = 0 for p, rows in parts.items(): p_lo = boundaries[p - 1] if p > 0 else 0 p_hi = boundaries[p] if p < len(boundaries) else 3000 if p_hi <= lo or p_lo >= hi: continue # 区间无交集,整个分区跳过 count += sum(1 for r in rows if lo <= r['year'] < hi) return count def main(): n = 200000 boundaries = [2020, 2022, 2024] print('生成订单数据...') orders = make_orders(n) parts = build_partitions(orders, boundaries) for lo, hi in [(2018, 2020), (2021, 2023), (2024, 2026)]: t0 = time.perf_counter() r1 = query_full_scan(orders, lo, hi) t1 = time.perf_counter() r2 = query_with_pruning(parts, boundaries, lo, hi) t2 = time.perf_counter() print('区间 [%d, %d): 全表=%d条(%.4fs) 裁剪=%d条(%.4fs)' % (lo, hi, r1, t1 - t0, r2, t2 - t1)) if __name__ == '__main__': main()
def partition_key(year, boundaries):分区键函数:按边界列表返回年份所属分区编号,对应仓库的“房间查找规则”。parts.setdefault(p, []).append(r)把每一行订单放入它所属的分区列表,这就是物理分区的构建过程。if p_hi <= lo or p_lo >= hi: continue裁剪核心:目标区间与某分区的区间没有交集时,整个分区直接跳过,一行业务数据都不看。query_full_scan(orders, lo, hi)返回遍历所有行做条件判断的结果,作为性能基准。parts = build_partitions(orders, boundaries)只构建一次分区结构,之后无数次查询都能复用,对应分区表“建表一次、查询万次”的收益模型。
常见误区
误区大表做了分区后,查询自然就快了,分区可以完全替代索引,不用再建任何索引。
正解分区解决的是“进哪个房间”的问题,索引解决的是“房间内哪本书”的问题。以图书馆为例,分区让你直达 2010 年代房,但房间里还有几十万本书,没有索引导航照样得一本本翻。实际做法是二者叠加:分区裁剪缩小范围,分区内索引精确定位。
为何常见:很多人把“扫描量减少”误当成“查找完全免费”,忽略了分区内部仍然需要数据定位机制。
误区只要能分区就行,任选一列做分区键效果差不多,反正数据都被拆开了。
正解分区键必须与高频查询的 WHERE 条件对应。如果读者总是按书名找书,你却按封皮颜色分区,每本书依然要翻遍所有房间。分区键选错,优化器无法裁剪,查询退化为全分区扫描,性能反而更差。
为何常见:只关注“分没分区”的形式,忽略了分区价值来自“查询条件能否命中分区规则”这个本质。
误区既然裁剪是按分区减少扫描量,那把表分成几千个分区,查询不就会快到飞起吗?
正解分区过多会带来元数据膨胀、文件句柄开销、优化器评估成本上升等问题,很多数据库对分区数量也有上限。以图书馆类比,把书分到几千个微型房间,管理员光查“该进哪间”就要花半天。分区数应该与数据量和查询模式匹配,通常几十到几百个足够。
为何常见:把公式 $D_{\text{total}}/n$ 做了线性外推,忽略了 n 增长带来的管理成本这一侧。
误区哈希分区跟范围分区一样,数据多了随时 ADD PARTITION 加一个新区就行。
正解哈希分区用 hash(key) mod n 决定归属,n 是固定分区数。增加一个分区后,取模基数变了,几乎全部旧数据的映射都要重算,等于把所有书重新盖一遍章,代价极大。范围分区加新区只需加边界,而哈希分区扩缩容通常要重建表。
为何常见:把范围分区的“加边界即加区”经验套用到哈希分区,忽略了取模运算对分区总数的强依赖。
误区表查询慢就做分区,分区后所有 SQL 都会变快,不用再分析其他原因。
正解分区只对“条件包含分区键”的查询有效。如果慢查询的 WHERE 里没有分区键,裁剪根本不生效;如果慢查询缺索引、SQL 写法糟糕,分区也救不了。正确姿势是先看执行计划定位瓶颈,再决定分区是否对症。
为何常见:把分区当成万能优化器,忽略了它只是“减少扫描范围”这一种手段,病因没找对,药自然不灵。
面试题
Q1. 分区表的本质是什么?
- A.按规则拆成多个物理存储单元← 正确答案
- B.将大表复制到多台服务器
- C.给大表添加更多二级索引
- D.将表数据进行压缩存储
答案:A。分区把一张表按规则拆成物理上独立的分区片段,逻辑上仍是一张表,就像把大仓库隔成小房间。复制到多服务器是读写分离,加索引是加速定位,压缩是减少存储,都不是分区的定义。
Q2. 日志表按天不断追加数据,最合适的分区方式是?
- A.范围分区← 正确答案
- B.列表分区
- C.哈希分区
- D.随机分区
答案:A。日志天然带时间属性、按时间持续追加,范围分区按时间区间切分最自然,还能支持时间范围查询裁剪。列表分区适合离散枚举,哈希分区适合均匀打散,都没有利用时间的有序性。
Q3. 哈希分区最主要的优势是什么?
- A.支持高效范围查询
- B.数据均匀避免热点← 正确答案
- C.任意列都可做键值
- D.可动态增加分区
答案:B。哈希分区通过取模运算把数据均匀摊到各个分区,避免某些分区写入集中。范围查询恰恰是它的弱项,分区数固定也不方便动态扩容,所以 B 才是它的核心价值。
Q4. 分区键设计的首要原则是什么?
- A.选择唯一索引主键列
- B.选择高频过滤条件列← 正确答案
- C.选择存储空间较小的列
- D.选择数值类型的主键列
答案:B。分区键必须出现在高频查询的 WHERE 条件里,裁剪才能生效。就像仓库要按读者最常用的找书方式分房间,而不是按书号长短。唯一索引列、小空间列、数值主键列如果不被查询条件命中,选了也白选。
Q5. 分区裁剪(Partition Pruning)指的是什么?
- A.自动删除无用的分区
- B.只扫描相关的目标分区← 正确答案
- C.压缩分区中的存量数据
- D.合并体积较小的分区
答案:B。裁剪是优化器在执行计划阶段排除无关分区的行为,查询只进入有数据的分区。删除分区、压缩数据、合并小分区都是维护操作,与裁剪完全是两回事。
Q6. 下列哪些场景适合使用列表分区?(多选)
- A.按地区维度分组数据← 正确答案
- B.按业务类型枚举切分← 正确答案
- C.连续日期范围查询日志
- D.无限增长的流水序号
答案:AB。列表分区针对离散枚举值,地区、业务类型都有有限且稳定的取值集合,天然适合。连续日期适合范围分区;无限增长的流水序号没有类别语义,不适合列表分区。
Q7. 关于范围分区与哈希分区,下列说法正确的是?
- A.哈希分区支持区间裁剪
- B.范围分区可能产生数据倾斜← 正确答案
- C.哈希分区可任意增加分区
- D.范围分区无法删除旧数据
答案:B。范围分区按区间切分,若某时间段数据暴涨,该分区会明显偏大,这就是数据倾斜。哈希分区用取模定位,不支持区间裁剪;扩分区要重算全部映射;范围分区可以用 DROP PARTITION 删除旧数据。B 正确。
Q8. 以下属于分区表维护操作的有?(多选)
- A.新增分区承接未来数据← 正确答案
- B.删除过期的历史数据分区← 正确答案
- C.拆分或合并调整现有分区← 正确答案
- D.修改表的主键列的类型
答案:ABC。分区维护围绕“分区”本身的增删改:ADD PARTITION、DROP PARTITION、REORGANIZE 拆分合并都属于维护。修改主键列类型是表结构变更,与分区维护无关。
Q9. 订单表按 order_time 范围分区,查询 WHERE user_id = 12345 时无法裁剪分区,根本原因是?
- A.分区键未参与查询条件← 正确答案
- B.user_id 列没有建立索引
- C.分区总数配置得不够多
- D.查询返回的结果集太大
答案:A。裁剪只认分区键。WHERE 里只有 user_id,没有 order_time,优化器无法判断该去哪几个分区,只能全部扫描。索引影响分区内的查找速度,分区数多少不改变“无法裁剪”的事实。
Q10. 关于哈希分区的调整与使用,正确的说法有?(多选)
- A.扩大分区数需要重算旧分布← 正确答案
- B.扩大分区数可直接复用旧映射
- C.固定分区数适合均匀写入场景← 正确答案
- D.哈希分区擅长时间范围裁剪
答案:AC。哈希分区按 hash(key) mod n 路由,n 变化后余数全变,旧数据必须重算分布,所以 B 错 A 对。固定分区数能在创建时把写入均匀摊开,适合打散热点,C 对。哈希分区对时间区间查询无法裁剪,D 错。