正确做法是建三张表实现多对多关系:posts、categories与post_categories,以及tags与post_tags;created_at和updated_at统一用DATETIME;正文用utf8mb4的LONGTEXT;预加载时用GROUP_CONCAT聚合或应用层两次查询。

文章表必须拆出独立的 categories 和 tags 关联表
直接在 posts 表里加 category_id 字段看似简单,但一旦要支持多分类、多标签,硬编码或逗号分隔字符串(如 "tech,mysql,backend")会立刻让查询、索引、更新全崩。MySQL 没有原生数组类型,FIND_IN_SET() 无法走索引,LIKE '%tag%' 更是全表扫描。
正确做法是三张表:
- posts(主键 id,不含分类/标签字段)
- categories(id, name, slug)
- post_categories(post_id, category_id,联合唯一索引)
同理,tags 和 post_tags 独立建模。这样查某分类下所有文章、某文章所有标签、按标签聚合统计,都能走索引。
DATETIME vs TIMESTAMP:发布时间和更新时间选哪个
created_at 和 updated_at 别用 TIMESTAMP —— 它自动转时区,且 MySQL 5.6+ 默认开启 explicit_defaults_for_timestamp 后行为更难控。本地开发设 Asia/Shanghai,上线切到 UTC,时间就对不上;备份恢复后还可能被悄悄重置。
统一用 DATETIME:
- created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
- updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMPDATETIME 存的是字面值,不转换,迁移、跨时区部署都稳。
正文内容字段别用 TEXT 就完事,注意 utf8mb4 和行大小限制
MySQL 的 TEXT 类型默认只支持 utf8(实际是 utf8mb3),存 emoji、生僻汉字会截断或报错 Incorrect string value。必须显式指定字符集:content TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci。
另外,TEXT 字段虽不计入行大小限制(65535 bytes),但每个 TEXT 会额外占用 9–12 字节指针。如果文章系统还要存封面图 URL、摘要、SEO 描述等字段,总列数多、单行数据大,容易触发 Row size too large 错误。建议:
- 摘要(excerpt)用 VARCHAR(512) 而非 TEXT
- 封面图路径用 VARCHAR(255)
- 正文强制用 LONGTEXT(最大 4GB),并确认 innodb_log_file_size 足够(至少 256MB)
预加载关联数据时,JOIN 多对多关系容易重复行
查一篇文章带所有分类和标签,写 SELECT * FROM posts p JOIN post_categories pc ON p.id = pc.post_id JOIN categories c ON pc.category_id = c.id JOIN post_tags pt ON p.id = pt.post_id JOIN tags t ON pt.tag_id = t.id,结果一条文章会因“分类×标签”组合爆炸出 N×M 行 —— 分类 2 个、标签 3 个,就返回 6 行相同文章数据。
两种解法:
- 应用层两次查询:先查 posts,再用 IN (id1,id2,...) 批量查 post_categories 和 post_tags,自己组装
- 数据库层用 GROUP_CONCAT() 聚合:
SELECT p.*, GROUP_CONCAT(c.name) AS category_names, GROUP_CONCAT(t.name) AS tag_names FROM posts p LEFT JOIN post_categories pc ON p.id = pc.post_id LEFT JOIN categories c ON pc.category_id = c.id LEFT JOIN post_tags pt ON p.id = pt.post_id LEFT JOIN tags t ON pt.tag_id = t.id GROUP BY p.id
注意加 LEFT JOIN,否则没分类或没标签的文章会被丢掉。
复杂点在于,GROUP_CONCAT 默认长度限制 1024,超长会被截断,得提前设 SET SESSION group_concat_max_len = 10000。这个值上线前容易忘调。










