Як зробити рекурсивний запит SELECT у MySQL?


79

Я отримав наступну таблицю:

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

Якщо користувач шукає "1", програма буде дивитись на те, col1що має "1", тоді вона отримає значення col3"5", тоді програма продовжить пошук "5" col1і отримає "3" в col3тощо. Отже, роздрукується:

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

Якщо користувач шукає "6", він роздрукує:

6   | o   | 2
2   | 0   | 8

Як побудувати SELECTзапит для цього?


У цій публікації є рішення для вашої проблеми stackoverflow.com/questions/14658378/recursive-mysql-select
medina

Відповіді:


70

Редагувати

Рішення, згадане @leftclickben, також ефективно. Для цього ми також можемо використовувати збережену процедуру.

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 //

Ми використовуємо тимчасову таблицю для зберігання результатів виводу, і оскільки тимчасові таблиці базуються на сеансах, ми не будемо мати проблем з неправильними вихідними даними.

SQL FIDDLE Demo

Спробуйте цей запит:

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 |


parent_idЗначення примітки має бути меншим, ніж child_idдля того, щоб це рішення працювало.


2
Люди, будь ласка, відзначають цю відповідь як оптимальне рішення, оскільки деякі інші рішення подібного питання (про рекурсивний вибір у mysql) досить складні, оскільки вимагають створення таблиці та вставки даних у неї. Це рішення дуже елегантне.
Тум

5
Просто будьте обережні з його рішенням, немає ніякої залежності від типу циклу, тоді він перейде до нескінченного циклу, і ще одна річ, що він знайде лише 1 запис цього col3типу, тому, якщо є кілька записів, це не буде працювати.
Мехерзад

2
@HamidSarfraz зараз це працює sqlfiddle.com/#!2/74f457/14 . Це буде працювати для вас. Оскільки це стосується послідовного пошуку, ідентифікатор завжди матиме більше значення, ніж батьківський, оскільки батьківський потрібно створити спочатку. Прохання повідомити, якщо вам потрібні додаткові деталі.
Мехерзад

5
Це не рішення. Це просто щасливий побічний ефект сканування таблиці. Уважно прочитайте відповідь @leftclickben, інакше ви витратите багато часу, як це зробив я.
jaeheung

2
Тум, я знаю, як працює рекурсивний SQL. MySQL не реалізував рекурсивні CTE, тому одним з життєздатних варіантів є той, що вказаний у посиланні (із використанням збережених процедур / функцій). Інший - використання змінних mysql. Однак відповідь тут не елегантна, а навпаки, просто жахлива. Він не відображає рекурсивний SQL. Якщо це спрацювало у вашому випадку, це було лише випадково, як правильно вказав @jaehung. І я не проти жахливих відповідей. Я просто проти них. Але жахлива відповідь на +50, я не заперечую.
ypercubeᵀᴹ

51

Прийнята відповідь @Meherzad працює, лише якщо дані в певному порядку. Трапляється, це працює з даними із запитання про ОП. У моєму випадку мені довелося змінити його для роботи зі своїми даними.

Примітка. Це працює лише тоді, коли "ідентифікатор" кожного запису (у запитанні col1) має значення ВИЩЕ, ніж "батьківський ідентифікатор" цього запису (у запитанні col3). Це часто буває так, оскільки зазвичай батька потрібно спочатку створити. Однак якщо ваша програма дозволяє змінити ієрархію, де елемент може бути повторно створений десь в іншому місці, тоді ви не можете на це покладатися.

Це мій запит на випадок, якщо це комусь допоможе; зауважте, що з даним питанням це не працює, оскільки дані не відповідають необхідній структурі, описаній вище.

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

Різниця полягає в тому table1, що замовлення здійснюється col1таким чином, що батько буде після нього (оскільки значення батьків col1нижче, ніж у дитини).


правильно, також якщо у дитини є 2 батьки, то вона може не вибрати обох
Тум

Спасибі людино. Teamworek зробив свою справу в цьому дописі! Я змусив це працювати, коли змінив значення @pv. Це те, що я точно шукав.
Мохамед Еннаді Ель Ідріссі

Що робити, якщо я хочу використовувати це як стовпець group_concat з батьківськими ідентифікаторами для кожного рядка у більшому виді (тобто значення змінної @pv має бути динамічним для кожного рядка). Приєднання до підзапиту не знає головного стовпця (до якого я намагаюся підключитися), використовуючи іншу змінну, воно теж не працює (завжди повертає NULL)
qdev

Я створив спеціальну функцію, яка генерує шлях до дерева за допомогою group_concat, і тепер я можу надіслати як параметр значення стовпця для кожного рядка;)
qdev

Що ви думаєте про нову відповідь, яку я опублікував? Не те, щоб ваш не був хорошим, але я хотів мати лише SELECT, який міг би підтримувати батьківський ідентифікатор> ідентифікатор дитини.
Master DJon

18

Відповідь leftclickben спрацювала для мене, але я хотів шлях від даного вузла назад до дерева до кореня, і, здавалося, вони йшли іншим шляхом, вниз по дереву. Отже, мені довелося перегорнути деякі поля навколо і перейменовано для ясності, і це працює для мене, на випадок, якщо це теж хоче хтось інший--

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

і

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;

дає:

item | parent
-------------
6    | 3
3    | 1
1    | null

@ BoB3K, чи це спрацює, якщо ідентифікатори не обов'язково в "порядку". Здається, це не спрацьовує, якщо ідентифікатор батьків по ланцюжку вище, ніж його дитина? Наприклад, ланцюг 1> 120 > 112 повернеться лише ((112, 120)), тоді як 2> 22> 221 поверне повний ланцюжок ((221,22), (22,2), (2, нуль))
Томер Кейган

Минув якийсь час, але, на мою думку, я пам’ятаю, як читав в оригінальних відповідях, що це не працює, якщо ідентифікатори елементів не в порядку, що зазвичай не є проблемою, якщо ідентифікатор - це ключ автоматичного збільшення.
BoB3K

Він працює добре, і я використовую його для свого сайту ... проблема тут полягає в тому, що неможливо замовити результати ASC. Натомість 1 3 6я використовую array_reverse()у php ..... будь-яке рішення sql для цього?
Джо

8

Збережена процедура - найкращий спосіб зробити це. Оскільки рішення Мехерзада спрацювало б, лише якщо дані слідують за тим самим порядком.

Якщо ми маємо таку структуру таблиці

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

Це не спрацює. SQL Fiddle Demo

Ось зразок коду процедури для досягнення того ж.

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;

Це надійне рішення, і я використовую його без проблем. Чи можете ви допомогти мені, коли йдете в іншому напрямку, тобто вниз по дереву - я знаходжу всі рядки, де батьківський id == inputNo, але багато ідентифікаторів можуть мати один батьківський ідентифікатор.
мілс

8

Якщо ви хочете мати можливість мати SELECT без проблем батьківського ідентифікатора, який повинен бути нижчим за дочірній ідентифікатор, можна використовувати функцію. Він також підтримує декілька дочірніх систем (як це повинно робити дерево), і дерево може мати кілька головок. Це також гарантує розрив, якщо в даних існує цикл.

Я хотів використовувати динамічний SQL, щоб мати можливість передавати імена таблиць / стовпців, але функції в MySQL цього не підтримують.

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$$

Тут таблицю testпотрібно змінити до справжньої назви таблиці, а стовпці (ParentId, Id), можливо, доведеться відкоригувати для ваших справжніх імен.

Використання:

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

Результат:

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

SQL для створення тесту:

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');

EDIT: Ось скрипка, щоб перевірити це самостійно. Це змусило мене змінити роздільник, використовуючи заздалегідь визначений, але це працює.


0

Нарощування майстра DJon

Ось спрощена функція, яка надає додаткову корисність повернення глибини (якщо ви хочете використовувати логіку для включення батьківського завдання або пошуку на певній глибині)

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$$

Використання:

select * from test where childDepth(1, id) <> -1;
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.