Как создать MySQL иерархический рекурсивный запрос

У меня есть таблица MySQL, которая выглядит следующим образом:

id | name        | parent_id
19 | category1   | 0
20 | category2   | 19
21 | category3   | 20
22 | category4   | 21
......

Теперь я хочу иметь один запрос MySQL, для которого я просто предоставляю идентификатор [например, скажем ‘id = 19’], тогда я должен получить все его дочерние идентификаторы [т.е. результат должен иметь идентификаторы ’20, 21,22 ‘] ….
Кроме того, иерархия детей не известна, она может варьироваться ….

Кроме того, у меня уже есть решение, использующее цикл for ….. Дайте мне знать, как добиться того же, используя один запрос MySQL, если это возможно.

175

Решение

Если вы на MySQL 8, то используйте рекурсивный with пункт:

with recursive cte (id, name, parent_id) as (
select     id,
name,
parent_id
from       products
where      parent_id = 19
union all
select     p.id,
p.name,
p.parent_id
from       products p
inner join cte
on p.parent_id = cte.id
)
select * from cte;

Значение, указанное в parent_id = 19 должен быть установлен на id из родителей вы хотите выбрать всех потомков.

Для версий MySQL, которые не поддерживают Common Table Expressions (до версии 5.7), этого можно достичь с помощью следующего запроса:

select  id,
name,
parent_id
from    (select * from products
order by parent_id, id) products_sorted,
(select @pv := '19') initialisation
where   find_in_set(parent_id, @pv)
and     length(@pv := concat(@pv, ',', id))

Вот играть на скрипке.

Здесь значение, указанное в @pv := '19' должен быть установлен на id из родителей вы хотите выбрать всех потомков.

Это будет работать также, если родитель имеет множественный дети. Однако требуется, чтобы каждая запись удовлетворяла условию parent_id < idиначе результаты не будут полными.

Переменные в запросе

Этот запрос использует определенный синтаксис MySQL: переменные назначаются и изменяются во время его выполнения. Некоторые предположения сделаны относительно порядка исполнения:

  • from пункт оценивается первым. Так вот где @pv инициализируется.
  • where Предложение оценивается для каждой записи в порядке извлечения из from псевдонимы. Таким образом, именно здесь ставится условие включения только тех записей, для которых родительский объект уже был идентифицирован как находящийся в дереве потомков (все потомки первичного родителя постепенно добавляются в @pv).
  • Условия в этом where пункты оцениваются по порядку, и оценка прерывается, как только общий результат определен. Поэтому второе условие должно быть на втором месте, так как оно добавляет id в родительский список, и это должно произойти только в том случае, если id проходит первое условие. length Функция вызывается только для того, чтобы убедиться, что это условие всегда выполняется, даже если pv По какой-то причине строка выдаст ложное значение.

В целом, эти предположения могут оказаться слишком рискованными, чтобы на них можно было положиться. документация предупреждает:

вы можете получить ожидаемые результаты, но это не гарантируется […] порядок вычисления для выражений с участием пользовательских переменных не определен.

Таким образом, даже несмотря на то, что он работает в соответствии с вышеуказанным запросом, порядок оценки может все еще изменяться, например, когда вы добавляете условия или используете этот запрос в качестве представления или подзапроса в большем запросе. Это «особенность», которая будет удален в будущем выпуске MySQL:

Предыдущие выпуски MySQL позволяли присваивать значение пользовательской переменной в операторах, отличных от SET, Эта функциональность поддерживается в MySQL 8.0 для обратной совместимости, но подлежит удалению в будущем выпуске MySQL.

Как указано выше, начиная с MySQL 8.0, вы должны использовать рекурсивный with синтаксис.

КПД

Для очень больших наборов данных это решение может стать медленным, так как find_in_set Операция — не самый идеальный способ найти число в списке, конечно же, не в списке, размер которого достигает того же порядка, что и количество возвращаемых записей.

Альтернатива 1: with recursive, connect by

Все больше и больше баз данных реализуют SQL: стандарт ISO 1999 WITH [RECURSIVE] синтаксис для рекурсивных запросов (например, Postgres 8.4+, SQL Server 2005+, DB2, Oracle 11gR2 +, SQLite 3.8.4+, Firebird 2.1+, H2, HyperSQL 2.1.0+, Teradata, MariaDB 10.2.2+). И по состоянию на версия 8.0, также MySQL поддерживает это. Смотрите верхнюю часть этого ответа для синтаксиса, чтобы использовать.

Некоторые базы данных имеют альтернативный, нестандартный синтаксис для иерархического поиска, такой как CONNECT BY пункт доступен на оракул, DB2, Informix, CUBRID и другие базы данных.

MySQL версии 5.7 не предлагает такую ​​функцию. Когда ваша база данных предоставляет этот синтаксис или вы можете перейти на тот, который это делает, тогда это, безусловно, лучший вариант. Если нет, то также рассмотрите следующие альтернативы.

Альтернатива 2: Идентификаторы стиля пути

Все станет намного проще, если вы назначите id значения, которые содержат иерархическую информацию: путь. Например, в вашем случае это может выглядеть так:

ID       | NAME
19       | category1
19/1     | category2
19/1/1   | category3
19/1/1/1 | category4

Тогда ваш select будет выглядеть так:

select  id,
name
from    products
where   id like '19/%'

Альтернатива 3: повторное самостоятельное соединение

Если вы знаете верхний предел того, насколько глубоким может стать ваше дерево иерархии, вы можете использовать стандарт sql запрос, как это:

select      p6.parent_id as parent6_id,
p5.parent_id as parent5_id,
p4.parent_id as parent4_id,
p3.parent_id as parent3_id,
p2.parent_id as parent2_id,
p1.parent_id as parent_id,
p1.id as product_id,
p1.name
from        products p1
left join   products p2 on p2.id = p1.parent_id
left join   products p3 on p3.id = p2.parent_id
left join   products p4 on p4.id = p3.parent_id
left join   products p5 on p5.id = p4.parent_id
left join   products p6 on p6.id = p5.parent_id
where       19 in (p1.parent_id,
p2.parent_id,
p3.parent_id,
p4.parent_id,
p5.parent_id,
p6.parent_id)
order       by 1, 2, 3, 4, 5, 6, 7;

Видеть это играть на скрипке

where условие указывает, от какого из родителей вы хотите получить потомков. Вы можете расширить этот запрос с большим количеством уровней по мере необходимости.

227

Другие решения

Из блога Управление иерархическими данными в MySQL

Структура таблицы

+-------------+----------------------+--------+
| category_id | name                 | parent |
+-------------+----------------------+--------+
|           1 | ELECTRONICS          |   NULL |
|           2 | TELEVISIONS          |      1 |
|           3 | TUBE                 |      2 |
|           4 | LCD                  |      2 |
|           5 | PLASMA               |      2 |
|           6 | PORTABLE ELECTRONICS |      1 |
|           7 | MP3 PLAYERS          |      6 |
|           8 | FLASH                |      7 |
|           9 | CD PLAYERS           |      6 |
|          10 | 2 WAY RADIOS         |      6 |
+-------------+----------------------+--------+

Запрос:

SELECT t1.name AS lev1, t2.name as lev2, t3.name as lev3, t4.name as lev4
FROM category AS t1
LEFT JOIN category AS t2 ON t2.parent = t1.category_id
LEFT JOIN category AS t3 ON t3.parent = t2.category_id
LEFT JOIN category AS t4 ON t4.parent = t3.category_id
WHERE t1.name = 'ELECTRONICS';

Выход

+-------------+----------------------+--------------+-------+
| lev1        | lev2                 | lev3         | lev4  |
+-------------+----------------------+--------------+-------+
| ELECTRONICS | TELEVISIONS          | TUBE         | NULL  |
| ELECTRONICS | TELEVISIONS          | LCD          | NULL  |
| ELECTRONICS | TELEVISIONS          | PLASMA       | NULL  |
| ELECTRONICS | PORTABLE ELECTRONICS | MP3 PLAYERS  | FLASH |
| ELECTRONICS | PORTABLE ELECTRONICS | CD PLAYERS   | NULL  |
| ELECTRONICS | PORTABLE ELECTRONICS | 2 WAY RADIOS | NULL  |
+-------------+----------------------+--------------+-------+

Большинство пользователей в то или иное время имели дело с иерархическими данными в базе данных SQL и, несомненно, узнали, что управление иерархическими данными — это не то, для чего предназначена реляционная база данных. Таблицы реляционной базы данных не являются иерархическими (например, XML), а представляют собой просто плоский список. Иерархические данные имеют родительско-дочерние отношения, которые естественным образом не представлены в таблице реляционной базы данных.
Прочитайте больше

Обратитесь к блогу для более подробной информации.

РЕДАКТИРОВАТЬ:

select @pv:=category_id as category_id, name, parent from category
join
(select @pv:=19)tmp
where parent=@pv

Выход:

category_id name    parent
19  category1   0
20  category2   19
21  category3   20
22  category4   21

Ссылка: Как сделать рекурсивный запрос SELECT в Mysql?

74

Сделал то же самое для другой очереди здесь

Mysql выберите рекурсивный получить все дочерние с несколькими уровнями

Запрос будет:

SELECT GROUP_CONCAT(lv SEPARATOR ',') FROM (
SELECT @pv:=(SELECT GROUP_CONCAT(id SEPARATOR ',') FROM table WHERE parent_id IN (@pv)) AS lv FROM table
JOIN
(SELECT @pv:=1)tmp
WHERE parent_id IN (@pv)) a;
7

Попробуйте это:

Определение таблицы:

DROP TABLE IF EXISTS category;
CREATE TABLE category (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20),
parent_id INT,
CONSTRAINT fk_category_parent FOREIGN KEY (parent_id)
REFERENCES category (id)
) engine=innodb;

Экспериментальные ряды:

INSERT INTO category VALUES
(19, 'category1', NULL),
(20, 'category2', 19),
(21, 'category3', 20),
(22, 'category4', 21),
(23, 'categoryA', 19),
(24, 'categoryB', 23),
(25, 'categoryC', 23),
(26, 'categoryD', 24);

Хранимая рекурсивная процедура:

DROP PROCEDURE IF EXISTS getpath;
DELIMITER $$
CREATE PROCEDURE getpath(IN cat_id INT, OUT path TEXT)
BEGIN
DECLARE catname VARCHAR(20);
DECLARE temppath TEXT;
DECLARE tempparent INT;
SET max_sp_recursion_depth = 255;
SELECT name, parent_id FROM category WHERE id=cat_id INTO catname, tempparent;
IF tempparent IS NULL
THEN
SET path = catname;
ELSE
CALL getpath(tempparent, temppath);
SET path = CONCAT(temppath, '/', catname);
END IF;
END$$
DELIMITER ;

Функция обертки для хранимой процедуры:

DROP FUNCTION IF EXISTS getpath;
DELIMITER $$
CREATE FUNCTION getpath(cat_id INT) RETURNS TEXT DETERMINISTIC
BEGIN
DECLARE res TEXT;
CALL getpath(cat_id, res);
RETURN res;
END$$
DELIMITER ;

Выберите пример:

SELECT id, name, getpath(id) AS path FROM category;

Выход:

+----+-----------+-----------------------------------------+
| id | name      | path                                    |
+----+-----------+-----------------------------------------+
| 19 | category1 | category1                               |
| 20 | category2 | category1/category2                     |
| 21 | category3 | category1/category2/category3           |
| 22 | category4 | category1/category2/category3/category4 |
| 23 | categoryA | category1/categoryA                     |
| 24 | categoryB | category1/categoryA/categoryB           |
| 25 | categoryC | category1/categoryA/categoryC           |
| 26 | categoryD | category1/categoryA/categoryB/categoryD |
+----+-----------+-----------------------------------------+

Фильтрация строк по определенному пути:

SELECT id, name, getpath(id) AS path FROM category HAVING path LIKE 'category1/category2%';

Выход:

+----+-----------+-----------------------------------------+
| id | name      | path                                    |
+----+-----------+-----------------------------------------+
| 20 | category2 | category1/category2                     |
| 21 | category3 | category1/category2/category3           |
| 22 | category4 | category1/category2/category3/category4 |
+----+-----------+-----------------------------------------+
7

Лучший подход, который я придумал,

  1. Используйте родословную для хранения \ сортировки \ трассировки деревьев. Этого более чем достаточно, и он работает в тысячи раз быстрее для чтения, чем любой другой подход.
    Это также позволяет остаться на этом паттерне, даже если DB изменится (так как ЛЮБОЙ БД позволит использовать этот паттерн)
  2. Используйте функцию, которая определяет происхождение для определенного идентификатора.
  3. Используйте его по своему желанию (в выборках, или в операциях CUD, или даже в заданиях).

Линейный подход descr. можно найти где угодно, например
Вот или же Вот.
По состоянию на — тот это то, что вдохновляло меня

В итоге — получилось более-менее простое, относительно быстрое и ПРОСТОЕ решение.

Тело функции

-- --------------------------------------------------------------------------------
-- Routine DDL
-- Note: comments before and after the routine body will not be stored by the server
-- --------------------------------------------------------------------------------
DELIMITER $$

CREATE DEFINER=`root`@`localhost` FUNCTION `get_lineage`(the_id INT) RETURNS text CHARSET utf8
READS SQL DATA
BEGIN

DECLARE v_rec INT DEFAULT 0;

DECLARE done INT DEFAULT FALSE;
DECLARE v_res text DEFAULT '';
DECLARE v_papa int;
DECLARE v_papa_papa int DEFAULT -1;
DECLARE csr CURSOR FOR
select _id,parent_id -- @n:=@n+1 as rownum,T1.*
from
(SELECT @r AS _id,
(SELECT @r := table_parent_id FROM table WHERE table_id = _id) AS parent_id,
@l := @l + 1 AS lvl
FROM
(SELECT @r := the_id, @l := 0,@n:=0) vars,
table m
WHERE @r <> 0
) T1
where T1.parent_id is not null
ORDER BY T1.lvl DESC;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
open csr;
read_loop: LOOP
fetch csr into v_papa,v_papa_papa;
SET v_rec = v_rec+1;
IF done THEN
LEAVE read_loop;
END IF;
-- add first
IF v_rec = 1 THEN
SET v_res = v_papa_papa;
END IF;
SET v_res = CONCAT(v_res,'-',v_papa);
END LOOP;
close csr;
return v_res;
END

И тогда ты просто

select get_lineage(the_id)

Надеюсь, это кому-нибудь поможет 🙂

7

Если вам нужна быстрая скорость чтения, лучше всего использовать закрывающую таблицу. Закрывающая таблица содержит строку для каждой пары предок / потомок. Итак, в вашем примере таблица закрытия будет выглядеть

ancestor | descendant | depth
0        | 0          | 0
0        | 19         | 1
0        | 20         | 2
0        | 21         | 3
0        | 22         | 4
19       | 19         | 0
19       | 20         | 1
19       | 21         | 3
19       | 22         | 4
20       | 20         | 0
20       | 21         | 1
20       | 22         | 2
21       | 21         | 0
21       | 22         | 1
22       | 22         | 0

Если у вас есть эта таблица, иерархические запросы становятся очень простыми и быстрыми. Чтобы получить всех потомков категории 20:

SELECT cat.* FROM categories_closure AS cl
INNER JOIN categories AS cat ON cat.id = cl.descendant
WHERE cl.ancestor = 20 AND cl.depth > 0

Конечно, есть большой недостаток, когда вы используете денормализованные данные, как это. Вы должны поддерживать таблицу закрытия рядом с таблицей категорий. Лучше всего, вероятно, использовать триггеры, но довольно сложно правильно отслеживать вставки / обновления / удаления для таблиц закрытия. Как и во всем, вам нужно посмотреть на ваши требования и решить, какой подход лучше для вас.

редактировать: Смотри вопрос Какие есть варианты хранения иерархических данных в реляционной базе данных? для большего количества вариантов. Существуют разные оптимальные решения для разных ситуаций.

4

Простой запрос для перечисления потомков первой рекурсии:

select @pv:=id as id, name, parent_id
from products
join (select @pv:=19)tmp
where parent_id=@pv

Результат:

id  name        parent_id
20  category2   19
21  category3   20
22  category4   21
26  category24  22

… с левым соединением:

select
@pv:=p1.id as id
, p2.name as parent_name
, p1.name name
, p1.parent_id
from products p1
join (select @pv:=19)tmp
left join products p2 on p2.id=p1.parent_id -- optional join to get parent name
where p1.parent_id=@pv

Решение @tincot перечислить все детские:

select  id,
name,
parent_id
from    (select * from products
order by parent_id, id) products_sorted,
(select @pv := '19') initialisation
where   find_in_set(parent_id, @pv) > 0
and     @pv := concat(@pv, ',', id)

Проверьте это онлайн с Sql Fiddle и посмотреть все результаты.

http://sqlfiddle.com/#!9/a318e3/4/0

4

Вы можете сделать это таким же образом в других базах данных с помощью рекурсивного запроса (YMMV по производительности).

Другой способ сделать это — сохранить два дополнительных бита данных, левое и правое значение. Левое и правое значение получаются из предварительного обхода древовидной структуры, которую вы представляете.

Это называется измененным обходом дерева предзаказа и позволяет вам выполнить простой запрос, чтобы получить все родительские значения одновременно. Он также называется «вложенный набор».

3
По вопросам рекламы [email protected]