1. 当数据需要身份认证时
在数据库设计的世界里,我们就像给数据核发身份证的户籍管理员。所有存放在数据库中的记录都需要明确的身份标识,这时候人们第一时间想到的往往是PRIMARY KEY
(主键)。但鲜为人知的是,UNIQUE
约束同样可以赋予数据"唯一身份证"的特性。
让我们看一个典型的用户表设计案例:
-- 技术栈:SQLite 3.39.4
-- 用户基本信息表(包含主键与唯一约束)
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT, -- 主键:自增的唯一标识
username TEXT NOT NULL UNIQUE, -- 用户名:唯一且非空
email TEXT UNIQUE, -- 邮箱:允许空值但必须唯一
phone TEXT UNIQUE -- 手机号:特殊字段验证
);
-- 插入合法数据
INSERT INTO users (username, email, phone) VALUES
('alice', 'alice@example.com', '13800138000'),
('bob', 'bob@example.com', NULL);
-- 触发唯一约束冲突的非法尝试
INSERT INTO users (username) VALUES ('alice'); -- 用户名重复导致失败
INSERT INTO users (email) VALUES (NULL); -- 允许但最多插入一次
这个案例展示了UNIQUE
的三个典型特征:
- 允许
NULL
值的重复(但每个NULL
占据独立位置) - 可以存在多个
UNIQUE
约束 - 适用于非主键的重要标识字段
2. PRIMARY KEY的严格哲学
主键就像公民身份证号码系统,遵循着更严格的规范:
-- 员工信息表(主键特征演示)
CREATE TABLE employees (
emp_id INTEGER PRIMARY KEY, -- 主键不允许NULL
name TEXT NOT NULL,
id_card TEXT UNIQUE -- 身份证字段建议设为UNIQUE
);
-- 合法数据插入
INSERT INTO employees VALUES (1, '王小明', '310101200001011234');
-- 触发主键规则的非法操作
INSERT INTO employees (name, id_card) VALUES ('李小红', '310101200002021234'); -- 缺失主键
INSERT INTO employees VALUES (NULL, '张三', '310101200003031234'); -- 主键为NULL
主键必须遵循的"三条铁律":
- 绝对禁止
NULL
值 - 每张表有且仅有一个主键
- 自动创建聚簇索引(影响物理存储)
3. 联合作战的组合约束
在真实业务场景中,我们经常遇到需要组合唯一性的需求:
# 技术栈:Python 3.9 + sqlite3
import sqlite3
conn = sqlite3.connect(':memory:')
cursor = conn.cursor()
# 创建会议室预定表(组合主键示例)
cursor.execute('''
CREATE TABLE meeting_room_booking (
room_id INTEGER,
date TEXT,
time_slot TEXT,
PRIMARY KEY (room_id, date, time_slot)
)
''')
# 改用组合UNIQUE的等效方案
cursor.execute('''
CREATE TABLE meeting_room_booking_v2 (
room_id INTEGER,
date TEXT,
time_slot TEXT,
UNIQUE (room_id, date, time_slot)
)
''')
# 插入验证数据
try:
cursor.executemany('INSERT INTO meeting_room_booking VALUES (?, ?, ?)', [
(101, '2023-08-01', '09:00-10:00'),
(101, '2023-08-01', '10:00-11:00')
])
conn.commit()
except sqlite3.IntegrityError as e:
print(f"冲突发生:{str(e)}")
组合约束的关键要点:
- 字段排列顺序影响索引结构
- 空值处理遵循逐字段校验原则
- 联合主键的物理存储效率更高
4. 从原理看本质差异
通过这个对比表格,我们能更清晰地把握两者的技术差异:
特性 | PRIMARY KEY | UNIQUE |
---|---|---|
NULL值接受度 | 完全拒绝 | 允许多个NULL |
索引类型 | 聚簇索引 | 普通索引 |
数量限制 | 每表仅一个 | 允许多个 |
自增特性 | 支持AUTOINCREMENT | 不支持 |
设计意图 | 实体标识 | 业务唯一性 |
外键引用可行性 | 可以作为外键目标 | 需要额外索引 |
5. 经典应用场景对决
适用PRIMARY KEY的场景
- 需要严格实体标识的领域
- 用户ID、订单编号等关键标识
- 需要外键引用的字段
CREATE TABLE orders ( order_id INTEGER PRIMARY KEY, user_id INTEGER REFERENCES users(id) );
- 需要自动增长的序列编号
优先使用UNIQUE的场景
- 业务唯一性验证字段
- 用户名、手机号、邮箱等
- 需要保持NULL灵活性的字段
CREATE TABLE products ( sku TEXT PRIMARY KEY, barcode TEXT UNIQUE -- 允许未录入条码的产品 );
- 多维度组合约束
- 地理坐标唯一性、时间槽预定等
6. 双剑合璧的最佳实践
在真实业务中组合使用两种约束能产生强大效果:
-- 电商商品规格表设计示例
CREATE TABLE product_specs (
product_id INTEGER REFERENCES products(id),
spec_type TEXT CHECK(spec_type IN ('color', 'size', 'material')),
spec_value TEXT,
display_order INTEGER,
PRIMARY KEY (product_id, spec_type),
UNIQUE (product_id, display_order)
);
-- 这种设计同时实现了:
-- 1. 主键确保规格类型不重复
-- 2. 唯一约束保证显示顺序不冲突
混合使用时的黄金法则:
- 主键用于数据库层面的实体标识
- UNIQUE约束用于业务层面的数据稽核
- 用
EXCLUDED
处理插入冲突# 使用upsert语法处理重复插入 cursor.execute(''' INSERT INTO products (sku, barcode) VALUES (?, ?) ON CONFLICT(barcode) DO UPDATE SET sku=EXCLUDED.sku ''', ('A001', '6920000000001'))
7. 避坑指南:那些年踩过的雷
空值陷阱
INSERT INTO users (username, email) VALUES ('tom', NULL); INSERT INTO users (username, email) VALUES ('jerry', NULL); -- 允许执行!
解决方案:
CREATE TABLE safe_users ( email TEXT UNIQUE NOT NULL -- 非空约束配合唯一约束 );
性能权衡 在某用户表上做大量插入操作时的耗时对比(单位:毫秒):
记录数 仅有主键 含3个UNIQUE 1万 120 450 10万 1400 5200 建议:对高频更新字段慎用多个UNIQUE
架构级注意事项
- 改变UNIQUE约束顺序可能影响查询性能
- 使用
ALTER TABLE ADD UNIQUE
时要检测现有数据 - 联合约束的字段顺序影响索引利用率
8. 智慧选择:常见困惑解答
问题一:主键是否天然包含唯一性? 回答:主键必定具有唯一性,但具有唯一性不一定是主键。二者的核心区别在于主键承担着更基础的系统级职责。
问题二:何时应该优先选择主键? 回答:当字段需要满足以下所有条件时:
- 绝对不允许NULL
- 需要作为其他表的外键参照
- 期望获得最优的查询性能
问题三:在已存在主键的情况下添加UNIQUE约束的必要性? 回答:需要根据业务逻辑决定。例如用户表的手机号字段,虽然主键是用户ID,但为了防止用户重复注册,手机号的UNIQUE约束仍然必要。