Skip to content

PostgreSQL 垂直拆分与水平拆分

核心概念

数据库拆分是将大型数据库拆分为多个较小的数据库或表,以提高系统的扩展性和性能。主要分为两种类型:

垂直拆分(Vertical Splitting)

垂直拆分是指将表按列拆分到不同的表或数据库中,也称为"列拆分"。

  • 拆分依据:根据列的访问频率和业务逻辑
  • 适用场景:表中包含大量不常用列,或不同列组有不同的访问模式
  • 优势
    • 减少单表宽度,提高查询性能
    • 可以针对不同列组优化存储和索引
    • 便于独立扩展和维护

水平拆分(Horizontal Splitting)

水平拆分是指将表按行拆分到不同的表或数据库中,也称为"行拆分"。

  • 拆分依据:根据行的某个关键字段(如ID、时间、地区等)
  • 适用场景:单表数据量过大(通常超过1000万行)
  • 优势
    • 分散数据存储,减少单表数据量
    • 提高并发处理能力
    • 便于分布式部署

垂直拆分实现方案

1. 表垂直拆分

示例:用户表垂直拆分

原始表结构

sql
CREATE TABLE users (
    user_id SERIAL PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(100) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    nickname VARCHAR(50),
    avatar_url VARCHAR(255),
    bio TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    last_login_at TIMESTAMP,
    is_active BOOLEAN DEFAULT true,
    -- 以下是不常用的扩展信息
    phone VARCHAR(20),
    address TEXT,
    birthday DATE,
    occupation VARCHAR(100),
    interests TEXT[]
);

垂直拆分后

sql
-- 核心信息表(高频访问)
CREATE TABLE users_core (
    user_id SERIAL PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(100) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    nickname VARCHAR(50),
    avatar_url VARCHAR(255),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    last_login_at TIMESTAMP,
    is_active BOOLEAN DEFAULT true
);

-- 扩展信息表(低频访问)
CREATE TABLE users_extended (
    user_id INTEGER PRIMARY KEY REFERENCES users_core(user_id) ON DELETE CASCADE,
    phone VARCHAR(20),
    address TEXT,
    birthday DATE,
    occupation VARCHAR(100),
    interests TEXT[]
);

-- 创建视图,方便查询完整用户信息
CREATE VIEW users AS
SELECT 
    uc.user_id,
    uc.username,
    uc.email,
    uc.password_hash,
    uc.nickname,
    uc.avatar_url,
    uc.created_at,
    uc.updated_at,
    uc.last_login_at,
    uc.is_active,
    ue.phone,
    ue.address,
    ue.birthday,
    ue.occupation,
    ue.interests
FROM users_core uc
LEFT JOIN users_extended ue ON uc.user_id = ue.user_id;

2. 数据库垂直拆分

数据库垂直拆分是将不同业务模块的数据存储到不同的数据库实例中。

示例:电商系统数据库垂直拆分

  • 用户数据库:存储用户相关数据
  • 商品数据库:存储商品相关数据
  • 订单数据库:存储订单相关数据
  • 支付数据库:存储支付相关数据

实现方法

  1. 数据迁移

    sql
    -- 1. 在目标数据库创建表结构
    -- 2. 从源数据库导出数据
    pg_dump -h 源数据库IP -p 5432 -U 用户名 -d 源数据库 -t 表名 -f 导出文件.sql
    
    -- 3. 导入数据到目标数据库
    psql -h 目标数据库IP -p 5432 -U 用户名 -d 目标数据库 -f 导出文件.sql
  2. 应用层改造

    • 修改数据库连接配置
    • 调整数据访问逻辑,处理跨数据库关联
    • 实现分布式事务处理(如使用Seata等中间件)

水平拆分实现方案

1. 基于分区表的水平拆分

PostgreSQL 10+ 支持原生分区表,是实现水平拆分的常用方案。

按时间范围分区

sql
-- 创建订单表,按订单日期范围分区
CREATE TABLE orders (
    order_id SERIAL,
    customer_id INT NOT NULL,
    order_date DATE NOT NULL,
    total_amount DECIMAL(10,2) NOT NULL,
    PRIMARY KEY (order_id, order_date)
) PARTITION BY RANGE (order_date);

-- 创建年度分区
CREATE TABLE orders_2023 PARTITION OF orders
    FOR VALUES FROM ('2023-01-01') TO ('2024-01-01');

CREATE TABLE orders_2024 PARTITION OF orders
    FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');

-- 设置自动创建分区(PostgreSQL 13+)
CREATE OR REPLACE FUNCTION create_order_partition()
RETURNS TRIGGER AS $$
BEGIN
    EXECUTE format(
        'CREATE TABLE IF NOT EXISTS orders_%s PARTITION OF orders FOR VALUES FROM (''%s-01-01'') TO (''%s-01-01'')',
        EXTRACT(YEAR FROM NEW.order_date),
        EXTRACT(YEAR FROM NEW.order_date),
        EXTRACT(YEAR FROM NEW.order_date) + 1
    );
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER create_order_partition_trigger
BEFORE INSERT ON orders
FOR EACH ROW
EXECUTE FUNCTION create_order_partition();

按哈希值分区

sql
-- 创建用户表,按用户ID哈希分区
CREATE TABLE users (
    user_id SERIAL PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(100) NOT NULL UNIQUE
) PARTITION BY HASH (user_id);

-- 创建4个哈希分区
CREATE TABLE users_0 PARTITION OF users
    FOR VALUES WITH (MODULUS 4, REMAINDER 0);

CREATE TABLE users_1 PARTITION OF users
    FOR VALUES WITH (MODULUS 4, REMAINDER 1);

CREATE TABLE users_2 PARTITION OF users
    FOR VALUES WITH (MODULUS 4, REMAINDER 2);

CREATE TABLE users_3 PARTITION OF users
    FOR VALUES WITH (MODULUS 4, REMAINDER 3);

2. 基于中间件的水平拆分

使用数据库中间件(如 ShardingSphere、MyCAT 等)实现水平拆分。

示例:ShardingSphere 水平拆分配置

yaml
# server.yaml
rules:
  - !SHARDING
    tables:
      orders:
        actualDataNodes: ds${0..1}.orders_${0..3}
        tableStrategy:
          standard:
            shardingColumn: order_id
            shardingAlgorithmName: order_table_hash
        databaseStrategy:
          standard:
            shardingColumn: customer_id
            shardingAlgorithmName: order_db_hash
    shardingAlgorithms:
      order_table_hash:
        type: HASH_MOD
        props:
          sharding-count: 4
      order_db_hash:
        type: HASH_MOD
        props:
          sharding-count: 2

3. 应用层水平拆分

在应用层实现数据的拆分和路由逻辑。

示例:Python 应用层水平拆分

python
# 示例:基于用户ID哈希的水平拆分
import hashlib
import psycopg2

# 数据库连接配置
DB_CONFIGS = [
    {
        'host': 'db-server-0',
        'port': 5432,
        'dbname': 'app_db',
        'user': 'app_user',
        'password': 'app_password'
    },
    {
        'host': 'db-server-1',
        'port': 5432,
        'dbname': 'app_db',
        'user': 'app_user',
        'password': 'app_password'
    }
]

# 获取数据分片索引
def get_shard_index(user_id, num_shards=2):
    return user_id % num_shards

# 获取数据库连接
def get_db_connection(user_id):
    shard_index = get_shard_index(user_id)
    return psycopg2.connect(**DB_CONFIGS[shard_index])

# 插入用户数据
def insert_user(user_data):
    # 生成用户ID(使用分布式ID生成器)
    user_id = generate_distributed_id()
    user_data['user_id'] = user_id
    
    # 获取对应分片的数据库连接
    conn = get_db_connection(user_id)
    
    # 执行插入操作
    cursor = conn.cursor()
    cursor.execute(
        "INSERT INTO users (user_id, username, email, password_hash) VALUES (%s, %s, %s, %s)",
        (user_data['user_id'], user_data['username'], user_data['email'], user_data['password_hash'])
    )
    conn.commit()
    cursor.close()
    conn.close()
    
    return user_id

# 查询用户数据
def get_user(user_id):
    # 获取对应分片的数据库连接
    conn = get_db_connection(user_id)
    
    # 执行查询操作
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM users WHERE user_id = %s", (user_id,))
    result = cursor.fetchone()
    cursor.close()
    conn.close()
    
    return result

最佳实践

垂直拆分最佳实践

  1. 拆分原则

    • 将高频访问的列和低频访问的列分开
    • 将大字段(如TEXT、BYTEA)与小字段分开
    • 按照业务逻辑将相关列分组
  2. 注意事项

    • 保持主外键关系,使用级联操作确保数据一致性
    • 创建视图方便查询完整数据
    • 考虑使用触发器自动维护关联数据
    • 避免过度拆分,导致复杂的关联查询
  3. 性能优化

    • 对拆分后的表分别优化索引
    • 根据访问模式调整缓存策略
    • 考虑使用列式存储优化分析型查询

水平拆分最佳实践

  1. 拆分键选择

    • 选择访问频率高的字段
    • 避免热点数据
    • 考虑数据分布均匀性
    • 优先选择数字类型字段,便于哈希计算
  2. 拆分数量

    • 考虑数据增长趋势
    • 考虑节点资源
    • 建议初始拆分数量为节点数的 2-4 倍
    • 预留足够的扩展空间
  3. 一致性保障

    • 避免跨分片事务,如需跨分片事务,使用分布式事务中间件
    • 实现最终一致性方案
    • 定期验证数据一致性
  4. 查询优化

    • 避免全表扫描,尽量在单个分片内完成查询
    • 使用分片键作为查询条件
    • 考虑使用读写分离

常见问题处理

  • 问题1:垂直拆分后关联查询性能下降 解决方法:

    • 创建适当的索引
    • 使用视图简化查询
    • 考虑使用缓存减少关联查询
    • 调整应用层查询逻辑,减少关联次数
  • 问题2:水平拆分后跨分片查询复杂 解决方法:

    • 优化查询设计,尽量使用分片键
    • 使用数据库中间件简化跨分片查询
    • 考虑使用只读副本处理分析型查询
    • 实现数据聚合服务
  • 问题3:数据迁移和扩容困难 解决方法:

    • 设计时考虑未来扩容,预留足够的分片数量
    • 使用一致性哈希算法减少扩容时的数据迁移
    • 实现在线数据迁移工具
    • 定期进行扩容测试
  • 问题4:分片键选择不当导致数据倾斜 解决方法:

    • 重新评估分片键选择
    • 使用复合分片键
    • 实现动态分片调整
    • 考虑使用范围分片结合哈希分片

常见问题(FAQ)

Q1:如何选择垂直拆分还是水平拆分?

A1:根据业务特点选择:

  • 垂直拆分:适合表字段多、不同列组访问频率差异大的场景
  • 水平拆分:适合单表数据量过大、并发访问高的场景
  • 实际应用中,经常结合使用两种拆分方式

Q2:垂直拆分后如何处理关联查询?

A2:关联查询处理方案:

  • 使用 JOIN 语句直接关联查询
  • 创建视图简化查询
  • 应用层两次查询,手动关联
  • 使用缓存减少关联查询次数

Q3:水平拆分后如何实现全局唯一ID?

A3:全局唯一ID生成方案:

  • 使用UUID:简单但查询性能差
  • 使用雪花算法(Snowflake):适合分布式环境
  • 使用数据库自增ID:通过设置不同的起始值和步长
  • 使用Redis生成ID:性能高但依赖外部系统

Q4:如何处理跨分片事务?

A4:跨分片事务处理方案:

  • 尽量避免跨分片事务
  • 使用分布式事务中间件(如Seata)
  • 采用最终一致性方案
  • 使用消息队列实现异步处理

Q5:如何监控拆分后的数据库性能?

A5:关键监控指标:

  • 各分片的查询延迟和吞吐量
  • 数据分布均匀性
  • 跨分片查询比例
  • 事务成功率
  • 资源使用率(CPU、内存、磁盘)

Q6:拆分后如何进行数据备份和恢复?

A6:备份恢复策略:

  • 分别备份各个分片
  • 实现跨分片的数据一致性检查
  • 定期进行恢复测试
  • 考虑使用增量备份减少备份时间

Q7:如何评估拆分效果?

A7:拆分效果评估指标:

  • 查询响应时间是否降低
  • 并发处理能力是否提升
  • 资源使用率是否优化
  • 系统扩展性是否增强
  • 维护成本是否可控