本文目录导读:

- 方案一:邻接表模型(Adjacency List)—— 最常用、最简单
- 方案二:路径枚举模型(Path Enumeration)—— 查询友好
- 方案三:嵌套集模型(Nested Set)—— 读密集场景最优
- 总结对比与选择建议
- 最佳实践(实战建议)
在PHP中设计无限分类(无限级分类)的数据库表,主要有三种主流方案,每种方案都有其优缺点,适用于不同的业务场景。
以下是详细的表结构设计和PHP实现思路:
邻接表模型(Adjacency List)—— 最常用、最简单
这是最常见的做法,通过 parent_id 字段关联父级ID。
表结构设计
CREATE TABLE `category` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL COMMENT '分类名称', `parent_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '父级ID,0为顶级', `sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序权重', `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态:1启用,0禁用', `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_parent_id` (`parent_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='无限级分类表';
核心操作(PHP实现)
(1)递归获取所有子分类(推荐使用引用算法,避免递归查询数据库)
<?php
/**
* 获取无限级分类树(一次查询+内存递归)
* @param array $data 数据库查出的所有记录
* @param int $parentId 父ID
* @return array 树形结构
*/
function buildTree(array $data, int $parentId = 0): array
{
$tree = [];
foreach ($data as $item) {
if ($item['parent_id'] == $parentId) {
// 先在内存中找到该节点的子节点
$children = buildTree($data, $item['id']);
if ($children) {
$item['children'] = $children;
}
$tree[] = $item;
}
}
return $tree;
}
// 使用方法:
$list = $db->query("SELECT * FROM category ORDER BY sort ASC")->fetchAll();
$tree = buildTree($list);
// print_r($tree);
(2)直接使用 MySQL WITH RECURSIVE(MySQL 8.0+)
-- 查询某个分类下的所有子分类(递归向下)
WITH RECURSIVE cte AS (
SELECT * FROM category WHERE id = 1 -- 从ID=1开始
UNION ALL
SELECT c.* FROM category c
INNER JOIN cte ON c.parent_id = cte.id
)
SELECT * FROM cte;
路径枚举模型(Path Enumeration)—— 查询友好
表结构设计
在基础表上增加一个 path 字段,存储从根节点到当前节点的完整路径。
CREATE TABLE `category` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, `parent_id` int(11) unsigned NOT NULL DEFAULT '0', `path` varchar(500) NOT NULL DEFAULT '' COMMENT '节点路径,0,1,4,9', `sort` int(11) NOT NULL DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
插入数据时的维护
插入时,父级的路径 + 父级ID = 当前节点的路径。
// 插入一个子分类
function insertCategory($name, $parentId) {
// 先查出父级的path
$parent_info = $db->query("SELECT path FROM category WHERE id = $parentId")->fetch();
if ($parent_info) {
$path = $parent_info['path'] . ',' . $parentId;
} else {
// 顶级分类
$path = '0';
}
$sql = "INSERT INTO category (name, parent_id, path) VALUES ('$name', $parentId, '$path')";
$db->exec($sql);
return $db->lastInsertId();
}
查询优势(速度极快)
查询某分类的所有子孙分类(不需要递归,单条SQL解决):
-- 查询ID为4的节点的所有子孙 SELECT * FROM category WHERE path LIKE '0,1,4,%' OR id = 4;
查询某分类的所有上级分类(面包屑导航):
-- 查询ID为9的所有上级 SELECT * FROM category WHERE id IN (SELECT REPLACE(path, ',', ' ') FROM category WHERE id = 9);
嵌套集模型(Nested Set)—— 读密集场景最优
适合“查询极其频繁,插入/删除较少”的场景(例如无限级菜单、地区选择),核心是给每个节点赋予 left 和 right 值。
表结构设计
CREATE TABLE `category` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, `lft` int(11) NOT NULL COMMENT '左边界', `rgt` int(11) NOT NULL COMMENT '右边界', `sort` int(11) NOT NULL DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
数据存储原理
- 根节点的
lft= 1,rgt= 节点总数 * 2。 - 子节点必须满足
lft> 父节点的lft且rgt< 父节点的rgt。
强大的查询能力(无需递归)
查询所有叶子节点:
SELECT * FROM category WHERE rgt = lft + 1;
查询某个节点的所有子孙:
-- 查询ID=4节点的所有子孙(假设4的lft=3, rgt=8) SELECT * FROM category WHERE lft BETWEEN 3 AND 8;
查询节点的深度(层级):
SELECT node.name, (COUNT(parent.id) - 1) AS depth FROM category AS node CROSS JOIN category AS parent WHERE node.lft BETWEEN parent.lft AND parent.rgt GROUP BY node.id ORDER BY node.lft;
总结对比与选择建议
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 邻接表 | 简单直观、增删改容易 | 查询子树需要递归(PHP递归或SQL递归),大数据量时性能差 | 分类数量较少(<1000),结构简单,开发效率优先 |
| 路径枚举 | 查询子树简单(LIKE),不用递归 | 插入时需要维护path;移动节点麻烦;path长度受限于字段长度 | 分类深度较浅且固定,对查询性能有要求 |
| 嵌套集 | 查询极其高效、无递归、无LIKE模糊 | 增删改复杂(需要重算lft/rgt);并发写入会锁表 | 分类结构极度稳定,只读查询为主(如地区/导航菜单) |
最佳实践(实战建议)
对于99%的通用PHP业务(如CMS后台、电商分类),推荐方案一(邻接表)+ 内存递归或MySQL递归CTE。
- 索引优化:确保
parent_id有索引。 - 数据缓存:将整个分类表(通常只有几百条)存入 Redis 或 PHP静态变量,用内存数组构建树,避免每次请求都查询数据库。
- 避免PHP递归查库:永远不要在一个递归函数里写
SELECT ... WHERE parent_id = $id,这会导致 N+1 次查询数据库,一定是 一次查出全表,在内存中构建树。
// 高效写法:一次查全表,内存构建(伪代码)
$data = $db->query("SELECT * FROM category")->fetchAll();
$tree = buildTree($data, 0); // 调用上面定义的内存递归函数
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播