# Jerarquías de datos: Gestión de datos jerárquicos con EF Core y PostgreSQL (Overview)

<!--category-- Entity Framework, PostgreSQL, EF Hierarchies -->
<datetime class="hidden">2025-12-06T09:00</datetime>

## Introducción

Los datos jerárquicos están en todas partes en el desarrollo de software: comentarios roscados, gráficos organizativos, sistemas de archivos, categorías de productos y discusiones en foros. La eterna pregunta de "¿cómo puedo almacenar un árbol en una base de datos relacional?" ha atormentado a los desarrolladores desde los primeros días de SQL, y francamente, todavía no hay una sola respuesta que haga felices a todos. [escribió por primera vez sobre este tema en 2004](/blog/1188), y los desafíos fundamentales siguen siendo los mismos hoy en día.

### Por qué las jerarquías son difíciles en SQL

Este es el problema fundamental: **las bases de datos relacionales piensan en conjuntos, no en árboles**. Como Joe Celko explica en su excelente libro [Pensando en conjuntos](https://www.amazon.com/Joe-Celkos-Thinking-Sets-Management/dp/0123741378), SQL opera en tablas enteras, no en filas individuales.

Cuando se escribe una consulta SQL, el motor de base de datos funciona en *conjuntos de filas*. Es brillante en operaciones como "encontrar todos los pedidos de más de 100" o "unir a los clientes a sus compras" - estas son operaciones de conjunto que encajan naturalmente con cómo funcionan las tablas. El resultado es siempre un conjunto plano de filas.

Pero una jerarquía es inherentemente *recursivo*. Para encontrar a todos los descendientes de un nodo, usted necesita:

1. Encuentre a los niños inmediatos
2. Para cada niño, encontrar *su* niños
3. Repita hasta que haya atravesado todo el subárbol.

Este traversal recursivo no mapea naturalmente para establecer operaciones. No puede expresar "dame a todos los descendientes a cualquier profundidad" en una sola y simple declaración SQL sin:

- [ETC recursivas](https://www.postgresql.org/docs/current/queries-with.html#QUERIES-WITH-RECURSIVE) (añadido a SQL:1999, pero computacionalmente caro)
- Múltiples viajes de ida y vuelta a la base de datos
- Desnormalización inteligente que precompleta las relaciones

Cada enfoque de esta serie representa una compensación diferente entre la complejidad de escritura, la complejidad de lectura, y los gastos de almacenamiento. No hay almuerzo gratis - siempre estás intercambiando un costo por otro. Para la referencia definitiva sobre este tema, consulte Joe Celko's [Árboles y jerarquías en SQL para Smarties](https://www.amazon.com/Hierarchies-Smarties-Kaufmann-Management-Systems/dp/0123877334), que abarca todos estos enfoques en profundidad.

[TOC]

## El ejemplo: Comentarios roscados

Para hacer las comparaciones concretas, usaremos comentarios roscados como nuestro ejemplo de ejecución - algo que este mismo blog usa. Un hilo de comentarios podría verse así:

```mermaid
flowchart TD
    subgraph Post["Post: How to Deploy Docker Containers"]
        C1["Comment 1: Great article!<br/>depth 0"]
        C2["Comment 2: Thanks!<br/>depth 1"]
        C3["Comment 3: Very helpful indeed<br/>depth 1"]
        C4["Comment 4: Agreed!<br/>depth 2"]
        C5["Comment 5: What about Kubernetes?<br/>depth 0"]
        C6["Comment 6: That's covered in part 2<br/>depth 1"]
    end

    C1 --> C2
    C1 --> C3
    C3 --> C4
    C5 --> C6

    style Post stroke:#10b981,stroke-width:2px
    style C1 stroke:#6366f1,stroke-width:2px
    style C2 stroke:#8b5cf6,stroke-width:2px
    style C3 stroke:#8b5cf6,stroke-width:2px
    style C4 stroke:#a855f7,stroke-width:2px
    style C5 stroke:#6366f1,stroke-width:2px
    style C6 stroke:#8b5cf6,stroke-width:2px
```

Tenemos que apoyar estas operaciones:

- **Obtener todos los comentarios para un post** (con estructura de rosca conservada)
- **Obtener todos los antepasados** de un comentario (trayectoria de migajas de pan)
- **Obtener todos los descendientes** de un comentario (todo subterráneo)
- **Añadir un nuevo comentario** (como respuesta a un comentario existente)
- **Eliminar un subárbol** (retirar un comentario y todas las respuestas)
- **Mover un subárbol** (respirar un comentario - más raro, pero a veces necesario)

## Los cinco enfoques

Cada enfoque se trata en detalle en su propio artículo. Aquí hay un resumen para ayudarle a elegir:

---


### 1. Lista de Adyacencias (Referencias de los padres)

**[Leer el artículo completo](/blog/efcore-hierarchical-data-adjacency)**

El enfoque más simple e intuitivo. Cada fila almacena una referencia a su nodo padre. Es lo que la mayoría de los desarrolladores alcanzan en primer lugar porque mapea naturalmente a cómo pensamos acerca de las jerarquías.

**Cómo funciona:** Cada comentario tiene un anulable `ParentCommentId` columna. Los comentarios de raíz tienen NULL, las respuestas apuntan a su padre.

**Lo mejor para:** Jerarquías muy gruesas (menos de 5-6 niveles), movimientos de subárbol frecuentes, cuando se desea que las propiedades de navegación de EF Core para trabajar de forma natural.

**Negociaciones:** Conseguir ancestros o descendientes requiere CTES recursivos o múltiples consultas. Simple de escribir, potencialmente lento para leer árboles profundos.

---


### 2. Cuadro de cierres

**[Leer el artículo completo](/blog/efcore-hierarchical-data-closure)**

Precompleta y almacena cada relación ancestro-descendiente en una tabla separada. En lugar de averiguar las relaciones en el momento de la consulta, las almacenamos explícitamente con su profundidad.

**Cómo funciona:** A separado `CommentClosure` las tiendas de mesa (ancestor_id, descendiente_id, profundidad) para cada par. Comentario 4 bajo Comentario 1 via Comentario 3 tendrían entradas: (1,4,2), (3,4,1), (4,4,0).

**Lo mejor para:** Aplicaciones de lectura pesadas, cuando se necesita consultar a profundidades arbitrarias, cuando rara vez se mueven subárboles.

**Negociaciones:** Inserciones más complejas (debe añadir entradas de cierre), el almacenamiento crece con profundidad, subárboles en movimiento requiere reconstruir cierres.

---


### 3. Camino materializado

**[Leer el artículo completo](/blog/efcore-hierarchical-data-path)**

Almacena el ancestro completo como una cadena delimitada. Piense en ello como almacenar la dirección postal completa en lugar de sólo el nombre de la calle.

**Cómo funciona:** Cada comentario tiene un `Path` columna como "1/3/7" que significa "raíz es 1, padre es 3, esto es 7". Los antepasados pueden ser analizados de la cadena; descendientes encontrados con consultas COMO.

**Lo mejor para:** Generación de migajas de pan, cuando se pregunta a los antepasados más que a los descendientes, cuando se quieren caminos legibles por el ser humano para la depuración.

**Negociaciones:** Las consultas COMO pueden ser lentas sin índices adecuados, las cadenas de ruta tienen límites de longitud, los subárboles en movimiento requieren actualizar todas las rutas descendientes.

---


### 4. Conjuntos anidados

**[Leer el artículo completo](/blog/efcore-hierarchical-data-nested)**

Asigna cada nodo a la izquierda y a la derecha números de frontera de una primera travesía de profundidad. Todos los descendientes tienen valores *entre* los límites de los padres.

**Cómo funciona:** Cada comentario tiene `Left` y `Right` valores. Un nodo con L=4, R=7 contiene todos los nodos donde 4 < Izquierda y Derecha < 7. Los descendientes se encuentran con simples consultas de rango.

**Lo mejor para:** Lectura-pesada, escritura-raramente escenarios como árboles de categoría. Excelente para "conseguir todo el subárbol en orden de visualización" consultas.

**Negociaciones:** Los insertos requieren actualizar muchas filas (desplazar todos los valores para hacer espacio), mover subárboles es complejo. No es adecuado para escrituras frecuentes.

---


### 5. PostgreSQL ltree

**[Leer el artículo completo](/blog/efcore-hierarchical-data-ltree)**

Extensión nativa de PostgreSQL para datos jerárquicos. Como rutas materializadas pero con optimización a nivel de base de datos y potente coincidencia de patrones.

**Cómo funciona:** Utiliza la `ltree` tipo de datos con rutas como "1.3.7". Los índices GiST permiten consultas eficientes de ancestros/descendientes utilizando operadores como `@>` y `<@`.

**Lo mejor para:** Implementaciones de sólo PostgreSQL donde el rendimiento importa, cuando necesita consultas de coincidencia de patrones, cuando desea lo mejor de las rutas materializadas.

**Negociaciones:** Sólo PostgreSQL, añade la dependencia de la extensión de la base de datos. [El proveedor de Npgsql soporta traducciones de LINQ](https://www.npgsql.org/efcore/mapping/translations.html#ltree-functions) para ltree a través de la `LTree` type, aunque los CTE recursivos todavía requieren SQL en bruto.

---


## Resumen de la comparación

Aproximación  Insertar  Consulta Subárbol  Consulta Ancestros  Mover Subárbol  Almacenamiento  EF Core Support
|----------|--------|---------------|-----------------|--------------|---------|-----------------|
| **Lista de Adyacencias** O(1)  O(n) con CTE  O(d) con CTE  O(1)  Mínimo  Excelente
| **Tabla de cierre** O(d)  O(1)  O(1)  O(s x d)  O(n x d)  Bueno
| **Ruta materializada** O(1)  O(1)* O(1)  O(s)  O(d) por nodo  Bueno
| **Conjuntos anidados** O(n)  O(1)  O(1)  O(n)  Mínimo  Bueno
| **ltree** O(1)  O(1)  O(1)  O(s)  O(d) por nodo  Bueno (a través de `LTree` tipo)

*n = nodos totales, d = profundidad, s = tamaño del subárbol*
*Con el índice adecuado

## Diagrama de flujo de la decisión

```mermaid
flowchart TD
    Start([Start]) --> Q1{Shallow tree?<br/>< 5 levels}
    Q1 -->|Yes| Q2{Need EF Core<br/>navigation properties?}
    Q2 -->|Yes| AL[Adjacency List]
    Q2 -->|No| Q3{Performance<br/>critical?}
    Q3 -->|No| AL
    Q3 -->|Yes| MP[Materialised Path]

    Q1 -->|No| Q4{Read-heavy?}
    Q4 -->|Yes| Q5{Writes rare?}
    Q5 -->|Yes| NS[Nested Sets]
    Q5 -->|No| CT[Closure Table]

    Q4 -->|No| Q6{PostgreSQL only?}
    Q6 -->|Yes| LT[ltree]
    Q6 -->|No| Q7{Need breadcrumbs?}
    Q7 -->|Yes| MP
    Q7 -->|No| CT

    style Start stroke:#10b981,stroke-width:2px
    style AL stroke:#6366f1,stroke-width:2px
    style MP stroke:#8b5cf6,stroke-width:2px
    style NS stroke:#ec4899,stroke-width:2px
    style CT stroke:#f59e0b,stroke-width:2px
    style LT stroke:#14b8a6,stroke-width:2px
```

## Elección del mundo real: este blog

Este blog utiliza un **Tabla de cierre** el enfoque para los comentarios.

1. Los comentarios se leen con mucha más frecuencia que los escritos
2. Tenemos que mostrar hilos de comentarios anidados de manera eficiente
3. Las consultas limitantes de profundidad son importantes para el rendimiento (tenemos un límite de 5 niveles de profundidad)
4. Mover comentarios es raro (moderadores ocasionalmente necesitan hacer esto)

Ver [Parte 1.2: Tabla de cierre](/blog/efcore-hierarchical-data-closure) para los detalles completos de la aplicación.

## Navegación en serie

- **Parte 1: Sinopsis** (este artículo)
- [Parte 1.1: Lista de Adyacencias](/blog/efcore-hierarchical-data-adjacency) - Referencias simples de los padres
- [Parte 1.2: Tabla de cierre](/blog/efcore-hierarchical-data-closure) - Relaciones precomputadas
- [Parte 1.3: Ruta materializada](/blog/efcore-hierarchical-data-path) - Cadenas de ruta
- [Parte 1.4: Conjuntos anidados](/blog/efcore-hierarchical-data-nested) - Límites izquierda/derecha
- [Parte 1.5: ltree](/blog/efcore-hierarchical-data-ltree) - Extensión nativa PostgreSQL
- Parte 2: Raw SQL y Dapper (pronto)