What are the options for storing hierarchical data in a relational database?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
Relational databases are built around tables, but many applications need to store tree-shaped data such as categories, menus, comment threads, and organizational charts. There is no single best schema for every hierarchy. The right model depends on how often the tree changes and which operations need to be fast: inserts, moves, ancestor lookups, or subtree reads.
Adjacency List
The simplest model stores a parent_id on each row.
This design is easy to understand and cheap to update. Inserting a node or moving it usually means changing only one row.
The tradeoff is recursive queries. To get a full subtree or full ancestor chain, you typically need a recursive CTE.
For many systems, adjacency list plus recursive CTEs is already good enough.
Materialized Path
Materialized path stores the full path as text, such as 1/4/9.
Reading a subtree becomes easy because everything under 1/4 shares a prefix.
This makes subtree reads simple, but moving a node means updating the path for the node and all descendants.
Nested Set
Nested set stores left and right boundary values for each node.
A subtree read is fast because descendants sit inside the numeric range.
The cost is updates. Inserting or moving nodes may require shifting many left and right values across the table.
Closure Table
Closure table stores ancestor-descendant relationships in a separate table.
This makes ancestor and descendant queries fast and explicit.
The tradeoff is extra storage and more work during inserts, deletes, and moves.
Which Model to Choose
A practical rule is:
- choose adjacency list when writes are common and recursive SQL is acceptable
- choose materialized path when prefix-based subtree reads matter and moves are rare
- choose nested set for read-heavy trees with infrequent structural updates
- choose closure table when ancestor and descendant queries must be fast and flexible
There is no prize for picking the most sophisticated design early. Start with the simplest model that matches your access pattern.
Modern SQL Changes the Tradeoff
Older advice often pushed developers away from adjacency lists because recursive queries were inconvenient. Modern databases with recursive CTE support make adjacency lists much more practical than they used to be.
That means you should not reach for nested sets or closure tables automatically. Only do it when the query pattern actually justifies the extra maintenance cost.
Common Pitfalls
- Picking a complex tree model before understanding whether reads or writes dominate the workload.
- Using adjacency lists without checking whether the database supports recursive CTEs well enough for the required queries.
- Choosing materialized path and then discovering that subtree moves are frequent and expensive.
- Choosing nested set for a tree that changes often, which turns updates into heavy rewrite operations.
- Forgetting that closure tables trade faster queries for more storage and more update logic.
Summary
- Hierarchical data can be stored relationally in several valid ways.
- Adjacency list is the simplest and is often enough with recursive CTE support.
- Materialized path makes subtree reads easy but makes moves more expensive.
- Nested set favors fast reads at the cost of expensive structural updates.
- Closure table is powerful for ancestor and descendant queries but needs extra tables and maintenance.

