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, y los desafíos fundamentales siguen siendo los mismos hoy en día.
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, 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:
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:
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, que abarca todos estos enfoques en profundidad.
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í:
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:
Cada enfoque se trata en detalle en su propio artículo. Aquí hay un resumen para ayudarle a elegir:
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.
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.
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.
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.
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 para ltree a través de la LTree type, aunque los CTE recursivos todavía requieren SQL en bruto.
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
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
Este blog utiliza un Tabla de cierre el enfoque para los comentarios.
Ver Parte 1.2: Tabla de cierre para los detalles completos de la aplicación.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.