3. 数据库约束:主键约束、外键约束、唯一约束、检查约束、默认值约束

约束这玩意儿,说白了就是数据库的「守门员」。我干了十几年运维,见过太多因为约束没设计好而引发的数据灾难。今天咱们就把五种核心约束掰开揉碎讲清楚。

核心观点:约束不是开发者的枷锁,而是数据质量的保险。没有约束的数据库,就像没有护栏的悬崖。

数据库五大约束体系 数据库约束 主键约束 外键约束 唯一约束 检查约束 默认值约束 约束的作用 • 主键约束:唯一标识每一行,保证实体完整性 • 外键约束:维护表间关系,保证引用完整性 • 唯一约束:防止重复值,但允许一个NULL • 检查约束:限制列值的范围,保证域完整性 • 默认值约束:未指定值时自动填充

3.1 主键约束:数据的身份证

主键约束,我习惯叫它「数据的身份证」。每个表必须有且只有一个主键,它保证每一行都能被唯一识别。

你想想看,如果没有主键,你怎么精确找到某条记录?靠行号?那玩意儿一删除数据就变了。

我的习惯:主键尽量用自增整数或UUID,别用业务字段。比如身份证号看起来唯一,但万一政策变了呢?我曾经有个客户把手机号当主键,结果携号转网政策一出,整个系统差点重写。

-- 创建带主键的表
CREATE TABLE users (
    user_id INT AUTO_INCREMENT,
    username VARCHAR(50) NOT NULL,
    email VARCHAR(100),
    PRIMARY KEY (user_id)
);

-- 复合主键示例
CREATE TABLE order_items (
    order_id INT,
    product_id INT,
    quantity INT,
    PRIMARY KEY (order_id, product_id)
);

注意:复合主键的列顺序会影响索引效率。高频查询的列放前面,这是我踩过的坑。

3.2 外键约束:表与表的「血缘关系」

外键约束维护的是引用完整性。说白了,就是保证子表里的数据,在父表里确实存在。

我记得有一次接手一个电商系统,订单表里有个用户ID,但用户表里根本查不到这个人。这就是没加外键约束的后果——数据成了「孤儿」。

-- 创建带外键的表
CREATE TABLE orders (
    order_id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT,
    order_date DATE,
    FOREIGN KEY (user_id) REFERENCES users(user_id)
    ON DELETE CASCADE
    ON UPDATE CASCADE
);
外键行为 说明 使用场景
CASCADE 父表删除/更新时,子表同步 强关联数据,如订单明细
SET NULL 父表删除时,子表置为NULL 可选关联,如用户注销后保留订单
RESTRICT 有子记录时禁止删除父记录 关键数据保护,如财务记录
NO ACTION 类似RESTRICT,检查时机不同 某些数据库默认行为

避坑指南:我曾经在生产环境直接用了CASCADE,结果删一个用户,连带删了三万条订单。从那以后,我建议先用SET NULL或RESTRICT,除非你非常确定业务逻辑。

3.3 唯一约束:拒绝重复,但允许一个空

唯一约束保证列值不重复,但允许一个NULL值。嗯,这里要注意——不同数据库对NULL的处理有差异。

MySQL里,唯一索引允许多个NULL。但SQL Server早期版本只允许一个。你想想看,如果业务上要求「邮箱唯一」,但允许用户不填邮箱,那NULL的处理就很重要。

-- 单列唯一约束
CREATE TABLE employees (
    emp_id INT PRIMARY KEY,
    email VARCHAR(100) UNIQUE,
    phone VARCHAR(20)
);

-- 复合唯一约束
CREATE TABLE product_warehouse (
    product_id INT,
    warehouse_id INT,
    quantity INT,
    UNIQUE KEY (product_id, warehouse_id)
);

关键区别:主键 vs 唯一约束

  • 主键:一个表只能有一个,不允许NULL
  • 唯一约束:一个表可以有多个,允许一个NULL
  • 主键自动创建聚集索引(MySQL InnoDB),唯一约束创建非聚集索引

3.4 检查约束:给数据加个「安检门」

检查约束,说白了就是给列值加个规则。比如年龄不能为负,工资不能低于最低标准。

我见过最离谱的案例:有个系统的「性别」字段存了「外星人」。为什么?因为没加检查约束,前端验证被绕过了。

-- 基本检查约束
CREATE TABLE employees (
    emp_id INT PRIMARY KEY,
    age INT CHECK (age >= 18 AND age <= 65),
    salary DECIMAL(10,2) CHECK (salary > 0),
    gender CHAR(1) CHECK (gender IN ('M', 'F', 'O'))
);

-- 命名检查约束(方便后续维护)
CREATE TABLE products (
    product_id INT PRIMARY KEY,
    price DECIMAL(10,2),
    discount DECIMAL(10,2),
    CONSTRAINT chk_price CHECK (price > 0),
    CONSTRAINT chk_discount CHECK (discount >= 0 AND discount <= price)
);

注意:MySQL 8.0.16之前,检查约束只是语法支持,实际不生效。我有个朋友升级数据库后才发现,之前「生效」的检查约束全是假的。升级后一堆脏数据冒了出来。

3.5 默认值约束:偷懒的艺术

默认值约束,就是当插入数据没指定某列时,数据库自动填一个值。这玩意儿用好了能省不少事。

我个人习惯,时间字段几乎必加默认值。比如创建时间用 CURRENT_TIMESTAMP,更新时间用 CURRENT_TIMESTAMP ON UPDATE。这样代码里少写一行是一行。

-- 默认值约束示例
CREATE TABLE articles (
    article_id INT PRIMARY KEY AUTO_INCREMENT,
    title VARCHAR(200) NOT NULL,
    status VARCHAR(20) DEFAULT 'draft',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    view_count INT DEFAULT 0
);

-- 插入测试
INSERT INTO articles (title) VALUES ('数据库约束详解');
-- 此时 status='draft', created_at=当前时间, view_count=0

我的经验:默认值别设成业务相关的硬编码。比如「状态默认是待审核」,万一哪天业务改了,改默认值可能影响历史数据。我一般用「未知」或「未设置」这类中性值。

3.6 约束的日常维护与检查

约束建好了不代表万事大吉。我每周的巡检脚本里,一定会检查约束状态。

-- MySQL 查看表约束
SELECT 
    TABLE_NAME,
    CONSTRAINT_NAME,
    CONSTRAINT_TYPE
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
WHERE TABLE_SCHEMA = 'your_database';

-- 检查外键是否被禁用
SELECT 
    TABLE_NAME,
    CONSTRAINT_NAME,
    REFERENCED_TABLE_NAME
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE REFERENCED_TABLE_SCHEMA = 'your_database'
  AND REFERENCED_TABLE_NAME IS NOT NULL;
巡检项 检查频率 异常处理
主键是否缺失 每周 立即添加,评估业务影响
外键是否失效 每日 检查数据一致性,重建约束
唯一约束是否被违反 实时(监控告警) 查找重复数据,清理后重建
检查约束是否被绕过 每月 全表扫描,修复异常数据

总结一下:约束是数据完整性的第一道防线。主键保实体,外键保引用,唯一防重复,检查控范围,默认省麻烦。五者配合,才能构建一个健壮的数据库体系。

嗯,关于约束就讲这么多。记住一点:约束不是写完就完事了,要定期检查、持续优化。我见过太多系统,上线时约束齐全,三年后约束全被「临时」禁用了——然后数据就乱了。