有趣的树/分层数据结构问题

Posted

技术标签:

【中文标题】有趣的树/分层数据结构问题【英文标题】:Interesting tree/hierarchical data structure problem 【发布时间】:2011-11-24 00:07:04 【问题描述】:

大学有不同的部门组织方式。有些学校去School -> Term -> Department。其他人则介于两者之间,最长的是School -> Sub_Campus -> Program -> Term -> Division -> Department

SchoolTermDepartment 是唯一始终存在于学校部门“树”中的部门。这些类别的顺序永远不会改变,我给你的第二个例子是最长的。每一步都是1:N的关系。

现在,我不确定如何设置表之间的关系。例如,Term 中有哪些列?它的父级可以是ProgramSub_CampusSchool。是哪一种取决于学校的制度。我可以设想设置Term 表来为所有这些设置外键(默认为NULL),但我不确定这是不是规范的处理方式。

【问题讨论】:

我不确定你在问什么——你想要一个解决方案来解决将多个不同层次数据模型拟合到同一个数据库实现中的问题吗?或者展示如何实现单个层次模型的解决方案? 哪一个更适合这个问题。 【参考方案1】:

我建议您最好使用通用表,例如包含 id 字段和自引用 parent 字段的实体。

每个相关表都将包含一个指向实体 id (1:1) 的字段。在某种程度上,每个表都是 Entity 表的子表。

【讨论】:

一个 parent 字段是可以的,但它需要特定于数据库的支持才能进行分层查询(例如,Oracle 的 connect by prior 构造)。另一种使查询更容易的方法是在单个编码列中表示层次结构,其中子树被定义为该列的字符串前缀。 是的,但这不是问题所在,添加这个也没什么大不了的。我最近为这个人写了这篇文章***.com/questions/7181489/… @evil otto:错了。数据库 model 可以是递归的,但是从数据库表中提取数据的 query 可以简单地迭代“直到找不到更多父记录”。这是大多数客户端应用程序无论如何都会这样做的方式。【参考方案2】:

这是一种设计可能性:

此选项利用您的特殊约束。基本上,您通过引入通用节点将所有层次结构概括为最长形式的层次结构。如果学校没有“子校区”,那么只需为其分配一个名为“主校区”的通用子校区。例如,School -> Term -> Department 可以被认为与School -> Sub_Campus = Main -> Program=Main -> Term -> Division=Main -> Department 相同。在这种情况下,当学校没有该节点时,我们默认分配一个名为“Main”的节点。现在,您可以为这些通用节点设置一个布尔标志属性,指示它们只是占位符,如果需要,此标志将允许您在中间层或 UX 中将其过滤掉。

这种设计将允许您像往常一样利用所有关系约束并简化代码中缺失节点类型的处理。

【讨论】:

【参考方案3】:
-- Enforcing a taxonomy by self-referential (recursive) tables.
-- Both the classes and the instances have a recursive structure.
-- The taxonomy is enforced mostly based on constraints on the classes,
-- the instances only need to check that their_class , parents_class 
-- form a valid pair.
--
DROP schema school CASCADE;
CREATE schema school;

CREATE TABLE school.category
  ( id INTEGER NOT NULL PRIMARY KEY
  , category_name VARCHAR
  );
INSERT INTO school.category(id, category_name) VALUES
  ( 1, 'School' )
  , ( 2, 'Sub_campus' )
  , ( 3, 'Program' )
  , ( 4, 'Term' )
  , ( 5, 'Division' )
  , ( 6, 'Department' )
  ;

-- This table contains a list of all allowable child->parent pairs.
-- As a convention, the "roots" of the trees point to themselves.
-- (this also avoids a NULL FK)
CREATE TABLE school.category_valid_parent
  ( category_id INTEGER NOT NULL REFERENCES school.category (id)
  , parent_category_id INTEGER NOT NULL REFERENCES school.category (id)
  );
ALTER TABLE school.category_valid_parent
  ADD PRIMARY KEY (category_id, parent_category_id)
  ;

INSERT INTO school.category_valid_parent(category_id, parent_category_id)
  VALUES
  ( 1,1) -- school -> school
  , (2,1) -- subcampus -> school
  , (3,1) -- program -> school
  , (3,2) -- program -> subcampus
  , (4,1) -- term -> school
  , (4,2) -- term -> subcampus
  , (4,3) -- term -> program
  , (5,4) -- division --> term
  , (6,4) -- department --> term
  , (6,5) -- department --> division
  ;

CREATE TABLE school.instance
  ( id INTEGER NOT NULL PRIMARY KEY
  , category_id INTEGER NOT NULL REFERENCES school.category (id)
  , parent_id INTEGER NOT NULL REFERENCES school.instance (id)
  -- NOTE: parent_category_id is logically redundant
  -- , but needed to maintain the constraint
  -- (without referencing a third table)
  , parent_category_id INTEGER NOT NULL REFERENCES school.category (id)
  , instance_name VARCHAR
  );      -- Forbid illegal combinations of parent_id, parent_category_id
ALTER TABLE school.instance ADD CONSTRAINT valid_cat UNIQUE (id,category_id);
ALTER TABLE school.instance
  ADD FOREIGN KEY (parent_id, parent_category_id)
      REFERENCES school.instance(id, category_id);
  ;
  -- Forbid illegal combinations of category_id, parent_category_id
ALTER TABLE school.instance
  ADD FOREIGN KEY (category_id, parent_category_id) 
      REFERENCES school.category_valid_parent(category_id, parent_category_id);
  ;

INSERT INTO school.instance(id, category_id
    , parent_id, parent_category_id
    , instance_name) VALUES
  -- Zulo
  (1,1,1,1, 'University of Utrecht' )
  , (2,2,1,1, 'Uithof' )
  , (3,3,2,2, 'Life sciences' )
  , (4,4,3,3, 'Bacherlor' )
  , (5,5,4,4, 'Biology' )
  , (6,6,5,5, 'Evolutionary Biology' )
  , (7,6,5,5, 'Botany' )
  -- Nulo
  , (11,1,11,1, 'Hogeschool Utrecht' )
  , (12,4,11,1, 'Journalistiek' )
  , (13,6,12,4, 'Begrijpend Lezen' )
  , (14,6,12,4, 'Typvaardigheid' )
  ;

  -- try to insert an invalid instance
INSERT INTO school.instance(id, category_id
    , parent_id, parent_category_id
    , instance_name) VALUES
  ( 15, 6, 3,3, 'Procreation' );

WITH RECURSIVE re AS (
  SELECT i0.parent_id AS pa_id
  , i0.parent_category_id AS pa_cat
  , i0.id AS my_id
  , i0.category_id AS my_cat
  FROM school.instance i0
  WHERE i0.parent_id = i0.id
  UNION
  SELECT i1.parent_id AS pa_id
  , i1.parent_category_id AS pa_cat
  , i1.id AS my_id
  , i1.category_id AS my_cat
  FROM school.instance i1
  , re
  WHERE re.my_id = i1.parent_id
  )
SELECT re.*
  , ca.category_name
  , ins.instance_name
  FROM re
  JOIN school.category ca ON (re.my_cat = ca.id)
  JOIN school.instance ins ON (re.my_id = ins.id)
  -- WHERE re.my_id = 14
  ;

输出:

INSERT 0 11
ERROR:  insert or update on table "instance" violates foreign key constraint "instance_category_id_fkey1"
DETAIL:  Key (category_id, parent_category_id)=(6, 3) is not present in table "category_valid_parent".
 pa_id | pa_cat | my_id | my_cat | category_name |     instance_name 
-------+--------+-------+--------+---------------+-----------------------
     1 |      1 |     1 |      1 | School        | University of Utrecht
    11 |      1 |    11 |      1 | School        | Hogeschool Utrecht
     1 |      1 |     2 |      2 | Sub_campus    | Uithof
    11 |      1 |    12 |      4 | Term          | Journalistiek
     2 |      2 |     3 |      3 | Program       | Life sciences
    12 |      4 |    13 |      6 | Department    | Begrijpend Lezen
    12 |      4 |    14 |      6 | Department    | Typvaardigheid
     3 |      3 |     4 |      4 | Term          | Bacherlor
     4 |      4 |     5 |      5 | Division      | Biology
     5 |      5 |     6 |      6 | Department    | Evolutionary Biology
     5 |      5 |     7 |      6 | Department    | Botany
(11 rows)

顺便说一句:我省略了属性。我建议他们可以通过 EAV 类型的数据模型与相关类别挂钩。

【讨论】:

注意:这都是关于“约束最小化”的。在这种情况下,约束确保了拓扑,即使拓扑在表中表示(因此:灵活)。更改允许的拓扑不会调用添加或更改约束。【参考方案4】:

我将首先讨论以关系方式实现单个层次模型(仅 1:N 关系)。

让我们使用您的示例School -> Term -> Department

这是我使用 mysqlWorkbench 生成的代码(我删除了一些内容以使其更清晰):

-- -----------------------------------------------------
-- Table `mydb`.`school`  
-- -----------------------------------------------------
-- each of these tables would have more attributes in a real implementation
-- using varchar(50)'s for PKs because I can -- :)

CREATE  TABLE IF NOT EXISTS `mydb`.`school` (
  `school_name` VARCHAR(50) NOT NULL ,
  PRIMARY KEY (`school_name`) 
);

-- -----------------------------------------------------
-- Table `mydb`.`term`
-- -----------------------------------------------------
CREATE  TABLE IF NOT EXISTS `mydb`.`term` (
  `term_name` VARCHAR(50) NOT NULL ,
  `school_name` VARCHAR(50) NOT NULL ,
  PRIMARY KEY (`term_name`, `school_name`) ,
  FOREIGN KEY (`school_name` )
    REFERENCES `mydb`.`school` (`school_name` )
);

-- -----------------------------------------------------
-- Table `mydb`.`department`
-- -----------------------------------------------------
CREATE  TABLE IF NOT EXISTS `mydb`.`department` (
  `dept_name` VARCHAR(50) NOT NULL ,
  `term_name` VARCHAR(50) NOT NULL ,
  `school_name` VARCHAR(50) NOT NULL ,
  PRIMARY KEY (`dept_name`, `term_name`, `school_name`) ,
  FOREIGN KEY (`term_name` , `school_name` )
    REFERENCES `mydb`.`term` (`term_name` , `school_name` )
);

这里是 MySQLWorkbench 版本的数据模型:

如您所见,位于层次结构顶部的school 只有school_name 作为其键,而department 有一个由三部分组成的键,包括其所有父级的键。

本方案的要点

使用自然键——但可以重构为使用代理键(SO question——连同UNIQUE对多列外键的约束) 每一级嵌套都会为键添加一列 每个表的 PK 是其上表的整个 PK,加上特定于该表的附加列

现在是你问题的第二部分。

我对问题的解释 有一个分层数据模型。但是,一些应用程序需要所有表,而其他应用程序只使用一些表,跳过其他表。我们希望能够实现1 个单一数据模型并将其用于这两种情况。

您可以使用上面给出的解决方案,并且正如 ShitalShah 所提到的,将默认值添加到任何不会使用的表中。让我们看一些示例数据,使用上面给出的模型,我们只想保存SchoolDepartment 信息(没有Terms):

+-------------+
| school_name |
+-------------+
| hogwarts    |
| uCollege    |
| uMatt       |
+-------------+
3 rows in set (0.00 sec)

+-----------+-------------+
| term_name | school_name |
+-----------+-------------+
| default   | hogwarts    |
| default   | uCollege    |
| default   | uMatt       |
+-----------+-------------+
3 rows in set (0.00 sec)

+-------------------------------+-----------+-------------+
| dept_name                     | term_name | school_name |
+-------------------------------+-----------+-------------+
| defense against the dark arts | default   | hogwarts    |
| potions                       | default   | hogwarts    |
| basket-weaving                | default   | uCollege    |
| history of magic              | default   | uMatt       |
| science                       | default   | uMatt       |
+-------------------------------+-----------+-------------+
5 rows in set (0.00 sec)

要点

term 中的每个值都有一个默认值,school 中的每个值 - 如果您在层次结构深处有一个应用程序不需要的表,这可能会很烦人 由于表架构没有改变,因此可以使用相同的查询 查询易于编写和移植 SO 似乎认为 default 的颜色应该不同

还有另一种将树存储在数据库中的解决方案。 Bill Karwin 讨论了它here, starting around slide 49,但我认为这不是您想要的解决方案。 Karwin 的解决方案适用于任何大小的树,而您的示例似乎是相对静态的。此外,他的解决方案也有自己的一系列问题(但不是所有问题?)。


希望对您的问题有所帮助。

【讨论】:

Matt,你好像漏掉了 Sub_Campuses、Programs 和 Divisions? @babonk 你是对的;我想节省空间并避免示例太长。是不是太不一样了,不清楚它是如何映射到 OP 的?【参考方案5】:

对于在关系数据库中拟合分层数据的一般问题,常见的解决方案是邻接列表(如您的示例中的父子链接)和nested sets。正如***文章中所述,甲骨文的 Tropashko 提出了一个替代方案 nested interval solution,但它仍然相当模糊。

适合您情况的最佳选择取决于您将如何查询结构,以及您使用的是哪个数据库。樱桃采摘文章:

使用嵌套集的查询预计会比查询快 使用存储过程遍历邻接表, 缺少本机递归查询的数据库的更快选项 构造,例如 MySQL

但是:

嵌套集的插入速度非常慢,因为它需要更新 lft 和 rgt 用于插入后表中的所有记录。这可能导致 大量的数据库颠簸,因为许多行被重写和索引 重建。

同样,根据查询结构的方式,您可以选择 NoSQL 样式的非规范化 Department 表,所有可能的父级都带有 nullable 外键,完全避免递归查询。

【讨论】:

【参考方案6】:

我会以一种非常灵活的方式来开发它,而且似乎也意味着最简单:

应该只有一个表,我们称之为 category_nodes:

-- possible content, of this could be stored in another table and create a
-- 1:N -> category:content relationship
drop table if exists category_nodes;
create table category_nodes (
  category_node_id int(11) default null auto_increment,
  parent_id int(11) not null default 1,
  name varchar(256),
  primary key(category_node_id)
);
-- set the first 2 records:
insert into category_nodes (parent_id, name) values( -1, 'root' );
insert into category_nodes (parent_id, name) values( -1, 'uncategorized' );

所以表中的每条记录都有唯一的 id、父 id 和名称。

现在在前2个插入之后:在category_nodes中category_node_id为0的地方是根节点(无论多少度都是所有节点的父节点。第二个只是一个小帮手,在category_node_id设置一个未分类的节点= 1 也是插入表时 parent_id 的默认值。

现在想象根类别是 School、Term 和 Dept,你会:

insert into category_nodes ( parent_id, name ) values ( 0, 'School' );
insert into category_nodes ( parent_id, name ) values ( 0, 'Term' );
insert into category_nodes ( parent_id, name ) values ( 0, 'Dept' );

然后获取所有的根类:

select * from category_nodes where parent_id = 0;

现在想象一个更复杂的模式:

-- School -> Division -> Department
-- CatX -> CatY
insert into category_nodes ( parent_id, name ) values ( 0, 'School' ); -- imaging gets pkey = 2 
insert into category_nodes ( parent_id, name ) values ( 2, 'Division' ); -- imaging gets pkey = 3
insert into category_nodes ( parent_id, name ) values ( 3, 'Dept' );
--
insert into category_nodes ( parent_id, name ) values ( 0, 'CatX' ); -- 5
insert into category_nodes ( parent_id, name ) values ( 5, 'CatY' );

现在获取学校的所有子类别,例如:

select * from category_nodes where parent_id = 2;
-- or even
select * from category_nodes where parent_id in ( select category_node_id from category_nodes 
    where name = 'School'
);

等等。由于 parent_id 的默认值 = 1,插入“未分类”类别变得简单:

<?php
$name = 'New cat name';
mysql_query( "insert into category_nodes ( name ) values ( '$name' )" );

干杯

【讨论】:

以上是关于有趣的树/分层数据结构问题的主要内容,如果未能解决你的问题,请参考以下文章

如何将分层列表排序为字典的树/pyrimid 模型?

邻接表的树结构

以高写入负载在 MySQL 中存储分层数据

将数据框转换为列表的树结构列表

分层数据和 Berkeley DB

当所有子项都折叠或隐藏时,如何在分层数据模板中隐藏扩展器?