全栈工程课 · 一个相册系统的上线之路
课程平台Chapter 2 · 数据库设计评审

CHAPTER 2 · DATABASE DESIGN REVIEW

02

MySQL 表能建出来,
就算设计好了吗?

AI 很容易生成一套“看起来合理、也能运行”的表结构。真正要训练的,是判断它是否经得起查询、约束、规模变化和故障

开场:先审查,不要先改写

这是 AI 为相册系统生成的第一版表设计。

先不问它“写得对不对”。把自己放到代码评审者的位置:它能运行,是否就适合被合并进真实项目?

schema.sql · AI 生成版本先阅读,再提出问题
CREATE TABLE albums (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  user_id BIGINT NOT NULL,
  name VARCHAR(100) NOT NULL,
  visibility VARCHAR(20) NOT NULL DEFAULT 'private',
  photo_count INT NOT NULL DEFAULT 0,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE photos (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  album_id BIGINT NOT NULL,
  name VARCHAR(100) NOT NULL,
  description VARCHAR(500),
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);

不要急着给答案

请先审查这 7 个问题。

这段 SQL 能运行吗?

能运行是否代表设计合理?

照片达到 100 万张时,查询还会快吗?

album_id 要不要加索引?

photo_count 应实时统计,还是单独保存?

visibility 使用字符串有什么风险?

相册被删除后,照片记录怎么办?

本页直接给结论。目标是建立一种判断习惯:看到一个表设计时,立刻追问查询、约束、规模和一致性。

七条评审规范

不背定义,直接审查真实取舍。

01

从高频查询反推索引

先问相册和照片最常怎么查,再决定联合索引;不要等慢了才盲目补索引。

WHERE album_id = ? ORDER BY created_at DESC

这条查询需要同时过滤和排序,所以 (album_id, created_at) 是有明确理由的索引。单独给每个字段建索引,未必能匹配这条访问路径。

02

列表接口按需查字段

列表最终只展示少数字段,就不要把未来所有字段都通过 SELECT * 带出来。

SELECT id, name, photo_count, created_at FROM albums ...

明确字段不仅是性能优化,也是在固定 API 契约:表结构增加内部字段时,不会自动泄露给前端。

03

持续增长的列表从一开始分页

50 张照片可以全取,但设计必须面对 10 万张照片时的内存、网络和页面体验。

LIMIT 20 OFFSET 0

分页不是后期优化,而是列表接口的基本协议。深分页的局限,留到性能页继续追问。

04

关键规则让数据库兜底

“先查询、再插入”的代码在并发下不可靠;关键唯一性不应只依赖前端或 Go 代码。

UNIQUE KEY uk_albums_user_name(user_id, name)

同一用户不能创建两个同名相册,这是数据完整性规则。数据库约束能挡住并发请求穿透业务判断。

05

相册表应该保存 photo_count

相册列表几乎一定会展示照片数量,频繁对照片表做 COUNT(*) 不划算。

photo_count INT UNSIGNED NOT NULL DEFAULT 0

结论:这里应该冗余保存。要一起讲清楚的是维护规则:上传成功后加一,删除成功后减一,并把照片记录和计数更新放在同一个数据库事务里。

06

字段类型要表达真实含义

不是所有东西都该塞进 VARCHAR(255);类型本身也是对错误数据的约束。

visibility TINYINT · created_at DATETIME · photo_count INT UNSIGNED

时间要能正确排序,数量不能为负,状态要限制取值范围。类型选择服务于数据质量和后续查询。

07

删除策略必须提前明确

物理删除、逻辑删除、级联删除都不是默认正确答案,它们对应恢复、审计和数据规模的不同要求。

DELETE FROM albums WHERE id = ?

先确定照片会不会成为孤儿数据、是否需要恢复、逻辑删除如何影响唯一索引,再选策略;不要机械地给每张表加 deleted_at

一个更可讨论的版本

这份表设计可以作为相册系统的推荐起点。

它不是所有系统的唯一答案,但对这个相册系统来说,关键取舍已经可以直接讲清楚。

schema.sql · 推荐起点关键行已标出
CREATE TABLE albums (
  id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  user_id BIGINT UNSIGNED NOT NULL,
  name VARCHAR(100) NOT NULL,
  visibility TINYINT NOT NULL DEFAULT 0,
  photo_count INT UNSIGNED NOT NULL DEFAULT 0,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  UNIQUE KEY uk_albums_user_name(user_id, name),
  KEY idx_albums_user_created(user_id, created_at)
);

CREATE TABLE photos (
  id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  album_id BIGINT UNSIGNED NOT NULL,
  name VARCHAR(100) NOT NULL,
  description VARCHAR(500),
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  KEY idx_photos_album_created(album_id, created_at)
);

直接结论

结论表:哪些该做,哪些不要做。

这里不做选择题,直接给出评审结论。每一行都按“结论 → 原因 → 代价”展开。

设计结论解释

所有字段都使用 VARCHAR

不建议

类型会丢失语义:时间、数量、状态都应该用能约束错误数据的类型。

所有列表接口都支持分页

应该做

照片和相册都会增长,分页是列表接口的基本设计,不是后期优化。

为每个字段都建立索引

不建议

索引会占空间、拖慢写入。只给高频查询路径建索引,尤其是过滤和排序同时出现的路径。

用户名或同一用户相册名增加唯一约束

应该做

关键唯一性必须让数据库兜底,避免并发请求绕过应用层判断。

相册保存 photo_count

应该做

相册列表通常要展示照片数,保存计数能避免频繁统计照片表。代价是上传、删除时必须同步维护。

所有数据都采用逻辑删除

不要一刀切

需要恢复、审计时适合逻辑删除;临时数据或可重建数据未必需要。删除策略要提前明确。

本页总结

  1. 从高频查询反推索引,而不是事后盲目加索引。
  2. 列表接口按需查询字段,并从一开始支持分页。
  3. 关键数据规则应由数据库约束兜底。
  4. photo_count 在相册列表里是高频展示字段,应该保存,但要用事务维护一致性。
  5. 字段类型、删除策略和索引设计都必须服务于真实业务场景。
这份设计现在可以上线了吗?
还不能确定。还要结合查询、删除、权限、数据规模和一致性场景继续判断。