一、主键设计的核心原则

主键是数据库表的灵魂所在,它就像每个人的身份证号码,必须满足几个基本特性:唯一性、非空性和不可变性。在设计MySQL表结构时,我们通常会考虑以下几种主键设计方案:

  1. 自增整数:最常用的方案,简单高效
  2. UUID:全局唯一标识符
  3. 业务主键:使用有业务意义的字段
  4. 复合主键:多个字段组合

让我们看一个典型的学生表设计示例(技术栈:MySQL):

CREATE TABLE students (
    id INT AUTO_INCREMENT PRIMARY KEY,  -- 自增主键
    student_no VARCHAR(20) UNIQUE,     -- 学号业务唯一键
    name VARCHAR(50) NOT NULL,
    class_id INT,
    INDEX idx_class (class_id)          -- 班级索引
) ENGINE=InnoDB;

这个设计采用了自增主键+业务唯一键的模式,既保证了插入性能,又满足了业务查询需求。值得注意的是,我们为class_id建立了普通索引,这是为了优化按班级查询的性能。

二、自增ID的优势与隐患

自增ID看似完美,实则暗藏玄机。先说说它的优点:

  1. 插入性能极佳:因为是顺序写入,不会导致页分裂
  2. 存储空间小:通常4字节就能满足
  3. 索引效率高:B+树结构对数字类型特别友好

但问题也随之而来,让我们通过一个电商订单系统的例子来说明(技术栈:MySQL):

-- 订单表设计
CREATE TABLE orders (
    order_id BIGINT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    amount DECIMAL(10,2),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_user (user_id)
);

-- 分库分表时可能出现的问题
INSERT INTO orders_1 (order_id, user_id, amount) 
VALUES (10001, 123, 99.9);  -- 分片1

INSERT INTO orders_2 (order_id, user_id, amount) 
VALUES (10001, 456, 199.9); -- 分片2出现相同ID!

这里暴露了自增ID的最大问题:在分布式系统中可能产生冲突。解决方案包括使用UUID、雪花算法等分布式ID生成方案。

三、业务主键的适用场景

不是所有表都适合用自增ID,有时候业务字段本身就是天然的主键。比如用户表,手机号或邮箱可能就是最佳选择:

CREATE TABLE users (
    mobile VARCHAR(20) PRIMARY KEY,  -- 使用手机号作为主键
    password VARCHAR(100) NOT NULL,
    nickname VARCHAR(50),
    created_at DATETIME DEFAULT NOW()
);

-- 查询示例
SELECT * FROM users WHERE mobile = '13800138000';

这种设计的好处是:

  1. 省去了额外的ID字段
  2. 业务查询直接命中主键索引
  3. 避免了不必要的JOIN操作

但要注意,如果业务键可能变更(比如手机号可修改),就不适合作为主键,因为主键应当是不可变的。

四、复合主键的艺术

当单个字段无法唯一标识记录时,复合主键就派上用场了。典型的例子是学生选课关系表:

CREATE TABLE student_courses (
    student_id INT NOT NULL,
    course_id INT NOT NULL,
    score TINYINT,
    selected_at DATETIME,
    PRIMARY KEY (student_id, course_id),  -- 复合主键
    INDEX idx_course (course_id)          -- 反向查询索引
);

-- 查询某个学生的所有课程
SELECT * FROM student_courses WHERE student_id = 1001;

-- 查询某门课程的所有学生
SELECT * FROM student_courses WHERE course_id = 2001;

复合主键的设计要考虑查询模式,将最常用的查询条件放在前面。同时要为反向查询建立适当的索引。

五、特殊场景下的主键选择

在高并发秒杀系统中,传统的主键设计可能成为瓶颈。来看一个优化案例(技术栈:MySQL):

-- 商品库存表
CREATE TABLE inventory (
    item_code VARCHAR(20) PRIMARY KEY,  -- 商品编码
    stock INT UNSIGNED NOT NULL,
    version INT UNSIGNED DEFAULT 0      -- 乐观锁版本号
);

-- 秒杀扣减库存操作
UPDATE inventory 
SET stock = stock - 1, 
    version = version + 1 
WHERE item_code = 'SKU123' 
AND stock >= 1 
AND version = #{oldVersion};

这里我们使用了业务主键+乐观锁的方案,避免了自增ID可能导致的性能瓶颈,同时保证了数据一致性。

六、主键与索引的协同设计

主键本身就是一种索引,但合理的索引设计能极大提升查询性能。看一个电商平台的例子:

CREATE TABLE products (
    id INT AUTO_INCREMENT PRIMARY KEY,
    category_id INT NOT NULL,
    brand_id INT NOT NULL,
    name VARCHAR(100) NOT NULL,
    price DECIMAL(10,2),
    INDEX idx_category_brand (category_id, brand_id),  -- 联合索引
    INDEX idx_name (name)                             -- 名称索引
);

-- 高效查询:命中联合索引
SELECT * FROM products 
WHERE category_id = 5 AND brand_id = 10;

-- 低效查询:无法使用联合索引
SELECT * FROM products 
WHERE brand_id = 10;  -- 只有category_id在前时才能用联合索引

这个例子告诉我们,联合索引的字段顺序至关重要,应该把选择性高的字段放在前面。

七、主键设计的最佳实践

经过前面的讨论,我们总结出以下最佳实践:

  1. 单机系统优先考虑自增ID,分布式系统考虑雪花算法
  2. 业务键作为主键的前提是不可变且唯一
  3. 复合主键要考虑最常用的查询模式
  4. 主键字段类型尽量小,INT比BIGINT更省空间
  5. 避免使用字符串作为主键,除非有特殊需求

最后看一个综合案例(技术栈:MySQL):

-- 消息表设计
CREATE TABLE messages (
    id BIGINT PRIMARY KEY,  -- 雪花算法生成的ID
    sender_id INT NOT NULL,
    receiver_id INT NOT NULL,
    content TEXT,
    is_read BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_sender (sender_id, created_at),  -- 发件箱查询
    INDEX idx_receiver (receiver_id, created_at) -- 收件箱查询
);

-- 分页查询收件箱
SELECT * FROM messages 
WHERE receiver_id = 1001 
ORDER BY created_at DESC 
LIMIT 0, 20;

这个设计考虑了分布式ID、查询模式优化和分页需求,是一个相对完善的设计方案。