尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SQL约束全面解析:数据完整性与六大约束实践

SQL约束全面解析:数据完整性与六大约束实践 1. SQL 约束核心能力速览很多初学者写 SQL 建表时只关注字段类型和长度却在数据正确性上反复踩坑重复数据混进来了、必填字段为空、关联记录被误删、分数列出现了负数。这些问题单靠应用程序判断并不可靠多一个入口就多一处漏写校验的逻辑。真正的兜底方案是在数据库层面把规则定死让数据库自己拒绝非法数据。这套规则就是 SQL 中的约束。能力项说明约束本质数据库主动校验数据合法性的规则向表中插入、更新、删除数据时自动检查常用约束NOT NULL、UNIQUE、PRIMARY KEY、FOREIGN KEY、CHECK、DEFAULT 六大类主要作用保证数据完整性、一致性、准确性减少脏数据作用时机插入INSERT时检查、更新UPDATE时检查、删除DELETE时按规则联动操作方式建表时定义、建表后用 ALTER TABLE 添加或删除应用场景订单、用户、库存等所有需要强一致性的业务表适用平台MySQL、SQL Server、PostgreSQL、Oracle、SQLite 等主流关系型数据库学习成本低掌握 CREATE TABLE 与 ALTER TABLE 即可先给结论约束不是可学可不学的知识点数据完整性、数据库设计、慢 SQL 排查、后端接口报错到最后都会绕回到约束上。这篇文章完整拆解六大约束的概念、SQL 语法、工程实践和常见坑位并且给出可直接复制的建表和修改语句。适合读者正在学数据库原理的学生刚接触 MySQL/SQL Server 的后端开发以及被脏数据折磨、想从源头控制数据质量的工程师。2. 约束到底是什么约束Constraint从字面理解就是限制条件是关系型数据库管理系统RDBMS提供的一套数据校验机制。它定义在表结构上对所有写入数据的操作生效只要违反规则数据库直接拒绝执行并返回错误。举个实际例子用户表通常要求邮箱不能重复CREATE TABLE users ( id INT PRIMARY KEY, email VARCHAR(100) UNIQUE NOT NULL );这条语句中出现了三个约束PRIMARY KEY约束id是主键不能为空且必须唯一。UNIQUE约束email不能重复。NOT NULL约束email不能为空。之后任何写入重复邮箱或空邮箱的操作都会被数据库拦截。约束与数据类型、索引、触发器有本质区别数据类型约束的是存储格式比如INT列不能写入字符串。约束约束的是业务语义比如“邮箱不能重复”“分数不能为负”。索引是加速查询的结构但UNIQUE约束会隐式创建唯一索引。触发器是主动执行额外逻辑约束是被动挡住非法操作性能开销远低于触发器。建立约束体系的意义可以总结为三句话数据正确性由数据库保证而不是依赖开发者自觉多应用入口写同一套校验的成本远高于在数据库定义一次线上数据的质量直接影响查询速度、统计报表和系统稳定性。这里需要特别区分约束与搜索引擎的热词之间的关系。在 FPGA、时序设计等硬件领域也大量出现“约束”一词例如“时钟约束”“IO 约束”“XDC 约束”它指的是时序和引脚分配规则。本文讨论的是数据库管理系统中的 SQL 约束两者完全不是一个技术领域不要混淆。3. 六大 SQL 约束逐一拆解3.1 NOT NULL 非空约束非空约束要求字段必须有值插入或更新为NULL时被拒绝。建表时直接写在字段类型之后CREATE TABLE student ( id INT NOT NULL, name VARCHAR(50) NOT NULL, gender CHAR(1) );上面的语句表示id和name都必须填值gender可以不填。这里注意一个细节NULL不是空字符串也不是数字 0它表示“未知值”。在数据库语义中空字符串是有效值NULL才是没有值。在修改已有表时使用ALTER TABLE student MODIFY COLUMN name VARCHAR(50) NOT NULL;如果表中已有NULL记录执行上面语句会失败。必须先处理脏数据再添加非空约束UPDATE student SET name unknown WHERE name IS NULL; ALTER TABLE student MODIFY COLUMN name VARCHAR(50) NOT NULL;对于 SQL Server语法略有不同ALTER TABLE student ALTER COLUMN name VARCHAR(50) NOT NULL;工程建议新建表时不要把约束留到联调阶段再补写建表语句就同步设计。后期给大表加 NOT NULL 约束在千万级数据量上会锁表较长时间影响线上业务。3.2 UNIQUE 唯一约束唯一约束保证一个字段或一组字段的所有值互不重复。和主键不同一张表可以有多个唯一约束且唯一约束允许存在 NULL 值不同数据库行为略有差异MySQL 中多个 NULL 不视为重复。CREATE TABLE customer ( id INT PRIMARY KEY, phone VARCHAR(20) UNIQUE, email VARCHAR(100) UNIQUE );唯一约束会自动创建唯一索引所以它同时也是查询优化的一个重要手段。按手机号或邮箱查用户时命中唯一索引效率很高。它解决的实际问题很典型用户注册时防止手机号被重复注册、订单表中防止订单号重复、库存系统中防止同一商品同一批次重复入库。需要组合多个字段唯一时写法是表级约束CREATE TABLE order_item ( order_id INT, product_id INT, quantity INT, UNIQUE (order_id, product_id) );这里表示同一个订单中不允许出现两行相同商品的记录。这种“联合唯一”是数据建模中非常常见的需求务必掌握。3.3 PRIMARY KEY 主键约束主键是表内数据的唯一身份标识它同时具备UNIQUE和NOT NULL两种特性并且一张表只能有一个主键。主键可以是单列也可以是多列组成的联合主键。最基础的单列主键写法CREATE TABLE dept ( dept_id INT PRIMARY KEY, dept_name VARCHAR(100) );联合主键用法CREATE TABLE course_selection ( student_id INT, course_id INT, semester VARCHAR(20), PRIMARY KEY (student_id, course_id, semester) );联合主键表示一个学生在一个学期内选同一门课只能有一条选课记录。使用联合主键时要注意复合主键字段越多索引体积越大写入速度越慢。避免使用过长字符串字段做主键占用空间大也会拖慢关联查询。生产环境中更常见的做法是使用自增整型代理主键AUTO_INCREMENT或IDENTITY把业务唯一性交给唯一约束解决这样表结构更清爽外键引用也更简单。不要用身份证号、手机号等敏感业务字段做主键业务值一旦变更会牵连所有外键引用。对于 SQL Server 的自增主键写法CREATE TABLE employee ( emp_id INT IDENTITY(1,1) PRIMARY KEY, emp_name VARCHAR(50) );MySQL 对应CREATE TABLE employee ( emp_id INT AUTO_INCREMENT PRIMARY KEY, emp_name VARCHAR(50) );3.4 FOREIGN KEY 外键约束外键约束建立表与表之间的关联保证从表的某个字段值必须存在于主表的被引用字段中。这是关系型数据库实现“引用完整性”的核心机制。订单表和用户表的经典场景CREATE TABLE customers ( id INT PRIMARY KEY, name VARCHAR(50) ); CREATE TABLE orders ( order_id INT PRIMARY KEY, customer_id INT, total_amount DECIMAL(10, 2), FOREIGN KEY (customer_id) REFERENCES customers(id) );这样设计后插入订单时customer_id必须对应customers表中真实存在的id否则插入失败。删除用户时如果该用户下还有订单删除会被阻止或触发级联操作取决于外键的联动规则。外键的 ON DELETE 和 ON UPDATE 规则规则行为实际场景RESTRICT存在关联记录时拒绝删除默认行为订单表有关联禁止删用户CASCADE删除主表记录时自动删除从表记录删除帖子时自动删除评论SET NULL主表删除后将从表外键置为 NULL删除商品后订单中商品 ID 置空NO ACTION与 RESTRICT 类似检查时机略有差异需要立即报错不隐式修改示例CREATE TABLE comments ( id INT PRIMARY KEY, post_id INT, content TEXT, FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE );工程中对外键的使用存在争议。互联网高并发系统中很多团队为了写入性能选择在应用层维护引用关系不使用数据库外键。但在内部管理系统、金融系统、ERP 等强一致性要求的系统中外键依然是数据质量的保障。从性能角度看外键约束会带来额外检查开销。插入从表数据时要查主表索引验证存在性批量导入时每一行都要验证导入速度会明显变慢。如果项目是低并发后台系统完整外键约束利大于弊如果用户量很大、数据量在千万级以上需要谨慎评估是否使用外键必要时用应用程序事务来保证一致性。3.5 CHECK 检查约束CHECK 约束最直接地体现“业务规则下沉到数据库”的思维它通过一个条件表达式来限制字段的取值范围。条件为真时数据才能写入。CREATE TABLE product ( id INT PRIMARY KEY, name VARCHAR(100), price DECIMAL(10, 2), quantity INT, CHECK (price 0), CHECK (quantity 0) );这条规则保证商品价格和数量不能为负数。年龄限制也是典型场景CREATE TABLE person ( id INT PRIMARY KEY, name VARCHAR(50), age INT, CHECK (age 0 AND age 150) );在 MySQL 8.0.16 之前CHECK 约束会被 MySQL 解析但不会真正生效8.0.16 之后才正式强制实施。这是一个很重要的兼容性问题如果你的团队使用老版本 MySQL检查约束可能并没有真正拦截非法数据。而 SQL Server、PostgreSQL、Oracle 则长期完整支持 CHECK 约束。列出 CHECK 约束ALTER TABLE person ADD CONSTRAINT chk_age CHECK (age 0 AND age 150);删除 CHECK 约束ALTER TABLE person DROP CONSTRAINT chk_age;CHECK 约束的局限是表达式必须能在数据库内部求值不能调用存储函数。涉及多表联合判断的复杂规则应该在应用层或触发器中处理。3.6 DEFAULT 默认值约束DEFAULT 不在严格意义的数据校验范围内它定义字段在未显式赋值时的默认值防止遗漏字段导致写入失败或产生 NULL。CREATE TABLE employee ( id INT PRIMARY KEY, name VARCHAR(50), hired_date DATE DEFAULT (CURRENT_DATE), status VARCHAR(20) DEFAULT active );插入时不指定status字段数据库自动填入active不指定hired_date自动使用当前日期。关键细节默认值可以是常量也可以是系统函数如NOW()、CURRENT_TIMESTAMP。DEFAULT不会在 UPDATE 时自动重置只在 INSERT 没有指定值时生效。如果 Default 设置后表已有大量数据新增字段时数据库会扫描全表填充默认值大表上有锁表风险。修改默认值用ALTER TABLE ... ALTER COLUMN ... SET DEFAULT具体语法在不同数据库中差异较大。-- SQL Server / PostgreSQL ALTER TABLE employee ALTER COLUMN status SET DEFAULT inactive;-- MySQL ALTER TABLE employee ALTER COLUMN status SET DEFAULT inactive;4. 约束的命名规范与查看方式写约束时如果不指定名称数据库会自动命名比如PRIMARY、symbol、constraint_1。这种随机命名让后续删除和排查变得困难。工程实践中应该显式命名。CREATE TABLE payment ( id INT, order_id INT, amount DECIMAL(10, 2), CONSTRAINT pk_payment PRIMARY KEY (id), CONSTRAINT fk_payment_order FOREIGN KEY (order_id) REFERENCES orders(id), CONSTRAINT chk_payment_amount CHECK (amount 0), CONSTRAINT uq_payment_order UNIQUE (order_id) );命名建议统一前缀主键pk_外键fk_唯一约束uq_检查约束chk_默认值约束df_。查看表的约束信息各数据库语法不同。MySQLSHOW CREATE TABLE payment;SQL ServerSELECT name, type_desc FROM sys.key_constraints WHERE parent_object_id OBJECT_ID(payment);PostgreSQLSELECT conname, contype FROM pg_constraint WHERE conrelid payment::regclass;5. 约束的管理添加、删除与修改建表后要维护约束通常使用ALTER TABLE语句。不同操作的目标不同语法也有差异。给已有表添加主键ALTER TABLE customer ADD PRIMARY KEY (id);给已有表添加外键ALTER TABLE orders ADD CONSTRAINT fk_orders_customer FOREIGN KEY (customer_id) REFERENCES customers(id);给已有表添加唯一约束ALTER TABLE customer ADD CONSTRAINT uq_customer_email UNIQUE (email);给已有表添加检查约束ALTER TABLE product ADD CONSTRAINT chk_product_price CHECK (price 0);删除约束时MySQL 不支持DROP CONSTRAINT统一写法必须分类型处理-- MySQL 删除主键 ALTER TABLE customer DROP PRIMARY KEY; -- MySQL 删除唯一约束 ALTER TABLE customer DROP INDEX uq_customer_email; -- MySQL 删除外键 ALTER TABLE orders DROP FOREIGN KEY fk_orders_customer;SQL Server 和 PostgreSQL 可以用统一语法ALTER TABLE customer DROP CONSTRAINT uq_customer_email;约束修改流程是先删除旧约束再添加新约束。没有直接修改约束的便捷语法。比如把检查条件从price 0改为price 0必须先删除再重新添加。约束添加失败的主要原因表中已有数据不满足新约束条件必须先清理数据。外键关联的字段在主表中没有对应索引部分数据库要求外键列必须建索引。主键字段中包含 NULL 或重复值。唯一约束字段中已有重复值。在线大表添加约束的风险前面提到过MySQL 5.6 之前ALTER TABLE会锁表5.6 之后使用ALGORITHMINPLACE可以降低影响但仍有风险。生产环境中对超大表加约束建议采用在线 DDL 工具并选择业务低峰期执行。6. 约束与索引的关系约束和索引是两个不同概念但关系密切。主键约束会自动创建唯一索引。UNIQUE 约束会自动创建唯一索引。外键约束不会自动创建索引但 MySQL 官方建议给外键列创建索引。没有索引时父表删除记录需要扫描整个子表去检查外键引用性能很差。CHECK 和 NOT NULL 不会创建索引也不能直接通过约束加速查询。理解了这一点就能明白约束的主要职责是保证数据正确性但它捎带影响了数据访问效率。主键和唯一索引对等值查询和关联查询有非常直接的加速效果。慢 SQL 排查时缺主键的表往往被首先怀疑。有一种说法是索引越多写入越慢因为每次插入都要维护所有索引。在添加唯一约束时要明确这个字段是不是真的需要全局唯一。运行中的大表加一个不必要的唯一约束等于新增一个索引会直接影响写入性能。7. 不同数据库中的约束差异主流数据库对 SQL 约束的支持程度和写法有明显区别迁移时经常踩坑。下表是核心差异汇总功能点MySQL 8.0SQL ServerPostgreSQLOracleCHECK 约束强制执行8.0.16 起强制支持支持支持级联删除支持支持支持支持延迟约束不支持部分支持支持支持自增语法AUTO_INCREMENTIDENTITYSERIAL / IDENTITYIDENTITY / SEQUENCE修改字段名语法CHANGE COLUMNRENAME COLUMNRENAME COLUMNRENAME COLUMN约束默认命名自动按表名生成自动生成随机名自动生成随机名自动生成 SYS_C 开头名CHECK 约束的兼容性是历史大坑。早版本 MySQL 中写入的 CHECK 约束会被解析但完全不生效做表结构迁移时要检查老表的约束是否真实在拦数据。建议用如下方式验证INSERT INTO product (id, name, price, quantity) VALUES (1, test, -100, -100);如果插入成功且没有报错说明 CHECK 约束没有生效。SQL Server 中还有一个特有概念叫过滤约束Filtered ConstraintPostgreSQL 同样支持部分唯一索引。比如只约束未删除记录的手机号唯一已删除的软删除记录可以允许重复。PostgreSQL 和 SQL Server 的部分唯一索引写法-- PostgreSQL CREATE UNIQUE INDEX uq_users_email_active ON users (email) WHERE is_deleted false; -- SQL Server CREATE UNIQUE INDEX uq_users_email_active ON users (email) WHERE is_deleted 0;这个特性对于软删除场景非常实用。标准 SQL 本身没有这种写法属于数据库扩展功能能处理常规 UNIQUE 约束做不到的“条件唯一”需求。8. 约束在批量任务与数据清洗中的应用约束不只是建表时用一次在批量数据导入、数据清洗、慢 SQL 优化中同样举足轻重。8.1 批量导入约束检查大批量导入数据通常先用临时表或无约束表接收数据清洗完成后一次性添加约束。这套流程比逐行校验快得多。推荐流程创建与目标表结构相同但无约束的暂存表。用LOAD DATA或BULK INSERT或程序批量写入。按业务规则清洗删除重复记录、修复空值、修正非法数据。执行 ALTER TABLE 添加约束。若约束添加失败根据错误定位脏数据并重新处理。SQL Server 的批量导入写法BULK INSERT staging_users FROM C:\\data\\users.csv WITH ( FIELDTERMINATOR ,, ROWTERMINATOR \\n, FIRSTROW 2 );MySQL 对应LOAD DATA INFILE /data/users.csv INTO TABLE staging_users FIELDS TERMINATED BY , LINES TERMINATED BY \\n IGNORE 1 ROWS;批量写入外键约束时逐行验证引用关系会明显拖慢导入速度。建议流程是先暂存表后校验再启用约束。这里最优做法是导入前先关闭约束导入完成后再重建。SQL Server 和 Oracle 支持NOCHECK或DISABLE CONSTRAINT指令。MySQL 没有直接关闭外键约束的语法但可以用SET FOREIGN_KEY_CHECKS 0临时跳过检查之后恢复SET FOREIGN_KEY_CHECKS 0; -- 执行批量导入 SET FOREIGN_KEY_CHECKS 1;8.2 约束对慢 SQL 的影响查询性能与数据分布密切相关。没有约束的表数据质量差COUNT(DISTINCT)、GROUP BY、JOIN的结果都可能令人怀疑。唯一约束和主键约束生成的索引对常用等值查询是明显的加速器。外键约束强制数据一致后多表 JOIN 不会因为缺失匹配数据产生大量意料外的空行。8.3 约束冲突数据定位添加约束失败时数据库往往只报“Duplicate entry”或者“Check constraint violated”不会直接告诉你哪些行有问题。常见定位 SQL 如下。查重复值SELECT email, COUNT(*) FROM users GROUP BY email HAVING COUNT(*) 1;查违反非空约束的记录SELECT * FROM users WHERE name IS NULL;查未匹配外键的记录SELECT * FROM orders o LEFT JOIN customers c ON o.customer_id c.id WHERE c.id IS NULL;9. 常见问题与排查方法问题现象可能原因排查方式解决方案添加唯一约束失败目标列已有重复值用 GROUP BY ... HAVING COUNT(*) 1 查重复去重或处理重复数据后再添加插入外键列报错外键值在主表不存在单独 SELECT 主表对应值修正引用值或先插入主表数据CHECK 约束不生效MySQL 版本低于 8.0.16SHOW CREATE TABLE 查看 DDL升级数据库或改用应用层校验加触发器删除主表数据被拒绝从表有外键引用查看关联表子记录先删从表或使用 ON DELETE CASCADEALTER TABLE 一直阻塞大表加约束DML 频繁查看 SHOW PROCESSLIST / sys.dm_exec_requests分批次加约束或使用在线 DDL 工具批量导入速度极慢每条数据逐行校验外键和唯一索引观察导入耗时临时关闭外键检查导入后重建索引约束命名冲突不同表约束名重复查看数据库约束信息约束名在 schema 级唯一重命名数据库迁移数据类型失败源库有违反目标库约束的数据导出时做数据校验用暂存表清洗后再导入补充一个重要注意点MySQL 中删除约束的方式因约束类型不同而不同最容易忘的是外键要使用DROP FOREIGN KEY而不是DROP INDEX。虽然 MySQL 的文档中DROP FOREIGN KEY会自动删除关联索引但如果操作写错会一直报语法错误。10. 最佳实践与使用建议10.1 建表阶段就设计约束建表是约束成本最低的时间点。表已经上线运行、灌入大量数据后再加约束会产生脏数据冲突、锁表风险、迁移脚本编写等大量额外工作。数据库建模评审阶段就要明确每个字段的非空性、唯一性、取值范围和引用关系。10.2 约束命名规范化所有约束显式命名统一pk_、fk_、uq_、chk_前缀。方便后续维护、排错和数据库迁移。自动命名的约束在多人协作和大规模表结构中完全不可维护。10.3 区分业务约束与数据库约束业务规则复杂多变的部分比如“订单金额不能超过账户余额”“一个用户最多创建 10 个店铺”不适合直接用 CHECK 或外键表达建议在应用层或存储过程中实现。数据库约束适合承载本质性的、稳定的数据规则比如主键唯一性、必填字段、金额非负。把易变业务规则放进数据库会导致频繁修改表结构维护成本极大。10.4 批量任务注意解耦批量初始化数据、清洗脏数据、导入历史数据时遵循先写入再校验的思路利用暂存表和分批添加约束避免反复触发逐行校验导致性能问题。10.5 关注大表操作窗口给一个大表加主键、唯一约束或者非空约束都可能产生长时间锁。操作前查看表大小和业务低峰期优先使用在线 DDL 工具涉及外键约束更需谨慎。10.6 数据导出迁移前先做合规校验涉及身份证、手机号、邮箱等敏感信息时导出前先做脱敏和合规评估。唯一约束的建立与敏感字段的业务属性相关时要同步考虑授权范围和数据使用边界。10.7 不要依赖数据库纠错约束是最后一道闸门不是开发的借口。开发接口时仍然要在应用层做参数校验给用户友好的错误提示。否则数据库报的“Duplicate entry”可能直接泄漏表结构信息在存档级安全要求较高的系统中也建议对数据库报错信息做统一包装。11. 总结与下一步SQL 约束的核心价值在于把数据规则沉淀到数据库层让每个写入数据的入口都遵守同一套规则。最值得优先掌握的是主键约束、非空约束和唯一约束这三个是绝大多数业务系统的标配也是最常出现在数据库设计和 SQL 面试中的内容。外键与 CHECK 约束次之使用时要结合业务场景和团队对数据库性能的要求来决定。下一步的建议很明确把你负责的核心业务表打开检查三条约束是否到位——每个表都有主键吗必填字段都有非空约束吗业务上不可重复的字段都建了唯一约束吗这三条检查完再考虑外键和 CHECK 的细化设计。训练时可以挑一个订单表结构自己写一套包含六大约束的建表语句再用ALTER TABLE完成增删改操作很快就能把 SQL 约束的使用手感建立起来。最容易踩的坑也再强调一次MySQL 8.0.16 之前的 CHECK 约束不生效批量导入时逐行校验拖慢速度大表加约束前务必先评估锁表风险。把这三个点记在脑子里再复杂的约束问题也能快速定位。
返回列表