Appearance
约束(Constraints)
概述
在 PostgreSQL 中,约束(Constraints) 是一种强制数据完整性的机制,确保数据库中的数据满足特定的业务规则和逻辑要求。约束比数据类型提供了更精细的数据控制能力。
为什么需要约束?
数据类型虽然能限制数据的基本格式,但对于复杂的业务逻辑来说还不够。比如:
- 产品价格必须为正数
- 每个产品编号只能出现一次
- 订单必须关联到已存在的产品
约束类型概览
约束类型 | 作用 | 适用场景 | 语法关键字 |
---|---|---|---|
检查约束 | 验证数据是否满足指定条件 | 价格为正数、年龄范围等 | CHECK |
非空约束 | 确保列不能为空值 | 必填字段 | NOT NULL |
唯一约束 | 确保列值在表中唯一 | 用户名、邮箱等 | UNIQUE |
主键约束 | 唯一标识表中的每一行 | 表的主要标识符 | PRIMARY KEY |
外键约束 | 维护表间的引用完整性 | 关联关系表 | REFERENCES |
排除约束 | 防止特定条件下的数据重叠 | 时间段不重叠等 | EXCLUDE |
5.1 检查约束(Check Constraints)
检查约束是最灵活的约束类型,允许您定义自定义的数据验证规则。
基本语法
sql
-- 列级检查约束
CREATE TABLE products (
product_no integer,
name text,
price numeric CHECK (price > 0) -- 价格必须为正数
);
-- 表级检查约束
CREATE TABLE products (
product_no integer,
name text,
price numeric,
CHECK (price > 0) -- 表级约束
);
命名约束
最佳实践
为约束指定有意义的名称,便于错误诊断和后续维护。
sql
CREATE TABLE products (
product_no integer,
name text,
price numeric CONSTRAINT positive_price CHECK (price > 0)
);
多列检查约束
sql
CREATE TABLE products (
product_no integer,
name text,
price numeric CHECK (price > 0),
discounted_price numeric CHECK (discounted_price > 0),
CHECK (price > discounted_price) -- 原价必须高于折扣价
);
复杂检查约束示例
sql
-- 员工信息表,包含多种业务规则
CREATE TABLE employees (
emp_id integer PRIMARY KEY,
name text NOT NULL,
age integer CONSTRAINT valid_age CHECK (age >= 18 AND age <= 65),
salary numeric CONSTRAINT positive_salary CHECK (salary > 0),
department text,
hire_date date DEFAULT CURRENT_DATE,
-- 表级约束:确保某些部门的最低工资标准
CONSTRAINT dept_salary_rule CHECK (
(department = 'IT' AND salary >= 50000) OR
(department = 'HR' AND salary >= 40000) OR
(department NOT IN ('IT', 'HR'))
)
);
sql
-- 成功插入
INSERT INTO employees (emp_id, name, age, salary, department)
VALUES (1, '张三', 28, 60000, 'IT');
-- 失败示例(工资不符合部门要求)
INSERT INTO employees (emp_id, name, age, salary, department)
VALUES (2, '李四', 25, 35000, 'IT');
-- ERROR: new row violates check constraint "dept_salary_rule"
NULL 值处理
IMPORTANT
检查约束在遇到 NULL 值时会返回 TRUE,因此不会阻止 NULL 值的插入。如需防止 NULL 值,需要配合 NOT NULL 约束。
sql
CREATE TABLE test_null (
id integer,
value integer CHECK (value > 0)
);
-- 这些插入都会成功
INSERT INTO test_null VALUES (1, 10); -- value > 0: TRUE
INSERT INTO test_null VALUES (2, NULL); -- NULL > 0: NULL (被视为TRUE)
约束的限制
重要限制
PostgreSQL 的 CHECK 约束不能:
- 引用其他表的数据
- 引用当前行以外的数据
- 包含子查询
- 调用非确定性函数(如
random()
、now()
)
5.2 非空约束(Not-Null Constraints)
非空约束是最简单但最常用的约束之一,确保列不能包含 NULL 值。
基本用法
sql
CREATE TABLE products (
product_no integer NOT NULL,
name text NOT NULL,
price numeric -- 允许为NULL
);
多个约束组合
sql
CREATE TABLE products (
product_no integer NOT NULL,
name text NOT NULL,
price numeric NOT NULL CHECK (price > 0) -- 既不能为空,又必须为正数
);
NULL vs NOT NULL 对比
sql
-- 显式允许NULL(默认行为)
CREATE TABLE example1 (
col1 integer NULL, -- 等同于没有指定
col2 integer -- 默认允许NULL
);
-- 要求非空
CREATE TABLE example2 (
col1 integer NOT NULL,
col2 text NOT NULL
);
实际应用示例
sql
CREATE TABLE users (
user_id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
password_hash TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- 可选字段
first_name VARCHAR(50),
last_name VARCHAR(50),
phone VARCHAR(20),
bio TEXT
);
sql
-- 成功插入(所有必填字段都有值)
INSERT INTO users (username, email, password_hash)
VALUES ('john_doe', '[email protected]', 'hashed_password');
-- 失败插入(缺少必填字段)
INSERT INTO users (username)
VALUES ('jane_doe');
-- ERROR: null value in column "email" violates not-null constraint
TIP
在大多数数据库设计中,核心业务字段都应该标记为 NOT NULL,这样可以避免很多数据质量问题。
5.3 唯一约束(Unique Constraints)
唯一约束确保列或列组合在表中的值是唯一的。
单列唯一约束
sql
-- 列级约束
CREATE TABLE products (
product_no integer UNIQUE,
name text,
price numeric
);
-- 表级约束
CREATE TABLE products (
product_no integer,
name text,
price numeric,
UNIQUE (product_no)
);
多列唯一约束
sql
CREATE TABLE flight_bookings (
booking_id SERIAL PRIMARY KEY,
flight_number VARCHAR(10),
passenger_name VARCHAR(100),
seat_number VARCHAR(5),
UNIQUE (flight_number, seat_number) -- 同一航班的座位号不能重复
);
命名唯一约束
sql
CREATE TABLE products (
product_no integer CONSTRAINT unique_product_no UNIQUE,
name text,
price numeric
);
NULL 值处理
NULL 值的特殊性
默认情况下,唯一约束允许多个 NULL 值,因为 NULL 不等于 NULL。
sql
CREATE TABLE test_unique (
id SERIAL PRIMARY KEY,
code VARCHAR(10) UNIQUE
);
-- 这些插入都会成功
INSERT INTO test_unique (code) VALUES ('A001');
INSERT INTO test_unique (code) VALUES (NULL);
INSERT INTO test_unique (code) VALUES (NULL); -- 第二个NULL也可以
-- 这个会失败
INSERT INTO test_unique (code) VALUES ('A001');
-- ERROR: duplicate key value violates unique constraint
NULLS NOT DISTINCT 选项
sql
CREATE TABLE products (
product_no integer,
code VARCHAR(10) UNIQUE NULLS NOT DISTINCT -- NULL值也被视为相同
);
-- 或者表级约束形式
CREATE TABLE products (
product_no integer,
code VARCHAR(10),
UNIQUE NULLS NOT DISTINCT (code)
);
实际应用示例
sql
CREATE TABLE user_accounts (
user_id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
phone VARCHAR(20) UNIQUE,
-- 命名约束便于管理
CONSTRAINT unique_username UNIQUE (username),
CONSTRAINT unique_email UNIQUE (email)
);
sql
CREATE TABLE products (
product_id SERIAL PRIMARY KEY,
sku VARCHAR(50) UNIQUE NOT NULL,
name VARCHAR(200) NOT NULL,
category_id INTEGER,
-- 确保同一分类下的产品名称唯一
UNIQUE (name, category_id)
);
5.4 主键约束(Primary Key Constraints)
主键约束是唯一约束和非空约束的组合,用于唯一标识表中的每一行。
基本概念
主键 = 唯一性 + 非空性
sql
-- 这两种定义是等价的
CREATE TABLE products (
product_no integer UNIQUE NOT NULL,
name text,
price numeric
);
CREATE TABLE products (
product_no integer PRIMARY KEY,
name text,
price numeric
);
单列主键
sql
CREATE TABLE users (
user_id SERIAL PRIMARY KEY, -- 自增主键
username VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL
);
复合主键
sql
CREATE TABLE order_items (
order_id INTEGER,
product_id INTEGER,
quantity INTEGER NOT NULL,
unit_price DECIMAL(10,2) NOT NULL,
PRIMARY KEY (order_id, product_id) -- 复合主键
);
主键的特性
实际应用示例
sql
-- 用户表
CREATE TABLE users (
user_id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL
);
-- 订单表
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(user_id),
order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
total_amount DECIMAL(12,2)
);
-- 订单详情表(复合主键)
CREATE TABLE order_details (
order_id INTEGER,
product_id INTEGER,
quantity INTEGER NOT NULL,
unit_price DECIMAL(10,2) NOT NULL,
PRIMARY KEY (order_id, product_id),
FOREIGN KEY (order_id) REFERENCES orders(order_id)
);
sql
-- 插入用户
INSERT INTO users (username, email)
VALUES ('alice', '[email protected]');
-- 插入订单
INSERT INTO orders (user_id, total_amount)
VALUES (1, 299.99);
-- 插入订单详情
INSERT INTO order_details (order_id, product_id, quantity, unit_price)
VALUES (1, 101, 2, 149.99);
-- 尝试插入重复的复合主键(会失败)
INSERT INTO order_details (order_id, product_id, quantity, unit_price)
VALUES (1, 101, 1, 199.99);
-- ERROR: duplicate key value violates unique constraint "order_details_pkey"
IMPORTANT
每个表最多只能有一个主键,但可以有多个唯一约束。主键通常用于:
- 表的逻辑标识
- 外键引用的目标
- 数据库复制和同步
5.5 外键约束(Foreign Key Constraints)
外键约束维护表之间的引用完整性,确保子表中的值在父表中确实存在。
基本概念
基本语法
sql
-- 父表
CREATE TABLE products (
product_id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
price DECIMAL(10,2) NOT NULL
);
-- 子表
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
product_id INTEGER REFERENCES products(product_id), -- 外键
quantity INTEGER NOT NULL
);
简化语法
sql
-- 当引用主键时,可以省略列名
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
product_id INTEGER REFERENCES products, -- 自动引用products的主键
quantity INTEGER NOT NULL
);
命名外键约束
sql
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
product_id INTEGER,
quantity INTEGER NOT NULL,
CONSTRAINT fk_order_product
FOREIGN KEY (product_id) REFERENCES products(product_id)
);
复合外键
sql
-- 父表
CREATE TABLE categories (
category_id INTEGER,
region_id INTEGER,
name VARCHAR(100),
PRIMARY KEY (category_id, region_id)
);
-- 子表
CREATE TABLE products (
product_id SERIAL PRIMARY KEY,
category_id INTEGER,
region_id INTEGER,
name VARCHAR(100),
FOREIGN KEY (category_id, region_id)
REFERENCES categories(category_id, region_id)
);
外键约束的行为选项
当父表中的记录被删除或更新时,外键约束可以采取不同的处理策略:
选项 | 行为 | 使用场景 |
---|---|---|
NO ACTION | 阻止操作(默认) | 需要手动处理关联数据 |
RESTRICT | 立即阻止操作 | 严格的引用完整性 |
CASCADE | 级联删除/更新 | 子记录依赖于父记录 |
SET NULL | 设置为NULL | 可选的关联关系 |
SET DEFAULT | 设置为默认值 | 有默认关联对象 |
删除行为示例
sql
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
customer_id INTEGER REFERENCES customers ON DELETE CASCADE,
product_id INTEGER REFERENCES products ON DELETE RESTRICT,
quantity INTEGER NOT NULL
);
-- 删除客户时,该客户的所有订单也会被删除
-- 删除产品时,如果有订单引用该产品,删除操作会被阻止
sql
CREATE TABLE employees (
emp_id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
manager_id INTEGER REFERENCES employees ON DELETE SET NULL
);
-- 当经理被删除时,下属员工的manager_id被设置为NULL
sql
CREATE TABLE posts (
post_id SERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
category_id INTEGER DEFAULT 1
REFERENCES categories ON DELETE SET DEFAULT
);
-- 当分类被删除时,相关文章的分类设置为默认分类(ID=1)
更新行为
sql
CREATE TABLE order_items (
item_id SERIAL PRIMARY KEY,
order_id INTEGER
REFERENCES orders(order_id) ON UPDATE CASCADE,
product_id INTEGER
REFERENCES products(product_id) ON UPDATE RESTRICT
);
自引用外键
sql
CREATE TABLE employees (
emp_id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
department VARCHAR(50),
manager_id INTEGER REFERENCES employees(emp_id)
);
完整的多表关系示例
sql
-- 客户表
CREATE TABLE customers (
customer_id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL
);
-- 产品表
CREATE TABLE products (
product_id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
price DECIMAL(10,2) NOT NULL CHECK (price > 0)
);
-- 订单表
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
customer_id INTEGER NOT NULL,
order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (customer_id)
REFERENCES customers(customer_id) ON DELETE RESTRICT
);
-- 订单详情表
CREATE TABLE order_items (
order_id INTEGER,
product_id INTEGER,
quantity INTEGER NOT NULL CHECK (quantity > 0),
unit_price DECIMAL(10,2) NOT NULL,
PRIMARY KEY (order_id, product_id),
FOREIGN KEY (order_id)
REFERENCES orders(order_id) ON DELETE CASCADE,
FOREIGN KEY (product_id)
REFERENCES products(product_id) ON DELETE RESTRICT
);
sql
-- 插入基础数据
INSERT INTO customers (name, email) VALUES
('张三', '[email protected]'),
('李四', '[email protected]');
INSERT INTO products (name, price) VALUES
('笔记本电脑', 5999.99),
('无线鼠标', 199.99);
-- 创建订单
INSERT INTO orders (customer_id) VALUES (1);
-- 添加订单详情
INSERT INTO order_items (order_id, product_id, quantity, unit_price) VALUES
(1, 1, 1, 5999.99),
(1, 2, 2, 199.99);
-- 尝试删除被引用的产品(会失败)
DELETE FROM products WHERE product_id = 1;
-- ERROR: update or delete on table "products" violates foreign key constraint
-- 删除订单(订单详情会被级联删除)
DELETE FROM orders WHERE order_id = 1;
MATCH 选项
sql
CREATE TABLE order_items (
order_id INTEGER,
product_id INTEGER,
supplier_id INTEGER,
FOREIGN KEY (product_id, supplier_id)
REFERENCES products(product_id, supplier_id) MATCH FULL
);
MATCH FULL
:所有外键列都必须非空,或者全部为空MATCH PARTIAL
:PostgreSQL 不支持- 默认行为:允许部分列为空
5.6 排除约束(Exclude Constraints)
排除约束是 PostgreSQL 特有的高级约束类型,用于防止某些条件下的数据重叠。
基本概念
排除约束确保任意两行在指定列上使用指定操作符比较时,至少有一个比较返回 false。
基本语法
sql
CREATE TABLE circles (
circle_id SERIAL PRIMARY KEY,
area CIRCLE,
EXCLUDE USING gist (area WITH &&) -- 确保圆形区域不重叠
);
时间段不重叠示例
sql
-- 需要安装btree_gist扩展
CREATE EXTENSION IF NOT EXISTS btree_gist;
CREATE TABLE room_bookings (
booking_id SERIAL PRIMARY KEY,
room_id INTEGER NOT NULL,
start_time TIMESTAMP NOT NULL,
end_time TIMESTAMP NOT NULL,
user_name VARCHAR(100) NOT NULL,
-- 检查时间范围有效性
CHECK (start_time < end_time),
-- 确保同一房间的预订时间不重叠
EXCLUDE USING gist (
room_id WITH =,
tsrange(start_time, end_time) WITH &&
)
);
sql
-- 成功插入
INSERT INTO room_bookings (room_id, start_time, end_time, user_name) VALUES
(1, '2024-01-15 09:00:00', '2024-01-15 10:00:00', '张三'),
(1, '2024-01-15 10:30:00', '2024-01-15 11:30:00', '李四');
-- 失败插入(时间重叠)
INSERT INTO room_bookings (room_id, start_time, end_time, user_name) VALUES
(1, '2024-01-15 09:30:00', '2024-01-15 10:30:00', '王五');
-- ERROR: conflicting key value violates exclusion constraint
几何对象不重叠
sql
-- 确保在同一层级上的区域不重叠
CREATE TABLE floor_areas (
area_id SERIAL PRIMARY KEY,
floor_level INTEGER NOT NULL,
area_polygon POLYGON NOT NULL,
area_name VARCHAR(100),
EXCLUDE USING gist (
floor_level WITH =,
area_polygon WITH &&
)
);
排除约束的操作符
数据类型 | 常用操作符 | 含义 |
---|---|---|
数值类型 | = | 相等 |
时间范围 | && | 重叠 |
几何类型 | && | 相交 |
数组类型 | && | 有公共元素 |
文本类型 | = | 相等 |
复杂排除约束示例
sql
CREATE EXTENSION IF NOT EXISTS btree_gist;
CREATE TABLE work_schedules (
schedule_id SERIAL PRIMARY KEY,
employee_id INTEGER NOT NULL,
shift_date DATE NOT NULL,
start_time TIME NOT NULL,
end_time TIME NOT NULL,
CHECK (start_time < end_time),
-- 确保同一员工在同一天不能有重叠的班次
EXCLUDE USING gist (
employee_id WITH =,
shift_date WITH =,
timerange(start_time, end_time) WITH &&
)
);
sql
CREATE TABLE ip_allocations (
allocation_id SERIAL PRIMARY KEY,
network_segment INET NOT NULL,
allocated_to VARCHAR(100),
allocation_date DATE DEFAULT CURRENT_DATE,
-- 确保IP网段不重叠
EXCLUDE USING gist (network_segment inet_ops WITH &&)
);
约束管理
添加约束
sql
-- 添加检查约束
ALTER TABLE products
ADD CONSTRAINT check_positive_price CHECK (price > 0);
-- 添加外键约束
ALTER TABLE orders
ADD CONSTRAINT fk_customer
FOREIGN KEY (customer_id) REFERENCES customers(customer_id);
-- 添加唯一约束
ALTER TABLE users
ADD CONSTRAINT unique_email UNIQUE (email);
删除约束
sql
-- 删除命名约束
ALTER TABLE products DROP CONSTRAINT check_positive_price;
-- 删除外键约束
ALTER TABLE orders DROP CONSTRAINT fk_customer;
查看约束信息
sql
SELECT
conname AS constraint_name,
contype AS constraint_type,
pg_get_constraintdef(oid) AS definition
FROM pg_constraint
WHERE conrelid = 'products'::regclass;
sql
SELECT
tc.table_name,
kcu.column_name,
ccu.table_name AS foreign_table_name,
ccu.column_name AS foreign_column_name
FROM
information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu
ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage AS ccu
ON ccu.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_name='orders';
性能考虑
索引自动创建
外键索引建议
sql
-- 在外键列上手动创建索引以提高性能
CREATE TABLE order_items (
item_id SERIAL PRIMARY KEY,
order_id INTEGER REFERENCES orders(order_id),
product_id INTEGER REFERENCES products(product_id),
quantity INTEGER
);
-- 手动创建外键索引
CREATE INDEX idx_order_items_order_id ON order_items(order_id);
CREATE INDEX idx_order_items_product_id ON order_items(product_id);
最佳实践
约束设计建议
- 命名规范:为所有约束指定有意义的名称
- 性能优化:为外键列创建索引
- 业务逻辑:用约束实现关键业务规则
- 数据完整性:优先在数据库层面保证数据一致性
- 约束组合:合理组合不同类型的约束
注意事项
- 约束会影响 INSERT/UPDATE 性能
- 外键约束可能导致锁等待
- 约束违反会导致操作失败
- 需要考虑约束对数据迁移的影响
总结
PostgreSQL 的约束系统提供了强大的数据完整性保障机制:
- 检查约束:最灵活的自定义验证规则
- 非空约束:确保关键字段不为空
- 唯一约束:保证数据的唯一性
- 主键约束:表的主要标识符
- 外键约束:维护表间关系的一致性
- 排除约束:处理复杂的数据重叠问题
通过合理使用这些约束,可以在数据库层面建立强大的数据质量保障体系,减少应用程序中的数据验证逻辑,提高系统的可靠性和数据的一致性。