¿Cuáles son las opciones para almacenar datos jerárquicos en una base de datos relacional?

Buenas descripciones

En términos generales, está tomando una decisión entre tiempos de lectura rápidos (por ejemplo, conjunto anidado) o tiempos de escritura rápidos (lista de adyacencia). Por lo general, terminará con una combinación de las siguientes opciones que mejor se adapten a sus necesidades. A continuación, se proporciona una lectura en profundidad:

Opciones

Los que conozco y características generales:

  1. Lista de adyacencia:
    • Columnas: ID, ParentID
    • Fácil de implementar.
    • Movimientos, inserciones y eliminaciones de nodos económicos.
    • Caro para encontrar el nivel, ascendencia y descendientes, camino
    • Evite N + 1 a través de Expresiones de tabla comunes en bases de datos que los apoyan
  2. Conjunto anidado (aka Desplazamiento del árbol de pedidos anticipados modificado)
    • Columnas: izquierda, derecha
    • Ascendencia barata, descendientes
    • Muy caro O(n/2) se mueve, inserta, elimina debido a la codificación volátil
  3. Mesa puente (Aka Tabla de cierre / w disparadores)
    • Utiliza una tabla de combinación separada con: ancestro, descendiente, profundidad (opcional)
    • Ascendencia y descendientes baratos
    • Escribe los costos O(log n) (tamaño del subárbol) para insertar, actualizar, eliminar
    • Codificación normalizada: buena para estadísticas RDBMS y planificador de consultas en combinaciones
    • Requiere varias filas por nodo
  4. Columna de linaje (Aka Camino materializado, Enumeración de ruta)
    • Columna: linaje (por ejemplo, padre / hijo / nieto / etc ...)
    • Descendientes baratos mediante consulta de prefijo (p. Ej. LEFT(lineage, #) = '/enumerated/path')
    • Escribe los costos O(log n) (tamaño del subárbol) para insertar, actualizar, eliminar
    • No relacional: se basa en el tipo de datos Array o el formato de cadena serializada
  5. Intervalos anidados
    • Como un conjunto anidado, pero con real / float / decimal para que la codificación no sea volátil (mover / insertar / eliminar de bajo costo)
    • Tiene problemas de precisión / representación real / flotante / decimal
    • Variante de codificación matricial agrega codificación de antepasados ​​(ruta materializada) de forma "gratuita", pero con la complejidad adicional del álgebra lineal.
  6. Mesa plana
    • Una lista de adyacencia modificada que agrega una columna de nivel y rango (por ejemplo, orden) a cada registro.
    • Barato para iterar / paginar
    • Mover y eliminar caro
    • Buen uso: discusión encadenada - foros / comentarios de blog
  7. Varias columnas de linaje
    • Columnas: una para cada nivel de linaje, se refiere a todos los padres hasta la raíz, los niveles inferiores al nivel del elemento se establecen en NULL
    • Antepasados ​​baratos, descendientes, nivel
    • Insertar, borrar, mover las hojas barato
    • Costoso insertar, eliminar, mover los nodos internos.
    • Límite estricto de la profundidad de la jerarquía

Notas específicas de la base de datos

MySQL

Oracle

  • Utilización de CONECTAR POR para atravesar listas de adyacencia

PostgreSQL

SQL Server

  • Resumen general
  • ofertas 2008 JerarquíaId El tipo de datos parece ayudar con el enfoque de Lineage Column y expandir la profundidad que se puede representar.

preguntado Oct 29 '10, 00:10

Según slideshare.net/billkarwin/sql-antipatterns-strike-back La página de 77, Closure Tables son superiores a Adjacency List, Path Enumeration y Nested Sets en términos de facilidad de uso (y supongo que rendimiento también). -

Extraño una versión muy simple aquí: un simple BLOB. Si su jerarquía solo tiene algunas docenas de elementos, un árbol serializado de identificadores podría ser la mejor opción. -

@Lothar: la pregunta es un wiki de la comunidad, así que no dudes en hacerlo. Mi pensamiento al respecto es que solo lo haría con aquellas bases de datos que admitan algún tipo de estructuración de blobs como XML con un lenguaje de consulta estable como XPATH. De lo contrario, no veo una buena forma de realizar consultas aparte de recuperar, deserializar y mezclar en el código, no en SQL. Y si realmente tiene un problema en el que necesita muchos elementos arbitrarios, es mejor que use la base de datos de Node como Neo4J, que he usado y me gustó, aunque nunca se llevó a la producción. -

Ese vínculo de MSDN para "Resumen general" ya no muestra el artículo. Estaba en la edición de septiembre de 2008 de MSDN Magazine, que puede descargar como archivo CHM o ver a través del archivo web en: web.archive.org/web/20080913041559/http://msdn.microsoft.com:80/… -

7 Respuestas

Mi respuesta favorita es la que sugirió la primera oración de este hilo. Utilice una lista de adyacencia para mantener la jerarquía y utilice conjuntos anidados para consultar la jerarquía.

El problema hasta ahora ha sido que el método de conversión de una lista de adyacencia a conjuntos anidados ha sido terriblemente lento porque la mayoría de la gente usa el método RBAR extremo conocido como "Push Stack" para realizar la conversión y se ha considerado demasiado caro. para alcanzar el Nirvana de la simplicidad de mantenimiento mediante la Lista de adyacencia y el asombroso rendimiento de los Conjuntos anidados. Como resultado, la mayoría de las personas terminan teniendo que conformarse con uno u otro, especialmente si hay más de, digamos, unos pésimos 100,000 nodos. El uso del método push stack puede llevar un día entero para realizar la conversión en lo que los profesionales de MLM considerarían una jerarquía de un pequeño millón de nodos.

Pensé en darle a Celko un poco de competencia ideando un método para convertir una Lista de adyacencia en conjuntos anidados a velocidades que parecen imposibles. Aquí está el rendimiento del método push stack en mi computadora portátil i5.

Duration for     1,000 Nodes = 00:00:00:870 
Duration for    10,000 Nodes = 00:01:01:783 (70 times slower instead of just 10)
Duration for   100,000 Nodes = 00:49:59:730 (3,446 times slower instead of just 100) 
Duration for 1,000,000 Nodes = 'Didn't even try this'

Y aquí está la duración del nuevo método (con el método push stack entre paréntesis).

Duration for     1,000 Nodes = 00:00:00:053 (compared to 00:00:00:870)
Duration for    10,000 Nodes = 00:00:00:323 (compared to 00:01:01:783)
Duration for   100,000 Nodes = 00:00:03:867 (compared to 00:49:59:730)
Duration for 1,000,000 Nodes = 00:00:54:283 (compared to something like 2 days!!!)

Si eso es correcto. 1 millón de nodos convertidos en menos de un minuto y 100,000 nodos en menos de 4 segundos.

Puede leer sobre el nuevo método y obtener una copia del código en la siguiente URL. http://www.sqlservercentral.com/articles/Hierarchy/94040/

También desarrollé una jerarquía "preagregada" utilizando métodos similares. Los usuarios de MLM y las personas que elaboran listas de materiales estarán particularmente interesados ​​en este artículo. http://www.sqlservercentral.com/articles/T-SQL/94570/

Si pasa por aquí para ver cualquiera de los artículos, acceda al enlace "Únase a la discusión" y déjeme saber lo que piensa.

respondido 04 mar '13, 06:03

¿Qué es un MLMer? - David Mann

MLM = "Marketing multinivel". Amway, Shaklee, ACN, etc., etc. - Jeff Moden

Esta es una respuesta muy parcial a su pregunta, pero espero que siga siendo útil.

Microsoft SQL Server 2008 implementa dos características que son extremadamente útiles para administrar datos jerárquicos:

  • que el JerarquíaId tipo de datos
  • expresiones de tabla comunes, usando el con palabra clave.

Echa un vistazo a "Modele sus jerarquías de datos con SQL Server 2008" por Kent Tegels en MSDN para empezar. Vea también mi propia pregunta: Consulta recursiva de la misma tabla en SQL Server 2008

Respondido 26 Jul 17, 17:07

Interesante, el HierarchyId, no sabía nada de eso: msdn.microsoft.com/en-us/library/bb677290.aspx - semillas de naranja

En efecto. Trabajo con una gran cantidad de datos jerárquicos recursivos y encuentro que las expresiones de tabla comunes son extremadamente útiles. Ver msdn.microsoft.com/en-us/library/ms186243.aspx para una introducción. - CesarGon

Este diseño aún no se mencionó:

Varias columnas de linaje

Aunque tiene limitaciones, si puedes soportarlas, es muy simple y muy eficiente. Características:

  • Columnas: una para cada nivel de linaje, se refiere a todos los padres hasta la raíz, los niveles por debajo del nivel de los elementos actuales se establecen en 0 (o NULL)
  • Hay un límite fijo para la profundidad de la jerarquía.
  • Antepasados ​​baratos, descendientes, nivel
  • Insertar, borrar, mover las hojas barato
  • Costoso insertar, eliminar, mover los nodos internos.

A continuación se muestra un ejemplo: árbol taxonómico de aves, por lo que la jerarquía es Clase / Orden / Familia / Género / Especie: la especie es el nivel más bajo, 1 fila = 1 taxón (que corresponde a la especie en el caso de los nodos de las hojas):

CREATE TABLE `taxons` (
  `TaxonId` smallint(6) NOT NULL default '0',
  `ClassId` smallint(6) default NULL,
  `OrderId` smallint(6) default NULL,
  `FamilyId` smallint(6) default NULL,
  `GenusId` smallint(6) default NULL,
  `Name` varchar(150) NOT NULL default ''
);

y el ejemplo de los datos:

+---------+---------+---------+----------+---------+-------------------------------+
| TaxonId | ClassId | OrderId | FamilyId | GenusId | Name                          |
+---------+---------+---------+----------+---------+-------------------------------+
|     254 |       0 |       0 |        0 |       0 | Aves                          |
|     255 |     254 |       0 |        0 |       0 | Gaviiformes                   |
|     256 |     254 |     255 |        0 |       0 | Gaviidae                      |
|     257 |     254 |     255 |      256 |       0 | Gavia                         |
|     258 |     254 |     255 |      256 |     257 | Gavia stellata                |
|     259 |     254 |     255 |      256 |     257 | Gavia arctica                 |
|     260 |     254 |     255 |      256 |     257 | Gavia immer                   |
|     261 |     254 |     255 |      256 |     257 | Gavia adamsii                 |
|     262 |     254 |       0 |        0 |       0 | Podicipediformes              |
|     263 |     254 |     262 |        0 |       0 | Podicipedidae                 |
|     264 |     254 |     262 |      263 |       0 | Tachybaptus                   |

Esto es genial porque de esta manera logra todas las operaciones necesarias de una manera muy fácil, siempre que las categorías internas no cambien su nivel en el árbol.

Respondido el 20 de junio de 20 a las 12:06

Modelo de adyacencia + modelo de conjuntos anidados

Lo busqué porque podía insertar nuevos elementos en el árbol fácilmente (solo necesita la identificación de una rama para insertar un nuevo elemento) y también consultarlo bastante rápido.

+-------------+----------------------+--------+-----+-----+
| category_id | name                 | parent | lft | rgt |
+-------------+----------------------+--------+-----+-----+
|           1 | ELECTRONICS          |   NULL |   1 |  20 |
|           2 | TELEVISIONS          |      1 |   2 |   9 |
|           3 | TUBE                 |      2 |   3 |   4 |
|           4 | LCD                  |      2 |   5 |   6 |
|           5 | PLASMA               |      2 |   7 |   8 |
|           6 | PORTABLE ELECTRONICS |      1 |  10 |  19 |
|           7 | MP3 PLAYERS          |      6 |  11 |  14 |
|           8 | FLASH                |      7 |  12 |  13 |
|           9 | CD PLAYERS           |      6 |  15 |  16 |
|          10 | 2 WAY RADIOS         |      6 |  17 |  18 |
+-------------+----------------------+--------+-----+-----+
  • Cada vez que necesita a todos los hijos de cualquier padre, solo tiene que consultar el parent columna.
  • Si necesita todos los descendientes de cualquier padre, consulte los elementos que tienen su lft entre lft y rgt de los padres.
  • Si necesita todos los padres de cualquier nodo hasta la raíz del árbol, consulte los elementos que tienen lft más bajo que el del nodo lft y rgt más grande que el del nodo rgt y ordenar por parent.

Necesitaba hacer que acceder al árbol y consultarlo fuera más rápido que las inserciones, por eso elegí esto

El único problema es arreglar el left y right columnas al insertar nuevos elementos. Bueno, creé un procedimiento almacenado para él y lo llamé cada vez que insertaba un nuevo elemento, lo cual era raro en mi caso, pero es realmente rápido. Obtuve la idea del libro de Joe Celko, y el procedimiento almacenado y cómo se me ocurrió se explica aquí en DBA SE https://dba.stackexchange.com/q/89051/41481

Respondido 13 Abr '17, 15:04

+1 este es un enfoque legítimo. Desde mi propia experiencia, la clave es decidir si está de acuerdo con las lecturas sucias cuando se producen grandes operaciones de actualización. De lo contrario, se convierte en un problema o en evitar que las personas consulten las tablas directamente y siempre pasen por una API: sprocs / funciones o código de DB. - semillas de naranja

Ésta es una solución interesante; sin embargo, no estoy seguro de que consultar la columna principal realmente ofrezca una ventaja importante al intentar encontrar elementos secundarios; es por eso que tenemos columnas izquierda y derecha, en primer lugar. - Thomas

@Thomas, hay una diferencia entre children y descendants. left y right se utilizan para encontrar los descendientes. - azerafati

Si su base de datos admite matrices, también puede implementar una columna de linaje o una ruta materializada como una matriz de identificadores de padres.

Específicamente con Postgres, puede usar los operadores establecidos para consultar la jerarquía y obtener un rendimiento excelente con los índices GIN. Esto hace que encontrar padres, hijos y profundidad sea bastante trivial en una sola consulta. Las actualizaciones también son bastante manejables.

Tengo un escrito completo sobre el uso matrices para caminos materializados si tienes curiosidad.

contestado el 15 de mayo de 13 a las 08:05

Esta es realmente una pregunta de clavija cuadrada, agujero redondo.

Si las bases de datos relacionales y SQL son el único martillo que tiene o está dispuesto a usar, entonces las respuestas que se han publicado hasta ahora son adecuadas. Sin embargo, ¿por qué no utilizar una herramienta diseñada para manejar datos jerárquicos? Base de datos de grafos son ideales para datos jerárquicos complejos.

Las ineficiencias del modelo relacional junto con las complejidades de cualquier solución de código / consulta para mapear un modelo gráfico / jerárquico en un modelo relacional simplemente no vale la pena el esfuerzo en comparación con la facilidad con la que una solución de base de datos de gráficos puede resolver el mismo problema.

Considere una lista de materiales como una estructura de datos jerárquica común.

class Component extends Vertex {
    long assetId;
    long partNumber;
    long material;
    long amount;
};

class PartOf extends Edge {
};

class AdjacentTo extends Edge {
};

Camino más corto entre dos subconjuntos: Algoritmo de recorrido de gráfico simple. Las rutas aceptables se pueden calificar según los criterios.

Similitud: ¿Cuál es el grado de similitud entre dos ensamblajes? Realice un recorrido en ambos subárboles calculando la intersección y unión de los dos subárboles. El porcentaje similar es la intersección dividida por la unión.

Clausura transitiva: Camine por el subárbol y resuma los campos de interés, por ejemplo, "¿Cuánto aluminio hay en un subconjunto?"

Sí, puede resolver el problema con SQL y una base de datos relacional. Sin embargo, existen enfoques mucho mejores si está dispuesto a utilizar la herramienta adecuada para el trabajo.

Respondido el 30 de Septiembre de 14 a las 17:09

Esta respuesta sería inmensamente más útil si los casos de uso demostraran, o mejor aún contrastaran, cómo consultar una base de datos gráfica con SPARQL, por ejemplo, en lugar de SQL en un RDBMS. - semillas de naranja

SPARQL es relevante para las bases de datos RDF que son una subclase del dominio más grande de bases de datos de grafos. Trabajo con InfiniteGraph, que no es una base de datos RDF y actualmente no es compatible con SPARQL. InfiniteGraph admite varios mecanismos de consulta diferentes: (1) una API de navegación de gráficos para configurar vistas, filtros, calificadores de ruta y controladores de resultados, (2) un lenguaje de coincidencia de patrones de ruta de gráfico complejo y (3) Gremlin. - djhallx

Estoy usando PostgreSQL con tablas de cierre para mis jerarquías. Tengo un procedimiento almacenado universal para toda la base de datos:

CREATE FUNCTION nomen_tree() RETURNS trigger
    LANGUAGE plpgsql
    AS $_$
DECLARE
  old_parent INTEGER;
  new_parent INTEGER;
  id_nom INTEGER;
  txt_name TEXT;
BEGIN
-- TG_ARGV[0] = name of table with entities with PARENT-CHILD relationships (TBL_ORIG)
-- TG_ARGV[1] = name of helper table with ANCESTOR, CHILD, DEPTH information (TBL_TREE)
-- TG_ARGV[2] = name of the field in TBL_ORIG which is used for the PARENT-CHILD relationship (FLD_PARENT)
    IF TG_OP = 'INSERT' THEN
    EXECUTE 'INSERT INTO ' || TG_ARGV[1] || ' (child_id,ancestor_id,depth) 
        SELECT $1.id,$1.id,0 UNION ALL
      SELECT $1.id,ancestor_id,depth+1 FROM ' || TG_ARGV[1] || ' WHERE child_id=$1.' || TG_ARGV[2] USING NEW;
    ELSE                                                           
    -- EXECUTE does not support conditional statements inside
    EXECUTE 'SELECT $1.' || TG_ARGV[2] || ',$2.' || TG_ARGV[2] INTO old_parent,new_parent USING OLD,NEW;
    IF COALESCE(old_parent,0) <> COALESCE(new_parent,0) THEN
      EXECUTE '
      -- prevent cycles in the tree
      UPDATE ' || TG_ARGV[0] || ' SET ' || TG_ARGV[2] || ' = $1.' || TG_ARGV[2]
        || ' WHERE id=$2.' || TG_ARGV[2] || ' AND EXISTS(SELECT 1 FROM '
        || TG_ARGV[1] || ' WHERE child_id=$2.' || TG_ARGV[2] || ' AND ancestor_id=$2.id);
      -- first remove edges between all old parents of node and its descendants
      DELETE FROM ' || TG_ARGV[1] || ' WHERE child_id IN
        (SELECT child_id FROM ' || TG_ARGV[1] || ' WHERE ancestor_id = $1.id)
        AND ancestor_id IN
        (SELECT ancestor_id FROM ' || TG_ARGV[1] || ' WHERE child_id = $1.id AND ancestor_id <> $1.id);
      -- then add edges for all new parents ...
      INSERT INTO ' || TG_ARGV[1] || ' (child_id,ancestor_id,depth) 
        SELECT child_id,ancestor_id,d_c+d_a FROM
        (SELECT child_id,depth AS d_c FROM ' || TG_ARGV[1] || ' WHERE ancestor_id=$2.id) AS child
        CROSS JOIN
        (SELECT ancestor_id,depth+1 AS d_a FROM ' || TG_ARGV[1] || ' WHERE child_id=$2.' 
        || TG_ARGV[2] || ') AS parent;' USING OLD, NEW;
    END IF;
  END IF;
  RETURN NULL;
END;
$_$;

Luego, para cada tabla en la que tengo una jerarquía, creo un disparador

CREATE TRIGGER nomenclature_tree_tr AFTER INSERT OR UPDATE ON nomenclature FOR EACH ROW EXECUTE PROCEDURE nomen_tree('my_db.nomenclature', 'my_db.nom_helper', 'parent_id');

Para completar una tabla de cierre de la jerarquía existente, uso este procedimiento almacenado:

CREATE FUNCTION rebuild_tree(tbl_base text, tbl_closure text, fld_parent text) RETURNS void
    LANGUAGE plpgsql
    AS $$
BEGIN
    EXECUTE 'TRUNCATE ' || tbl_closure || ';
    INSERT INTO ' || tbl_closure || ' (child_id,ancestor_id,depth) 
        WITH RECURSIVE tree AS
      (
        SELECT id AS child_id,id AS ancestor_id,0 AS depth FROM ' || tbl_base || '
        UNION ALL 
        SELECT t.id,ancestor_id,depth+1 FROM ' || tbl_base || ' AS t
        JOIN tree ON child_id = ' || fld_parent || '
      )
      SELECT * FROM tree;';
END;
$$;

Las tablas de cierre se definen con 3 columnas: ANCESTOR_ID, DESCENDANT_ID, DEPTH. Es posible (e incluso aconsejo) almacenar registros con el mismo valor para ANCESTOR y DESCENDANTE, y un valor de cero para PROFUNDIDAD. Esto simplificará las consultas para la recuperación de la jerarquía. Y son realmente muy simples:

-- get all descendants
SELECT tbl_orig.*,depth FROM tbl_closure LEFT JOIN tbl_orig ON descendant_id = tbl_orig.id WHERE ancestor_id = XXX AND depth <> 0;
-- get only direct descendants
SELECT tbl_orig.* FROM tbl_closure LEFT JOIN tbl_orig ON descendant_id = tbl_orig.id WHERE ancestor_id = XXX AND depth = 1;
-- get all ancestors
SELECT tbl_orig.* FROM tbl_closure LEFT JOIN tbl_orig ON ancestor_id = tbl_orig.id WHERE descendant_id = XXX AND depth <> 0;
-- find the deepest level of children
SELECT MAX(depth) FROM tbl_closure WHERE ancestor_id = XXX;

Respondido 01 ago 16, 17:08

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.