PHP无限分类怎么设计表

wen PHP项目 3

本文目录导读:

PHP无限分类怎么设计表

  1. 方案一:邻接表模型(Adjacency List)—— 最常用、最简单
  2. 方案二:路径枚举模型(Path Enumeration)—— 查询友好
  3. 方案三:嵌套集模型(Nested Set)—— 读密集场景最优
  4. 总结对比与选择建议
  5. 最佳实践(实战建议)

在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)—— 读密集场景最优

适合“查询极其频繁,插入/删除较少”的场景(例如无限级菜单、地区选择),核心是给每个节点赋予 leftright 值。

表结构设计

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 > 父节点的 lftrgt < 父节点的 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

  1. 索引优化:确保 parent_id 有索引。
  2. 数据缓存:将整个分类表(通常只有几百条)存入 RedisPHP静态变量,用内存数组构建树,避免每次请求都查询数据库。
  3. 避免PHP递归查库:永远不要在一个递归函数里写 SELECT ... WHERE parent_id = $id,这会导致 N+1 次查询数据库,一定是 一次查出全表,在内存中构建树
// 高效写法:一次查全表,内存构建(伪代码)
$data = $db->query("SELECT * FROM category")->fetchAll();
$tree = buildTree($data, 0); // 调用上面定义的内存递归函数

抱歉,评论功能暂时关闭!