CHAPTER 2 · DATABASE DESIGN REVIEW
02
MySQL 表能建出来,
就算设计好了吗?
AI 很容易生成一套“看起来合理、也能运行”的表结构。真正要训练的,是判断它是否经得起查询、约束、规模变化和故障。
开场:先审查,不要先改写
这是 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 使用字符串有什么风险?
相册被删除后,照片记录怎么办?
本页直接给结论。目标是建立一种判断习惯:看到一个表设计时,立刻追问查询、约束、规模和一致性。
七条评审规范
不背定义,直接审查真实取舍。
从高频查询反推索引
先问相册和照片最常怎么查,再决定联合索引;不要等慢了才盲目补索引。
WHERE album_id = ? ORDER BY created_at DESC这条查询需要同时过滤和排序,所以 (album_id, created_at) 是有明确理由的索引。单独给每个字段建索引,未必能匹配这条访问路径。
列表接口按需查字段
列表最终只展示少数字段,就不要把未来所有字段都通过 SELECT * 带出来。
SELECT id, name, photo_count, created_at FROM albums ...明确字段不仅是性能优化,也是在固定 API 契约:表结构增加内部字段时,不会自动泄露给前端。
持续增长的列表从一开始分页
50 张照片可以全取,但设计必须面对 10 万张照片时的内存、网络和页面体验。
LIMIT 20 OFFSET 0分页不是后期优化,而是列表接口的基本协议。深分页的局限,留到性能页继续追问。
关键规则让数据库兜底
“先查询、再插入”的代码在并发下不可靠;关键唯一性不应只依赖前端或 Go 代码。
UNIQUE KEY uk_albums_user_name(user_id, name)同一用户不能创建两个同名相册,这是数据完整性规则。数据库约束能挡住并发请求穿透业务判断。
相册表应该保存 photo_count
相册列表几乎一定会展示照片数量,频繁对照片表做 COUNT(*) 不划算。
photo_count INT UNSIGNED NOT NULL DEFAULT 0结论:这里应该冗余保存。要一起讲清楚的是维护规则:上传成功后加一,删除成功后减一,并把照片记录和计数更新放在同一个数据库事务里。
字段类型要表达真实含义
不是所有东西都该塞进 VARCHAR(255);类型本身也是对错误数据的约束。
visibility TINYINT · created_at DATETIME · photo_count INT UNSIGNED时间要能正确排序,数量不能为负,状态要限制取值范围。类型选择服务于数据质量和后续查询。
删除策略必须提前明确
物理删除、逻辑删除、级联删除都不是默认正确答案,它们对应恢复、审计和数据规模的不同要求。
DELETE FROM albums WHERE id = ?先确定照片会不会成为孤儿数据、是否需要恢复、逻辑删除如何影响唯一索引,再选策略;不要机械地给每张表加 deleted_at。
一个更可讨论的版本
这份表设计可以作为相册系统的推荐起点。
它不是所有系统的唯一答案,但对这个相册系统来说,关键取舍已经可以直接讲清楚。
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
相册列表通常要展示照片数,保存计数能避免频繁统计照片表。代价是上传、删除时必须同步维护。
所有数据都采用逻辑删除
不要一刀切需要恢复、审计时适合逻辑删除;临时数据或可重建数据未必需要。删除策略要提前明确。
本页总结
- 从高频查询反推索引,而不是事后盲目加索引。
- 列表接口按需查询字段,并从一开始支持分页。
- 关键数据规则应由数据库约束兜底。
photo_count在相册列表里是高频展示字段,应该保存,但要用事务维护一致性。- 字段类型、删除策略和索引设计都必须服务于真实业务场景。
这份设计现在可以上线了吗?
还不能确定。还要结合查询、删除、权限、数据规模和一致性场景继续判断。