Skip to content

水平分表 - 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亿行: 几乎不可用

具体问题:

  1. 查询慢: B+Tree从3层→5层,IO次数增加
  2. 索引大: Buffer Pool命中率低
  3. DDL慢: ALTER TABLE锁表数小时
  4. 备份慢: 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_0

Python实现:

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%
查询响应时间2000ms10ms200倍
TPS30005000016倍
DDL耗时8小时5分钟-99%

最佳实践 ​

1. 分表数量选择 ​

推荐公式:
分表数量 = 预期最大数据量 / 单表合理行数

例如:
- 预期3年数据: 10亿行
- 单表合理行数: 1000万行
- 分表数量: 10亿 / 1000万 = 100张

建议取2的幂次: 64、128、256

2. 分片键选择 ​

好的分片键:
✓ 高频查询字段
✓ 数据分布均匀
✓ 基数大(不同值多)
✓ 写多读少

常见选择:
- 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: 初始版本,讲解水平分表核心原理与实践

Released under MIT License.