PostgreSQL 范围、空间唯一性约束
在 PostgreSQL 中,实现数据唯一性不仅限于 UNIQUE
和 PRIMARY KEY
,还可以使用 EXCLUDE
约束配合 GiST 索引来实现更灵活的唯一性检查。
1. UNIQUE
约束
作用
- 保证某列(或多列组合)的值在表中不重复。
- 可以有多个
UNIQUE
约束。 - 允许列中出现多个
NULL
(因为NULL
被认为是“未知”,彼此不相等)。
测试语句
DROP TABLE IF EXISTS customers;
postgres=# CREATE TABLE customers (
postgres(# customerid INT UNIQUE,
postgres(# name TEXT
postgres(# );
CREATE TABLE
postgres=#
postgres=# -- 插入测试数据
postgres=# INSERT INTO customers VALUES (1, 'Alice'); -- 成功
INSERT 0 1
postgres=# INSERT INTO customers VALUES (2, 'Bob'); -- 成功
INSERT 0 1
postgres=# INSERT INTO customers VALUES (1, 'Charlie'); -- 报错:违反唯一约束
ERROR: duplicate key value violates unique constraint "customers_customerid_key"
DETAIL: Key (customerid)=(1) already exists.
postgres=#
postgres=# -- NULL 测试
postgres=# INSERT INTO customers VALUES (NULL, 'David'); -- 成功
INSERT 0 1
postgres=# INSERT INTO customers VALUES (NULL, 'Eve'); -- 成功(UNIQUE 允许多个 NULL)
特点总结
可在同一个表中定义多个。可接受多个 NULL。自动创建唯一 B-Tree 索引。
2. PRIMARY KEY
约束
作用
唯一标识表中每一行数据。每个表只能有一个 PRIMARY KEY。自动包含 NOT NULL 和唯一性约束。
测试语句
DROP TABLE IF EXISTS customers_pk;
postgres=# CREATE TABLE customers_pk (
postgres(# customerid INT PRIMARY KEY,
postgres(# name TEXT
postgres(# );
CREATE TABLE
postgres=# -- 插入测试数据
postgres=# INSERT INTO customers_pk VALUES (1, 'Alice'); -- 成功
INSERT 0 1
postgres=# INSERT INTO customers_pk VALUES (2, 'Bob'); -- 成功
INSERT 0 1
postgres=# INSERT INTO customers_pk VALUES (1, 'Charlie'); -- 报错:违反主键唯一性
ERROR: duplicate key value violates unique constraint "customers_pk_pkey"
DETAIL: Key (customerid)=(1) already exists.
postgres=# -- NULL 测试
postgres=# INSERT INTO customers_pk VALUES (NULL, 'David');
ERROR: null value in column "customerid" of relation "customers_pk" violates not-null constraint
DETAIL: Failing row contains (null, David).
-- 报错:主键列不允许 NULL
特点总结
每表只能有一个主键,可由一列或多列组合。自动 NOT NULL。自动创建唯一 B-Tree 索引。
3. EXCLUDE USING gist
作用
用 GiST 索引实现更灵活的唯一性检查,不仅能比较相等,还能比较空间重叠、范围重叠等复杂条件。常见于几何类型(point、box)、范围类型(int4range、tsrange 等)。
几何类型示例
postgres=# CREATE TABLE boxes (
postgres(# id SERIAL PRIMARY KEY,
postgres(# position box,
postgres(# EXCLUDE USING gist (position WITH &&)
postgres(# );
CREATE TABLE
postgres=#
postgres=# -- 插入测试数据
postgres=# INSERT INTO boxes (position) VALUES (box(point(0,0), point(1,1))); -- 成功
INSERT 0 1
postgres=# INSERT INTO boxes (position) VALUES (box(point(2,2), point(3,3))); -- 成功
INSERT 0 1
postgres=# INSERT INTO boxes (position) VALUES (box(point(0.5,0.5), point(1.5,1.5)));
ERROR: conflicting key value violates exclusion constraint "boxes_position_excl"
DETAIL: Key ("position")=((1.5,1.5),(0.5,0.5)) conflicts with existing key ("position")=((1,1),(0,0)).
-- 报错:两个矩形重叠
时间区间冲突检测示例
假设我们要做一个 会议室预约系统,要求同一个会议室不能在同一时间段被重复预约。我们可以用 EXCLUDE USING gist 来做时间范围的唯一性约束。满足以下三点要求
1、同一会议室 + confirmed 状态 → 不能有时间交叉。
2、不同会议室 → 不受影响。
3、status 不是 confirmed → 不受影响。
-- 1) GiST 索引支持等值比较
CREATE EXTENSION IF NOT EXISTS btree_gist; --GiST 索引默认能处理几何类型、范围类型的操作符(如 &&),本范例中加入= 的比较,所以增加btree_gist插件辅助。
-- 2) 表结构
drop table if exists reservations;
CREATE TABLE reservations (
id serial PRIMARY KEY,
room text NOT NULL,
during tstzrange NOT NULL,
status text NOT NULL DEFAULT 'pending',
-- 同一 room 在 status='confirmed' 时,不允许 during 区间重叠
CONSTRAINT no_overlap_confirmed
EXCLUDE USING gist (
room WITH =, -- 同一个会议室
during WITH && -- 时间范围有重叠
)
WHERE (status = 'confirmed')
);
postgres=# -- ✅ 成功:第一次预约,会议室 A
postgres=# INSERT INTO reservations (room, during, status)
postgres-# VALUES ('A', '[2025-08-12 09:00,2025-08-12 10:00)', 'confirmed');
INSERT 0 1
postgres=# -- ❌ 失败:同一会议室 A,时间区间有交叉
postgres=# INSERT INTO reservations (room, during, status)
postgres-# VALUES ('A', '[2025-08-12 09:30,2025-08-12 10:30)', 'confirmed');
ERROR: conflicting key value violates exclusion constraint "no_overlap_confirmed"
DETAIL: Key (room, during)=(A, ["2025-08-12 09:30:00+08","2025-08-12 10:30:00+08")) conflicts with existing key (room, during)=(A, ["2025-08-12 09:00:00+08","2025-08-12 10:00:00+08"))
postgres=# INSERT INTO reservations (room, during, status)
postgres-# VALUES ('A', '[2025-08-12 10:00,2025-08-12 11:00)', 'confirmed');
INSERT 0 1
postgres=#
postgres=# -- ✅ 成功:会议室 B,即使时间重叠也没问题
postgres=# INSERT INTO reservations (room, during, status)
postgres-# VALUES ('B', '[2025-08-12 09:30,2025-08-12 10:30)', 'confirmed');
INSERT 0 1
postgres=#
postgres=# -- ✅ 成功:会议室 A,但 status 不是 confirmed,不受限制
postgres=# INSERT INTO reservations (room, during, status)
postgres-# VALUES ('A', '[2025-08-12 09:30,2025-08-12 10:30)', 'pending');
INSERT 0 1
小结
UNIQUE
:适合需要唯一性但允许多个 NULL 的列。
PRIMARY KEY
:适合作为表的主标识,不允许 NULL,只能有一个。
EXCLUDE USING gist
:适合范围/空间/时间等需要复杂冲突检测的场景,比唯一约束灵活。