Suponga que tiene una tabla plana que almacena una jerarquía de árbol ordenada:
Id Name ParentId Order
1 'Node 1' 0 10
2 'Node 1.1' 1 10
3 'Node 2' 0 20
4 'Node 1.1.1' 2 10
5 'Node 2.1' 3 10
6 'Node 1.2' 1 20
Aquí hay un diagrama, donde tenemos [id] Name
. El nodo raíz 0 es ficticio.
[0] RAÍZ / \ [1] Nodo 1 [3] Nodo 2 / \ \ [2] Nodo 1.1 [6] Nodo 1.2 [5] Nodo 2.1 / / [4] Nodo 1.1.1
¿Qué enfoque minimalista usarías para enviar eso a HTML (o texto, para el caso) como un árbol correctamente ordenado y correctamente sangrado?
Supongamos además que solo tiene estructuras de datos básicas (matrices y hashmaps), sin objetos sofisticados con referencias padre / hijo, sin ORM, sin marco, solo sus dos manos. La tabla se representa como un conjunto de resultados, al que se puede acceder aleatoriamente.
El pseudo código o el inglés simple está bien, esta es una pregunta puramente conceptual.
Pregunta adicional: ¿existe una forma fundamentalmente mejor de almacenar una estructura de árbol como esta en un RDBMS?
EDICIONES Y ADICIONES
Para responder a la pregunta de un comentarista ( Mark Bessey ): No es necesario un nodo raíz, porque de todos modos nunca se mostrará. ParentId = 0 es la convención para expresar "estos son de nivel superior". La columna Orden define cómo se ordenarán los nodos con el mismo padre.
El "conjunto de resultados" del que hablé se puede representar como una matriz de hashmaps (para mantenerse en esa terminología). Para mi ejemplo, estaba destinado a estar allí. Algunas respuestas hacen un esfuerzo adicional y lo construyen primero, pero está bien.
El árbol puede ser arbitrariamente profundo. Cada nodo puede tener N hijos. Sin embargo, no tenía exactamente en mente un árbol de "millones de entradas".
No confunda mi elección de nombre de nodo ('Nodo 1.1.1') con algo en lo que confiar. Los nodos también podrían llamarse 'Frank' o 'Bob', no implica una estructura de nombres, esto era simplemente para que fuera legible.
He publicado mi propia solución para que ustedes puedan hacerla pedazos.
Respuestas:
Ahora que MySQL 8.0 admite consultas recursivas , podemos decir que todas las bases de datos SQL populares admiten consultas recursivas en sintaxis estándar.
Probé consultas recursivas en MySQL 8.0 en mi presentación Recursive Query Throwdown en 2017.
A continuación se muestra mi respuesta original de 2008:
Hay varias formas de almacenar datos estructurados en árbol en una base de datos relacional. Lo que muestra en su ejemplo utiliza dos métodos:
Otra solución se llama Conjuntos anidados , y también se puede almacenar en la misma tabla. Lea " Árboles y jerarquías en SQL para Smarties " de Joe Celko para obtener más información sobre estos diseños.
Por lo general, prefiero un diseño llamado Tabla de cierre (también conocido como "Relación de adyacencia") para almacenar datos estructurados en árbol. Requiere otra tabla, pero luego consultar árboles es bastante fácil.
Cubro la Tabla de cierre en mi presentación Modelos para datos jerárquicos con SQL y PHP y en mi libro Antipatterns de SQL: Evitar las trampas de la programación de bases de datos .
Almacene todas las rutas en la Tabla de cierre, donde hay una ascendencia directa de un nodo a otro. Incluya una fila para que cada nodo haga referencia a sí mismo. Por ejemplo, usando el conjunto de datos que mostró en su pregunta:
Ahora puede obtener un árbol que comienza en el nodo 1 como este:
El resultado (en el cliente MySQL) tiene el siguiente aspecto:
En otras palabras, los nodos 3 y 5 están excluidos, porque son parte de una jerarquía separada, que no desciende del nodo 1.
Re: comentario de e-satis sobre hijos inmediatos (o padres inmediatos). Puede agregar una
path_length
columna " " a laClosureTable
para facilitar la consulta específica de un hijo o padre inmediato (o cualquier otra distancia).Luego, puede agregar un término en su búsqueda para consultar los elementos secundarios inmediatos de un nodo determinado. Estos son descendientes cuyo
path_length
es 1.Re comentario de @ashraf: "¿Qué tal si ordenamos todo el árbol [por nombre]?"
Aquí hay una consulta de ejemplo para devolver todos los nodos que son descendientes del nodo 1, unirlos a la FlatTable que contiene otros atributos de nodo como
name
y ordenarlos por el nombre.Re comentar de @Nate:
Un usuario sugirió una edición hoy. Los moderadores de SO aprobaron la edición, pero la estoy revocando.
La edición sugirió que ORDER BY en la última consulta anterior debería ser
ORDER BY b.path_length, f.name
, probablemente para asegurarse de que el orden coincida con la jerarquía. Pero esto no funciona, porque ordenaría "Nodo 1.1.1" después de "Nodo 1.2".Si desea que el orden coincida con la jerarquía de una manera sensata, es posible, pero no simplemente ordenando por la longitud de la ruta. Por ejemplo, vea mi respuesta a la base de datos jerárquica de MySQL Closure Table: cómo extraer la información en el orden correcto .
fuente
parent_id
) solo tiene una fila para expresar cada relación padre-hijo, pero la Tabla de cierre tiene muchas.Si usa conjuntos anidados (a veces denominados Recorrido de árbol de pedido anticipado modificado) puede extraer toda la estructura de árbol o cualquier subárbol dentro de ella en orden de árbol con una sola consulta, a costa de que las inserciones sean más caras, ya que necesita administrar columnas que describen una ruta en orden a través de la estructura de árbol.
Para django-mptt , utilicé una estructura como esta:
Que describe un árbol que se ve así (con la
id
representación de cada elemento):O, como un diagrama de conjunto anidado que hace más obvio cómo funcionan los valores
lft
yrght
:Como se puede ver, para obtener todo el subárbol de un nodo dado, con el fin árbol, sólo tiene que seleccionar todas las filas que tienen
lft
yrght
los valores entre suslft
yrght
valores. También es simple recuperar el árbol de antepasados para un nodo dado.La
level
columna es un poco de desnormalización por conveniencia más que nada y latree_id
columna le permite reiniciarlft
yrght
numerar para cada nodo de nivel superior, lo que reduce el número de columnas afectadas por inserciones, movimientos y eliminaciones, ya que las columnaslft
yrght
deben ser ajustado en consecuencia cuando estas operaciones tienen lugar para crear o cerrar brechas. Tomé algunas notas de desarrollo en el momento en que estaba tratando de entender las consultas requeridas para cada operación.En términos de trabajar realmente con estos datos para mostrar un árbol, creé una
tree_item_iterator
función de utilidad que, para cada nodo, debería proporcionarle información suficiente para generar cualquier tipo de visualización que desee.Más información sobre MPTT:
fuente
lft
yrght
para los nombres de columna, quiero decir, ¿cuántos caracteres no tuvimos que escribir? ¡¿uno?!Es una pregunta bastante antigua, pero como tiene muchas opiniones, creo que vale la pena presentar una alternativa, y en mi opinión, una solución muy elegante.
Para leer una estructura de árbol, puede usar expresiones de tabla comunes (CTE) recursivas. Ofrece la posibilidad de recuperar toda la estructura de árbol a la vez, tener la información sobre el nivel del nodo, su nodo principal y el orden dentro de los hijos del nodo principal.
Déjame mostrarte cómo funcionaría esto en PostgreSQL 9.1.
Crear una estructura
Escribir una consulta
Aquí están los resultados:
Los nodos del árbol están ordenados por un nivel de profundidad. En el resultado final los presentaríamos en las siguientes líneas.
Para cada nivel, se ordenan por parent_id y node_order dentro del padre. Esto nos dice cómo presentarlos en el nodo de enlace de salida al padre en este orden.
Con una estructura así, no sería difícil hacer una presentación realmente agradable en HTML.
Los CTE recursivos están disponibles en PostgreSQL, IBM DB2, MS SQL Server y Oracle .
Si desea leer más sobre consultas SQL recursivas, puede consultar la documentación de su DBMS favorito o leer mis dos artículos que cubren este tema:
fuente
A partir de Oracle 9i, puede usar CONNECT BY.
A partir de SQL Server 2005, puede usar una expresión de tabla común recursiva (CTE).
Ambos generarán los siguientes resultados.
fuente
La respuesta de Bill es bastante buena, esta respuesta le agrega algunas cosas que me hacen desear respuestas roscadas tan compatibles.
De todos modos, quería apoyar la estructura de árbol y la propiedad Order. Incluí una sola propiedad en cada Nodo llamado
leftSibling
que hace lo mismo queOrder
debe hacer en la pregunta original (mantener el orden de izquierda a derecha).Más detalles y código SQL en mi blog .
Gracias Bill, tu respuesta fue útil para comenzar.
fuente
Bien dada la opción, estaría usando objetos.
children
Crearía un objeto para cada registro donde cada objeto tiene una colección y los almacenaría en una matriz asociada (/ hashtable) donde el Id es la clave. Y revise la colección una vez, agregando a los niños a los campos de niños relevantes.Simple.Pero debido a que no estás siendo divertido al restringir el uso de una buena POO, probablemente repetiría en base a:
Editar: esto es similar a un par de otras entradas, pero creo que es un poco más limpio. Una cosa que agregaré: esto es extremadamente intensivo en SQL. Es desagradable . Si tiene la opción, vaya a la ruta OOP.
fuente
Esto fue escrito rápidamente, y no es ni bonito ni eficiente (¡además de autoboxes mucho, convertir entre
int
yInteger
es molesto!), Pero funciona.Probablemente rompa las reglas ya que estoy creando mis propios objetos, pero bueno, estoy haciendo esto como una distracción del trabajo real :)
Esto también supone que el resultSet / table se lee completamente en algún tipo de estructura antes de comenzar a construir Nodos, lo que no sería la mejor solución si tiene cientos de miles de filas.
fuente
Existen soluciones realmente buenas que explotan la representación interna btree de los índices sql. Esto se basa en una gran investigación realizada alrededor de 1998.
Aquí hay una tabla de ejemplo (en mysql).
Los únicos campos necesarios para la representación del árbol son:
Aquí hay un ejemplo de población de 24 nodos, ordenado por tw:
Cada resultado de árbol se puede hacer de forma no recursiva. Por ejemplo, para obtener una lista de antepasados del nodo en tw = '22 '
Antepasados
Los hermanos y los niños son triviales, solo use el ordenamiento de campo pa por tw.
Descendientes
Por ejemplo, el conjunto (rama) de nodos que están enraizados en tw = 17.
Notas adicionales
Esta metodología es extremadamente útil para cuando hay un número mucho mayor de lecturas que inserciones o actualizaciones.
Debido a que la inserción, movimiento o actualización de un nodo en el árbol requiere que el árbol se ajuste, es necesario bloquear la tabla antes de comenzar con la acción.
El costo de inserción / eliminación es alto porque los valores de índice tw y sz (tamaño de rama) deberán actualizarse en todos los nodos después del punto de inserción, y para todos los antepasados, respectivamente.
El movimiento de rama implica mover el valor tw de la rama fuera del rango, por lo que también es necesario deshabilitar las restricciones de clave externa al mover una rama. Básicamente, se requieren cuatro consultas para mover una rama:
Ajustar consultas de árbol
La apertura / cierre de huecos en el árbol es una subfunción importante utilizada por los métodos crear / actualizar / eliminar, así que la incluyo aquí.
Necesitamos dos parámetros: una bandera que represente si estamos reduciendo o reduciendo el tamaño, y el índice tw del nodo. Entonces, por ejemplo tw = 18 (que tiene un tamaño de rama de 5). Supongamos que estamos reduciendo el tamaño (eliminando tw); esto significa que estamos usando '-' en lugar de '+' en las actualizaciones del siguiente ejemplo.
Primero usamos una función ancestro (ligeramente alterada) para actualizar el valor sz.
Luego, necesitamos ajustar el tw para aquellos cuyo tw es más alto que la rama que se va a eliminar.
Luego, necesitamos ajustar el padre para aquellos cuyo tw de pa es más alto que la rama que se eliminará.
fuente
Suponiendo que sabe que los elementos raíz son cero, aquí está el pseudocódigo para enviar al texto:
fuente
Puede emular cualquier otra estructura de datos con un hashmap, por lo que no es una limitación terrible. Al escanear de arriba a abajo, crea un hashmap para cada fila de la base de datos, con una entrada para cada columna. Agregue cada uno de estos hashmaps a un hashmap "maestro", tecleado en la identificación. Si algún nodo tiene un "padre" que aún no ha visto, cree una entrada de marcador de posición para él en el mapa hash maestro y complételo cuando vea el nodo real.
Para imprimirlo, haga un simple pase de profundidad primero a través de los datos, haciendo un seguimiento del nivel de sangría en el camino. Puede hacer esto más fácil manteniendo una entrada "secundaria" para cada fila y rellenándola mientras escanea los datos.
En cuanto a si hay una "mejor" forma de almacenar un árbol en una base de datos, eso depende de cómo va a utilizar los datos. He visto sistemas que tenían una profundidad máxima conocida que usaban una tabla diferente para cada nivel en la jerarquía. Eso tiene mucho sentido si los niveles en el árbol no son del todo equivalentes después de todo (las categorías de nivel superior son diferentes a las hojas).
fuente
Si se pueden crear mapas o matrices hash anidados, entonces simplemente puedo bajar de la tabla desde el principio y agregar cada elemento a la matriz anidada. Debo rastrear cada línea hasta el nodo raíz para saber en qué nivel de la matriz anidada insertar. Puedo emplear la memorización para no tener que buscar al mismo padre una y otra vez.
Editar: Primero leería toda la tabla en una matriz, por lo que no consultará la base de datos repetidamente. Por supuesto, esto no será práctico si su mesa es muy grande.
Después de construir la estructura, primero debo atravesarla en profundidad e imprimir el HTML.
No hay una mejor manera fundamental de almacenar esta información usando una tabla (aunque podría estar equivocado;), y me encantaría ver una mejor solución). Sin embargo, si crea un esquema para emplear tablas db creadas dinámicamente, entonces abre un mundo completamente nuevo con el sacrificio de la simplicidad y el riesgo del infierno SQL;).
fuente
Si los elementos están en orden de árbol, como se muestra en su ejemplo, puede usar algo como el siguiente ejemplo de Python:
Lo que esto hace es mantener una pila que representa la posición actual en el árbol. Para cada elemento de la tabla, muestra elementos de la pila (cerrando los divs coincidentes) hasta que encuentra el elemento primario del elemento actual. Luego genera el inicio de ese nodo y lo empuja a la pila.
Si desea generar el árbol utilizando sangría en lugar de elementos anidados, simplemente puede omitir las declaraciones de impresión para imprimir los divs e imprimir un número de espacios igual a algún múltiplo del tamaño de la pila antes de cada elemento. Por ejemplo, en Python:
También podría usar fácilmente este método para construir un conjunto de listas anidadas o diccionarios.
Editar: veo por su aclaración que los nombres no estaban destinados a ser rutas de nodo. Eso sugiere un enfoque alternativo:
Esto construye un árbol de matrices de tuplas (!). idx [0] representa la raíz (s) del árbol. Cada elemento en una matriz es una tupla de 2 que consta del nodo en sí y una lista de todos sus elementos secundarios. Una vez construido, puede conservar idx [0] y descartar idx, a menos que desee acceder a los nodos por su ID.
fuente
Para ampliar la solución SQL de Bill, básicamente puede hacer lo mismo con una matriz plana. Además, si todas sus cadenas tienen la misma longitud y se conoce su número máximo de hijos (digamos en un árbol binario), puede hacerlo usando una sola cadena (matriz de caracteres). Si tiene un número arbitrario de hijos, esto complica un poco las cosas ... Tendría que revisar mis notas anteriores para ver qué se puede hacer.
Luego, sacrificando un poco de memoria, especialmente si su árbol es escaso y / o no equilibrado, puede, con un poco de matemática de índice, acceder a todas las cadenas de forma aleatoria almacenando su árbol, el ancho primero en la matriz de esta manera (para un binario árbol):
sabes la longitud de tu cuerda, lo sabes
Ahora estoy en el trabajo, así que no puedo pasar mucho tiempo en ello, pero con interés puedo obtener un poco de código para hacer esto.
Solíamos hacerlo para buscar en árboles binarios hechos de codones de ADN, un proceso construyó el árbol, luego lo aplanamos para buscar patrones de texto y cuando lo encontramos, aunque el índice matemático (al revés desde arriba) recuperamos el nodo ... muy rápido y eficiente, resistente, nuestro árbol rara vez tenía nodos vacíos, pero podíamos buscar gigabytes de datos en un santiamén.
fuente
Piense en usar herramientas nosql como neo4j para estructuras jerárquicas. por ejemplo, una aplicación en red como linkedin usa couchbase (otra solución nosql)
Pero use nosql solo para consultas de nivel de data-mart y no para almacenar / mantener transacciones
fuente