唯一索引与 NULL 值:PostgreSQL 主键约束与 NULLS NOT DISTINCT

唯一索引与 NULL 值:PostgreSQL 主键约束与 NULLS NOT DISTINCT

标签:#PostgreSQL #索引 #唯一约束 #NULL

一、前言

你有没有遇到过:给 email 列建了唯一索引,插入两条 email = NULL 的记录居然没有报错

这不是 bug,而是 PostgreSQL 的标准行为 :唯一索引 / 唯一约束默认把不同的 NULL 视为互不相等 ,因此唯一列允许存在多个 NULL。这与很多开发者的直觉相反(毕竟 MySQL 也类似,但有些数据库不是这样)。如果你确实需要"NULL 也唯一",PostgreSQL 15 起提供了 NULLS NOT DISTINCT------但要不要用,得先评估

二、核心开发规范

  1. 主键约束会自动创建一个唯一索引,唯一约束同样自动建唯一索引------唯一性保证的底层都是唯一 B-tree 索引;
  2. 唯一索引 / 唯一约束默认允许列中有多个 NULL:不同的 NULL 值被视为不相等(符合 SQL 标准),这是默认行为,不是配置错误;
  3. 业务确实要求"NULL 也唯一"时 ,才用 NULLS NOT DISTINCT(PG 15+)------使用前必须评估数据现状和业务语义,否则建索引会直接失败。

三、底层原理通俗讲解

  • 主键 = 非空 + 唯一PRIMARY KEY 约束要求列值唯一且非空,一个表只能有一个主键;创建主键时 PostgreSQL 自动创建同名的唯一 B-tree 索引 (如 orders_pkey);
  • 唯一约束 vs 唯一索引UNIQUE 约束(建表时声明)和 CREATE UNIQUE INDEX 底层都是唯一的 B-tree 索引,效果等价;区别在语义层------约束是声明式的、可被外键引用、记录在 information_schema 中;
  • NULL 的独特语义 :SQL 标准里 NULL = NULL 的结果是"未知"而非"真",所以唯一性比较时两个 NULL 永远不相等 → 唯一列允许插入多个 NULL。官方文档原话:"不同的 NULL 值在唯一性比较中永远不会被视为相等";
  • NULLS NOT DISTINCT(PG 15+) :把 NULL 也纳入唯一性比较------CREATE UNIQUE INDEX ... NULLS NOT DISTINCT 之后,列中只允许一个 NULL;建表约束同样支持 UNIQUE NULLS NOT DISTINCT
  • 多列唯一索引 :只拒绝"所有索引列都相等"的行组合;只要组合中任意一列不同(包括 NULL 参与比较),就允许插入。

四、实战错误案例&优化方案

场景1:唯一索引默认允许多个 NULL(最高频认知偏差)

表设计:users 表,email VARCHAR(255) UNIQUE,业务期望"邮箱全局唯一"

❌ 认知错误(以为唯一索引会拒绝重复 NULL)

sql 复制代码
CREATE TABLE users (
  id    BIGSERIAL PRIMARY KEY,
  email VARCHAR(255) UNIQUE
);

-- 插入两条 email 为 NULL 的记录:都成功了!
INSERT INTO users (email) VALUES (NULL), (NULL);
-- 第二条没有报错------NULL 与 NULL 不相等,不触发唯一性冲突

✅ 正确认知(根据业务语义选择方案)

sql 复制代码
-- 方案A:邮箱是业务必填 → 加 NOT NULL,从源头杜绝 NULL
CREATE TABLE users (
  id    BIGSERIAL PRIMARY KEY,
  email VARCHAR(255) NOT NULL UNIQUE
);

-- 方案B:允许无邮箱用户,但每个用户只允许一条 NULL
--        → PG 15+ 用 NULLS NOT DISTINCT(见场景2)

关键结论 :唯一索引默认不拦 NULL ;想要"邮箱唯一"且"邮箱必填",请用 NOT NULL UNIQUE------大多数业务要的其实是这个组合。

场景2:业务要求"NULL 也唯一"(NULLS NOT DISTINCT)

表设计:users 表,email 允许为空,但空值也只能出现一次(如每个用户最多一条未绑定邮箱记录)

❌ 错误写法(默认行为下 NULL 可重复)

sql 复制代码
CREATE UNIQUE INDEX idx_users_email ON users (email);
-- 两条 NULL 仍可插入,业务校验形同虚设

✅ 正确写法(PG 15+:NULLS NOT DISTINCT 让 NULL 参与唯一性)

sql 复制代码
CREATE UNIQUE INDEX idx_users_email ON users (email) NULLS NOT DISTINCT;
-- 再次插入第二条 NULL 时:
-- ERROR: duplicate key value violates unique constraint "idx_users_email"

关键结论NULLS NOT DISTINCT 是唯一索引的专属选项(PG 15+),让"NULL 也只允许一个";建表约束写法是 UNIQUE (email) NULLS NOT DISTINCT

场景3:验证主键自动创建的唯一索引

表设计:ordersid BIGSERIAL PRIMARY KEY

✅ 正确认知(主键背后确实有一个唯一索引)

sql 复制代码
CREATE TABLE orders (
  id   BIGSERIAL PRIMARY KEY,
  user_id BIGINT NOT NULL
);

-- 查看自动创建的索引:
SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'orders';
-- indexname: orders_pkey
-- indexdef:  CREATE UNIQUE INDEX orders_pkey ON orders USING btree (id)

关键结论 :主键 / 唯一约束会自动建唯一 B-tree 索引,不需要 再手动 CREATE UNIQUE INDEX 一遍(那是冗余索引);手动建的唯一索引则不会出现在 information_schema 约束表里。

场景4:多列唯一索引与 NULL 的"漏网之鱼"

表设计:orders 表,业务规则"同一用户同一状态只允许一条记录"

❌ 认知错误(以为组合唯一能拦住带 NULL 的重复)

sql 复制代码
CREATE UNIQUE INDEX idx_orders_user_status ON orders (user_id, status);

-- 下面两条都能插入成功(status 为 NULL 时组合不相等):
INSERT INTO orders (user_id, status) VALUES (10086, NULL);
INSERT INTO orders (user_id, status) VALUES (10086, NULL);

✅ 正确做法(按业务语义选择)

sql 复制代码
-- 方案A:status 不允许为空 → NOT NULL UNIQUE
-- 方案B:空状态也只允许一条 → NULLS NOT DISTINCT
CREATE UNIQUE INDEX idx_orders_user_status
  ON orders (user_id, status) NULLS NOT DISTINCT;

关键结论 :多列唯一索引只拒绝"所有列都相等"的组合;只要组合里有一列是 NULL,重复就拦不住------这是最容易在生产上踩的隐性坑。

五、绝对禁止的写法汇总

  • 用唯一索引 / 唯一约束默认行为去拦截重复 NULL(默认拦不住,先想清楚业务语义);
  • 未评估数据现状 就加 NULLS NOT DISTINCT(表里已有多个 NULL → 建索引直接失败);
  • PG 15 以下 使用 NULLS NOT DISTINCT 语法(直接语法错误);
  • 已有主键 / 唯一约束,再手动建一个相同列的唯一索引(冗余索引);
  • 把唯一约束和唯一索引混为一谈 (外键引用、information_schema 语义不同)。

六、最终评审口诀(记住不踩坑)

唯一索引 NULL 不相等,多个 NULL 不报错;

业务硬要 NULL 唯一,NOT DISTINCT 再斟酌。

七、总结

  • 机制:主键约束自动创建唯一 B-tree 索引;唯一约束 / 唯一索引底层等价,都是唯一 B-tree;
  • NULL 行为:默认"NULL 之间不相等"(SQL 标准),唯一列允许多个 NULL;多列唯一索引只拒绝全列相等的组合;
  • 进阶手段 :PG 15+ 的 NULLS NOT DISTINCT 让 NULL 参与唯一性,但用之前必须评估:版本是否够、数据现状有没有多个 NULL、业务是否真的需要"NULL 也唯一";
  • 最稳方案 :多数"唯一 + 必填"业务直接用 NOT NULL UNIQUE,比 NULLS NOT DISTINCT 更简单、更可读。

标签:PostgreSQL 数据库 唯一索引


参考来源

相关推荐
大勇前进1 小时前
从 OC 到 Swift,老 iOS 开发者踩过的 10 个语法大坑
后端
我的xiaodoujiao1 小时前
Django 基础知识详细图文教程 9-Django 模板引擎 2
开发语言·数据库·后端·django
蓝速科技2 小时前
商用复杂环境翻译机耐用性选型指南丨蓝速科技
大数据·运维·数据库·人工智能·科技
烈风逍遥2 小时前
第五篇:通用 LLM 流式对话:前后端联接的完整实现
前端·后端·架构
Ticnix2 小时前
别再手动上线了:一条命令带备份、健康检查和自动回滚
后端·python·ci/cd
ArkPppp2 小时前
如何从零上线一个耐造的Redis缓存系统——最直接最不绕弯子的方式
后端·ai编程
Kyrie_kk2 小时前
Java--ProcessBuilder操作系统进程
java·后端
Ticnix2 小时前
迁移脚本能跑通,不代表你回滚得回来
后端·python
starzy19902 小时前
Flink Sliding Window 详解及代码实现:从窗口重叠到状态爆炸防控
运维·数据库·flink