分类表必须用自增BIGINT UNSIGNED主键,但对外标识应使用小写英文/数字/短横线组成的slug字段;文章与分类用中间表post_categories关联,支持主分类与多分类;层级限制两级,用parent_id+level字段管理;统计数通过post_count字段+原子更新维护。

分类表要不要用自增主键
要用,但别只用自增 ID 做业务标识。分类的 id 作为主键必须是自增整型(BIGINT UNSIGNED 更稳妥),方便关联和索引;但对外展示或 URL 中的分类标识,应该另加一个 slug 字段,比如 tech、life,类型设为 VARCHAR(64) 并加唯一索引。
常见错误是直接用 id 拼在 URL 里(如 /category/3),后期改名或合并分类时 URL 失效;也有人用中文名做主键或 slug,结果插入失败或排序错乱。
-
slug必须小写 + 英文字母/数字/短横线,入库前强制转换 - 避免用
name字段做唯一约束——同名分类(如“随笔”和“随笔杂谈”)可能语义不同 - 如果支持多语言站点,
slug应按语言拆分(比如加lang字段),不建议靠翻译表硬关联
文章和分类之间用什么关系模型
博客文章通常属于**一个主分类**,但常需支持**多个标签或二级分类**。所以不要用简单的外键字段 category_id 硬绑定到文章表——这会锁死扩展性。
正确做法是建一张中间表 post_categories,结构至少包含:post_id、category_id、is_primary(TINYINT(1))、sort_order(SMALLINT)。这样既能支持单主分类,也能支持多分类归档,还能控制显示顺序。
- 文章表
posts里删掉任何category_id字段,避免数据冗余和不一致 -
post_categories的联合索引要覆盖查询高频路径:比如(post_id, is_primary)用于查主分类,(category_id, is_primary)用于查某分类下所有文章 - 别用 JSON 字段存分类 ID 列表——没法走索引,连
COUNT()都慢
分类层级怎么处理才不踩坑
MySQL 不适合递归查询,所以别用「父 ID」字段(parent_id)硬搞无限级分类。真需要多级(比如「编程 > 后端 > MySQL」),就用闭包表(Closure Table)或物化路径(Materialized Path)。
更现实的选择是:限制最多两级,并在分类表里加 parent_id + level 字段(TINYINT,值为 1 或 2),然后靠应用层校验层级关系。既保持简单,又避免 JOIN 套娃和查询爆炸。
- 加触发器或应用逻辑确保
level = 1时parent_id为NULL,level = 2时parent_id必须指向level = 1的记录 - 查某一级分类下的所有二级分类时,用
WHERE parent_id = ? AND level = 2,别写子查询找子孙 - 如果未来真要三级以上,优先考虑把分类维度拆出去用 Elasticsearch 或专用图数据库,别在 MySQL 里死磕
分类统计数(如“该分类下有 42 篇文章”)怎么维护
别每次查的时候用 COUNT(*) 聚合计算——高并发列表页会拖垮性能。应该在 categories 表里加一个 post_count 字段,由业务逻辑或数据库触发器维护。
推荐用应用层更新:当文章状态变更(发布/撤回/修改分类)时,在事务中同步增减对应分类的 post_count。比触发器更可控,也避免跨表触发的死锁风险。
- 初始化时用
UPDATE categories c JOIN (SELECT category_id, COUNT(*) cnt FROM post_categories GROUP BY category_id) t ON c.id = t.category_id SET c.post_count = t.cnt一次性补全 - 注意并发场景:用
UPDATE categories SET post_count = post_count + 1 WHERE id = ?这种原子操作,别先SELECT再UPDATE - 如果分类数量极少(
slug 的生成逻辑和 post_categories 表的索引覆盖是否匹配真实查询模式——这两处一出问题,URL 404 和分类页加载慢就会同时出现。









