2008-11-25 11 views
35

¿Cómo obtendría los datos estructurados en árbol de una base de datos con el mejor rendimiento? Por ejemplo, supongamos que tiene una jerarquía de carpetas en una base de datos. Donde la carpeta-database-row tiene ID, Nombre y ParentID columnas.SQL optimizado para estructuras de árbol

¿Utilizaría un algoritmo especial para obtener todos los datos a la vez, minimizando la cantidad de llamadas a la base de datos y procesándolo en el código?

¿O utilizarías muchas llamadas a la base de datos y obtendría la estructura desde la base de datos directamente?

¿Tal vez hay diferentes respuestas basadas en x cantidad de filas de bases de datos, profundidad de jerarquía o lo que sea?

Editar: uso Microsoft SQL Server, pero las respuestas desde otras perspectivas también son interesantes.

+0

¿Qué RDMS estás usando? ¿Servidor SQL? MySQL? ¿Oráculo? –

Respuesta

13

observe el modelo de jerarquía nested sets. es genial y útil.

+1

@Seb Nilsson: Esta notación es muy rápida con lecturas (especialmente para hacer selecciones de subárbol). Pero no tanto por inserciones. Un cambio en términos de uso de números negativos (para izquierda y derecha) equivale a la cantidad promedio de nodos que deben cambiarse para cada inserción. –

+0

¿Podría ampliar este comentario un poco más Robert? – DFTR

1

Si tiene muchos árboles en la base de datos, y solo obtendrá el árbol completo, almacenaría una ID de árbol (o ID de nodo raíz) y una ID de nodo padre para cada nodo en la base de datos, obtener todos los nodos para una identificación de árbol particular, y el proceso en la memoria.

Sin embargo, si va a obtener subárboles, solo puede obtener un subárbol de una ID de nodo padre particular, por lo que necesita almacenar todos los nodos principales de cada nodo para usar el método anterior o realizar múltiples consultas SQL como usted desciende al árbol (¡espero que no haya ciclos en su árbol!), aunque puede reutilizar el mismo Estado Preparado (suponiendo que los nodos son del mismo tipo y están todos almacenados en una sola tabla) para evitar volver a compilar el SQL , por lo que podría no ser más lento, de hecho con optimizaciones de bases de datos aplicadas a la consulta podría ser preferible. Es posible que desee ejecutar algunas pruebas para averiguarlo.

Si solo está almacenando un árbol, su pregunta se convierte en una pregunta de subárboles solamente, y la segunda respuesta se aplica.

1

yo soy un fan del simple método de almacenar un ID asociado con su parentID:

ID  ParentID 
1  null 
2  null 
3  1 
4  2 
... ... 

Es fácil de mantener, y muy escalable.

+0

-1: repite cosas en la pregunta. –

+0

No estaba allí cuando respondí originalmente. – Galwegian

+3

En realidad, no es escalable. Si trabaja frecuentemente con un árbol completo de profundidad n, necesitará n consultas para obtener todos los datos. Para árboles altos y ocupados (por ejemplo, un foro), esto puede ser un asesino de rendimiento. – staticsan

1

Google de "Camino materializada" o "árboles genéticos" ...

1

En Oracle existe SELECT ... CONNECT BY para recuperar los árboles.

15

Realmente depende de cómo va a acceder al árbol.

Una técnica inteligente es dar a cada nodo una identificación de cadena, donde la identificación del padre es una subcadena predecible del niño. Por ejemplo, el padre podría ser '01', y los hijos serían '0100', '0101', '0102', etc. De esta manera puede seleccionar un subárbol completo de la base de datos a la vez con:

SELECT * FROM treedata WHERE id LIKE '0101%'; 

Como el criterio es una subcadena inicial, un índice en la columna de ID aceleraría la consulta.

+1

Simplemente debe asegurarse de que la cantidad de dígitos por nivel (2 en este caso) * la cantidad de niveles esté permitida en esa columna CHAR. Esto impone algunas limitaciones artificiales (pero manejables). –

+0

@Ned Batchelder Probaré este método para la estructura de mi mesa. Sin embargo, ¿no es difícil mover un subárbol a otro? ¿Qué sucede si se inserta un nuevo nodo en el medio de la jerarquía? ¿Debo mantener las columnas parentId también? ¿O esta identificación siempre es suficiente para manejar la estructura? Gracias. –

2

Hay varios tipos comunes de consultas en una jerarquía. La mayoría de otros tipos de consultas son variaciones de estos.

  1. De un padre, encuentre a todos los niños.

    a. A una profundidad específica. Por ejemplo, dado mi padre inmediato, todos los niños a una profundidad de 1 serán mis hermanos.

    b. Al pie del árbol.

  2. De un niño, encuentre a todos sus padres.

    a. A una profundidad específica. Por ejemplo, mi padre inmediato es padres a una profundidad de 1.

    b. A una profundidad ilimitada

Las (a) cajas (una profundidad específica) son más fáciles en SQL. El caso especial (profundidad = 1) es trivial en SQL. La profundidad distinta de cero es más difícil. Una profundidad finita, pero distinta de cero, se puede hacer a través de un número finito de uniones. Los (b) casos, con profundidad indefinida (arriba, abajo), son realmente difíciles.

Si el árbol es enormes (millones de nodos), entonces estamos en un mundo de dolor, no importa lo que intente hacer.

Si su árbol tiene menos de un millón de nodos, simplemente tráigalo todo a la memoria y trabaje allí. La vida es mucho más simple en un mundo OO. Simplemente busque las filas y construya el árbol a medida que se devuelven las filas.

Si tiene un árbol enorme, tiene dos opciones.

  • cursores recursivos para manejar la búsqueda ilimitada. Esto significa que el mantenimiento de la estructura es O (1): simplemente actualice algunos nodos y listo. Sin embargo, la búsqueda es O (n * log (n)) porque debe abrir un cursor para cada nodo con hijos.

  • Los algoritmos inteligentes de "numeración de pila" pueden codificar el origen de cada nodo. Una vez que cada nodo está correctamente numerado, se puede usar un SQL SELECT trivial para los cuatro tipos de consultas. Los cambios en la estructura del árbol, sin embargo, requieren volver a numerar los nodos, lo que hace que el costo de un cambio sea bastante alto en comparación con el costo de recuperación.

+0

SQL CTE elimina la necesidad de cursores recursivos y tiene alguna optimización para el plegado de unión, pero aún así es una llamada costosa enumerar jerarquías grandes. – stephbu

+0

Lo mismo que Oracles CONNECT-BY. Funciona, pero es S ... L ... O ... W ... –

+0

Si tienes millones de nodos, podrías hacer árboles de árbol. Cada árbol contenido en el DB como BLOB. Lees el árbol superior (con millones de hojas) donde cada hoja tendrá una identificación para su subárbol con millones de hojas. De esta forma tendrá miles de millones de hojas y una lectura rápida si las consultas no tienen más que unos pocos subárboles. –

0

This article es interesante, ya que muestra algunos métodos de recuperación, así como una manera de almacenar el linaje como una columna derivada. El linaje proporciona un método de acceso directo para recuperar la jerarquía sin demasiadas uniones.

6

En el producto en el que trabajo tenemos algunas estructuras de árbol almacenadas en SQL Server y utilizamos la técnica mencionada anteriormente para almacenar la jerarquía de un nodo en el registro. es decir

tblTreeNode 
TreeID = 1 
TreeNodeID = 100 
ParentTreeNodeID = 99 
Hierarchy = ".33.59.99.100." 
[...] (actual data payload for node) 

El mantenimiento de la jerarquía es el truco por supuesto y hace uso de desencadenantes. Pero generarlo en una inserción/eliminación/movimiento nunca es recursivo, porque la jerarquía del padre o del niño tiene toda la información que necesita.

puede obtener todos los descendientes de los ganglios de esta manera:

SELECT * FROM tblNode WHERE Hierarchy LIKE '%.100.%' 

Aquí está el desencadenador de inserción:

--Setup the top level if there is any 
UPDATE T 
SET T.TreeNodeHierarchy = '.' + CONVERT(nvarchar(10), T.TreeNodeID) + '.' 
FROM tblTreeNode AS T 
    INNER JOIN inserted i ON T.TreeNodeID = i.TreeNodeID 
WHERE (i.ParentTreeNodeID IS NULL) AND (i.TreeNodeHierarchy IS NULL) 

WHILE EXISTS (SELECT * FROM tblTreeNode WHERE TreeNodeHierarchy IS NULL) 
    BEGIN 
     --Update those items that we have enough information to update - parent has text in Hierarchy 
     UPDATE CHILD 
     SET CHILD.TreeNodeHierarchy = PARENT.TreeNodeHierarchy + CONVERT(nvarchar(10),CHILD.TreeNodeID) + '.' 
     FROM tblTreeNode AS CHILD 
      INNER JOIN tblTreeNode AS PARENT ON CHILD.ParentTreeNodeID = PARENT.TreeNodeID 
     WHERE (CHILD.TreeNodeHierarchy IS NULL) AND (PARENT.TreeNodeHierarchy IS NOT NULL) 
    END 

Y aquí está el desencadenador de actualización:

--Only want to do something if Parent IDs were changed 
IF UPDATE(ParentTreeNodeID) 
    BEGIN 
     --Update the changed items to reflect their new parents 
     UPDATE CHILD 
     SET CHILD.TreeNodeHierarchy = CASE WHEN PARENT.TreeNodeID IS NULL THEN '.' + CONVERT(nvarchar,CHILD.TreeNodeID) + '.' ELSE PARENT.TreeNodeHierarchy + CONVERT(nvarchar, CHILD.TreeNodeID) + '.' END 
     FROM tblTreeNode AS CHILD 
      INNER JOIN inserted AS I ON CHILD.TreeNodeID = I.TreeNodeID 
      LEFT JOIN tblTreeNode AS PARENT ON CHILD.ParentTreeNodeID = PARENT.TreeNodeID 

     --Now update any sub items of the changed rows if any exist 
     IF EXISTS (
       SELECT * 
       FROM tblTreeNode 
        INNER JOIN deleted ON tblTreeNode.ParentTreeNodeID = deleted.TreeNodeID 
      ) 
      UPDATE CHILD 
      SET CHILD.TreeNodeHierarchy = NEWPARENT.TreeNodeHierarchy + RIGHT(CHILD.TreeNodeHierarchy, LEN(CHILD.TreeNodeHierarchy) - LEN(OLDPARENT.TreeNodeHierarchy)) 
      FROM tblTreeNode AS CHILD 
       INNER JOIN deleted AS OLDPARENT ON CHILD.TreeNodeHierarchy LIKE (OLDPARENT.TreeNodeHierarchy + '%') 
       INNER JOIN tblTreeNode AS NEWPARENT ON OLDPARENT.TreeNodeID = NEWPARENT.TreeNodeID 

    END 

un bit más, una Comprobar restricción para evitar una referencia circular en nodos de árbol:

ALTER TABLE [dbo].[tblTreeNode] WITH NOCHECK ADD CONSTRAINT [CK_tblTreeNode_TreeNodeHierarchy] CHECK 
((charindex(('.' + convert(nvarchar(10),[TreeNodeID]) + '.'),[TreeNodeHierarchy],(charindex(('.' + convert(nvarchar(10),[TreeNodeID]) + '.'),[TreeNodeHierarchy]) + 1)) = 0)) 

También recomendaría disparadores para evitar que más de un nodo raíz (matriz nula) por árbol, y para mantener los nodos relacionados con la pertenencia a diferentes TreeIDs (pero los que son un poco más trivial de los anteriores.)

Querrá verificar su caso particular para ver si esta solución funciona de manera aceptable. ¡Espero que esto ayude!

4
14

De todas las formas de almacenar un árbol en un RDMS los más comunes son listas de adyacencia y conjuntos anidados. Los conjuntos anidados están optimizados para lecturas y pueden recuperar un árbol completo en una sola consulta. Las listas de adyacencia están optimizadas para escrituras y se pueden agregar en una consulta simple.

Con listas de adyacencia cada nodo una tiene columna que se refiere al nodo padre o nodo hijo (otros enlaces son posibles). Al usar eso, puede construir la jerarquía en base a las relaciones primarias de los padres. Desafortunadamente, a menos que restrinja la profundidad de su árbol, no puede extraer todo en una consulta y, por lo general, leer es más lento que actualizarlo.

Con el modelo de conjunto anidado el inverso es cierto, la lectura es rápida y fácil, pero las actualizaciones Obtener compleja, ya que debe mantener el sistema de numeración. El modelo de conjunto anidado codifica la paternidad y el orden de clasificación al enumerar todos los nodos utilizando un sistema de numeración basado en preorden.

He utilizado el modelo de conjunto anidado y si bien es compleja para lectura optimizar una gran jerarquía que merece la pena.Una vez que hagas algunos ejercicios para dibujar el árbol y numerar los nodos deberías entenderlo.

Mi investigación sobre este método comenzó en este artículo: Managing Hierarchical Data in MySQL.

0

no va a funcionar para todas las situaciones, pero, por ejemplo, dada una estructura comentario:

ID | ParentCommentID 

También podría almacenar TopCommentID que representa la parte superior más comentario:

ID | ParentCommentID | TopCommentID 

Cuando el TopCommentID y ParentCommentID son null o 0 cuando es el comentario más avanzado. Para comentarios secundarios, ParentCommentID apunta al comentario anterior y TopCommentID apunta al padre superior.

Cuestiones relacionadas