Data Modeling & Schema Design

Modeling Hierarchies and Graphs

Hierarchical data can be stored with parent references, encoded paths, subtree intervals, or an explicit transitive closure. The right model follows the dominant reads, mutation rate, integrity rules, and whether the structure is truly a tree.
  • An adjacency list stores the direct edge and is the simplest normalized tree model.
  • A materialized path makes ancestry visible in each node.
  • Nested sets encode containment with left and right bounds.
  • A closure table stores every ancestor–descendant pair.
  • Cycles, multiple parents, sibling order, and orphan policy are separate constraints that the storage shape does not solve automatically.
  • When edges have identity or attributes such as rank, validity, or provenance, model them as first-class relationship rows.

Four models for commerce categories

Hierarchy model comparison
ModelSubtree readInsert or moveStorageCharacteristic risk
Adjacency listRecursive traversalLocal parent updateOne parent key per nodeCycles and deep recursive reads
Materialized pathPath-prefix predicateRewrite moved subtree pathsPath repeated per nodeEncoding and stale descendant paths
Nested setsRange predicate on boundsPotentially broad renumberingTwo bounds per nodeWrite amplification and concurrent moves
Closure tableDirect lookup by ancestorInsert/delete closure rowsOne row per reachable pairMaintenance bugs and quadratic growth in chains
Category tree and closure rows
Depth-zero self rows make “self plus descendants” queries uniform.
Adjacency and closure-table logical schemas
CREATE TABLE categories (
  category_id BIGINT PRIMARY KEY,
  parent_category_id BIGINT REFERENCES categories(category_id),
  name VARCHAR(200) NOT NULL,
  CHECK (parent_category_id IS NULL OR parent_category_id <> category_id)
);

CREATE TABLE category_closure (
  ancestor_id BIGINT NOT NULL REFERENCES categories(category_id),
  descendant_id BIGINT NOT NULL REFERENCES categories(category_id),
  depth INTEGER NOT NULL CHECK (depth >= 0),
  PRIMARY KEY (ancestor_id, descendant_id)
);
The simple check blocks only self-loops; preventing longer cycles requires traversal-aware write logic.