答案 0 :(得分:6)
您正在使用图书SQL Antipatterns称之为“邻接列表”的设计。这是大多数程序员选择在关系数据库中表示分层数据的方式,因为它相当健壮,存储经济,易于理解,并且可以使用外键约束来防止创建无效的父子关系。但它的一大缺点是您无法在单个查询中检索整个树或子树。您只能获得节点的直接子节点,它是直接父节点或其兄弟节点,除非您使用的数据库允许递归查询,这是非标准的。递归仅由某些数据库实现,因为没有标准,所以它们都以不同的方式实现。
本书提供了许多用于处理层次结构的替代模式,我最喜欢的是Closure Table。在该方法中,不是将父行存储在类别表中,而是将关系数据存储在完全独立的表中。您的类别表将具有唯一的类别ID,类别名称等,然后您的闭包表将存储以下信息:
所以你的类别表看起来像这样:
CREATE TABLE categories (
cat_id INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
cat_name VARCHAR(64),
# Other columns...
) ENGINE=InnoDB;
并且您关联的闭包表看起来像......
CREATE TABLE category_relations (
ancestor_id INT(11) UNSIGNED,
descendant_id INT(11) UNSIGNED,
depth INT(11) UNSIGNED
PRIMARY KEY (ancestor_id, descendant_id),
FOREIGN KEY (ancestor_id) REFERENCES categories.cat_id,
FOREIGN KEY (descendant_id) REFERENCES categories.cat_id
) ENGINE=InnoDB;
祖先和后代ID的组合是闭包表的主键。稍后将使用深度字段来优化某些查询类,但我们已经超越了自己。
每当您在类别表中插入新值时,您现在也将始终在类别关系表中插入至少一个节点。
INSERT INTO categories (
cat_name
) VALUES (
'animal'
);
获取新插入节点的ID后,还应运行以下查询(我们假设新行的ID为1):
INSERT INTO category_relations (
ancestor_id,
descendant_id,
depth
) VALUES (
1,
1,
0
);
这称为自引用行。您可以使用触发器执行此操作,或者在代码中执行其他操作,这取决于您。
现在我们有一个“动物”节点,我们可以将其视为根节点。如果我们想添加“猫科动物”字段,我们会这样做:
INSERT INTO categories (
cat_name
) VALUES (
'feline'
);
# We'll assume the new ID is 2
INSERT INTO category_relations (
ancestor_id,
descendant_id,
depth
) VALUES (
2,
2,
0
);
但是我们如何让“猫”成为“动物”的孩子?我们通过在关系表中插入其他节点来实现:
INSERT INTO category_relations (
ancestor_id,
descendant_id,
depth
) VALUES (
1,
2,
1
);
现在说我们要在“猫”中添加“家猫”和“狮子”孩子
INSERT INTO categories (
cat_name
) VALUES (
'house cat'
);
# We'll assume the new ID is 3
INSERT INTO category_relations (
ancestor_id,
descendant_id,
depth
) VALUES (
3,
3,
0
);
INSERT INTO categories (
cat_name
) VALUES (
'lion'
);
# We'll assume the new ID is 4
INSERT INTO category_relations (
ancestor_id,
descendant_id,
depth
) VALUES (
4,
4,
0
);
现在我们将这些节点作为“猫科动物”节点的子节点。
INSERT INTO category_relations (
ancestor_id,
descendant_id,
depth
) VALUES (
2,
3,
1
);
INSERT INTO category_relations (
ancestor_id,
descendant_id,
depth
) VALUES (
2,
4,
1
);
现在家猫和狮子是猫科动物的孩子,但他们也必须是动物的祖先。
INSERT INTO category_relations (
ancestor_id,
descendant_id,
depth
) VALUES (
1,
3,
2
);
INSERT INTO category_relations (
ancestor_id,
descendant_id,
depth
) VALUES (
1,
4,
2
);
到目前为止,该表应如下所示:
ancestor_id | descendant_id | depth
=============+===============+=======
1 | 1 | 0
2 | 2 | 0
3 | 3 | 0
4 | 4 | 0
1 | 2 | 1
2 | 3 | 1
2 | 4 | 1
1 | 3 | 2
1 | 4 | 2
现在,如果你想获得整个动物树,你可以进行简单的查询而不需要迭代或递归。
SELECT c.*
FROM category AS c
JOIN category_relations AS r ON c.cat_id = r.descendant_id
WHERE r.ancestor_id = 1;
此查询将在关系表中选择以下行:
ancestor_id | descendant_id | depth
=============+===============+=======
1 | 1 | 0
1 | 2 | 1
1 | 3 | 2
1 | 4 | 2
并将返回类别行:
cat_id | cat_name
========+==========
1 | animal
2 | feline
3 | house cat
4 | lion
如果我们想要从Feline开始的子树,我们只需使用猫科动物类别ID作为祖先。
SELECT c.*
FROM categories AS c
JOIN category_relations AS r ON c.cat_id = r.descendant_id
WHERE r.ancestor_id = 2;
ancestor_id | descendant_id | depth
=============+===============+=======
2 | 2 | 0
2 | 3 | 1
2 | 4 | 1
cat_id | cat_name
========+==========
2 | feline
3 | house cat
4 | lion
但是如果你只想要给定节点的子节点而不是整个子树呢?这就是深度领域的用武之地:
SELECT c.*
FROM categories AS c
JOIN category_relations AS r ON c.cat_id = r.descendant_id
WHERE r.ancestor_id = 1
AND r.depth = 1;
ancestor_id | descendant_id | depth
=============+===============+=======
1 | 2 | 1
cat_id | cat_name
========+==========
2 | feline
希望你现在得到这个想法。您还可以执行更多操作,例如删除和移动子树,等等。
这里的例子可能会让你看起来像是要做很多工作来维护闭包表,但是我可以通过触发器巧妙地使用连接的查询来自动化我在这里完成的很多工作。等等,但我不会详细介绍,因为SQL Antipatterns的书都比我更好,并且互联网上还有很多关闭表教程。
您可能还认为存储了大量额外数据而不是简单的邻接列表,而且您是对的,有。但是,在大多数数据库中存储整数表通常非常有效,除非你处理完全庞大的树,否则你不应该遇到由闭包表引起的任何短缺。使用的额外存储空间是您无需经历使用迭代或递归遍历树的严苛考验所支付的价格。
如果您打算处理存储在SQL中的任意树结构,我强烈建议您查看闭包表。在非常简单的情况下,邻接列表模型很好,所以你不应该完全折扣它,但是当事情开始变得越来越复杂时,闭包表方法带来了很多好处。
答案 1 :(得分:1)
您可以使用某种递归函数,例如:
// Recursive function
function getList($rows, $id=0){
// Start a new list
$newList = null;
foreach($rows as $key => $row) {
if ($row['parent_id'] == $id) {
// Add an UL tag when LI exists
$newList == null ? $newList = "<ul>" : $newList;
$newList .= "<li>";
$newList .= "<a href=\"\">{$row['category']}</a>";
// Find childrens
$newList .= getList(array_slice($rows,$key),$row['id']);
$newList .= "</li>";
}
}
// Add an UL end tag
strlen($newList) > 0 ? $newList .= "</ul>" : $newList;
return $newList;
}
// Your database registers
$rows = [
['id'=>1,'category'=>'Indland','parent_id'=>0],
['id'=>2,'category'=>'Udland','parent_id'=>0],
['id'=>3,'category'=>'Sport','parent_id'=>0],
['id'=>4,'category'=>'Handbold','parent_id'=>3],
['id'=>5,'category'=>'Fodbold','parent_id'=>3],
['id'=>6,'category'=>'Sample','parent_id'=>4]
];
print_r(getList($rows));
但它仅适用于有序行。
答案 2 :(得分:0)
尝试此查询
select a.id,a.category,b.category from tablename a
left outer join tablename b on a.id=b.parent_id