返回 筑基・数据元府藏真

表结构设计进阶:数据类型、约束与扩展性

博主
大约 20 分钟

表结构设计进阶:数据类型、约束与扩展性

问题引入:一个主键选择引发的危机

2022年,我们的订单系统上线一年后,数据量突破10亿条。这时我们发现了一个严重问题:

当初的选择:使用 BIGINT AUTO_INCREMENT 作为主键

带来的问题

  • 分库分表后,主键冲突
  • 订单号容易被猜测(连续递增)
  • 跨数据中心同步困难
  • 业务数据暴露风险

紧急方案:迁移到雪花算法ID,耗时3周,风险极高。

这个教训让我深刻认识到:表结构设计的前期决策会影响系统整个生命周期。

现象描述:设计缺陷的连锁反应

主键冲突灾难

-- 分库分表前,单库自增ID
INSERT INTO orders (user_id, amount) VALUES (1, 100.00);
-- 生成订单ID: 1000001

-- 分库分表后,两个库同时插入
-- DB1生成: 1000002
-- DB2生成: 1000002  -- 冲突!

冲突场景

时间DB1DB2结果
T1插入订单A,ID=1001-成功
T2-插入订单B,ID=1001主键冲突!
T3同步到DB2-同步失败

数据类型选择不当的代价

-- 错误:金额使用FLOAT
CREATE TABLE bad_order (
    order_id BIGINT PRIMARY KEY,
    amount FLOAT  -- 精度丢失!
);

INSERT INTO bad_order VALUES (1, 0.1 + 0.2);
SELECT * FROM bad_order;  -- 结果: 0.30000001192092896

-- 正确:金额使用DECIMAL
CREATE TABLE good_order (
    order_id BIGINT PRIMARY KEY,
    amount DECIMAL(10,2)  -- 精确计算
);

INSERT INTO good_order VALUES (1, 0.1 + 0.2);
SELECT * FROM good_order;  -- 结果: 0.30

扩展性不足的痛苦

-- 初期设计:没有预留扩展字段
CREATE TABLE user (
    user_id BIGINT PRIMARY KEY,
    username VARCHAR(50),
    email VARCHAR(100)
    -- 没有扩展字段
);

-- 3个月后需要添加字段
ALTER TABLE user ADD COLUMN phone VARCHAR(20);      -- 耗时5分钟
ALTER TABLE user ADD COLUMN address VARCHAR(200);   -- 耗时8分钟
ALTER TABLE user ADD COLUMN wechat_id VARCHAR(50);  -- 耗时6分钟
-- 表锁期间,业务几乎不可用

原因分析:设计原理与最佳实践

1. 主键策略深度对比

自增ID(AUTO_INCREMENT)

CREATE TABLE auto_inc_table (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50)
);

优点

  • 简单、无需额外代码
  • 有序递增,插入性能高
  • 存储空间小(8字节)

缺点

  • 单点瓶颈,不适合分布式
  • 数据量暴露(容易被猜测总记录数)
  • 分库分表后主键冲突
  • 迁移困难

适用场景:单机、小数据量、无分库分表需求

UUID

CREATE TABLE uuid_table (
    id CHAR(36) PRIMARY KEY,  -- UUID字符串
    name VARCHAR(50)
);

-- 或使用BINARY(16)存储
CREATE TABLE uuid_binary_table (
    id BINARY(16) PRIMARY KEY,
    name VARCHAR(50)
);

优点

  • 全局唯一,去中心化
  • 无需协调,生成简单
  • 数据分布均匀

缺点

  • 无序,插入性能差(随机IO)
  • 存储空间大(36字符或16字节)
  • 索引效率低(页分裂)
  • 可读性差

适用场景:分布式系统、低并发、对性能要求不高

雪花算法(Snowflake)

@Component
public class SnowflakeIdGenerator {
    
    // 起始时间戳(2024-01-01)
    private final long EPOCH = 1704067200000L;
    
    // 位数分配
    private final long WORKER_ID_BITS = 5L;      // 工作节点ID占5位
    private final long DATACENTER_ID_BITS = 5L;  // 数据中心ID占5位
    private final long SEQUENCE_BITS = 12L;      // 序列号占12位
    
    // 最大值
    private final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
    private final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_ID_BITS);
    private final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
    
    // 移位
    private final long WORKER_ID_SHIFT = SEQUENCE_BITS;
    private final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
    private final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS;
    
    private long workerId;
    private long datacenterId;
    private long sequence = 0L;
    private long lastTimestamp = -1L;
    
    public SnowflakeIdGenerator(long workerId, long datacenterId) {
        if (workerId > MAX_WORKER_ID || workerId < 0) {
            throw new IllegalArgumentException("Worker ID out of range");
        }
        if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
            throw new IllegalArgumentException("Datacenter ID out of range");
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }
    
    public synchronized long nextId() {
        long timestamp = System.currentTimeMillis();
        
        if (timestamp < lastTimestamp) {
            throw new RuntimeException("Clock moved backwards");
        }
        
        if (timestamp == lastTimestamp) {
            sequence = (sequence + 1) & SEQUENCE_MASK;
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }
        
        lastTimestamp = timestamp;
        
        return ((timestamp - EPOCH) << TIMESTAMP_SHIFT)
                | (datacenterId << DATACENTER_ID_SHIFT)
                | (workerId << WORKER_ID_SHIFT)
                | sequence;
    }
    
    private long tilNextMillis(long lastTimestamp) {
        long timestamp = System.currentTimeMillis();
        while (timestamp <= lastTimestamp) {
            timestamp = System.currentTimeMillis();
        }
        return timestamp;
    }
}

ID结构

| 1位符号位 | 41位时间戳 | 5位数据中心 | 5位机器ID | 12位序列号 |
|----------|-----------|------------|----------|-----------|
| 0        | 毫秒级时间  | 0-31       | 0-31     | 0-4095    |

优点

  • 趋势递增,插入性能高
  • 全局唯一,支持分布式
  • 生成速度快(每秒409.6万个ID)
  • 包含时间信息,可反解

缺点

  • 依赖时钟,时钟回拨会出问题
  • 需要配置workerId和datacenterId
  • 不是严格连续

适用场景:高并发分布式系统、订单系统、支付系统

业务编码

-- 订单号:年月日 + 用户ID后4位 + 序列号
-- 示例:2024111500010001

CREATE TABLE order_code_table (
    order_no VARCHAR(20) PRIMARY KEY,
    user_id BIGINT,
    amount DECIMAL(10,2)
);

优点

  • 可读性强,包含业务信息
  • 便于人工识别和客服处理
  • 可自定义规则

缺点

  • 设计复杂,规则变更困难
  • 长度不固定
  • 可能暴露业务数据

适用场景:订单号、流水号、需要人工识别的场景

2. 数据类型选择指南

整数类型选择

类型存储空间有符号范围无符号范围适用场景
TINYINT1字节-128~1270~255状态、布尔值
SMALLINT2字节-32768~327670~65535小范围计数
MEDIUMINT3字节-838万~838万0~1677万中等范围
INT4字节-21亿~21亿0~43亿一般主键
BIGINT8字节-922京~922京0~1844京大数据量
-- ❌ 错误:过度设计
CREATE TABLE bad_example (
    id BIGINT,                    -- 明明INT够用
    status INT,                   -- 状态用4字节
    age BIGINT,                   -- 年龄用8字节
    is_active INT                 -- 布尔值用4字节
);

-- ✅ 正确:按需选择
CREATE TABLE good_example (
    id INT UNSIGNED AUTO_INCREMENT,  -- 4字节,支持42亿条
    status TINYINT UNSIGNED,          -- 1字节,0-255足够
    age TINYINT UNSIGNED,             -- 1字节,0-255足够
    is_active TINYINT(1)              -- 1字节,0或1
);

字符串类型选择

类型最大长度存储特点适用场景
CHAR255字符定长,空格填充固定长度(如手机号)
VARCHAR65535字节变长,1-2字节长度前缀可变长度文本
TEXT65535字节离线存储,不缓存大文本
MEDIUMTEXT16MB离线存储文章、日志
LONGTEXT4GB离线存储超大文本
-- ❌ 错误:全部用VARCHAR(255)
CREATE TABLE bad_string (
    phone VARCHAR(255),      -- 手机号固定11位
    email VARCHAR(255),      -- 邮箱最长254字符
    username VARCHAR(255),   -- 用户名限制50字符
    description VARCHAR(255) -- 描述可能需要更长
);

-- ✅ 正确:根据实际限制
CREATE TABLE good_string (
    phone CHAR(11),              -- 定长,效率高
    email VARCHAR(254),          -- RFC标准最大长度
    username VARCHAR(50),        -- 业务限制50字符
    description TEXT             -- 大文本用TEXT
);

金额类型选择

-- ❌ 错误:使用FLOAT/DOUBLE
CREATE TABLE bad_amount (
    price FLOAT,           -- 精度丢失!
    total DOUBLE           -- 精度丢失!
);

-- 精度丢失示例
INSERT INTO bad_amount VALUES (0.1 + 0.2, 123.456);
SELECT * FROM bad_amount;  -- 0.30000001192092896, 123.4560000000000

-- ✅ 正确:使用DECIMAL
CREATE TABLE good_amount (
    price DECIMAL(10,2),   -- 精确到分,最大99999999.99
    total DECIMAL(14,4)    -- 精确到万分之一
);

-- DECIMAL精度说明
-- DECIMAL(M,D): M是总位数,D是小数位数
-- DECIMAL(10,2): 整数8位,小数2位,如 12345678.90
-- DECIMAL(14,4): 整数10位,小数4位,如 1234567890.1234

时间类型选择

类型存储空间范围时区支持适用场景
DATE3字节1000-01-01 ~ 9999-12-31生日、纪念日
TIME3字节-838:59:59 ~ 838:59:59时间段
DATETIME8字节1000-01-01 00:00:00 ~ 9999-12-31 23:59:59一般时间存储
TIMESTAMP4字节1970-01-01 00:00:01 ~ 2038-01-19 03:14:07跨时区应用
YEAR1字节1901 ~ 2155年份
-- ❌ 错误:时间用字符串
CREATE TABLE bad_time (
    created_at VARCHAR(20)  -- 无法计算、比较
);

-- ✅ 正确:使用TIMESTAMP或DATETIME
CREATE TABLE good_time (
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,           -- 自动设置创建时间
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,  -- 自动更新
    birthday DATE,                                             -- 日期类型
    work_start TIME,                                           -- 时间类型
    event_datetime DATETIME                                    -- 完整日期时间
);

3. 软删除设计模式

-- 推荐方案:逻辑删除 + 删除时间 + 删除人
CREATE TABLE user (
    user_id BIGINT PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    email VARCHAR(100),
    
    -- 软删除字段
    is_deleted TINYINT DEFAULT 0 COMMENT '0:正常 1:已删除',
    deleted_at TIMESTAMP NULL COMMENT '删除时间,用于清理',
    deleted_by BIGINT NULL COMMENT '删除人,审计需要',
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    -- 唯一索引需要包含is_deleted
    UNIQUE KEY uk_username (username, is_deleted)
) COMMENT='用户表';

-- 查询时默认过滤已删除
SELECT * FROM user WHERE is_deleted = 0;

-- 软删除操作
UPDATE user SET 
    is_deleted = 1, 
    deleted_at = NOW(), 
    deleted_by = 1001 
WHERE user_id = 1;

-- 恢复操作
UPDATE user SET 
    is_deleted = 0, 
    deleted_at = NULL, 
    deleted_by = NULL 
WHERE user_id = 1;

-- 定期清理(可选)
DELETE FROM user 
WHERE is_deleted = 1 
AND deleted_at < DATE_SUB(NOW(), INTERVAL 90 DAY);

4. 多租户数据隔离方案

方案对比

方案实现方式优点缺点适用场景
独立数据库每个租户一个库隔离性最强,可独立扩展成本高,维护复杂大客户、金融
共享数据库独立Schema每个租户一个Schema隔离性好,资源复用跨租户查询困难中等规模租户
共享表租户ID隔离表中加tenant_id成本低,简单灵活隔离性弱,需要额外过滤SaaS、小租户
混合方案重要租户独立,其他共享平衡成本和隔离性架构复杂综合场景

推荐方案:共享表 + 租户ID + 分区

-- 共享表 + 租户ID + 分区
CREATE TABLE tenant_order (
    order_id BIGINT NOT NULL,
    tenant_id INT NOT NULL COMMENT '租户ID',
    user_id BIGINT NOT NULL,
    total_amount DECIMAL(10,2),
    status TINYINT DEFAULT 1,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    -- 分区键必须是主键或唯一键的一部分
    PRIMARY KEY (order_id, tenant_id),
    KEY idx_tenant_user (tenant_id, user_id),
    KEY idx_tenant_created (tenant_id, created_at)
) ENGINE=InnoDB
PARTITION BY LIST(tenant_id) (            -- 按租户分区
    PARTITION p_tenant_1 VALUES IN (1),
    PARTITION p_tenant_2 VALUES IN (2),
    PARTITION p_tenant_3 VALUES IN (3),
    PARTITION p_tenant_other VALUES IN (4,5,6,7,8,9,10)
) COMMENT='租户订单表';

-- 查询时自动路由到对应分区
SELECT * FROM tenant_order 
WHERE tenant_id = 1 AND order_id = 123;

-- 添加新租户分区
ALTER TABLE tenant_order 
ADD PARTITION (PARTITION p_tenant_11 VALUES IN (11));

应用层租户隔离

@Component
public class TenantContext {
    private static final ThreadLocal<Integer> CURRENT_TENANT = new ThreadLocal<>();
    
    public static void setCurrentTenant(Integer tenantId) {
        CURRENT_TENANT.set(tenantId);
    }
    
    public static Integer getCurrentTenant() {
        return CURRENT_TENANT.get();
    }
    
    public static void clear() {
        CURRENT_TENANT.remove();
    }
}

// MyBatis拦截器自动添加租户过滤
@Intercepts({
    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
    @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
@Component
public class TenantInterceptor implements Interceptor {
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object parameter = args[1];
        
        // 获取当前租户ID
        Integer tenantId = TenantContext.getCurrentTenant();
        if (tenantId != null && parameter instanceof Map) {
            ((Map) parameter).put("tenantId", tenantId);
        }
        
        return invocation.proceed();
    }
}

解决方案:表结构设计最佳实践

1. 主键设计决策树

开始设计主键
    ↓
是否需要分布式部署?
    ↓ 是
数据量预计超过10亿?
    ↓ 是
是否需要包含时间信息?
    ↓ 是
→ 使用雪花算法ID

是否需要人工识别?
    ↓ 是
→ 使用业务编码

单机小数据量?
    ↓ 是
→ 使用自增ID

2. 数据类型选择检查清单

□ 整数:能用TINYINT不用INT,能用INT不用BIGINT
□ 金额:必须使用DECIMAL,禁止使用FLOAT/DOUBLE
□ 时间:使用TIMESTAMP或DATETIME,禁止用字符串
□ 字符串:CHAR定长,VARCHAR变长,TEXT大文本
□ 状态:使用TINYINT,配合注释说明含义
□ 布尔:使用TINYINT(1),不用BOOL(MySQL中BOOL就是TINYINT)

3. 扩展性设计

-- 预留扩展字段
CREATE TABLE extensible_table (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    
    -- 业务字段
    name VARCHAR(50),
    status TINYINT DEFAULT 1,
    
    -- 扩展字段(JSON格式,应对需求变更)
    ext_fields JSON COMMENT '扩展字段',
    
    -- 备用字段(应对紧急需求)
    reserve_1 VARCHAR(100) COMMENT '备用字段1',
    reserve_2 VARCHAR(100) COMMENT '备用字段2',
    reserve_3 INT COMMENT '备用字段3',
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) COMMENT='可扩展表设计示例';

-- 扩展字段使用示例
INSERT INTO extensible_table (name, ext_fields) VALUES (
    '示例',
    '{"source": "web", "campaign": "summer_sale", "referrer": "google"}'
);

-- 查询扩展字段
SELECT 
    name,
    JSON_UNQUOTE(JSON_EXTRACT(ext_fields, '$.source')) as source,
    JSON_UNQUOTE(JSON_EXTRACT(ext_fields, '$.campaign')) as campaign
FROM extensible_table
WHERE JSON_EXTRACT(ext_fields, '$.source') = 'web';

实施步骤:表结构设计流程

步骤1:需求分析

□ 识别核心业务实体
□ 分析数据增长趋势(1年、3年、5年)
□ 确定查询模式(读多写少/读写均衡/写多读少)
□ 明确数据一致性要求
□ 了解数据生命周期(热数据/温数据/冷数据)

步骤2:字段设计

-- 以订单表为例
CREATE TABLE order_design_example (
    -- 主键:雪花算法ID
    order_id BIGINT PRIMARY KEY COMMENT '订单ID,雪花算法生成',
    
    -- 业务字段
    user_id BIGINT NOT NULL COMMENT '用户ID',
    order_no VARCHAR(20) NOT NULL COMMENT '订单编号,业务编码',
    total_amount DECIMAL(12,2) NOT NULL COMMENT '订单总金额',
    status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1-待支付 2-已支付 3-已发货 4-已完成 5-已取消',
    
    -- 时间字段
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    paid_at TIMESTAMP NULL COMMENT '支付时间',
    shipped_at TIMESTAMP NULL COMMENT '发货时间',
    completed_at TIMESTAMP NULL COMMENT '完成时间',
    
    -- 软删除字段
    is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除:0-否 1-是',
    deleted_at TIMESTAMP NULL COMMENT '删除时间',
    
    -- 扩展字段
    ext_info JSON COMMENT '扩展信息',
    reserve_1 VARCHAR(100) COMMENT '备用字段1',
    reserve_2 VARCHAR(100) COMMENT '备用字段2',
    
    -- 索引
    UNIQUE KEY uk_order_no (order_no),
    KEY idx_user_status (user_id, status),
    KEY idx_created_at (created_at),
    KEY idx_status_created (status, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表示例';

步骤3:索引设计

-- 根据查询场景设计索引
-- 场景1:用户查询自己的订单列表
KEY idx_user_created (user_id, created_at);

-- 场景2:按状态查询订单
KEY idx_status_created (status, created_at);

-- 场景3:按订单号查询
UNIQUE KEY uk_order_no (order_no);

-- 场景4:时间范围查询
KEY idx_created_at (created_at);

步骤4:评审检查

□ 主键策略是否合理?
□ 数据类型是否最优?
□ 是否预留扩展字段?
□ 索引是否覆盖主要查询?
□ 是否实现软删除?
□ 字符集是否为utf8mb4?
□ 是否有适当的注释?
□ 是否考虑数据归档策略?

效果验证:优化前后对比

存储空间对比

优化前优化后节省
用户表(100万行)285MB198MB30.5%
订单表(1000万行)2.8GB2.1GB25%
日志表(1亿行)15GB8GB46.7%

查询性能对比

查询类型优化前优化后提升
主键查询2ms1ms50%
范围查询500ms120ms76%
分页查询2s200ms90%

经验总结:表结构设计最佳实践

✅ 应该做的

  1. 主键选择:高并发用雪花算法,小系统用自增ID
  2. 数据类型:能小则小,避免过度设计
  3. 金额字段:必须使用DECIMAL,禁止FLOAT/DOUBLE
  4. 时间字段:使用TIMESTAMP/DATETIME,禁止字符串
  5. 软删除:必须实现,便于数据恢复
  6. 扩展字段:预留JSON字段应对需求变更
  7. 注释规范:每个字段都要有注释
  8. 字符集:统一使用utf8mb4

❌ 不应该做的

  1. 使用UUID作为主键(性能杀手)
  2. 金额用FLOAT/DOUBLE(精度丢失)
  3. 时间用字符串存储(无法计算)
  4. 没有软删除(误删无法恢复)
  5. 字段长度设计过大(浪费空间)
  6. 忽略字符集(乱码问题)
  7. 没有预留扩展字段(需求变更困难)
  8. 过度规范化(查询性能差)

深度案例分析:电商系统表结构重构

项目背景

某电商平台上线3年后,面临以下问题:

  • 订单表数据量达到5亿条
  • 查询性能急剧下降
  • 表结构僵化,无法快速响应需求变更
  • 数据存储成本高昂

原表结构问题

-- 原订单表(设计问题多多)
CREATE TABLE old_order (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,  -- 自增ID,分库冲突
    user_id BIGINT,
    amount FLOAT,                           -- FLOAT精度丢失!
    status INT,                             -- 4字节浪费
    create_time VARCHAR(20),                -- 字符串存时间
    is_delete INT,                          -- 没有删除时间
    ext1 VARCHAR(255),                      -- 无意义扩展字段
    ext2 VARCHAR(255),
    ext3 VARCHAR(255)
) ENGINE=MyISAM;                           -- 旧引擎,不支持事务

重构方案

-- 新订单表
CREATE TABLE new_order (
    -- 主键:雪花算法ID
    order_id BIGINT PRIMARY KEY COMMENT '订单ID',
    
    -- 业务编码:便于人工识别
    order_no VARCHAR(20) NOT NULL COMMENT '订单编号:年月日+用户后4位+序列号',
    
    -- 用户关联
    user_id BIGINT NOT NULL COMMENT '用户ID',
    
    -- 金额:DECIMAL精确计算
    total_amount DECIMAL(12,2) NOT NULL COMMENT '订单总金额',
    discount_amount DECIMAL(10,2) DEFAULT 0.00 COMMENT '优惠金额',
    pay_amount DECIMAL(12,2) NOT NULL COMMENT '实付金额',
    
    -- 状态:TINYINT节省空间
    status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1-待支付 2-已支付 3-已发货 4-已完成 5-已取消',
    pay_type TINYINT COMMENT '支付方式:1-微信 2-支付宝 3-银行卡',
    
    -- 时间:TIMESTAMP类型
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    paid_at TIMESTAMP NULL COMMENT '支付时间',
    shipped_at TIMESTAMP NULL COMMENT '发货时间',
    completed_at TIMESTAMP NULL COMMENT '完成时间',
    
    -- 软删除
    is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除:0-否 1-是',
    deleted_at TIMESTAMP NULL COMMENT '删除时间',
    deleted_by BIGINT COMMENT '删除人ID',
    
    -- 扩展信息:JSON格式
    ext_info JSON COMMENT '扩展信息:收货地址、发票信息等',
    
    -- 备用字段
    reserve_1 VARCHAR(100) COMMENT '备用字段1',
    reserve_2 VARCHAR(100) COMMENT '备用字段2',
    reserve_3 BIGINT COMMENT '备用字段3',
    
    -- 索引
    UNIQUE KEY uk_order_no (order_no),
    KEY idx_user_status_created (user_id, status, created_at),
    KEY idx_status_created (status, created_at),
    KEY idx_created_at (created_at)
    
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表'
PARTITION BY RANGE (YEAR(created_at)) (
    PARTITION p2022 VALUES LESS THAN (2023),
    PARTITION p2023 VALUES LESS THAN (2024),
    PARTITION p2024 VALUES LESS THAN (2025),
    PARTITION p_future VALUES LESS THAN MAXVALUE
);

重构效果

指标重构前重构后提升
存储空间45GB28GB37.8% ↓
主键查询15ms2ms86.7% ↓
用户订单查询800ms80ms90% ↓
数据一致性无保障软删除保障大幅提升

表结构设计决策树

开始设计表结构
    ↓
数据量预计超过1亿?
    ↓ 是
使用雪花算法ID作为主键
    ↓
是否需要分库分表?
    ↓ 是
设计分片键和路由策略
    ↓
是否需要多租户?
    ↓ 是
添加tenant_id字段并分区
    ↓
是否需要软删除?
    ↓ 是
添加is_deleted和deleted_at字段
    ↓
是否需要扩展性?
    ↓ 是
添加ext_info(JSON)和reserve字段
    ↓
完成设计

常见误区与避坑指南

❌ 误区1:主键必须使用自增ID

问题:分布式场景下自增ID会导致冲突。

正确做法:根据场景选择合适的主键策略。

❌ 误区2:所有字段都用最大长度

问题:VARCHAR(255)滥用导致存储浪费。

正确做法:根据业务限制设置合理长度。

❌ 误区3:忽略软删除

问题:误删数据无法恢复,造成业务损失。

正确做法:必须实现软删除机制。

❌ 误区4:不预留扩展字段

问题:需求变更时需要ALTER TABLE,影响线上服务。

正确做法:预留JSON扩展字段和备用字段。

❌ 误区5:使用MyISAM引擎

问题:不支持事务、行锁、外键。

正确做法:统一使用InnoDB引擎。

表结构设计评估模板

技术评估维度

维度权重评分标准得分
存储效率20%数据类型是否合理/10
查询性能25%索引设计是否优化/10
扩展性20%是否预留扩展字段/10
可维护性20%注释是否完整/10
数据安全15%是否有软删除/10
加权总分100%/10

设计检查清单

□ 主键策略是否满足未来3年数据量?
□ 数据类型是否选择最优?
□ 是否有适当的索引支持查询?
□ 是否考虑软删除需求?
□ 是否预留扩展字段?
□ 字符集是否为utf8mb4?
□ 时间字段是否使用TIMESTAMP/DATETIME?
□ 金额是否使用DECIMAL?
□ 外键约束是否在应用层实现?
□ 是否有数据归档策略?
□ 是否设计了分区策略?
□ 是否有适当的注释?

附录

A. 数据类型选择速查表

数据类型使用场景示例
TINYINT状态、布尔值status TINYINT DEFAULT 1
INT一般计数view_count INT DEFAULT 0
BIGINT主键、大数据量user_id BIGINT PRIMARY KEY
DECIMAL金额price DECIMAL(10,2)
VARCHAR可变长度文本username VARCHAR(50)
CHAR固定长度phone CHAR(11)
TEXT大文本content TEXT
TIMESTAMP时间戳created_at TIMESTAMP
DATE日期birthday DATE
JSON扩展信息ext_info JSON

B. 主键生成策略对比表

策略唯一性有序性存储空间性能可读性适用场景
自增ID单机唯一严格有序8字节单机系统
UUID全局唯一无序36字节分布式低并发
雪花算法全局唯一趋势有序8字节高并发分布式
业务编码需保证部分有序可变需要人工识别

C. 表结构评审模板

## 表结构评审记录

### 基本信息
- 表名:
- 业务模块:
- 设计人:
- 评审日期:

### 评审项目
| 检查项 | 结果 | 备注 |
|-------|------|------|
| 主键策略合理 | □通过 □不通过 | |
| 数据类型最优 | □通过 □不通过 | |
| 索引设计合理 | □通过 □不通过 | |
| 预留扩展字段 | □通过 □不通过 | |
| 软删除实现 | □通过 □不通过 | |
| 注释完整 | □通过 □不通过 | |
| 字符集正确 | □通过 □不通过 | |

### 评审结论
□ 通过  □ 需修改

### 修改建议
1.
2.
3.

读者练习

  1. 思考题:分析你项目中的表结构,找出3个可以优化的地方。

  2. 实践题:设计一个用户系统的表结构,包含:用户信息、账户信息、登录记录,要求支持软删除和扩展性。

  3. 挑战题:实现一个雪花算法ID生成器,并解决时钟回拨问题。


系列上一篇数据库设计范式与反范式的实战平衡

系列下一篇高效SQL编写:从基础到高级技巧

知识点测试

读完文章了?来测试一下你对知识点的掌握程度吧!

评论区

使用 GitHub 账号登录后即可发表评论,支持 Markdown 格式。

如果评论系统无法加载,请确保:

  • 您的网络可以访问 GitHub
  • giscus GitHub App 已安装到仓库
  • 仓库已启用 Discussions 功能