用户表和文章表必须分设,用外键关联;分类与标签须用多对多中间表;评论表需支持嵌套和审核;关键字段如user_id、slug、status+published_at等必须加索引。

用户表和文章表必须分开设计,不能用单表模拟关系
很多人一开始想省事,把用户信息和文章内容全塞进一个 posts 表里,加一堆 author_name、author_email 字段。这会导致数据冗余、更新异常,而且无法支持多作者协作或作者资料修改——改一次邮箱就得扫全表更新。
正确做法是拆成 users 和 posts 两张表,用 user_id 外键关联:
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
<p>CREATE TABLE posts (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
title VARCHAR(200) NOT NULL,
slug VARCHAR(200) UNIQUE, -- 用于 URL,需索引
content TEXT NOT NULL,
status ENUM('draft', 'published', 'archived') DEFAULT 'draft',
published_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);-
ON DELETE CASCADE要谨慎:删用户时自动删其文章,适合个人博客;团队博客建议改成SET NULL并允许user_id为 NULL -
slug必须加UNIQUE索引,否则重复 URL 会出错;生成逻辑应在应用层做(如用标题转小写+连字符),不要依赖 MySQL 自动生成 -
status用ENUM比VARCHAR更安全,避免非法值,但别滥用——如果状态流转复杂(比如加“reviewing”“rejected”),就该换TINYINT+ 注释说明
分类和标签必须用多对多中间表,别硬编码字段
看到有人在 posts 表里加 category 和 tag1/tag2/tag3 字段,这是典型反模式。它限制了每篇文章最多 3 个标签,且无法高效查“所有带‘MySQL’标签的文章”。
标准解法是三张表:分类表、标签表、中间关联表:
CREATE TABLE categories ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL UNIQUE, slug VARCHAR(50) UNIQUE NOT NULL ); <p>CREATE TABLE tags ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL UNIQUE, slug VARCHAR(50) UNIQUE NOT NULL );</p><p>CREATE TABLE post_categories ( post_id INT NOT NULL, category_id INT NOT NULL, PRIMARY KEY (post_id, category_id), FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE, FOREIGN KEY (category_id) REFERENCES categories(id) );</p><p>CREATE TABLE post_tags ( post_id INT NOT NULL, tag_id INT NOT NULL, PRIMARY KEY (post_id, tag_id), FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE, FOREIGN KEY (tag_id) REFERENCES tags(id) );
- 分类一般是一对一(一篇文章只属一个主分类),但用中间表更灵活,未来可扩展为多分类
- 标签一定是多对多,
post_tags的联合主键能天然去重,也避免重复插入同一标签 - 查某文章的所有标签:用
JOIN,不是子查询——SELECT t.name FROM tags t JOIN post_tags pt ON t.id = pt.tag_id WHERE pt.post_id = 123
评论表要预判灌水和嵌套需求,别只建个线性结构
只建 comments 表,字段含 post_id、author_name、content、created_at,短期够用,但很快会卡在两个地方:一是被机器人刷屏(没防刷机制),二是用户想回复某条评论(没父子关系)。
基础版先加层级支持和简单风控字段:
CREATE TABLE comments (
id INT PRIMARY KEY AUTO_INCREMENT,
post_id INT NOT NULL,
parent_id INT DEFAULT NULL, -- NULL 表示根评论,否则指向另一条评论 id
user_id INT NULL, -- 可为空,支持游客评论
author_name VARCHAR(100) NOT NULL,
author_email VARCHAR(100),
content TEXT NOT NULL,
status ENUM('pending', 'approved', 'spam', 'rejected') DEFAULT 'pending',
ip_address VARCHAR(45), -- 支持 IPv6
user_agent TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
FOREIGN KEY (parent_id) REFERENCES comments(id) ON DELETE SET NULL
);-
parent_id设为REFERENCES comments(id),并用ON DELETE SET NULL,这样删掉某条评论,它的子评论不会丢失,只是变成根评论 -
status初始设pending,配合后台审核;自动标spam需应用层调用反垃圾 API,MySQL 本身不处理 - 别在评论表里存用户密码或敏感信息——
user_id是可选的,游客评论就留空,靠author_email+ip_address去重
索引不是越多越好,这几个字段必须加,其余看查询模式
刚建完表就给所有外键和字符串字段加索引?反而拖慢写入。MySQL 的 B+ 树索引有维护成本,尤其高并发写入时。
优先保证以下索引存在:
-
posts.user_id:查某个用户所有文章,必走索引 -
posts.slug:URL 路由匹配,必须UNIQUE索引 -
posts.status+posts.published_at组合索引:查“已发布且按时间倒序”的列表页,INDEX(status, published_at)比单列更高效 -
comments.post_id和comments.status:查某篇文章的已审核评论,组合索引INDEX(post_id, status)能覆盖查询 -
post_tags.tag_id和post_tags.post_id:查某标签下所有文章,两个方向都要快,所以联合主键本身已是双向索引
其他字段如 users.email、categories.slug 也应单独建索引,但像 comments.content 这种大文本字段,别加普通索引——要用就上 FULLTEXT,且仅限 MyISAM 或 InnoDB 5.6+,还得配合 MATCH ... AGAINST 语法。
真正容易被忽略的是:上线后用 SHOW INDEX FROM table_name 定期检查,再结合慢查询日志(slow_query_log),看哪些查询没走索引。没有监控的索引,和没写的代码一样不可靠。










