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

时间:2011-09-23 21:28:52

标签: sql database tree normalization hierarchy

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

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

现在,我不确定如何设置表之间的关系。例如,Term中的列是什么?其父级可以是ProgramSub_CampusSchool。它取决于学校的系统。我可以设想将Term表设置为具有所有这些的外键(所有这些都默认为NULL),但我不确定这是在这里做事的规范方式。

6 个答案:

答案 0 :(得分:3)

这是一种设计可能性:

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

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

答案 1 :(得分:3)

我建议你最好使用一般表,例如包含 id 字段和自引用字段的实体。

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

答案 2 :(得分: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)

BTW:我遗漏了属性。我建议他们可以通过EAV类型的数据模型与相关类别挂钩。

答案 3 :(得分:1)

我将首先讨论相关地实现单个层次模型(仅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版本:
MySQLWorkbench version

正如您所看到的,school位于层次结构的顶部,只有school_name作为其键,而department包含三部分键,包括所有键的键它的父母。

此解决方案的要点

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

现在问题的第二部分。

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

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

+-------------+
| 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中的每个值都有一个默认值 - 如果您在应用程序不需要的层次结构中有一个表,这可能会非常烦人
  • 由于表架构未更改,因此可以使用相同的查询
  • 查询易于编写和移植
  • 似乎认为default应该有不同的颜色

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


我希望这有助于解决你的问题。

答案 4 :(得分:1)

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

您情况的最佳选择取决于您将如何查询结构以及您正在使用的数据库。樱桃挑选文章:

  

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

然而:

  

嵌套集对于插入来说非常慢,因为它需要更新lft   和插入后表中所有记录的rgt。这可能会导致   很多数据库都会被重写和索引   重修。

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

答案 5 :(得分:0)

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

应该只有一个表,我们称之为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和名称。

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

现在想象根类别是学校,学期和部门你会:

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' )" );

干杯