我正在做一个项目,它需要一个类别树,组织为 id、parent、title 表。在 Postgres 中检索类别及其子类别(以及完整的树,如果根类别的 parent=0)的最佳方法是什么?我正在寻找一个纯粹的数据库解决方案,但如果有一种适用于 Ruby 和 PHP 的方法 - 它也会很棒。
主要目标是选择子句的速度,因为此表中的数据对于更新/插入/删除速度并不重要。
UPD:还会有路径搜索,我的意思是从当前顶点(类别)到根类别的路径。
我正在做一个项目,它需要一个类别树,组织为 id、parent、title 表。在 Postgres 中检索类别及其子类别(以及完整的树,如果根类别的 parent=0)的最佳方法是什么?我正在寻找一个纯粹的数据库解决方案,但如果有一种适用于 Ruby 和 PHP 的方法 - 它也会很棒。
主要目标是选择子句的速度,因为此表中的数据对于更新/插入/删除速度并不重要。
UPD:还会有路径搜索,我的意思是从当前顶点(类别)到根类别的路径。
检索类别及其子类别
如果您只有有限深度的子项,您可以使用自连接来执行此操作,例如。两层深:
SELECT *
FROM categories AS child
LEFT JOIN categories AS parent ON parent.id=child.parent
LEFT JOIN categories AS grandparent ON grandparent.id=parent.parent
WHERE child.id=(id) OR parent.id=(id) OR grandparent.id=(id);
您不能使用标准 SQL 在“parent-id-foreign-key”类型架构上对任意深度的层次结构执行此操作。
一些 DBMS 提供了非标准的分层工具,以各种方式允许这样的事情,但如果您想坚持跨 DBMS 兼容的代码,您需要将您的模式重新调整为表示层次结构的更好模型之一。两个流行的是:
每种方法都有优点和缺点,并且有许多变体(例如,稀疏嵌套集编号,AR 中的“距离”)会影响各种类型的添加/删除/移动位置操作的成本。我个人倾向于默认使用简化的嵌套集模型,因为它包含的冗余比 AR 少。
看看“ltree” contrib 模块。
我一直在玩ltree,这是一个 PostgreSQL contrib 模块,看看它是否适合线程注释。您在表中创建一个存储路径的列并在其上创建一个 ltree 索引。然后您可以执行如下查询:
ltreetest=# select path from test where path ~ '*.Astronomy.*';
path
-----------------------------------------------
Top.Science.Astronomy
Top.Science.Astronomy.Astrophysics
Top.Science.Astronomy.Cosmology
Top.Collections.Pictures.Astronomy
Top.Collections.Pictures.Astronomy.Stars
Top.Collections.Pictures.Astronomy.Galaxies
Top.Collections.Pictures.Astronomy.Astronauts
我还没有充分利用它来确定它在插入、更新或删除等方面的表现如何。我假设删除看起来像:
DELETE FROM test WHERE path ~ '*.Astronomy.*';
我在想,一个线程化的评论表可能看起来像:
CREATE SEQUENCE comment_id_seq
INCREMENT 1
MINVALUE 1
MAXVALUE 9223372036854775807
START 78616
CACHE 1;
CREATE TABLE comments (
comment_id int PRIMARY KEY,
path ltree,
comment text
);
CREATE INDEX comments_path_idx ON comments USING gist (path);
插入将粗略(且未经测试)看起来像:
CREATE FUNCTION busted_add_comment(text the_comment, int parent_comment_id) RETURNS void AS
$BODY$
DECLARE
INT _new_comment_id; -- our new comment_id
TEXT _parent_path; -- the parent path
BEGIN
_new_comment_id := nextval('comment_id_seq'::regclass);
SELECT path INTO _parent_path FROM comments WHERE comment_id = parent_comment_id;
-- this is probably busted SQL, but you get the idea... this comment's path looks like
-- the.parent.path.US
--
-- eg (if parent_comment_id was 5 and our new comment_id is 43):
-- 3.5.43
INSERT INTO comments (comment_id, comment, path) VALUES (_new_comment_id, the_comment, CONCAT(_parent_path, '.', _new_comment_id));
END;
$BODY$
LANGUAGE 'plpgsql' VOLATILE;
或者其他的东西。基本上,路径只是由所有主键组成的层次结构。
对于这种情况,我已经喜欢嵌套集合模型。更新和插入可能有点棘手,但选择通常非常简洁和快速。如果添加对节点父节点的实际引用,性能会更好(在某些情况下会消除连接。它还包括子节点的自然排序。
当前节点和所有子节点的典型查询如下所示:
select name
from nestedSet c inner join nestedSet p ON c.lft BETWEEN p.lft AND p.rgt
where p.id = 1
order by lft
一些放置得当的group by
子句还将为您提供有关树的一些快速统计信息。
Rails有一个acts_as_tree
插件,过去对我来说效果很好。不过,我有一棵相当小的树 - 大约 15,000 个节点。
补充一点,在 MySQL 中管理分层数据一文对邻接列表模型和嵌套集模型有很好的解释,包括用于树操作的示例 SQL 等。
RDBMS 中的层次结构是一个困难的话题。我的愿望清单上有Joe Celko 的 Trees and Hierarchies in SQL for Smarties,希望有一天可以购买和阅读。