Necesito calcular la profundidad de un descendiente de su antepasado. Cuando un registro tiene object_id = parent_id = ancestor_id
, se considera un nodo raíz (el antepasado). He estado tratando de WITH RECURSIVE
ejecutar una consulta con PostgreSQL 9.4 .
No controlo los datos o las columnas. El esquema de datos y tablas proviene de una fuente externa. La mesa está creciendo continuamente . En este momento por unos 30k registros por día. Puede faltar cualquier nodo en el árbol y se extraerá de una fuente externa en algún momento. Por lo general, se extraen en created_at DESC
orden, pero los datos se extraen con trabajos en segundo plano asincrónicos.
Inicialmente teníamos una solución de código para este problema, pero ahora con más de 5 millones de filas, tarda casi 30 minutos en completarse.
Ejemplo de definición de tabla y datos de prueba:
CREATE TABLE objects (
id serial NOT NULL PRIMARY KEY,
customer_id integer NOT NULL,
object_id integer NOT NULL,
parent_id integer,
ancestor_id integer,
generation integer NOT NULL DEFAULT 0
);
INSERT INTO objects(id, customer_id , object_id, parent_id, ancestor_id, generation)
VALUES (2, 1, 2, 1, 1, -1), --no parent yet
(3, 2, 3, 3, 3, -1), --root node
(4, 2, 4, 3, 3, -1), --depth 1
(5, 2, 5, 4, 3, -1), --depth 2
(6, 2, 6, 5, 3, -1), --depth 3
(7, 1, 7, 7, 7, -1), --root node
(8, 1, 8, 7, 7, -1), --depth 1
(9, 1, 9, 8, 7, -1); --depth 2
Tenga en cuenta que object_id
no es único, pero la combinación (customer_id, object_id)
es única.
Ejecutando una consulta como esta:
WITH RECURSIVE descendants(id, customer_id, object_id, parent_id, ancestor_id, depth) AS (
SELECT id, customer_id, object_id, parent_id, ancestor_id, 0
FROM objects
WHERE object_id = parent_id
UNION
SELECT o.id, o.customer_id, o.object_id, o.parent_id, o.ancestor_id, d.depth + 1
FROM objects o
INNER JOIN descendants d ON d.parent_id = o.object_id
WHERE
d.id <> o.id
AND
d.customer_id = o.customer_id
) SELECT * FROM descendants d;
Me gustaría que la generation
columna se establezca como la profundidad que se calculó. Cuando se agrega un nuevo registro, la columna de generación se establece como -1. Hay algunos casos en los que es parent_id
posible que todavía no se haya retirado. Si parent_id
no existe, debería dejar la columna de generación establecida en -1.
Los datos finales deberían verse así:
id | customer_id | object_id | parent_id | ancestor_id | generation
2 1 2 1 1 -1
3 2 3 3 3 0
4 2 4 3 3 1
5 2 5 4 3 2
6 2 6 5 3 3
7 1 7 7 7 0
8 1 8 7 7 1
9 1 9 8 7 2
El resultado de la consulta debe ser actualizar la columna de generación a la profundidad correcta.
Comencé a trabajar a partir de las respuestas a esta pregunta relacionada sobre SO .
fuente
update
la mesa con el resultado de tu CTE recursivo?ancestor_id
ya está configurado, ¿solo necesita asignar la generación desde la profundidad CTE?Respuestas:
La consulta que tienes es básicamente correcta. El único error está en la segunda parte (recursiva) del CTE donde tiene:
Debería ser de otra manera:
Desea unir los objetos con sus padres (que ya se han encontrado).
Por lo tanto, la consulta que calcula la profundidad puede escribirse (nada más cambió, solo el formato):
Para la actualización, simplemente reemplace el último
SELECT
, con elUPDATE
, uniendo el resultado del cte, de vuelta a la tabla:Probado en SQLfiddle
Comentarios adicionales:
ancestor_id
parent_id
No se necesita y el para estar en la lista de selección (el antepasado es obvio, el padre es un poco difícil de entender por qué), por lo que puede mantenerlos en laSELECT
consulta si lo desea, pero puede eliminarlos de forma seguraUPDATE
.(customer_id, object_id)
parece ser un candidato para unaUNIQUE
restricción. Si sus datos cumplen con esto, agregue dicha restricción. Las uniones realizadas en el CTE recursivo no tendrían sentido si no fuera único (de lo contrario, un nodo podría tener 2 padres).(customer_id, parent_id)
sería un candidato para unaFOREIGN KEY
restricción queREFERENCES
el (único)(customer_id, object_id)
. Lo más probable es que no embargo, lo desee agregar esa restricción FK, ya que según su descripción, está agregando nuevas filas y algunas filas pueden hacer referencia a otras que aún no se han agregado.En
AND o.generation = -1
la actualización final se asegurará de que las filas que se actualizaron en la primera ejecución no se actualizarán nuevamente, pero el CTE sigue siendo una parte costosa.El siguiente es un intento de abordar estos problemas: mejorar el CTE para considerar la menor cantidad de filas posible y usar en
(customer_id, obejct_id)
lugar de(id)
identificar filas (por lo queid
se elimina por completo de la consulta. Se puede usar como la primera actualización o una posterior:Observe cómo el CTE tiene 3 partes. Los dos primeros son las partes estables. La primera parte encuentra los nodos raíz que no se han actualizado antes y todavía
generation=-1
lo tienen, por lo que deben ser nodos recién agregados. La segunda parte encuentra elementos secundarios (congeneration=-1
) de nodos principales que se han actualizado previamente.La tercera parte, recursiva, encuentra a todos los descendientes de las dos primeras partes, como antes.
Probado en SQLfiddle-2
fuente
@ypercube ya ofrece una amplia explicación, por lo que voy a ir al grano lo que tengo que agregar.
Supongo que se supone que esto se aplica de forma recursiva, es decir, el resto del árbol siempre tiene
generation = -1
después de cualquier nodo faltante.Si falta algún nodo en el árbol (todavía), necesitamos encontrar filas con
generation = -1
eso ...... son nodos raíz
... o tener un padre con
generation > -1
.Y atraviesa el árbol desde allí. Los nodos secundarios de esta selección también deben tener
generation = -1
.Tome el
generation
del padre incrementado en uno o retroceda a 0 para los nodos raíz:La parte no recursiva es única de
SELECT
esta manera, pero lógicamente equivalente a las dos uniones de @ ypercubeSELECT
. No estoy seguro de cuál es más rápido, tendrás que probar.El punto mucho más importante para el rendimiento es:
¡Índice!
Si agrega repetidamente filas a una tabla grande de esta manera, agregue un índice parcial :
Esto logrará más rendimiento que todas las otras mejoras discutidas hasta ahora, para pequeñas adiciones repetidas a una gran mesa.
Agregué la condición de índice a la parte recursiva del CTE (aunque lógicamente redundante) para ayudar al planificador de consultas a comprender que el índice parcial es aplicable.
Además, probablemente también debería tener la
UNIQUE
restricción sobre(object_id, customer_id)
ese @ypercube ya mencionado. O, si no puede imponer unicidad por alguna razón (¿por qué?) Agregue un índice simple en su lugar. El orden de las columnas de índice es importante, por cierto:fuente
ON objects (customer_id, parent_id, object_id) WHERE generation = -1;
y quizás otroON objects (customer_id, object_id) WHERE generation > -1;
. La actualización también tendrá que "cambiar" todas las filas actualizadas de un índice a otro, por lo que no estoy seguro de si es una buena idea para la ejecución inicial de la ACTUALIZACIÓN.