프로그래밍데이터 엔지니어

SQL에서 계층적 (트리구조) 데이터를 효율적으로 처리하고 저장하는 방법은 무엇인가요? 이를 위해 어떤 방법이 있으며, 문제에 적합한 방법을 어떻게 선택하나요?

Hintsage AI 어시스턴트로 면접 통과

답변.

계층적 구조를 다루는 것은 관계형 데이터베이스의 고전입니다. 예: 디렉토리, 트리형 메뉴, 조직 구조.

문제의 역사: 데이터베이스는 테이블 모델입니다. 트리형 데이터에 대해서는 몇 가지 접근 방식이 있으며, 각각 장단점이 있습니다.

문제: 표준 모델인 parent_id는 느린 재귀 SELECT를 초래합니다. 실제 작업(모든 자식 검색, 경로 찾기, 서브트리 계산)은 최적화를 요구합니다.

해결책:

  • 인접 리스트(Adjacency List) — parent_id에 대한 간단한 참조입니다.
  • 물리 경로(Materialized Path) — 전체 경로를 표현하는 문자열입니다.
  • 네스트 세트(Nested Sets) — 왼쪽 및 오른쪽 경계 (left/right)를 저장하여 서브트리를 쉽게 추출할 수 있습니다.
  • 클로저 테이블(Closure Table) — 트리 내의 모든 관계를 반영하는 별도의 관계 테이블입니다 (from->to).

물리 경로를 위한 예제 코드 (PostgreSQL):

CREATE TABLE categories ( id SERIAL PRIMARY KEY, name TEXT, path TEXT ); -- 경로 저장 예: '1/2/5/' (루트/서브카테고리/현재) SELECT * FROM categories WHERE path LIKE '1/2/%'; -- 2의 모든 자식

네스트 세트를 위한 예제 코드:

CREATE TABLE nested_categories ( id SERIAL PRIMARY KEY, name TEXT, lft INT NOT NULL, rgt INT NOT NULL ); -- 자식 노드 SELECT * FROM nested_categories WHERE lft > 2 AND rgt < 15;

주요 특징:

  • 인접 리스트는 단순한 트리 구조에 적합합니다 (낮은 중첩).
  • 물리 경로는 서브트리를 빠르게 추출할 수 있으며 구현이 쉽습니다.
  • 네스트 세트는 모든 자식을 즉시 얻을 수 있으며 읽기가 빠르지만 수정이 복잡합니다.

함정 질문.

모든 자식을 찾기 위해 parent_id로 중첩 SELECT를 단순히 사용할 수 있나요?

이것은 작은 깊이에서만 작동합니다. 재귀 검색을 위해서는 재귀 CTE (WITH RECURSIVE) 또는 더 복잡한 스키마가 필요하며, 단순 JOIN은 상당한 호출 수와 성능 저하로 이어집니다.

예:

WITH RECURSIVE cte AS ( SELECT id, parent_id, name FROM categories WHERE id = 1 UNION ALL SELECT c.id, c.parent_id, c.name FROM categories c INNER JOIN cte ON c.parent_id = cte.id ) SELECT * FROM cte;

왜 분자 트리(네스트 세트)는 서브트리를 빠르게 추출하지만 노드를 추가/삭제하는 것은 느리나요?

트리 구조를 변경할 때 여러 행의 lft/rgt를 동시에 변경해야 합니다 (변경 단계 — 삽입/삭제 보다 큰 모든 값). 읽기에는 이상적이지만 자주 변경되는 경우 다른 방법을 사용하세요.

언제 클로저 테이블을 사용하고, parent_id 또는 물리 경로를 사용하지 않나요?

클로저 테이블은 모든 레벨의 자식에 대해 반복적인 쿼리에 잘 작동하며 관계를 정기적으로 재계산하는 데 적합합니다. 단점은 더 많은 공간을 요구합니다.

예:

CREATE TABLE closure ( ancestor INT, descendant INT, depth INT );

일반적인 오류 및 안티 패턴

  • 빠른 탐색이 필요한 구조에서 parent_id만으로 계층을 저장하는 것.
  • 보조 함수 없이 경로 또는 lft/rgt를 수동으로 재계산하는 것.
  • 핵심 열(parent_id/path/lft/rgt)의 인덱스 부재.

실생활 예

부정적인 케이스

인터넷 쇼핑몰에서 카테고리는 parent_id로 구현되었습니다. 모든 하위 항목은 수동으로 설정되며, 하위 카테고리 검색은 중첩 SELECT로 수행됩니다.

장점:

  • 단순함, 확장된 지원이 필요 없습니다.

단점:

  • 3-4 수준의 중첩에서도 성능이 저하됩니다.
  • 노드를 이동하는 작업이 불분명하고 논리적 오류를 초래합니다.

긍정적인 케이스

물리 경로 또는 클로저 테이블이 사용됩니다. 모든 하위 카테고리에 대한 쿼리는 즉각적이며, 재계산은 그룹 스크립트로 수행됩니다.

장점:

  • 확장성.
  • 빠른 하위 선택.

단점:

  • 추가적인 계획이 필요합니다.
  • 구조 변경 시 부하가 증가합니다.