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的场景

  1. 需要严格实体标识的领域
    • 用户ID、订单编号等关键标识
  2. 需要外键引用的字段
    CREATE TABLE orders (
        order_id INTEGER PRIMARY KEY,
        user_id INTEGER REFERENCES users(id)
    );
    
  3. 需要自动增长的序列编号

优先使用UNIQUE的场景

  1. 业务唯一性验证字段
    • 用户名、手机号、邮箱等
  2. 需要保持NULL灵活性的字段
    CREATE TABLE products (
        sku TEXT PRIMARY KEY,
        barcode TEXT UNIQUE  -- 允许未录入条码的产品
    );
    
  3. 多维度组合约束
    • 地理坐标唯一性、时间槽预定等

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. 唯一约束保证显示顺序不冲突

混合使用时的黄金法则:

  1. 主键用于数据库层面的实体标识
  2. UNIQUE约束用于业务层面的数据稽核
  3. EXCLUDED处理插入冲突
    # 使用upsert语法处理重复插入
    cursor.execute('''
        INSERT INTO products (sku, barcode) 
        VALUES (?, ?)
        ON CONFLICT(barcode) DO UPDATE SET sku=EXCLUDED.sku
    ''', ('A001', '6920000000001'))
    

7. 避坑指南:那些年踩过的雷

  1. 空值陷阱

    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  -- 非空约束配合唯一约束
    );
    
  2. 性能权衡 在某用户表上做大量插入操作时的耗时对比(单位:毫秒):

    记录数 仅有主键 含3个UNIQUE
    1万 120 450
    10万 1400 5200

    建议:对高频更新字段慎用多个UNIQUE

  3. 架构级注意事项

    • 改变UNIQUE约束顺序可能影响查询性能
    • 使用ALTER TABLE ADD UNIQUE时要检测现有数据
    • 联合约束的字段顺序影响索引利用率

8. 智慧选择:常见困惑解答

问题一:主键是否天然包含唯一性? 回答:主键必定具有唯一性,但具有唯一性不一定是主键。二者的核心区别在于主键承担着更基础的系统级职责。

问题二:何时应该优先选择主键? 回答:当字段需要满足以下所有条件时:

  • 绝对不允许NULL
  • 需要作为其他表的外键参照
  • 期望获得最优的查询性能

问题三:在已存在主键的情况下添加UNIQUE约束的必要性? 回答:需要根据业务逻辑决定。例如用户表的手机号字段,虽然主键是用户ID,但为了防止用户重复注册,手机号的UNIQUE约束仍然必要。