pgsql-05 约束详解
1. 概述
**约束(Constraint)**是数据库在存储层面强制保证数据完整性的规则。把校验放在数据库层而非全靠应用层,好处是:无论数据从哪个入口写入(应用、脚本、手工 SQL)都无法绕过,是数据正确性的最后一道防线。
PostgreSQL 支持六种基础约束(主键、唯一、外键、检查、非空、默认)以及一个 MySQL 没有的强大特色——排他约束(EXCLUDE)。本文逐一讲解,并突出与 MySQL 的差异和面试点。
索引与约束关系密切(主键/唯一约束底层依赖索引),但索引本身是独立的大主题,已单独成篇,见 索引原理与类型。
2. 六种基础约束
2.1 主键约束(PRIMARY KEY)
-- 方式1: 创建表时定义
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL
);
-- 方式2: 命名主键
CREATE TABLE products (
id SERIAL,
sku VARCHAR(50) NOT NULL,
name VARCHAR(200),
CONSTRAINT pk_products PRIMARY KEY (id)
);
-- 方式3: 复合主键
CREATE TABLE order_items (
order_id INTEGER,
product_id INTEGER,
quantity INTEGER,
price NUMERIC(10, 2),
PRIMARY KEY (order_id, product_id)
);
-- 添加主键约束
ALTER TABLE categories ADD PRIMARY KEY (id);
-- 删除主键约束
ALTER TABLE categories DROP CONSTRAINT categories_pkey;
与MySQL对比:
- PostgreSQL主键自动创建唯一索引
- MySQL主键是聚簇索引(InnoDB)
2.2 唯一约束(UNIQUE)
-- 单列唯一
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE,
email VARCHAR(100) UNIQUE NOT NULL
);
-- 命名唯一约束
CREATE TABLE products (
id SERIAL PRIMARY KEY,
sku VARCHAR(50),
CONSTRAINT uk_products_sku UNIQUE (sku)
);
-- 复合唯一约束
CREATE TABLE user_roles (
user_id INTEGER,
role_id INTEGER,
UNIQUE (user_id, role_id)
);
-- 部分唯一(PostgreSQL特色)
CREATE UNIQUE INDEX uk_active_users_email
ON users(email) WHERE is_active = TRUE;
-- 添加唯一约束
ALTER TABLE users ADD CONSTRAINT uk_users_email UNIQUE (email);
-- 删除唯一约束
ALTER TABLE users DROP CONSTRAINT uk_users_email;
2.3 外键约束(FOREIGN KEY)
-- 基本外键
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
total_amount NUMERIC(10, 2)
);
-- 命名外键
CREATE TABLE order_items (
id SERIAL PRIMARY KEY,
order_id INTEGER,
product_id INTEGER,
CONSTRAINT fk_order_items_order FOREIGN KEY (order_id) REFERENCES orders(id),
CONSTRAINT fk_order_items_product FOREIGN KEY (product_id) REFERENCES products(id)
);
-- 级联操作
CREATE TABLE comments (
id SERIAL PRIMARY KEY,
post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE,
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
content TEXT
);
-- ON DELETE选项:
-- CASCADE: 删除父记录时,删除子记录
-- SET NULL: 删除父记录时,子记录外键设为NULL
-- SET DEFAULT: 设为默认值
-- RESTRICT: 有子记录时禁止删除(默认)
-- NO ACTION: 同RESTRICT
-- ON UPDATE选项(同上)
CREATE TABLE order_logs (
id SERIAL PRIMARY KEY,
order_id INTEGER REFERENCES orders(id)
ON UPDATE CASCADE
ON DELETE CASCADE
);
-- 添加外键
ALTER TABLE orders
ADD CONSTRAINT fk_orders_user
FOREIGN KEY (user_id) REFERENCES users(id);
-- 删除外键
ALTER TABLE orders DROP CONSTRAINT fk_orders_user;
2.4 检查约束(CHECK)
-- 单列检查
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(200),
price NUMERIC(10, 2) CHECK (price >= 0),
stock INTEGER CHECK (stock >= 0)
);
-- 命名检查约束
CREATE TABLE employees (
id SERIAL PRIMARY KEY,
age INTEGER,
salary NUMERIC(10, 2),
CONSTRAINT chk_employees_age CHECK (age >= 18 AND age <= 65),
CONSTRAINT chk_employees_salary CHECK (salary > 0)
);
-- 多列检查
CREATE TABLE events (
id SERIAL PRIMARY KEY,
start_date DATE,
end_date DATE,
CHECK (end_date >= start_date)
);
-- 复杂检查
CREATE TABLE discounts (
id SERIAL PRIMARY KEY,
discount_type VARCHAR(20),
discount_value NUMERIC(5, 2),
CHECK (
(discount_type = 'percentage' AND discount_value BETWEEN 0 AND 100) OR
(discount_type = 'fixed' AND discount_value >= 0)
)
);
-- 添加检查约束
ALTER TABLE products
ADD CONSTRAINT chk_price_positive CHECK (price >= 0);
-- 删除检查约束
ALTER TABLE products DROP CONSTRAINT chk_price_positive;
2.5 NOT NULL约束
-- 创建表时定义
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
phone VARCHAR(20) -- 可为NULL
);
-- 添加NOT NULL
ALTER TABLE users ALTER COLUMN phone SET NOT NULL;
-- 删除NOT NULL
ALTER TABLE users ALTER COLUMN phone DROP NOT NULL;
2.6 DEFAULT约束
-- 默认值
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
status VARCHAR(20) DEFAULT 'draft',
view_count INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
is_published BOOLEAN DEFAULT FALSE
);
-- 表达式默认值
CREATE TABLE logs (
id SERIAL PRIMARY KEY,
log_date DATE DEFAULT CURRENT_DATE,
random_id UUID DEFAULT gen_random_uuid()
);
-- 修改默认值
ALTER TABLE posts ALTER COLUMN status SET DEFAULT 'pending';
-- 删除默认值
ALTER TABLE posts ALTER COLUMN status DROP DEFAULT;
3. 排他约束(EXCLUDE,PostgreSQL 特色)
排他约束是 PG 独有的强大能力,MySQL 无法用约束直接实现。它保证"任意两行在某种运算下不能同时成立",最经典的用途是防止时间段/空间重叠。
-- 需要 GiST 索引支持
CREATE EXTENSION IF NOT EXISTS btree_gist;
-- 会议室预订:同一房间的时间段不能重叠
CREATE TABLE reservations (
id SERIAL PRIMARY KEY,
room_id INTEGER,
during TSRANGE, -- 时间范围类型
-- room_id 相等 且 时间段重叠(&&) 的两行不允许同时存在
EXCLUDE USING GiST (room_id WITH =, during WITH &&)
);
INSERT INTO reservations (room_id, during)
VALUES (1, tsrange('2026-07-01 09:00', '2026-07-01 10:00')); -- OK
INSERT INTO reservations (room_id, during)
VALUES (1, tsrange('2026-07-01 09:30', '2026-07-01 11:00')); -- 报错:与上一条重叠
用普通唯一约束无法表达"重叠",只能靠应用层加锁查询判断(有并发漏洞)。排他约束在数据库层原子地保证,是 PG 的杀手锏。范围类型见 数组与范围类型。
4. 约束的高级特性
4.1 延迟约束(DEFERRABLE)
默认约束在每条语句执行后立即检查。延迟约束可推迟到事务提交时才检查,用于解决"循环外键/互相引用"或批量调整时的中间态问题。
CREATE TABLE nodes (
id INTEGER PRIMARY KEY,
next_id INTEGER REFERENCES nodes(id) DEFERRABLE INITIALLY DEFERRED
);
BEGIN;
-- 插入时互相引用,若立即检查会失败;延迟到提交才校验
INSERT INTO nodes VALUES (1, 2), (2, 1);
COMMIT; -- 此时才检查外键,通过
DEFERRABLE INITIALLY IMMEDIATE:可延迟但默认立即(可在事务内SET CONSTRAINTS ... DEFERRED)。DEFERRABLE INITIALLY DEFERRED:默认延迟到提交。
4.2 NOT VALID:给大表加约束不阻塞
给已有大量数据的表加约束时,全表校验会长时间锁表。用 NOT VALID 先只对新写入生效、跳过存量校验,之后再择机 VALIDATE(不锁写)。
-- 1) 先加约束但不校验存量数据(很快,短暂锁)
ALTER TABLE orders ADD CONSTRAINT chk_amount CHECK (amount >= 0) NOT VALID;
-- 2) 业务低峰再校验存量(只加 SHARE UPDATE EXCLUSIVE,不阻塞读写)
ALTER TABLE orders VALIDATE CONSTRAINT chk_amount;
这是生产环境给大表加约束的标准手法。
4.3 查看与管理约束
-- 查看某表所有约束
SELECT conname, contype, pg_get_constraintdef(oid)
FROM pg_constraint
WHERE conrelid = 'orders'::regclass;
-- contype: p=主键 u=唯一 f=外键 c=检查 x=排他
-- 删除约束
ALTER TABLE orders DROP CONSTRAINT chk_amount;
5. PostgreSQL vs MySQL 约束对比
| 特性 | PostgreSQL | MySQL |
|---|---|---|
| CHECK 约束 | 一直完整支持 | 8.0.16 前会被静默忽略,8.0.16+ 才真正生效 |
| 外键 | 完整支持 | InnoDB 支持,MyISAM 不支持 |
| 排他约束 EXCLUDE | ✅ 独有 | ❌ 不支持 |
| 延迟约束 DEFERRABLE | ✅ 支持 | ❌ 不支持(外键无法延迟) |
| NOT VALID 加约束 | ✅ 支持 | ❌ 无对应机制 |
| 部分唯一 | ✅ 部分唯一索引 | ❌ 需变通 |
| 主键底层 | 唯一索引(非聚簇) | 聚簇索引 |
面试要点:MySQL 早期版本(8.0.16 之前)的 CHECK 约束是"语法接受但完全不生效"的,很多老项目的数据完整性其实没被保护——这是常被问到的坑;PostgreSQL 则一贯严格执行。
6. 面试高频问答
Q1:约束应该放在数据库层还是应用层? 关键完整性约束(主键、外键、非空、唯一、值域 CHECK)应放数据库层,因为它无法被任何写入入口绕过,是最后防线;应用层校验用于提升体验和减少无效请求,两者互补而非替代。
Q2:PostgreSQL 有什么 MySQL 没有的约束能力? 排他约束(EXCLUDE,防时间/空间重叠)、延迟约束(DEFERRABLE,提交时才校验)、NOT VALID 加约束(大表不阻塞)、部分唯一(带 WHERE 的唯一索引),这些 MySQL 都不支持。
Q3:外键有什么级联选项? ON DELETE / ON UPDATE 可设 CASCADE(级联)、SET NULL、SET DEFAULT、RESTRICT(默认,有子记录禁止)、NO ACTION。
Q4:给一张亿级大表加 CHECK 约束,如何不长时间锁表?
先用 ADD CONSTRAINT ... NOT VALID 只对新数据生效(短暂锁),再在低峰 VALIDATE CONSTRAINT 校验存量(不阻塞读写)。
Q5:什么场景用延迟约束? 互相引用的外键、需要在事务中间临时违反约束、批量重排序等场景,用 DEFERRABLE INITIALLY DEFERRED 把校验推迟到事务提交时统一进行。
7. 下一步
学习 基础查询语句,开始掌握数据检索。
xingliuhua