¿Cómo hacer la consulta SELECT recursiva en MySQL?

79

Tengo una siguiente tabla:

col1 | col2 | col3
-----+------+-------
1    | a    | 5
5    | d    | 3
3    | k    | 7
6    | o    | 2
2    | 0    | 8

Si un usuario busca "1", el programa verá el col1que tiene "1", luego obtendrá un valor en col3"5", luego el programa continuará buscando "5" col1y obtendrá "3" en col3, y así sucesivamente. Entonces se imprimirá:

1   | a   | 5
5   | d   | 3
3   | k   | 7

Si un usuario busca "6", se imprimirá:

6   | o   | 2
2   | 0   | 8

¿Cómo crear una SELECTconsulta para hacer eso?

Tum
fuente
Hay una solución para su problema en esta publicación stackoverflow.com/questions/14658378/recursive-mysql-select
medina

Respuestas:

70

Editar

La solución mencionada por @leftclickben también es efectiva. También podemos usar un procedimiento almacenado para el mismo.

CREATE PROCEDURE get_tree(IN id int)
 BEGIN
 DECLARE child_id int;
 DECLARE prev_id int;
 SET prev_id = id;
 SET child_id=0;
 SELECT col3 into child_id 
 FROM table1 WHERE col1=id ;
 create TEMPORARY  table IF NOT EXISTS temp_table as (select * from table1 where 1=0);
 truncate table temp_table;
 WHILE child_id <> 0 DO
   insert into temp_table select * from table1 WHERE col1=prev_id;
   SET prev_id = child_id;
   SET child_id=0;
   SELECT col3 into child_id
   FROM TABLE1 WHERE col1=prev_id;
 END WHILE;
 select * from temp_table;
 END //

Estamos usando una tabla temporal para almacenar los resultados de la salida y, como las tablas temporales se basan en sesiones, no habrá ningún problema con los datos de salida que sean incorrectos.

SQL FIDDLE Demo

Prueba esta consulta:

SELECT 
    col1, col2, @pv := col3 as 'col3' 
FROM 
    table1
JOIN 
    (SELECT @pv := 1) tmp
WHERE 
    col1 = @pv

SQL FIDDLE Demo:

| COL1 | COL2 | COL3 |
+------+------+------+
|    1 |    a |    5 |
|    5 |    d |    3 |
|    3 |    k |    7 |

El
parent_id valor de la nota debe ser menor que child_idpara que esta solución funcione.

Meherzad
fuente
2
People Pls marca esta respuesta como una solución óptima, ya que algunas otras soluciones de preguntas similares (sobre Recursive Select en mysql) son bastante complicadas, ya que requieren crear una tabla e insertar datos en ella. Esta solución es muy elegante.
Tum
5
Solo tenga cuidado con su solución, no hay dependencia de tipo de ciclo, luego irá al bucle infinito y una cosa más, solo encontrará 1 registro de ese col3tipo, por lo que si hay varios registros, entonces no funcionará.
Meherzad
2
@HamidSarfraz ahora funciona sqlfiddle.com/#!2/74f457/14 . Esto funcionará para ti. Como ocurre con la búsqueda secuencial, la identificación siempre tendrá mayor valor que el padre, ya que el padre debe crearse primero. Por favor, infórmenos si necesita más detalles.
Meherzad
5
Esta no es una solución. Es solo un efecto secundario afortunado de un escaneo de mesa. Lea la respuesta de @leftclickben detenidamente o perderá mucho tiempo como yo.
jaeheung
2
Tum sé cómo funciona SQL recursivo. MySQL no ha implementado CTE recursivas, por lo que una opción viable es la del enlace que proporcionó (utilizando procedimientos / funciones almacenados). Otro es el uso de variables de mysql. Sin embargo, la respuesta aquí no es elegante, sino todo lo contrario, simplemente horrible. No muestra SQL recursivo. Si funcionó en su caso, fue solo por accidente, como @jaehung señaló correctamente. Y no me importan las respuestas horribles. Yo solo los rechazo. Pero una respuesta horrible en +50, me importa.
ypercubeᵀᴹ
51

La respuesta aceptada por @Meherzad solo funciona si los datos están en un orden particular. Sucede que funciona con los datos de la pregunta OP. En mi caso, tuve que modificarlo para que funcionara con mis datos.

Nota: Esto solo funciona cuando el "id" de cada registro (col1 en la pregunta) tiene un valor MAYOR que el "id principal" de ese registro (col3 en la pregunta). Este suele ser el caso, porque normalmente será necesario crear primero el padre. Sin embargo, si su aplicación permite cambios en la jerarquía, donde un elemento puede volver a ser parental en otro lugar, entonces no puede confiar en esto.

Esta es mi consulta en caso de que ayude a alguien; tenga en cuenta que no funciona con la pregunta dada porque los datos no siguen la estructura requerida descrita anteriormente.

select t.col1, t.col2, @pv := t.col3 col3
from (select * from table1 order by col1 desc) t
join (select @pv := 1) tmp
where t.col1 = @pv

La diferencia es que table1está ordenado por col1para que el padre esté detrás de él (ya que el col1valor del padre es menor que el del hijo).

leftclickben
fuente
u correcto, también si un niño tiene 2 padres, entonces no puede elegir a ambos
Tum
Gracias hombre. ¡Teamworek hizo su hazaña en esta publicación! Lo hice funcionar cuando cambié el valor de @pv. Eso es exactamente lo que estaba buscando.
Mohamed Ennahdi El Idrissi
¿Qué sucede si quiero usar esto como una columna group_concat de ID de padres para cada fila en una selección más grande (lo que significa que el valor de la variable @pv sea dinámico para cada fila)? La combinación en la subconsulta no conoce la columna maestra (a la que trato de conectarme), usando otra variable tampoco funciona (siempre devuelve NULL)
qdev
He creado una función personalizada que genera la ruta del árbol usando group_concat, y ahora puedo enviar como parámetro el valor de la columna para cada fila;)
qdev
¿Qué opinas de la nueva respuesta que publiqué? No es que el tuyo no sea bueno, pero quería tener un SELECT solo que pudiera admitir la identificación del padre> la identificación del niño.
Master DJ el
18

leftclickben answer funcionó para mí, pero quería una ruta desde un nodo dado hasta la raíz del árbol, y estos parecían ir en sentido contrario, hacia abajo del árbol. Entonces, tuve que cambiar algunos de los campos y renombrarlos para mayor claridad, y esto funciona para mí, en caso de que esto sea lo que alguien más quiera también:

item | parent
-------------
1    | null
2    | 1
3    | 1
4    | 2
5    | 4
6    | 3

y

select t.item_id as item, @pv:=t.parent as parent
from (select * from item_tree order by item_id desc) t
join
(select @pv:=6)tmp
where t.item_id=@pv;

da:

item | parent
-------------
6    | 3
3    | 1
1    | null
BoB3K
fuente
@ BoB3K, ¿funcionaría esto si los ID no están necesariamente en "orden"? ¿Parece no funcionar en caso de que la identificación de un padre a lo largo de la cadena sea mayor que la de su hijo? Por ejemplo, la cadena 1> 120 > 112 solo devolverá ((112, 120)) mientras que 2> 22> 221 devuelve la cadena completa ((221,22), (22,2), (2, null))
Tomer Cagan
Ha pasado un tiempo, pero creo que recuerdo haber leído en las respuestas originales que esto no funciona si los identificadores de los elementos no están en orden, lo que generalmente no es un problema si el identificador es una clave de incremento automático.
BoB3K
Funciona bien y lo uso para mi sitio ... el problema aquí es que no es posible ordenar los resultados ASC. 1 3 6Yo uso array_reverse()en php en su lugar ... ¿alguna solución sql para eso?
joe
8

El procedimiento almacenado es la mejor manera de hacerlo. Porque la solución de Meherzad funcionaría solo si los datos siguen el mismo orden.

Si tenemos una estructura de tabla como esta

col1 | col2 | col3
-----+------+------
 3   | k    | 7
 5   | d    | 3
 1   | a    | 5
 6   | o    | 2
 2   | 0    | 8

No funcionará. SQL Fiddle Demo

Aquí hay un código de procedimiento de muestra para lograr lo mismo.

delimiter //
CREATE PROCEDURE chainReaction 
(
    in inputNo int
) 
BEGIN 
    declare final_id int default NULL;
    SELECT col3 
    INTO final_id 
    FROM table1
    WHERE col1 = inputNo;
    IF( final_id is not null) THEN
        INSERT INTO results(SELECT col1, col2, col3 FROM table1 WHERE col1 = inputNo);
        CALL chainReaction(final_id);   
    end if;
END//
delimiter ;

call chainReaction(1);
SELECT * FROM results;
DROP TABLE if exists results;
Jazmin
fuente
Esta es una solución sólida y la estoy usando sin problemas. ¿Puede ayudarme cuando vaya en la otra dirección, es decir, hacia abajo del árbol? Encuentro todas las filas donde la identificación principal == inputNo, pero muchas identificaciones pueden tener una identificación principal.
mils
8

Si desea poder tener un SELECT sin problemas de que la identificación principal tenga que ser menor que la identificación secundaria, se podría usar una función. También admite varios niños (como debería hacer un árbol) y el árbol puede tener varias cabezas. También asegura que se rompa si existe un bucle en los datos.

Quería usar SQL dinámico para poder pasar los nombres de tablas / columnas, pero las funciones en MySQL no admiten esto.

DELIMITER $$

CREATE FUNCTION `isSubElement`(pParentId INT, pId INT) RETURNS int(11)
DETERMINISTIC    
READS SQL DATA
BEGIN
DECLARE isChild,curId,curParent,lastParent int;
SET isChild = 0;
SET curId = pId;
SET curParent = -1;
SET lastParent = -2;

WHILE lastParent <> curParent AND curParent <> 0 AND curId <> -1 AND curParent <> pId AND isChild = 0 DO
    SET lastParent = curParent;
    SELECT ParentId from `test` where id=curId limit 1 into curParent;

    IF curParent = pParentId THEN
        SET isChild = 1;
    END IF;
    SET curId = curParent;
END WHILE;

RETURN isChild;
END$$

Aquí, la tabla testdebe modificarse al nombre real de la tabla y es posible que las columnas (ParentId, Id) tengan que ajustarse a sus nombres reales.

Uso:

SET @wantedSubTreeId = 3;
SELECT * FROM test WHERE isSubElement(@wantedSubTreeId,id) = 1 OR ID = @wantedSubTreeId;

Resultado:

3   7   k
5   3   d
9   3   f
1   5   a

SQL para la creación de pruebas:

CREATE TABLE IF NOT EXISTS `test` (
  `Id` int(11) NOT NULL,
  `ParentId` int(11) DEFAULT NULL,
  `Name` varchar(300) NOT NULL,
  PRIMARY KEY (`Id`)
) ENGINE=InnoDB  DEFAULT CHARSET=latin1;

insert into test (id, parentid, name) values(3,7,'k');
insert into test (id, parentid, name) values(5,3,'d');
insert into test (id, parentid, name) values(9,3,'f');
insert into test (id, parentid, name) values(1,5,'a');
insert into test (id, parentid, name) values(6,2,'o');
insert into test (id, parentid, name) values(2,8,'c');

EDITAR: Aquí hay un violín para probarlo usted mismo. Me obligó a cambiar el delimitador usando el predefinido, pero funciona.

Maestro DJon
fuente
0

Partiendo de Master DJon

Aquí hay una función simplificada que proporciona la utilidad adicional de devolver la profundidad (en caso de que desee utilizar la lógica para incluir la tarea principal o buscar a una profundidad específica)

DELIMITER $$
FUNCTION `childDepth`(pParentId INT, pId INT) RETURNS int(11)
    READS SQL DATA
    DETERMINISTIC
BEGIN
DECLARE depth,curId int;
SET depth = 0;
SET curId = pId;

WHILE curId IS not null AND curId <> pParentId DO
    SELECT ParentId from test where id=curId limit 1 into curId;
    SET depth = depth + 1;
END WHILE;

IF curId IS NULL THEN
    set depth = -1;
END IF;

RETURN depth;
END$$

Uso:

select * from test where childDepth(1, id) <> -1;
Patricio
fuente