返回 筑基・数据元府藏真
表结构设计进阶:数据类型、约束与扩展性
博主
大约 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 -- 冲突!
冲突场景:
| 时间 | DB1 | DB2 | 结果 |
|---|---|---|---|
| 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. 数据类型选择指南
整数类型选择
| 类型 | 存储空间 | 有符号范围 | 无符号范围 | 适用场景 |
|---|---|---|---|---|
| TINYINT | 1字节 | -128~127 | 0~255 | 状态、布尔值 |
| SMALLINT | 2字节 | -32768~32767 | 0~65535 | 小范围计数 |
| MEDIUMINT | 3字节 | -838万~838万 | 0~1677万 | 中等范围 |
| INT | 4字节 | -21亿~21亿 | 0~43亿 | 一般主键 |
| BIGINT | 8字节 | -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
);
字符串类型选择
| 类型 | 最大长度 | 存储特点 | 适用场景 |
|---|---|---|---|
| CHAR | 255字符 | 定长,空格填充 | 固定长度(如手机号) |
| VARCHAR | 65535字节 | 变长,1-2字节长度前缀 | 可变长度文本 |
| TEXT | 65535字节 | 离线存储,不缓存 | 大文本 |
| MEDIUMTEXT | 16MB | 离线存储 | 文章、日志 |
| LONGTEXT | 4GB | 离线存储 | 超大文本 |
-- ❌ 错误:全部用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
时间类型选择
| 类型 | 存储空间 | 范围 | 时区支持 | 适用场景 |
|---|---|---|---|---|
| DATE | 3字节 | 1000-01-01 ~ 9999-12-31 | 否 | 生日、纪念日 |
| TIME | 3字节 | -838:59:59 ~ 838:59:59 | 否 | 时间段 |
| DATETIME | 8字节 | 1000-01-01 00:00:00 ~ 9999-12-31 23:59:59 | 否 | 一般时间存储 |
| TIMESTAMP | 4字节 | 1970-01-01 00:00:01 ~ 2038-01-19 03:14:07 | 是 | 跨时区应用 |
| YEAR | 1字节 | 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万行) | 285MB | 198MB | 30.5% |
| 订单表(1000万行) | 2.8GB | 2.1GB | 25% |
| 日志表(1亿行) | 15GB | 8GB | 46.7% |
查询性能对比
| 查询类型 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 主键查询 | 2ms | 1ms | 50% |
| 范围查询 | 500ms | 120ms | 76% |
| 分页查询 | 2s | 200ms | 90% |
经验总结:表结构设计最佳实践
✅ 应该做的
- 主键选择:高并发用雪花算法,小系统用自增ID
- 数据类型:能小则小,避免过度设计
- 金额字段:必须使用DECIMAL,禁止FLOAT/DOUBLE
- 时间字段:使用TIMESTAMP/DATETIME,禁止字符串
- 软删除:必须实现,便于数据恢复
- 扩展字段:预留JSON字段应对需求变更
- 注释规范:每个字段都要有注释
- 字符集:统一使用utf8mb4
❌ 不应该做的
- 使用UUID作为主键(性能杀手)
- 金额用FLOAT/DOUBLE(精度丢失)
- 时间用字符串存储(无法计算)
- 没有软删除(误删无法恢复)
- 字段长度设计过大(浪费空间)
- 忽略字符集(乱码问题)
- 没有预留扩展字段(需求变更困难)
- 过度规范化(查询性能差)
深度案例分析:电商系统表结构重构
项目背景
某电商平台上线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
);
重构效果
| 指标 | 重构前 | 重构后 | 提升 |
|---|---|---|---|
| 存储空间 | 45GB | 28GB | 37.8% ↓ |
| 主键查询 | 15ms | 2ms | 86.7% ↓ |
| 用户订单查询 | 800ms | 80ms | 90% ↓ |
| 数据一致性 | 无保障 | 软删除保障 | 大幅提升 |
表结构设计决策树
开始设计表结构
↓
数据量预计超过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.
读者练习
-
思考题:分析你项目中的表结构,找出3个可以优化的地方。
-
实践题:设计一个用户系统的表结构,包含:用户信息、账户信息、登录记录,要求支持软删除和扩展性。
-
挑战题:实现一个雪花算法ID生成器,并解决时钟回拨问题。
系列上一篇:数据库设计范式与反范式的实战平衡
系列下一篇:高效SQL编写:从基础到高级技巧
知识点测试
读完文章了?来测试一下你对知识点的掌握程度吧!
评论区
使用 GitHub 账号登录后即可发表评论,支持 Markdown 格式。
如果评论系统无法加载,请确保:
- 您的网络可以访问 GitHub
- giscus GitHub App 已安装到仓库
- 仓库已启用 Discussions 功能