📊 SQL 入门 Day 21:表设计与约束

好的表设计是高性能查询的基石,而约束是保证数据完整性的第一道防线。

一、为什么表设计如此重要?

想象一下,如果一张表允许「用户名字段为空」且「没有唯一限制」------那你可能会存入两个同名空用户,后续业务逻辑直接崩溃。约束就是数据库的「交通规则」,没有它,数据会变成混乱的垃圾场。

二、6 大约束类型详解

1. NOT NULL --- 非空约束

确保字段不能为 NULL,适用于必有值的列(如用户名、邮箱)。

sql 复制代码
CREATE TABLE users (
    id INT PRIMARY KEY,
    username VARCHAR(50) NOT NULL,  -- 用户名字段不允许为空
    email VARCHAR(100) NOT NULL
);

常见坑: 空字符串 '' 不等于 NULLNOT NULL 只禁止 NULL,不禁止空字符串。

2. UNIQUE --- 唯一约束

保证列(或列组合)的值全局唯一。

sql 复制代码
CREATE TABLE users (
    id INT PRIMARY KEY,
    email VARCHAR(100) UNIQUE,  -- 每个邮箱只能注册一次
    phone VARCHAR(20) UNIQUE
);

多列联合唯一:

sql 复制代码
CREATE TABLE follows (
    follower_id INT,
    followee_id INT,
    UNIQUE (follower_id, followee_id)  -- 不能重复关注同一个人
);

3. PRIMARY KEY --- 主键约束

= NOT NULL + UNIQUE,每张表有且仅有一个主键,作为行的唯一标识。

sql 复制代码
-- 单列主键
CREATE TABLE orders (
    order_id INT PRIMARY KEY,
    customer_id INT NOT NULL,
    amount DECIMAL(10,2)
);

-- 复合主键(联合主键)
CREATE TABLE order_items (
    order_id INT,
    product_id INT,
    quantity INT,
    PRIMARY KEY (order_id, product_id)  -- 一个订单中的每个产品唯一
);

性能提示: 自增整数主键在 InnoDB 中性能最佳(聚簇索引按主键顺序排列)。

4. FOREIGN KEY --- 外键约束

保证引用完整性------子表中的值必须在父表存在。

sql 复制代码
CREATE TABLE orders (
    order_id INT PRIMARY KEY,
    customer_id INT,
    FOREIGN KEY (customer_id) REFERENCES customers(id)
        ON DELETE CASCADE       -- 删除客户时自动删除其订单
        ON UPDATE CASCADE       -- 客户 ID 更新时同步更新
);

ON DELETE / ON UPDATE 选项:

选项 行为
CASCADE 父表变更时,子表自动同步
SET NULL 父表删除时,子表外键置为 NULL
RESTRICT 有子记录时禁止删除父记录(默认)
NO ACTION 同 RESTRICT(MySQL 下等同)

⚠️ 生产建议: 高并发场景下外键会带来性能开销,许多团队选择在应用层保证引用完整性,数据库只用索引。

5. CHECK --- 检查约束

确保字段值满足指定条件。

sql 复制代码
CREATE TABLE products (
    id INT PRIMARY KEY,
    price DECIMAL(10,2) CHECK (price > 0),         -- 价格必须为正
    stock INT CHECK (stock >= 0),                   -- 库存不能为负
    status VARCHAR(20) CHECK (status IN ('上架', '下架', '预售'))
);

注意: MySQL 8.0.16+ 才开始真正支持 CHECK(之前版本语法通过但不生效)。

6. DEFAULT --- 默认值约束

为字段指定默认值,插入时若未提供则自动填充。

sql 复制代码
CREATE TABLE articles (
    id INT PRIMARY KEY,
    title VARCHAR(200) NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,     -- 默认当前时间
    view_count INT DEFAULT 0,                         -- 默认阅读数为0
    is_published TINYINT DEFAULT 0                    -- 默认未发布
);

三、表设计最佳实践

1. 范式化 vs 反范式化

三范式(3NF)核心原则:

范式 规则 反例
1NF 每列不可再分 一个字段存多个手机号 '138xxx,139xxx'
2NF 非主键列完全依赖主键 订单表存了 customer_name(应只存 customer_id
3NF 非主键列不传递依赖 存了 customer_city 而非通过 customer_id 关联

何时反范式化? 查询频繁、写入少的场景,适当冗余可减少 JOIN,提升查询速度。

2. 选择合适的数据类型

基本原则:够用就好,越小越快。

sql 复制代码
-- ❌ 过度设计
CREATE TABLE bad_design (
    id BIGINT PRIMARY KEY,              -- 实际只有几百条数据
    is_active CHAR(10) DEFAULT 'true',  -- 用 CHAR(10) 存布尔值
    price VARCHAR(20)                    -- 金额用字符串存
);

-- ✅ 合理设计
CREATE TABLE good_design (
    id INT PRIMARY KEY AUTO_INCREMENT,   -- 数据量不大用 INT 足够
    is_active TINYINT DEFAULT 1,         -- 布尔值用 TINYINT(1)
    price DECIMAL(10,2)                   -- 金额用 DECIMAL 避免精度丢失
);

3. 索引设计原则

  • 主键自动成为聚簇索引,InnoDB 表必须有主键
  • 经常出现在 WHERE / JOIN / ORDER BY 的列考虑加索引
  • 联合索引遵循最左前缀原则 :索引 (a, b, c) 能加速 WHERE a=?WHERE a=? AND b=?,但无法加速 WHERE b=?
  • 不要对频繁更新的列加过多索引(维护成本高)

四、实战案例分析

场景:设计一个电商订单系统

sql 复制代码
-- 用户表
CREATE TABLE customers (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL,
    email VARCHAR(100) NOT NULL UNIQUE,
    phone VARCHAR(20),
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_email (email)  -- 加速登录查询
);

-- 商品表
CREATE TABLE products (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(200) NOT NULL,
    price DECIMAL(10,2) NOT NULL CHECK (price > 0),
    stock INT NOT NULL DEFAULT 0 CHECK (stock >= 0),
    status ENUM('上架', '下架') DEFAULT '上架',
    INDEX idx_status (status)
);

-- 订单表
CREATE TABLE orders (
    id INT PRIMARY KEY AUTO_INCREMENT,
    customer_id INT NOT NULL,
    total_amount DECIMAL(10,2) DEFAULT 0.00,
    status ENUM('待支付', '已支付', '已发货', '已完成', '已取消') DEFAULT '待支付',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE RESTRICT,
    INDEX idx_customer (customer_id),
    INDEX idx_status_created (status, created_at)  -- 联合索引加速常见查询
);

-- 订单明细表
CREATE TABLE order_items (
    id INT PRIMARY KEY AUTO_INCREMENT,
    order_id INT NOT NULL,
    product_id INT NOT NULL,
    quantity INT NOT NULL CHECK (quantity > 0),
    price DECIMAL(10,2) NOT NULL,  -- 快照价格,不受商品改价影响
    FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
    FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE RESTRICT,
    UNIQUE (order_id, product_id)  -- 同一订单中每种商品只出现一次
);

五、常见设计陷阱

陷阱 问题 正确做法
万能字段 一个字段存多个值(JSON/逗号分隔) 拆成关联表
过度索引 给表建了 20 个索引,插入极慢 只给查询字段建索引
不用外键 数据不一致,出现孤儿记录 应用层保证或加外键
类型滥用 VARCHAR(255) 存所有文本 按实际长度选择 CHAR/VARCHAR/TEXT
缺少时间戳 不知道数据何时插入/更新 created_at / updated_at

总结

  • 约束:NOT NULL、UNIQUE、PRIMARY KEY、FOREIGN KEY、CHECK、DEFAULT
  • 设计三范式:原子性、完全依赖、不传递依赖
  • 反范式化:用空间换时间,适合读多写少场景
  • 数据类型:选小不选大,选对不选贵
  • 索引:最左前缀、适度索引、主键必设

好的表设计,能让后续的查询优化省 80% 的力气。花时间在表设计上,是性价比最高的投入。

相关推荐
SamDeepThinking1 小时前
警惕那些很长时间没有编写任何代码、却在设计系统的人
java·后端·架构
aloha_1 小时前
基于Spring Boot + Vue 3的前后端一体化部署方案
后端
星火10241 小时前
【Groovy翻译-进阶篇】Groovy 中的设计模式
后端·设计模式·groovy
Zane19941 小时前
ArrayList 插入慢,LinkedList 一定快吗
java·后端
花生智源1 小时前
RAG检索优化:查询改写、重排序与缓存策略
后端
吃饱了得干活1 小时前
从类爆炸到协作——DDD战略设计登场
java·后端·架构
程序员cxuan1 小时前
我用 DeepSeek-V4-Pro,完美复刻了苹果官网
人工智能·后端·程序员
wno7042 小时前
Spring Boot异常处理
java·spring boot·后端
AI多Agent协作实战派2 小时前
AI多Agent协作系统实战(四十五):漏声明了一个变量,整个页面的按钮都死了
后端