PostgreSQL 范围、空间唯一性约束

发布于:2025-08-13 ⋅ 阅读:(17) ⋅ 点赞:(0)

PostgreSQL 范围、空间唯一性约束

在 PostgreSQL 中,实现数据唯一性不仅限于 UNIQUEPRIMARY 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:适合范围/空间/时间等需要复杂冲突检测的场景,比唯一约束灵活。