관계형 데이터베이스에 계층 적 데이터를 저장하기위한 옵션은 무엇입니까? [닫은]


1333

좋은 개요

일반적으로 빠른 읽기 시간 (예 : 중첩 집합) 또는 빠른 쓰기 시간 (인접 목록)을 결정합니다. 일반적으로 요구 사항에 가장 적합한 아래 옵션을 조합하여 사용합니다. 다음은 몇 가지 심층적 인 내용입니다.

옵션

내가 알고있는 것 및 일반적인 특징 :

  1. 인접 목록 :
    • 열 : ID, ParentID
    • 구현하기 쉽습니다.
    • 저렴한 노드 이동, 삽입 및 삭제
    • 수준, 조상 및 후손, 경로를 찾는 데 비용이 많이 든다
    • 이를 지원하는 데이터베이스에서 공통 테이블 표현식 을 통해 N + 1을 피하십시오.
  2. 중첩 된 세트 (일명 수정 된 프리오더 트리 탐색 )
    • 열 : 왼쪽, 오른쪽
    • 저렴한 조상, 자손
    • O(n/2)휘발성 인코딩으로 인한 매우 비싼 이동, 삽입, 삭제
  3. 브리지 테이블 (일명 마감 테이블 / w 트리거 )
    • 조상, 자손, 깊이 (선택 사항)와 함께 별도의 조인 테이블을 사용합니다.
    • 저렴한 조상과 자손
    • O(log n)삽입, 업데이트, 삭제 비용 (하위 트리 크기)을 씁니다.
    • 정규화 된 인코딩 : RDBMS 통계 및 조인의 쿼리 플래너에 적합
    • 노드 당 여러 행 필요
  4. 계보 열 (일명 구체화 된 경로 , 경로 열거)
    • 열 : 계보 (예 : / parent / child / grandchild / etc ...)
    • 접두사 쿼리를 통해 저렴한 후손들 (예를 들어 LEFT(lineage, #) = '/enumerated/path')
    • O(log n)삽입, 업데이트, 삭제 비용 (하위 트리 크기)을 씁니다.
    • 비 관계형 : Array 데이터 유형 또는 직렬화 된 문자열 형식에 의존
  5. 중첩 된 간격
    • 중첩 세트와 같지만 실수 / 부동 / 소수를 사용하여 인코딩이 일시적이지 않도록합니다 (고가의 이동 / 삽입 / 삭제)
    • 실수 / 부동 / 소수점 표현 / 정밀도 문제가 있습니다
    • 매트릭스 인코딩 변형 은 "자유"에 대한 조상 인코딩 (구체화 된 경로)을 추가하지만 선형 대수의 까다로운 추가 기능을 제공합니다.
  6. 플랫 테이블
    • 각 레코드에 레벨 및 순위 (예 : 순서) 열을 추가하는 수정 된 인접 목록.
    • 반복 / 페이지 매김에 저렴
    • 비싼 이동 및 삭제
    • 좋은 사용 : 스레드 토론-포럼 / 블로그 댓글
  7. 여러 계보 열
    • 열 : 각 계보 수준에 대해 하나씩, 루트까지의 모든 상위 항목을 참조하고, 항목 수준에서 아래 수준은 NULL로 설정됩니다.
    • 저렴한 조상, 자손, 레벨
    • 잎의 저렴한 삽입, 삭제, 이동
    • 내부 노드의 비싼 삽입, 삭제, 이동
    • 계층 구조의 깊이에 대한 제한

데이터베이스 별 노트

MySQL

신탁

  • 연결 목록을 사용 하여 인접 목록을 통과

PostgreSQL

SQL 서버

  • 일반 요약
  • 2008은 계층 열 데이터 형식이 계보 열 접근 방식을 지원하고 표현할 수있는 깊이를 확장하는 데 도움이됩니다.

5
에 따르면 slideshare.net/billkarwin/sql-antipatterns-strike-back 77 페이지, Closure Tables우월 Adjacency List, Path EnumerationNested Sets사용의 용이성의 측면에서 (내가 아니라 성능을 추측하고있어).
길리

나는 여기에 매우 간단한 버전이 있습니다 : 간단한 BLOB. 계층 구조에 수십 개의 항목 만있는 경우 직렬화 된 ID 트리가 가장 좋습니다.
Lothar

@Lothar : 질문은 커뮤니티 위키이므로 자유롭게 사용하십시오. 내 생각은 XPATH와 같은 안정적인 쿼리 언어가있는 XML과 같은 일종의 얼룩 구조를 지원하는 데이터베이스에서만 수행한다는 것입니다. 그렇지 않으면 SQL이 아닌 코드에서 검색, 역 직렬화 및 병합을 제외하고 좋은 쿼리 방법을 보지 못합니다. 그리고 많은 임의의 요소가 필요한 곳에 실제로 문제가있는 경우 Neo4J와 같은 노드 데이터베이스를 사용하는 것이 좋습니다.
orangepips


2
"일반 요약"에 대한 MSDN 링크는 더 이상 기사를 표시하지 않습니다. MSDN Magazine 2008 년 9 월호에 CHM 파일로 다운로드하거나 웹 아카이브를 통해 볼 수 있습니다 : web.archive.org/web/20080913041559/http://msdn.microsoft.com:80/ …
kͩeͣmͮpͥ ͩ

답변:


66

내가 가장 좋아하는 대답은이 글의 첫 문장이 제안한 것입니다. 인접 목록을 사용하여 계층을 유지하고 중첩 집합을 사용하여 계층을 쿼리하십시오.

지금까지 문제는 대부분의 사람들이 변환을 수행하기 위해 "푸시 스택 (Push Stack)"으로 알려진 극단적 인 RBAR 방법을 사용하고 비용이 많이 드는 방법으로 간주 되었기 때문에 Adjacecy List에서 Nested Sets까지의 적용 방법이 엄청나게 느리다는 것입니다 Adjacency List를 통해 유지 보수가 간단하고 Nested Sets의 뛰어난 성능을 제공합니다. 결과적으로 대부분의 사람들은 특히 10 만 개 이상의 노드가있는 경우 특히 둘 중 하나에 정착해야합니다. 푸시 스택 방법을 사용하면 MLM이 작은 백만 노드 계층으로 간주하는 것에 대한 변환을 하루 종일 걸릴 수 있습니다.

나는 Adjacency List를 Nested 세트로 변환하는 방법을 불가능하게 보이는 속도로 Celko에게 약간의 경쟁을 줄 것이라고 생각했습니다. 다음은 i5 랩탑에서 푸시 스택 방법의 성능입니다.

Duration for     1,000 Nodes = 00:00:00:870 
Duration for    10,000 Nodes = 00:01:01:783 (70 times slower instead of just 10)
Duration for   100,000 Nodes = 00:49:59:730 (3,446 times slower instead of just 100) 
Duration for 1,000,000 Nodes = 'Didn't even try this'

여기에 새로운 방법의 지속 시간이 있습니다 (괄호 안에 푸시 스택 방법 사용).

Duration for     1,000 Nodes = 00:00:00:053 (compared to 00:00:00:870)
Duration for    10,000 Nodes = 00:00:00:323 (compared to 00:01:01:783)
Duration for   100,000 Nodes = 00:00:03:867 (compared to 00:49:59:730)
Duration for 1,000,000 Nodes = 00:00:54:283 (compared to something like 2 days!!!)

네 맞습니다. 1 초 이내에 1 백만 개의 노드와 4 초 이내에 100,000 개의 노드가 변환되었습니다.

새 메소드에 대해 읽고 다음 URL에서 코드 사본을 얻을 수 있습니다. http://www.sqlservercentral.com/articles/Hierarchy/94040/

또한 비슷한 방법을 사용하여 "사전 집계 된"계층 구조를 개발했습니다. MLM 담당자와 BOM을 작성하는 사람들이이 기사에 특히 관심을 가질 것입니다. http://www.sqlservercentral.com/articles/T-SQL/94570/

두 기사 중 하나를 살펴 보려면 "토론 참여"링크로 이동하여 의견을 알려주십시오.


MLMer 란 무엇입니까?
David Mann

MLM = "다단계 마케팅". Amway, Shaklee, ACN 등
Jeff Moden

31

이것은 귀하의 질문에 대한 부분적인 답변이지만 여전히 유용합니다.

Microsoft SQL Server 2008은 계층 적 데이터 관리에 매우 유용한 두 가지 기능을 구현합니다.

MSDN의 Kent Tegels가 시작한 "SQL Server 2008을 사용한 데이터 계층 모델링"을 살펴보십시오 . 내 질문도 참조하십시오. SQL Server 2008의 재귀 동일한 테이블 쿼리 참조


2
흥미로운 HierarchyId는 그 점에 대해 몰랐습니다. msdn.microsoft.com/en-us/library/bb677290.aspx
orangepips

1
과연. 나는 재귀 적으로 많은 계층 적 데이터로 작업하며 공통 테이블 표현식이 매우 유용하다는 것을 알았습니다. 소개는 msdn.microsoft.com/en-us/library/ms186243.aspx 를 참조 하십시오 .
CesarGon

28

이 디자인은 아직 언급되지 않았습니다 :

여러 계보 열

한계가 있지만, 견딜 수 있다면 매우 간단하고 효율적입니다. 풍모:

  • 열 : 각 계보 수준에 대해 하나씩, 루트까지 모든 부모를 참조하고 현재 항목 수준 아래의 수준은 0 (또는 NULL)으로 설정됩니다.
  • 계층 구조의 깊이에 대한 고정 된 제한이 있습니다
  • 저렴한 조상, 자손, 레벨
  • 잎의 저렴한 삽입, 삭제, 이동
  • 내부 노드의 비싼 삽입, 삭제, 이동

다음은 예제입니다. 분류학 조류의 나무이므로 계층 구조는 클래스 / 주문 / 가족 / 속 / 종입니다.

CREATE TABLE `taxons` (
  `TaxonId` smallint(6) NOT NULL default '0',
  `ClassId` smallint(6) default NULL,
  `OrderId` smallint(6) default NULL,
  `FamilyId` smallint(6) default NULL,
  `GenusId` smallint(6) default NULL,
  `Name` varchar(150) NOT NULL default ''
);

그리고 데이터의 예 :

+---------+---------+---------+----------+---------+-------------------------------+
| TaxonId | ClassId | OrderId | FamilyId | GenusId | Name                          |
+---------+---------+---------+----------+---------+-------------------------------+
|     254 |       0 |       0 |        0 |       0 | Aves                          |
|     255 |     254 |       0 |        0 |       0 | Gaviiformes                   |
|     256 |     254 |     255 |        0 |       0 | Gaviidae                      |
|     257 |     254 |     255 |      256 |       0 | Gavia                         |
|     258 |     254 |     255 |      256 |     257 | Gavia stellata                |
|     259 |     254 |     255 |      256 |     257 | Gavia arctica                 |
|     260 |     254 |     255 |      256 |     257 | Gavia immer                   |
|     261 |     254 |     255 |      256 |     257 | Gavia adamsii                 |
|     262 |     254 |       0 |        0 |       0 | Podicipediformes              |
|     263 |     254 |     262 |        0 |       0 | Podicipedidae                 |
|     264 |     254 |     262 |      263 |       0 | Tachybaptus                   |

내부 카테고리가 트리에서 레벨을 변경하지 않는 한, 필요한 모든 조작을 매우 쉽게 수행 할 수 있기 때문에이 방법이 유용합니다.


22

인접 모델 + 중첩 세트 모델

트리에 새 항목을 쉽게 삽입 할 수 있기 때문에 (새 항목을 삽입하려면 지점의 ID가 필요함) 아주 빨리 쿼리하기 때문에 그것을 보았습니다.

+-------------+----------------------+--------+-----+-----+
| category_id | name                 | parent | lft | rgt |
+-------------+----------------------+--------+-----+-----+
|           1 | ELECTRONICS          |   NULL |   1 |  20 |
|           2 | TELEVISIONS          |      1 |   2 |   9 |
|           3 | TUBE                 |      2 |   3 |   4 |
|           4 | LCD                  |      2 |   5 |   6 |
|           5 | PLASMA               |      2 |   7 |   8 |
|           6 | PORTABLE ELECTRONICS |      1 |  10 |  19 |
|           7 | MP3 PLAYERS          |      6 |  11 |  14 |
|           8 | FLASH                |      7 |  12 |  13 |
|           9 | CD PLAYERS           |      6 |  15 |  16 |
|          10 | 2 WAY RADIOS         |      6 |  17 |  18 |
+-------------+----------------------+--------+-----+-----+
  • 부모의 모든 자식이 필요할 때마다 parent열을 쿼리하기 만하면 됩니다.
  • 부모의 모든 자손이 필요하면 그 lft사이 lftrgt부모 사이 에있는 항목을 쿼리합니다 .
  • 당신이 트리의 루트 노드까지의 모든 학부모 필요한 경우, 당신은 필요 항목에 대한 쿼리 lft노드의보다 낮은 lftrgt노드의보다 큰 rgt및 정렬에 의해 parent.

삽입물보다 트리에 빠르게 액세스하고 쿼리해야했기 때문에 이것을 선택했습니다.

유일한 문제는 새 항목을 삽입 할 때 leftright열 을 수정하는 것 입니다. 글쎄 나는 그것을 위해 저장 프로 시저를 만들고 내 경우에는 드물지만 새 항목을 삽입 할 때마다 호출했지만 실제로 빠릅니다. Joe Celko의 책에서 아이디어를 얻었으며 저장 프로 시저와 그 방법을 DBA SE https://dba.stackexchange.com/q/89051/41481에 설명했습니다.


3
+1 이것은 합법적 인 접근 방식입니다. 내 경험에 비추어 볼 때 대규모 업데이트 작업이 수행 될 때 더티 읽기로 문제가 없는지 결정하는 것이 핵심입니다. 그렇지 않으면 문제가되거나 사람들이 테이블을 직접 쿼리하지 않고 항상 API-DB sprocs / functions 또는 code를 거치지 못하게됩니다.
orangepips

1
이것은 흥미로운 해결책입니다. 그러나 부모 열을 쿼리하면 실제로 자식을 찾으려고 할 때 큰 이점이 있습니다. 먼저 왼쪽과 오른쪽 열이 있습니다.
Thomas

2
@Thomas, children와 (과 )에는 차이가 descendants있습니다. left그리고 right후손을 찾는 데 사용된다.
azerafati

14

데이터베이스가 배열을 지원하는 경우 계보 열 또는 구체화 된 경로를 부모 ID의 배열로 구현할 수도 있습니다.

특히 Postgres를 사용하면 set 연산자를 사용하여 계층 구조를 쿼리하고 GIN 인덱스로 뛰어난 성능을 얻을 수 있습니다. 따라서 단일 쿼리에서 부모, 자녀 및 깊이를 찾는 것이 매우 간단합니다. 업데이트도 매우 관리하기 쉽습니다.

궁금한 경우 구체화 된 경로에 배열을 사용하는 것에 대한 전체 기사를 작성했습니다 .


9

이것은 실제로 사각 페그, 둥근 구멍 질문입니다.

관계형 데이터베이스와 SQL이 당신이 가지고 있거나 사용하고자하는 유일한 망치라면 지금까지 게시 된 답변이 적합합니다. 그러나 계층 적 데이터를 처리하도록 설계된 도구를 사용하지 않는 이유는 무엇입니까? 그래프 데이터베이스 는 복잡한 계층 적 데이터에 이상적입니다.

그래프 / 계층 모델을 관계형 모델에 매핑하기위한 코드 / 쿼리 솔루션의 복잡성과 함께 관계형 모델의 비 효율성은 그래프 데이터베이스 솔루션이 동일한 문제를 쉽게 해결할 수있는 것과 비교할 때 노력할 가치가 없습니다.

BOM을 일반적인 계층 적 데이터 구조로 간주하십시오.

class Component extends Vertex {
    long assetId;
    long partNumber;
    long material;
    long amount;
};

class PartOf extends Edge {
};

class AdjacentTo extends Edge {
};

두 개의 하위 어셈블리 사이의 최단 경로 : 간단한 그래프 순회 알고리즘. 허용되는 경로는 기준에 따라 규정 될 수 있습니다.

유사성 : 두 어셈블리 간의 유사도는 얼마입니까? 두 하위 트리의 교차와 합집합을 계산하는 두 하위 트리에서 순회를 수행합니다. 비슷한 비율은 교차로를 합집합으로 나눈 값입니다.

전이 폐쇄 : 하위 트리를 따라 이동하여 관심있는 필드를 요약합니다 (예 : "하위 조립품에 얼마나 많은 알루미늄이 있습니까?").

예, SQL 및 관계형 데이터베이스의 문제점을 해결할 수 있습니다. 그러나 작업에 적합한 도구를 기꺼이 사용하려는 경우 훨씬 더 나은 방법이 있습니다.


5
이 답변은 유스 케이스가 RDBMS의 SQL 대신 SPARQL을 사용하여 그래프 데이터베이스를 쿼리하는 방법을 보여 주거나 더 나은 대조를 이루면 훨씬 유용합니다.
orangepips

1
SPARQL은 더 큰 그래프 데이터베이스 도메인의 서브 클래스 인 RDF 데이터베이스와 관련이 있습니다. RDF 데이터베이스가 아니며 현재 SPARQL을 지원하지 않는 InfiniteGraph로 작업합니다. InfiniteGraph는 (1) 뷰, 필터, 경로 한정자 및 결과 핸들러를 설정하기위한 그래프 탐색 API, (2) 복잡한 그래프 경로 패턴 일치 언어 및 (3) Gremlin과 같은 여러 가지 쿼리 메커니즘을 지원합니다.
djhallx

6

계층에 대한 클로저 테이블과 함께 PostgreSQL을 사용하고 있습니다. 전체 데이터베이스에 대해 하나의 범용 저장 프로 시저가 있습니다.

CREATE FUNCTION nomen_tree() RETURNS trigger
    LANGUAGE plpgsql
    AS $_$
DECLARE
  old_parent INTEGER;
  new_parent INTEGER;
  id_nom INTEGER;
  txt_name TEXT;
BEGIN
-- TG_ARGV[0] = name of table with entities with PARENT-CHILD relationships (TBL_ORIG)
-- TG_ARGV[1] = name of helper table with ANCESTOR, CHILD, DEPTH information (TBL_TREE)
-- TG_ARGV[2] = name of the field in TBL_ORIG which is used for the PARENT-CHILD relationship (FLD_PARENT)
    IF TG_OP = 'INSERT' THEN
    EXECUTE 'INSERT INTO ' || TG_ARGV[1] || ' (child_id,ancestor_id,depth) 
        SELECT $1.id,$1.id,0 UNION ALL
      SELECT $1.id,ancestor_id,depth+1 FROM ' || TG_ARGV[1] || ' WHERE child_id=$1.' || TG_ARGV[2] USING NEW;
    ELSE                                                           
    -- EXECUTE does not support conditional statements inside
    EXECUTE 'SELECT $1.' || TG_ARGV[2] || ',$2.' || TG_ARGV[2] INTO old_parent,new_parent USING OLD,NEW;
    IF COALESCE(old_parent,0) <> COALESCE(new_parent,0) THEN
      EXECUTE '
      -- prevent cycles in the tree
      UPDATE ' || TG_ARGV[0] || ' SET ' || TG_ARGV[2] || ' = $1.' || TG_ARGV[2]
        || ' WHERE id=$2.' || TG_ARGV[2] || ' AND EXISTS(SELECT 1 FROM '
        || TG_ARGV[1] || ' WHERE child_id=$2.' || TG_ARGV[2] || ' AND ancestor_id=$2.id);
      -- first remove edges between all old parents of node and its descendants
      DELETE FROM ' || TG_ARGV[1] || ' WHERE child_id IN
        (SELECT child_id FROM ' || TG_ARGV[1] || ' WHERE ancestor_id = $1.id)
        AND ancestor_id IN
        (SELECT ancestor_id FROM ' || TG_ARGV[1] || ' WHERE child_id = $1.id AND ancestor_id <> $1.id);
      -- then add edges for all new parents ...
      INSERT INTO ' || TG_ARGV[1] || ' (child_id,ancestor_id,depth) 
        SELECT child_id,ancestor_id,d_c+d_a FROM
        (SELECT child_id,depth AS d_c FROM ' || TG_ARGV[1] || ' WHERE ancestor_id=$2.id) AS child
        CROSS JOIN
        (SELECT ancestor_id,depth+1 AS d_a FROM ' || TG_ARGV[1] || ' WHERE child_id=$2.' 
        || TG_ARGV[2] || ') AS parent;' USING OLD, NEW;
    END IF;
  END IF;
  RETURN NULL;
END;
$_$;

그런 다음 계층 구조가있는 각 테이블에 대해 트리거를 만듭니다.

CREATE TRIGGER nomenclature_tree_tr AFTER INSERT OR UPDATE ON nomenclature FOR EACH ROW EXECUTE PROCEDURE nomen_tree('my_db.nomenclature', 'my_db.nom_helper', 'parent_id');

기존 계층 구조에서 클로저 테이블을 채우려면이 저장 프로 시저를 사용합니다.

CREATE FUNCTION rebuild_tree(tbl_base text, tbl_closure text, fld_parent text) RETURNS void
    LANGUAGE plpgsql
    AS $$
BEGIN
    EXECUTE 'TRUNCATE ' || tbl_closure || ';
    INSERT INTO ' || tbl_closure || ' (child_id,ancestor_id,depth) 
        WITH RECURSIVE tree AS
      (
        SELECT id AS child_id,id AS ancestor_id,0 AS depth FROM ' || tbl_base || '
        UNION ALL 
        SELECT t.id,ancestor_id,depth+1 FROM ' || tbl_base || ' AS t
        JOIN tree ON child_id = ' || fld_parent || '
      )
      SELECT * FROM tree;';
END;
$$;

마감 테이블은 3 개의 열 (ANCESTOR_ID, DESCENDANT_ID, DEPTH)로 정의됩니다. ANCESTOR와 DESCENDANT에 대해 동일한 값을 갖고 DEPTH에 대해 0의 값을 가진 레코드를 저장할 수 있습니다 (그리고 심지어 조언도 가능합니다). 그러면 계층 구조 검색 쿼리가 단순화됩니다. 그리고 그들은 실제로 매우 간단합니다.

-- get all descendants
SELECT tbl_orig.*,depth FROM tbl_closure LEFT JOIN tbl_orig ON descendant_id = tbl_orig.id WHERE ancestor_id = XXX AND depth <> 0;
-- get only direct descendants
SELECT tbl_orig.* FROM tbl_closure LEFT JOIN tbl_orig ON descendant_id = tbl_orig.id WHERE ancestor_id = XXX AND depth = 1;
-- get all ancestors
SELECT tbl_orig.* FROM tbl_closure LEFT JOIN tbl_orig ON ancestor_id = tbl_orig.id WHERE descendant_id = XXX AND depth <> 0;
-- find the deepest level of children
SELECT MAX(depth) FROM tbl_closure WHERE ancestor_id = XXX;
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.