순서가 지정된 트리 계층 구조를 저장하는 플랫 테이블이 있다고 가정하십시오.
Id Name ParentId Order
1 'Node 1' 0 10
2 'Node 1.1' 1 10
3 'Node 2' 0 20
4 'Node 1.1.1' 2 10
5 'Node 2.1' 3 10
6 'Node 1.2' 1 20
여기에 다이어그램이 있습니다 [id] Name
. 루트 노드 0은 허구입니다.
[0] 루트 / \ [1] 노드 1 [3] 노드 2 / \ \ [2] 노드 1.1 [6] 노드 1.2 [5] 노드 2.1 / [4] 노드 1.1.1
올바르게 정렬되고 들여 쓰기 된 트리로 HTML (또는 그 문제에 대한 텍스트)로 출력하는 데 사용하는 최소한의 접근법은 무엇입니까?
또한 기본 데이터 구조 (배열 및 해시 맵) 만 있고 부모 / 자식 참조가있는 멋진 객체가 없으며 ORM이 없으며 프레임 워크가 없으며 두 손만 있다고 가정하십시오. 테이블은 결과 세트로 표시되며 임의로 액세스 할 수 있습니다.
의사 코드 또는 일반 영어는 괜찮습니다. 이것은 순전히 개념적인 질문입니다.
보너스 질문 : 이와 같은 트리 구조를 RDBMS에 저장하는 기본적으로 더 좋은 방법이 있습니까?
편집 및 추가
한 논평자 ( Mark Bessey ‘s)의 질문에 대답하려면 : 루트 노드는 절대로 표시되지 않기 때문에 필요하지 않습니다. ParentId = 0은 “이것은 최상위 수준”을 나타내는 규칙입니다. Order 열은 동일한 부모를 가진 노드가 정렬되는 방법을 정의합니다.
내가 말한 “결과 세트”는 해시 맵의 배열로 묘사 될 수있다 (해당 용어를 유지하기 위해). 내 예는 이미 거기에 있었다. 어떤 대답은 여분의 마일을 가지고 그것을 먼저 구성하지만 괜찮습니다.
나무는 임의로 깊을 수 있습니다. 각 노드에는 N 개의 자식이있을 수 있습니다. 그래도 정확히 “수백만 개의 항목”트리가 없었습니다.
내가 선택해야 할 노드 이름 지정 ( ‘노드 1.1.1’)을 착각하지 마십시오. 노드는 똑같이 ‘프랭크’또는 ‘밥’이라고 불릴 수 있으며, 명명 구조는 암시되지 않으며 이는 단지 읽기 가능하도록하기위한 것입니다.
나는 내 자신의 솔루션을 게시 했으므로 여러분은 그것을 조각으로 가져올 수 있습니다.
답변
이제 MySQL 8.0은 재귀 쿼리를 지원하므로 모든 인기있는 SQL 데이터베이스 는 표준 구문으로 재귀 쿼리 를 지원 한다고 말할 수 있습니다 .
WITH RECURSIVE MyTree AS (
SELECT * FROM MyTable WHERE ParentId IS NULL
UNION ALL
SELECT m.* FROM MyTABLE AS m JOIN MyTree AS t ON m.ParentId = t.Id
)
SELECT * FROM MyTree;
2017의 Recursive Query Throwdown 프레젠테이션에서 MySQL 8.0의 재귀 쿼리를 테스트했습니다 .
아래는 2008 년의 원래 답변입니다.
트리 구조 데이터를 관계형 데이터베이스에 저장하는 방법에는 여러 가지가 있습니다. 예제에서 보여주는 것은 두 가지 방법을 사용합니다.
- 인접 목록 ( “부모”열) 및
- 경로 열거 (이름 열의 점으로 구분 된 숫자)
다른 솔루션을 Nested Sets 라고 하며 같은 테이블에 저장할 수도 있습니다. 이러한 디자인에 대한 자세한 내용은 Joe Celko의 ” SQL for Smarties의 트리 및 계층 구조 “를 읽으십시오 .
저는 일반적으로 트리 구조 데이터를 저장하기 위해 클로저 테이블 (일명 “인접 관계”) 이라는 디자인을 선호합니다 . 다른 테이블이 필요하지만 트리 쿼리는 매우 쉽습니다.
SQL 및 PHP를 사용한 계층 적 데이터 모델 프레젠테이션 및 저서 SQL 안티 패턴 : 데이터베이스 프로그래밍의 함정 피하기 에서 클로저 테이블을 다룹니다 .
CREATE TABLE ClosureTable (
ancestor_id INT NOT NULL REFERENCES FlatTable(id),
descendant_id INT NOT NULL REFERENCES FlatTable(id),
PRIMARY KEY (ancestor_id, descendant_id)
);
한 노드에서 다른 노드로 직접 조상이있는 클로저 테이블에 모든 경로를 저장하십시오. 각 노드가 자신을 참조하도록 행을 포함 시키십시오. 예를 들어 질문에 표시 한 데이터 세트를 사용하는 경우 :
INSERT INTO ClosureTable (ancestor_id, descendant_id) VALUES
(1,1), (1,2), (1,4), (1,6),
(2,2), (2,4),
(3,3), (3,5),
(4,4),
(5,5),
(6,6);
이제 다음과 같이 노드 1에서 시작하는 트리를 얻을 수 있습니다.
SELECT f.*
FROM FlatTable f
JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1;
출력 (MySQL 클라이언트의 경우)은 다음과 같습니다.
+----+
| id |
+----+
| 1 |
| 2 |
| 4 |
| 6 |
+----+
다시 말해, 노드 3과 5는 노드 1에서 내려 가지 않고 별도의 계층 구조의 일부이므로 제외됩니다.
다시 : 직계 자녀 (또는 직계 부모)에 대한 전자 불만의 의견. ” path_length
“열을에 추가 ClosureTable
하면 직계 자녀 나 부모 (또는 다른 거리)에 대해보다 쉽게 쿼리 할 수 있습니다.
INSERT INTO ClosureTable (ancestor_id, descendant_id, path_length) VALUES
(1,1,0), (1,2,1), (1,4,2), (1,6,1),
(2,2,0), (2,4,1),
(3,3,0), (3,5,1),
(4,4,0),
(5,5,0),
(6,6,0);
그런 다음 주어진 노드의 직계 자식을 쿼리하기 위해 검색어에 용어를 추가 할 수 있습니다. 이들은 path_length
1의 자손입니다 .
SELECT f.*
FROM FlatTable f
JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1
AND path_length = 1;
+----+
| id |
+----+
| 2 |
| 6 |
+----+
@ashraf의 코멘트 : “전체 트리를 [이름별로] 정렬하는 것은 어떻습니까?”
다음은 노드 1의 자손 인 모든 노드를 반환하고와 같은 다른 노드 속성이 포함 된 FlatTable에 조인하고 name
이름을 기준으로 정렬하는 쿼리 예제 입니다.
SELECT f.name
FROM FlatTable f
JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1
ORDER BY f.name;
@Nate의 의견 :
SELECT f.name, GROUP_CONCAT(b.ancestor_id order by b.path_length desc) AS breadcrumbs
FROM FlatTable f
JOIN ClosureTable a ON (f.id = a.descendant_id)
JOIN ClosureTable b ON (b.descendant_id = a.descendant_id)
WHERE a.ancestor_id = 1
GROUP BY a.descendant_id
ORDER BY f.name
+------------+-------------+
| name | breadcrumbs |
+------------+-------------+
| Node 1 | 1 |
| Node 1.1 | 1,2 |
| Node 1.1.1 | 1,2,4 |
| Node 1.2 | 1,6 |
+------------+-------------+
사용자가 오늘 수정을 제안했습니다. SO 중재자가 편집을 승인했지만 되돌리고 있습니다.
편집 ORDER BY b.path_length, f.name
은 순서가 계층과 일치하는지 확인하기 위해 위의 마지막 쿼리에서 ORDER BY가되어야한다고 제안했습니다 . 그러나 “노드 1.2″다음에 “노드 1.1.1″을 주문하므로 작동하지 않습니다.
순서가 합리적인 방식으로 계층 구조와 일치하도록하려면 경로 길이를 기준으로 정렬하는 것이 아니라 가능합니다. 예를 들어 MySQL Closure Table 계층 데이터베이스에 대한 나의 답변 -올바른 순서로 정보를 가져 오는 방법을 참조하십시오 .
답변
중첩 된 집합 (때로는 수정 된 사전 순서 트리 탐색이라고 함)을 사용하는 경우 단일 쿼리를 사용하여 트리 구조에서 전체 트리 구조 또는 하위 트리를 추출 할 수 있습니다. 트리 구조를 통한 순서 경로를 설명하는 열을 관리합니다.
들어 장고 – mptt ,이 같은 구조를 사용 :
id parent_id tree_id 레벨 lft rght ---------- ------- ----- --- ---- 1 null 1014 2 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 5 1 1 8 13 6 5 1 2 9 10 7 5 1 2 11 12
다음과 같은 트리를 설명합니다 ( id
각 항목 을 나타냄).
1 +-2 | +-3 | +-4 | +-5 +-6 +-7
또는 lft
및 rght
값의 작동 방식을보다 명확하게하는 중첩 된 집합 다이어그램으로 :
__________________________________________________________________________ | 루트 1 | | ________________________________ ________________________________ | | | 아동 1.1 | | 아동 1.2 | | | | ___________ ___________ | | ___________ ___________ | | | | | C 1.1.1 | | C 1.1.2 | | | | C 1.2.1 | | C 1.2.2 | | | 1 2 3___________4 5___________6 7 8 9___________10 11__________12 13 14 | | ________________________________ | | ________________________________ | | | __________________________________________________________________________ |
당신이 볼 수 있듯이, 트리 위해, 당신은 단순히이 모든 행을 선택해야합니다, 주어진 노드의 전체 하위 트리를 얻을 수 lft
및 rght
그 사이의 값 lft
과 rght
값을. 주어진 노드에 대한 조상 트리를 검색하는 것도 간단합니다.
level
열은 무엇보다 편의를 위해 denormalisation의 비트를하고 tree_id
열은 다시 시작 할 수 있습니다 lft
와 rght
는 AS, 삽입, 이동 및 삭제에 의해 영향을 열 수를 줄이고 각 최상위 노드에 대한 번호 lft
와 rght
열해야 간격을 만들거나 닫기 위해 이러한 작업이 수행 될 때 적절하게 조정됩니다. 좀 만들어 개발 노트 나 각 작업에 필요한 쿼리 주위에 내 머리를 정리하려고했던 때를.
실제로이 데이터를 사용하여 트리를 표시하는 관점에서 tree_item_iterator
, 각 노드에 대해 원하는 종류의 표시를 생성하기에 충분한 정보를 제공 하는 유틸리티 기능을 작성했습니다 .
MPTT에 대한 추가 정보 :
답변
그것은 매우 오래된 질문이지만 많은 견해를 가지고 있기 때문에 대안을 제시 할 가치가 있다고 생각합니다. 제 의견으로는 매우 우아합니다.
트리 구조를 읽기 위해 재귀 공통 테이블 표현식을 사용할 수 있습니다 (CTE)을 . 한 번에 전체 트리 구조를 페치하고 노드 레벨, 상위 노드 및 상위 노드의 하위 내 순서에 대한 정보를 가질 수 있습니다.
PostgreSQL 9.1에서 어떻게 작동하는지 보여 드리겠습니다.
-
구조 만들기
CREATE TABLE tree ( id int NOT NULL, name varchar(32) NOT NULL, parent_id int NULL, node_order int NOT NULL, CONSTRAINT tree_pk PRIMARY KEY (id), CONSTRAINT tree_tree_fk FOREIGN KEY (parent_id) REFERENCES tree (id) NOT DEFERRABLE ); insert into tree values (0, 'ROOT', NULL, 0), (1, 'Node 1', 0, 10), (2, 'Node 1.1', 1, 10), (3, 'Node 2', 0, 20), (4, 'Node 1.1.1', 2, 10), (5, 'Node 2.1', 3, 10), (6, 'Node 1.2', 1, 20);
-
검색어 작성
WITH RECURSIVE tree_search (id, name, level, parent_id, node_order) AS ( SELECT id, name, 0, parent_id, 1 FROM tree WHERE parent_id is NULL UNION ALL SELECT t.id, t.name, ts.level + 1, ts.id, t.node_order FROM tree t, tree_search ts WHERE t.parent_id = ts.id ) SELECT * FROM tree_search WHERE level > 0 ORDER BY level, parent_id, node_order;
결과는 다음과 같습니다.
id | name | level | parent_id | node_order ----+------------+-------+-----------+------------ 1 | Node 1 | 1 | 0 | 10 3 | Node 2 | 1 | 0 | 20 2 | Node 1.1 | 2 | 1 | 10 6 | Node 1.2 | 2 | 1 | 20 5 | Node 2.1 | 2 | 3 | 10 4 | Node 1.1.1 | 3 | 2 | 10 (6 rows)
트리 노드는 깊이 수준으로 정렬됩니다. 최종 출력에서 우리는 그것들을 다음 줄에 제시 할 것입니다.
각 레벨에 대해 상위 내에서 parent_id 및 node_order로 정렬됩니다. 이것은 우리에게 그것들을 출력-링크 노드에서 부모에게이 순서대로 제시하는 방법을 알려줍니다.
이러한 구조를 가지면 HTML로 멋진 프리젠 테이션을하는 것이 어렵지 않습니다.
재귀 CTE는 PostgreSQL, IBM DB2, MS SQL Server 및 Oracle에서 사용 가능 .
재귀 SQL 쿼리에 대한 자세한 내용을 보려면 원하는 DBMS 설명서를 확인하거나이 항목을 다루는 두 기사를 읽으십시오.
답변
Oracle 9i부터 CONNECT BY를 사용할 수 있습니다.
SELECT LPAD(' ', (LEVEL - 1) * 4) || "Name" AS "Name"
FROM (SELECT * FROM TMP_NODE ORDER BY "Order")
CONNECT BY PRIOR "Id" = "ParentId"
START WITH "Id" IN (SELECT "Id" FROM TMP_NODE WHERE "ParentId" = 0)
SQL Server 2005부터는 재귀 공통 테이블 식 (CTE)을 사용할 수 있습니다.
WITH [NodeList] (
[Id]
, [ParentId]
, [Level]
, [Order]
) AS (
SELECT [Node].[Id]
, [Node].[ParentId]
, 0 AS [Level]
, CONVERT([varchar](MAX), [Node].[Order]) AS [Order]
FROM [Node]
WHERE [Node].[ParentId] = 0
UNION ALL
SELECT [Node].[Id]
, [Node].[ParentId]
, [NodeList].[Level] + 1 AS [Level]
, [NodeList].[Order] + '|'
+ CONVERT([varchar](MAX), [Node].[Order]) AS [Order]
FROM [Node]
INNER JOIN [NodeList] ON [NodeList].[Id] = [Node].[ParentId]
) SELECT REPLICATE(' ', [NodeList].[Level] * 4) + [Node].[Name] AS [Name]
FROM [Node]
INNER JOIN [NodeList] ON [NodeList].[Id] = [Node].[Id]
ORDER BY [NodeList].[Order]
둘 다 다음과 같은 결과를 출력합니다.
이름 '노드 1' '노드 1.1' '노드 1.1.1' '노드 1.2' '노드 2' '노드 2.1'
답변
Bill의 대답은 꽤 훌륭합니다.이 답변에는 스레드 지원 답변이 필요합니다.
어쨌든 트리 구조와 Order 속성을 지원하고 싶었습니다. 나는 원래 질문 (왼쪽에서 오른쪽 순서로 유지) leftSibling
과 같은 일을하는 각 노드에 단일 속성을 포함 Order
했습니다.
mysql> desc 노드; + ------------- + -------------- + -------- + ----- + --------- -+ ---------------- + | 분야 | 타입 | 널 | 키 | 기본 | 추가 | + ------------- + -------------- + -------- + ----- + --------- -+ ---------------- + | 아이디 | int (11) | 아니요 | PRI | NULL | 자동 증가 | | 이름 | varchar (255) | 예 | | NULL | | | leftSibling | int (11) | 아니요 | | 0 | | + ------------- + -------------- + -------- + ----- + --------- -+ ---------------- + 3 행 세트 (0.00 초) mysql> 디스크 인접성; + ------------ + --------- + ------ + ----- + --------- + --- ------------- + | 분야 | 타입 | 널 | 키 | 기본 | 추가 | + ------------ + --------- + ------ + ----- + --------- + --- ------------- + | relationId | int (11) | 아니요 | PRI | NULL | 자동 증가 | | 부모 | int (11) | 아니요 | | NULL | | | 아이 | int (11) | 아니요 | | NULL | | | pathLen | int (11) | 아니요 | | NULL | | + ------------ + --------- + ------ + ----- + --------- + --- ------------- + 4 행 세트 (0.00 초)
내 블로그에 자세한 내용과 SQL 코드가 있습니다.
감사합니다 Bill 귀하의 답변이 시작하는 데 도움이되었습니다!
답변
선택의 여지가 있다면 객체를 사용하고 있습니다. 각 객체에 children
컬렉션 이있는 각 레코드에 대한 객체를 만들고 Id가 키 인 assoc 배열 (/ hashtable)에 모두 저장합니다. 컬렉션을 한 번 습격하면 관련 어린이 필드에 어린이가 추가됩니다. 단순한.
그러나 좋은 OOP 사용을 제한하여 재미가 없기 때문에 아마도 다음을 기반으로 반복합니다.
function PrintLine(int pID, int level)
foreach record where ParentID == pID
print level*tabs + record-data
PrintLine(record.ID, level + 1)
PrintLine(0, 0)
편집 : 이것은 다른 몇 가지 항목과 유사하지만 약간 깨끗하다고 생각합니다. 한 가지 더할 것 : 이것은 매우 SQL 집약적입니다. 그것은이다 불쾌한 . 선택할 수 있으면 OOP 경로로 이동하십시오.
답변
이것은 신속하게 작성되었으며 예쁘거나 효율적이지는 않지만 (자동 상자가 많이 있고, 전환 int
하고 Integer
성가시다!) 작동합니다.
내 자신의 객체를 생성하고 있기 때문에 규칙을 어길 수도 있지만 실제 작업과의 전환 으로이 작업을 수행하고 있습니다. 🙂
또한 Node를 만들기 전에 resultSet / table이 어떤 종류의 구조로 완전히 읽혀 진다고 가정합니다. 수십만 개의 행이 있으면 가장 좋은 해결책은 아닙니다.
public class Node {
private Node parent = null;
private List<Node> children;
private String name;
private int id = -1;
public Node(Node parent, int id, String name) {
this.parent = parent;
this.children = new ArrayList<Node>();
this.name = name;
this.id = id;
}
public int getId() {
return this.id;
}
public String getName() {
return this.name;
}
public void addChild(Node child) {
children.add(child);
}
public List<Node> getChildren() {
return children;
}
public boolean isRoot() {
return (this.parent == null);
}
@Override
public String toString() {
return "id=" + id + ", name=" + name + ", parent=" + parent;
}
}
public class NodeBuilder {
public static Node build(List<Map<String, String>> input) {
// maps id of a node to it's Node object
Map<Integer, Node> nodeMap = new HashMap<Integer, Node>();
// maps id of a node to the id of it's parent
Map<Integer, Integer> childParentMap = new HashMap<Integer, Integer>();
// create special 'root' Node with id=0
Node root = new Node(null, 0, "root");
nodeMap.put(root.getId(), root);
// iterate thru the input
for (Map<String, String> map : input) {
// expect each Map to have keys for "id", "name", "parent" ... a
// real implementation would read from a SQL object or resultset
int id = Integer.parseInt(map.get("id"));
String name = map.get("name");
int parent = Integer.parseInt(map.get("parent"));
Node node = new Node(null, id, name);
nodeMap.put(id, node);
childParentMap.put(id, parent);
}
// now that each Node is created, setup the child-parent relationships
for (Map.Entry<Integer, Integer> entry : childParentMap.entrySet()) {
int nodeId = entry.getKey();
int parentId = entry.getValue();
Node child = nodeMap.get(nodeId);
Node parent = nodeMap.get(parentId);
parent.addChild(child);
}
return root;
}
}
public class NodePrinter {
static void printRootNode(Node root) {
printNodes(root, 0);
}
static void printNodes(Node node, int indentLevel) {
printNode(node, indentLevel);
// recurse
for (Node child : node.getChildren()) {
printNodes(child, indentLevel + 1);
}
}
static void printNode(Node node, int indentLevel) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < indentLevel; i++) {
sb.append("\t");
}
sb.append(node);
System.out.println(sb.toString());
}
public static void main(String[] args) {
// setup dummy data
List<Map<String, String>> resultSet = new ArrayList<Map<String, String>>();
resultSet.add(newMap("1", "Node 1", "0"));
resultSet.add(newMap("2", "Node 1.1", "1"));
resultSet.add(newMap("3", "Node 2", "0"));
resultSet.add(newMap("4", "Node 1.1.1", "2"));
resultSet.add(newMap("5", "Node 2.1", "3"));
resultSet.add(newMap("6", "Node 1.2", "1"));
Node root = NodeBuilder.build(resultSet);
printRootNode(root);
}
//convenience method for creating our dummy data
private static Map<String, String> newMap(String id, String name, String parentId) {
Map<String, String> row = new HashMap<String, String>();
row.put("id", id);
row.put("name", name);
row.put("parent", parentId);
return row;
}
}