水平分表 - Horizontal Partitioning详解
定义
水平分表 (Horizontal Partitioning/Sharding) 是一种数据库横向扩展技术,它将一张包含海量数据的表,按照某种规则(如ID哈希、时间范围、地域等)拆分成多张结构完全相同的子表。每张子表只包含原表的一部分数据,但具有相同的表结构。
核心特征
| 特征 | 说明 |
|---|---|
| 拆分维度 | 按行(Row)拆分,而非按列 |
| 表结构 | 所有子表结构完全相同 |
| 数据分布 | 每条记录只属于一个子表 |
| 适用场景 | 单表>1000万行、高并发写入 |
| 拆分策略 | Hash分表、Range分表、List分表 |
与垂直分表对比
垂直分表 (Vertical Partitioning)
• 按列拆分 • 减少每行宽度 • 优化IO效率
users_core (id, username, email)
users_profile (id, bio, avatar)
水平分表 (Horizontal Partitioning)
• 按行拆分 • 减少每表行数 • 分散写入压力
orders_0 (id=1,101,201...)
orders_1 (id=2,102,202...)
orders_2 (id=3,103,203...)为什么需要水平分表?
问题1: 单表数据量过大
sql
-- 订单表,每天100万单,一年3.6亿行
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
amount DECIMAL(10,2),
status TINYINT,
created_at DATETIME,
INDEX idx_user (user_id)
);
-- 运行3年后: 10亿+行, 500GB+性能退化:
- 1000万行: 性能开始下降
- 5000万行: 明显变慢
- 1亿行: 严重性能问题
- 10亿行: 几乎不可用
具体问题:
- 查询慢: B+Tree从3层→5层,IO次数增加
- 索引大: Buffer Pool命中率低
- DDL慢: ALTER TABLE锁表数小时
- 备份慢: mysqldump需要几十小时
问题2: 写入瓶颈
单实例MySQL写入能力:
- TPS上限: 约5000~10000
- 超过此值: CPU满载、磁盘IO饱和、锁竞争加剧
业务需求:
- 电商平台双11: 50,000 TPS
- 社交应用消息: 100,000 TPS
- IoT传感器数据: 500,000 TPS
→ 单机无法满足!水平分表策略
策略1: Hash分表(最常用)
原理: 对分片键进行哈希运算,根据哈希值决定数据存储在哪张表
sql
-- 计算目标表:
table_index = HASH(user_id) % table_count
-- 假设4张表:
user_id = 1001 → HASH(1001) % 4 = 1 → orders_1
user_id = 1002 → HASH(1002) % 4 = 2 → orders_2
user_id = 1003 → HASH(1003) % 4 = 3 → orders_3
user_id = 1004 → HASH(1004) % 4 = 0 → orders_0Python实现:
python
import hashlib
class OrderShardingService:
def __init__(self, table_count=16):
self.table_count = table_count
self.tables = [f"orders_{i}" for i in range(table_count)]
def get_table_name(self, user_id):
"""根据用户ID获取目标表名"""
hash_value = hashlib.md5(str(user_id).encode()).hexdigest()
index = int(hash_value, 16) % self.table_count
return self.tables[index]
def insert_order(self, user_id, order_data):
"""插入订单"""
table_name = self.get_table_name(user_id)
sql = f"INSERT INTO {table_name} (user_id, ...) VALUES (%s, ...)"
execute_sql(sql, (user_id, ...))
def get_user_orders(self, user_id, page=1, size=20):
"""查询用户订单"""
table_name = self.get_table_name(user_id)
sql = f"""
SELECT * FROM {table_name}
WHERE user_id = %s
ORDER BY created_at DESC
LIMIT %s OFFSET %s
"""
return execute_sql(sql, (user_id, size, (page-1)*size))优点:
- ✓ 数据分布均匀
- ✓ 写入负载均衡
- ✓ 易于扩展
缺点:
- ✗ 跨表查询困难
- ✗ 无法范围查询
- ✗ 扩容需要重新哈希
策略2: Range分表(时间范围)
原理: 按照字段的取值范围进行分表
sql
-- 按月分表
orders_2024_01: created_at BETWEEN '2024-01-01' AND '2024-01-31'
orders_2024_02: created_at BETWEEN '2024-02-01' AND '2024-02-29'
orders_2024_03: created_at BETWEEN '2024-03-01' AND '2024-03-31'Python实现:
python
from datetime import datetime
class TimeRangeSharding:
def get_table_name(self, created_at):
"""根据时间获取分表名"""
if isinstance(created_at, str):
created_at = datetime.fromisoformat(created_at)
# 按月分表
return f"orders_{created_at.strftime('%Y_%m')}"
def query_orders(self, start_date, end_date, user_id=None):
"""时间范围查询"""
# 计算涉及的分表
tables = self.get_tables_in_range(start_date, end_date)
results = []
for table_name in tables:
sql = f"SELECT * FROM {table_name} WHERE created_at BETWEEN %s AND %s"
params = [start_date, end_date]
if user_id:
sql += " AND user_id = %s"
params.append(user_id)
results.extend(execute_sql(sql, params))
return results优点:
- ✓ 适合时间序列数据
- ✓ 范围查询高效
- ✓ 便于归档历史数据(直接DROP旧表)
- ✓ 扩容简单(创建新表即可)
缺点:
- ✗ 数据可能不均匀
- ✗ 热点表问题(当前月数据最多)
- ✗ 跨范围查询需要UNION
策略3: List分表(枚举值)
原理: 按照枚举值或离散值列表分表
sql
-- 按地域分表
orders_beijing: city_id IN (1, 2, 3)
orders_shanghai: city_id IN (4, 5, 6)
orders_guangzhou: city_id IN (7, 8, 9)
-- 按业务类型分表
orders_retail: order_type = 'retail'
orders_wholesale: order_type = 'wholesale'优点:
- ✓ 业务语义清晰
- ✓ 便于按维度统计分析
缺点:
- ✗ 数据分布可能极不均匀
- ✗ 新增枚举值需要新建表
分表中间件
ShardingSphere(推荐)
配置示例:
yaml
spring:
shardingsphere:
rules:
sharding:
tables:
orders:
actual-data-nodes: ds$->{0..1}.orders_$->{0..15}
table-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: user-hash
sharding-algorithms:
user-hash:
type: INLINE
props:
algorithm-expression: orders_$->{Math.abs(user_id.hashCode()) % 16}使用方式(对应用透明):
java
@Mapper
public interface OrderMapper {
// 自动路由到对应的分表
@Select("SELECT * FROM orders WHERE user_id = #{userId}")
List<Order> selectByUserId(@Param("userId") Long userId);
// 自动插入到正确的分表
@Insert("INSERT INTO orders (user_id, amount) VALUES (#{userId}, #{amount})")
void insert(Order order);
}实际案例
案例: 电商平台订单系统
背景:
- 日均订单: 200万
- 年订单量: 7亿
- 峰值TPS: 15,000
分表方案:
sql
-- 复合分表: 按时间范围 + 用户哈希
-- 每月16张表
orders_2024_01_0 ~ orders_2024_01_15
orders_2024_02_0 ~ orders_2024_02_15
...
-- 路由算法:
table_name = "orders_" + YYYY_MM + "_" + (HASH(user_id) % 16)效果对比:
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 单表行数 | 7亿 | 450万 | -99% |
| 查询响应时间 | 2000ms | 10ms | 200倍 |
| TPS | 3000 | 50000 | 16倍 |
| DDL耗时 | 8小时 | 5分钟 | -99% |
最佳实践
1. 分表数量选择
推荐公式:
分表数量 = 预期最大数据量 / 单表合理行数
例如:
- 预期3年数据: 10亿行
- 单表合理行数: 1000万行
- 分表数量: 10亿 / 1000万 = 100张
建议取2的幂次: 64、128、2562. 分片键选择
好的分片键:
✓ 高频查询字段
✓ 数据分布均匀
✓ 基数大(不同值多)
✓ 写多读少
常见选择:
- user_id (用户相关)
- order_id (订单相关)
- device_id (IoT设备)3. 跨表查询优化
sql
-- 避免跨表查询的设计
-- ❌ 糟糕: 需要扫描所有分表
SELECT SUM(amount) FROM orders WHERE created_at > '2024-01-01';
-- ✓ 优秀: 建立统计表
CREATE TABLE order_stats_daily (
stat_date DATE,
total_amount DECIMAL(15,2),
order_count INT,
PRIMARY KEY (stat_date)
);
-- 实时维护统计数据
INSERT INTO order_stats_daily
VALUES (CURDATE(), 100000, 5000)
ON DUPLICATE KEY UPDATE
total_amount = total_amount + VALUES(total_amount),
order_count = order_count + VALUES(order_count);参考资料
相关术语
技术框架
版本历史:
- 2026-04-12: 初始版本,讲解水平分表核心原理与实践