получить родителей и детей структуры дерева папок в моем sql <8 и без CTE

У меня есть таблица папок, которая присоединяется к себе по отношению id, parent_id:

CREATE TABLE folders (
  id int(10) unsigned NOT NULL AUTO_INCREMENT,
  title nvarchar(255) NOT NULL,
  parent_id int(10) unsigned DEFAULT NULL,
  PRIMARY KEY (id)
);

INSERT INTO folders(id, title, parent_id) VALUES(1, 'root', null);
INSERT INTO folders(id, title, parent_id) values(2, 'one', 1);
INSERT INTO folders(id, title, parent_id) values(3, 'target', 2);
INSERT INTO folders(id, title, parent_id) values(4, 'child one', 3);
INSERT INTO folders(id, title, parent_id) values(5, 'child two', 3);
INSERT INTO folders(id, title, parent_id) values(6, 'root 2', null);
INSERT INTO folders(id, title, parent_id) values(7, 'other child one', 6);
INSERT INTO folders(id, title, parent_id) values(8, 'other child two', 6);

Я хочу запрос, который возвращает всех родителей этой записи, обратно к маршруту и любых детей.

Поэтому, если я запрашиваю папку с id=3, я получаю записи: 1, 2, 3, 4, 5. Я застрял, как получить родителей.

Версия MYSQL 5.7, и нет никаких планов по обновлению, так что, к сожалению, CTE не вариант.

Я создал эту скрипку sql

Ответ 1

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

Следующий запрос дает вам родителей данной записи (включая саму запись):

with recursive parent_cte (id, title, parent_id) as (
  select id, title, parent_id
  from folders
  where id = 3
  union all
  select  f.id, f.title, f.parent_id
  from folders f
  inner join parent_cte pc on f.id = pc.parent_id
)
select * from parent_cte;
| id  | title  | parent_id |
| --- | ------ | --------- |
| 3   | target | 2         |
| 2   | one    | 1         |
| 1   | root   |           |

А вот немного другой запрос, который возвращает дочернее дерево данной записи:

with recursive children_cte (id, title, parent_id) as (
  select id, title, parent_id
  from folders
  where parent_id = 3
  union all
  select  f.id, f.title, f.parent_id
  from folders f
  inner join children_cte cc on f.parent_id = cc.id
)
select * from children_cte;
| id  | title     | parent_id |
| --- | --------- | --------- |
| 4   | child one | 3         |
| 5   | child two | 3         |

Оба запроса могут быть объединены следующим образом:

with recursive parent_cte (id, title, parent_id) as (
  select id, title, parent_id
  from folders
  where id = 3
  union all
  select  f.id, f.title, f.parent_id
  from folders f
  inner join parent_cte pc on f.id = pc.parent_id
),
children_cte (id, title, parent_id) as (
  select id, title, parent_id
  from folders
  where parent_id = 3
  union all
  select  f.id, f.title, f.parent_id
  from folders f
  inner join children_cte cc on f.parent_id = cc.id
)
select * from parent_cte
union all select * from children_cte;
| id  | title     | parent_id |
| --- | --------- | --------- |
| 3   | target    | 2         |
| 2   | one       | 1         |
| 1   | root      |           |
| 4   | child one | 3         |
| 5   | child two | 3         |

Демо на БД Fiddle

Ответ 2

В вашей схеме таблицы ID и PARENT_ID соответствуют " Модели списка PARENT_ID " для хранения дерева.

Существует еще одна конструкция, называемая " Модель вложенного набора ", которая упрощает выполнение необходимых операций.

Посмотрите эту прекрасную статью Майка Хиллиера, в которой описываются оба: управление-иерархическими-данными-в-mysql

В итоге:

Дерево хранится в виде таблицы:

CREATE TABLE nested_category (
        category_id INT AUTO_INCREMENT PRIMARY KEY,
        name VARCHAR(20) NOT NULL,
        lft INT NOT NULL,
        rgt INT NOT NULL
);

Нахождение пути от корня к заданному узлу (здесь, "FLASH"):

SELECT parent.name
FROM nested_category AS node,
        nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
        AND node.name = 'FLASH'
ORDER BY parent.lft;

Поиск всех дочерних элементов данного узла (здесь "ПОРТАТИВНАЯ ЭЛЕКТРОНИКА"):

SELECT node.name, (COUNT(parent.name) - (sub_tree.depth + 1)) AS depth
FROM nested_category AS node,
        nested_category AS parent,
        nested_category AS sub_parent,
        (
                SELECT node.name, (COUNT(parent.name) - 1) AS depth
                FROM nested_category AS node,
                        nested_category AS parent
                WHERE node.lft BETWEEN parent.lft AND parent.rgt
                        AND node.name = 'PORTABLE ELECTRONICS'
                GROUP BY node.name
                ORDER BY node.lft
        )AS sub_tree
WHERE node.lft BETWEEN parent.lft AND parent.rgt
        AND node.lft BETWEEN sub_parent.lft AND sub_parent.rgt
        AND sub_parent.name = sub_tree.name
GROUP BY node.name
HAVING depth <= 1
ORDER BY node.lft;

После переименования в таблицу папок

  • TABLE nested_category → TABLE папки
  • Столбец category_id → Идентификатор столбца
  • Название столбца → Название столбца

Решение:

CREATE TABLE folders (
        id INT AUTO_INCREMENT PRIMARY KEY,
        title VARCHAR(20) NOT NULL,
        lft INT NOT NULL,
        rgt INT NOT NULL
);

INSERT INTO folders(id, title, lft, rgt) values(1, 'root', 1, 10);
INSERT INTO folders(id, title, lft, rgt) values(2, 'one', 2, 9);
INSERT INTO folders(id, title, lft, rgt) values(3, 'target', 3, 8);
INSERT INTO folders(id, title, lft, rgt) values(4, 'child one', 4, 5);
INSERT INTO folders(id, title, lft, rgt) values(5, 'child two', 6, 7);
INSERT INTO folders(id, title, lft, rgt) values(6, 'root 2', 11, 16);
INSERT INTO folders(id, title, lft, rgt) values(7, 'other child one', 12, 13);
INSERT INTO folders(id, title, lft, rgt) values(8, 'other child two', 14, 15);

Путь к цели:

SELECT parent.title
FROM folders AS node,
        folders AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
        AND node.title = 'target'
ORDER BY parent.lft;

Целевые дети:

SELECT node.title, (COUNT(parent.title) - (sub_tree.depth + 1)) AS depth
    FROM folders AS node,
            folders AS parent,
            folders AS sub_parent,
            (
              SELECT node.title, (COUNT(parent.title) - 1) AS depth
                    FROM folders AS node,
                            folders AS parent
                    WHERE node.lft BETWEEN parent.lft AND parent.rgt
                            AND node.title = 'target'
                    GROUP BY node.title
                    ORDER BY node.lft
            )AS sub_tree
    WHERE node.lft BETWEEN parent.lft AND parent.rgt
            AND node.lft BETWEEN sub_parent.lft AND sub_parent.rgt
            AND sub_parent.title = sub_tree.title
    GROUP BY node.title
    HAVING depth <= 1
    ORDER BY node.lft;

Смотрите sqlfiddle

Чтобы получить все данные в одном запросе, union должно сделать.

Ответ 3

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

mysql> CREATE TABLE folders_closure (
 ancestor INT UNSIGNED NOT NULL,
 descendant INT UNSIGNED NOT NULL,
 PRIMARY KEY (ancestor, descendant),
 depth INT UNSIGNED NOT NULL
);

Загрузите в эту таблицу кортежи всех пар предок-потомок, включая те, на которые ссылается узел в дереве (путь длиной 0).

mysql> INSERT INTO folders_closure VALUES
     (1,1,0), (2,2,0), (3,3,0), (4,4,0), (5,5,0), (6,6,0),
     (1,2,1), (2,3,1), (3,4,1), (3,5,1), (1,4,2), (1,5,2),
     (6,7,1), (6,8,1);

Теперь вы можете запросить дерево под данным узлом, запросив все пути, которые начинаются на верхнем узле, и присоединить его потомок к таблице folders.

mysql> SELECT d.id, d.title, cl.depth FROM folders_closure cl
     JOIN folders d ON d.id=cl.descendant WHERE cl.ancestor=1;
+----+-----------+-------+
| id | title     | depth |
+----+-----------+-------+
|  1 | root      |     0 |
|  2 | one       |     1 |
|  4 | child one |     2 |
|  5 | child two |     2 |
+----+-----------+-------+

Я вижу, что многие люди рекомендуют решение Nested Sets, которое было представлено в 1992 году и стало популярным после того, как Джо Селко включил его в свою книгу " SQL для умных людей" в 1995 году. Но мне не нравится техника Nested Sets, потому что цифры на самом деле не являются ссылки на первичные ключи узлов в вашем дереве, и это требует перенумерации множества строк при добавлении или удалении узла.

Я написал о методе закрывающих таблиц в разделе Каков наиболее эффективный/элегантный способ разбить плоский стол на дерево? и некоторые другие мои ответы с тегом иерархических данных.

Я сделал презентацию об этом: Модели для иерархических данных.

Я также рассказал об этом в главе моей книги " Антипаттерны SQL: предотвращение ловушек программирования баз данных".

Ответ 4

Если это гарантирует, что дочерние узлы всегда имеют более высокий идентификатор, чем родительский, тогда вы можете использовать пользовательские переменные.

Получить потомков:

select f.*, @l := concat_ws(',', @l, id) as dummy
from folders f
cross join (select @l := 3) init_list
where find_in_set(parent_id, @l)
order by id

Результат:

id | title     | parent_id | dummy
---|-----------|-----------|------
 4 | child one |         3 | 3,4
 5 | child two |         3 | 3,4,5

Получить предков (в том числе и себя):

select f.*, @l := concat_ws(',', @l, parent_id) as dummy
from folders f
cross join (select @l := 3) init_list
where find_in_set(id, @l)
order by id desc

Результат:

id | title  | parent_id | dummy
 3 | target |         2 | 3,2
 2 | one    |         1 | 3,2,1
 1 | root   |      null | 3,2,1

демонстрация

Обратите внимание, что этот метод основан на недокументированном порядке оценки и не будет возможен в будущих версиях.

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

Для больших таблиц я бы рассмотрел более сложное решение, такое как следующая хранимая процедура:

create procedure get_related_nodes(in in_id int)
begin
  set @list = in_id;
  set @parents = @list;

  repeat
    set @sql = '
      select group_concat(id) into @children
      from folders
      where parent_id in ({parents})
    ';
    set @sql = replace(@sql, '{parents}', @parents);
    prepare stmt from @sql;
    execute stmt;
    set @list = concat_ws(',', @list, @children);
    set @parents = @children;
  until (@children is null) end repeat;

  set @child = in_id;
  repeat
    set @sql = '
      select parent_id into @parent
      from folders
      where id = ({child})
    ';
    set @sql = replace(@sql, '{child}', @child);
    prepare stmt from @sql;
    execute stmt;
    set @list = concat_ws(',', @parent, @list);
    set @child = @parent;
  until (@parent is null) end repeat;

  set @sql = '
    select *
    from folders
    where id in ({list})
  ';
  set @sql = replace(@sql, '{list}', @list);
  prepare stmt from @sql;
  execute stmt;
end

Используйте это с

call get_related_nodes(3)

Это вернется

id | title     | parent_id
---|-----------|----------
 1 | root      | 
 2 | one       | 1
 3 | target    | 2
 4 | child one | 3
 5 | child two | 3

демонстрация

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

Ответ 5

Вы можете выполнить объединение между родительскими и дочерними строками следующим образом:

select title, id, @parent:=parent_id as parent from
               (select @parent:=3 ) a join (select * from folders order by id desc) b where @parent=id
union select title, id, parent_id as parent from folders where  parent_id=3 ORDER BY id

вот образец dbfiddle

Ответ 6

Примечание. Мое решение более или менее совпадает с @Marc Alff. Не понимал, что это было уже там, прежде чем набирать/готовить ответ в редакторе.

Очень трудно получить запрос для достижения вашей цели (или других типичных требований к иерархическому набору данных) без использования CTE или другой поддержки иерархических запросов (например, ранее, соединение в Oracle). Это был основной драйвер для баз данных, чтобы придумывать CTE и т.д.

Много-много лет назад, когда такая поддержка моделирования иерархических объектов не была доступна в базах данных, требования, изложенные вами и многими другими, были решены путем моделирования таких объектов немного по-другому.

Концепция проста. По сути, в иерархической таблице (или в отдельном внешнем ключе таблицы в иерархической таблице) введены еще два атрибута, которые называются left_boundary и right_boundary (вызывайте все, что хотите, после того, что в имени). Для каждой строки значения (числа) для этих атрибутов выбираются так, чтобы они охватывали значения этих атрибутов для всех их дочерних элементов. Другими словами, левая и правая границы ребенка будут находиться между левой и правой границами его родителей.

Кстати пример

enter image description here

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

Я собираюсь использовать это решение для достижения вашей цели. Во-первых, я представлю вторую таблицу (мог бы ввести атрибуты в той же таблице, решил не беспокоить вашу модель данных)

CREATE TABLE folder_boundaries (
  id int(10) unsigned NOT NULL AUTO_INCREMENT,
  folder_id int(10) unsigned NOT NULL,
  left_boundary int(10) unsigned,
  right_boundary int(10) unsigned,
  PRIMARY KEY (id),
  FOREIGN KEY (folder_id) REFERENCES folders(id)
);

Данные для этой таблицы основаны на вашем наборе данных

NSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(1, 1, 10);
INSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(2, 2, 9);
INSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(3, 3, 8);
INSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(4, 4, 4);
INSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(5, 4, 4);
INSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(6, 21, 25);
INSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(7, 22, 22);
INSERT INTO folder_boundaries(folder_id, left_boundary, right_boundary) VALUES(7, 22, 22);

Вот запрос для достижения того, что вы после

select f.id, f.title
from folders f
join folder_boundaries fb on f.id = fb.folder_id
where fb.left_boundary < (select left_boundary from folder_boundaries where folder_id = 3)
and fb.right_boundary > (select right_boundary from folder_boundaries where folder_id = 3)
union all
select f.id, f.title
from folders f
join folder_boundaries fb on f.id = fb.folder_id
where fb.left_boundary >= (select left_boundary from folder_boundaries where folder_id = 3)
and fb.right_boundary <= (select right_boundary from folder_boundaries where folder_id = 3)

Результат

enter image description here

Ответ 7

Небольшой код с использованием хранимых процедур, протестированный на 5.6:

drop procedure if exists test;
DELIMITER //
create procedure test(in testid int)
begin
    DECLARE parent int;
    set parent = testid;

    drop temporary table if exists pars;
    CREATE temporary TABLE pars (
      id int(10) unsigned NOT NULL AUTO_INCREMENT,
      title nvarchar(255) NOT NULL,
      parent_id int(10) unsigned DEFAULT NULL,
      PRIMARY KEY (id)
    );

    #For getting heirarchy
    while parent is not null do
        insert into pars
        select  * from folders where id = parent;
        set parent = (select parent_id from folders where id = parent);
    end while;

    #For getting child
    insert into pars
    select  * from folders where parent_id = testid;

    select * from pars;
end //
DELIMITER ;

ниже приведен вызов коду:

call test(3);

И вывод:

enter image description here

Конечный результат может быть отформатирован со строкой, объединенной как требуется, как только мы получим таблицу, остальное должно быть легким, я думаю. Кроме того, если идентификатор может быть отсортирован, это было бы здорово для форматирования.

Не говоря уже о том, что поля id и parent_id должны быть индексами, чтобы это работало эффективно.

Ответ 8

Предположим, вы знаете максимальную глубину дерева, вы можете "создать" цикл, чтобы получить то, что вы хотите:

Получить родительские узлы:

SELECT  @id :=
        (
        SELECT  parent_id
        FROM    folders
        WHERE   id = @id
        ) AS folderId, vars.id
FROM    (
        SELECT  @id := 7 AS id
        ) vars
INNER JOIN (
    SELECT 0 AS nbr UNION ALL SELECT 1 UNION ALL SELECT 2 
 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 
 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 
 UNION ALL SELECT 9) temp
WHERE @id IS NOT NULL

Получить дочерние узлы:

SELECT  @id :=
        (
        SELECT  GROUP_CONCAT(id)
        FROM    folders
        WHERE   FIND_IN_SET(parent_id, @id)
        ) AS folderIds, vars.id
FROM    (
        SELECT  @id := 1 AS id
        ) vars
INNER JOIN (
    SELECT 0 AS nbr UNION ALL SELECT 1 UNION ALL SELECT 2 
 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 
 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 
 UNION ALL SELECT 9) temp
WHERE @id IS NOT NULL

Это работает

  • Создание соединения между статическим переменным подзапросом (SELECT @id := 1 AS id) и статическим набором из 10 строк в этом случае (максимальная глубина)
  • использование подзапроса в select для обхода дерева и поиска всех родительских или дочерних узлов

Цель объединения - создать результирующий набор из 10 строк, чтобы подзапрос в выборе выполнялся 10 раз.

В качестве альтернативы, если вы не знаете максимальную глубину, вы можете заменить объединенный подзапрос на

INNER JOIN (
SELECT 1 FROM folder) temp

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

INNER JOIN (
SELECT 1 FROM folder LIMIT 100) temp

Ссылки: - Иерархические запросы в MySQL

Ответ 9

если ваш parent_id всегда идет в порядке возрастания, тогда запрос ниже - отличное решение.

если вы получите в результате ваш идентификатор в нулевое родительское значение, тогда, пожалуйста, перейдите по ссылке http://www.sqlfiddle.com/#!9/b40b8/258 (при передаче id = 6) http://www.sqlfiddle.com/#! 9/b40b8/259 (при передаче id = 3)

SELECT * FROM folders f
WHERE id = 3 
OR 
  (Parent_id <=3 AND Parent_id >= 
  (SELECT id FROM folders Where id <= 3 AND parent_id IS NULL Order by ID desc LIMIT 1)) OR (id <= 3 AND IFNULL(Parent_id,0) = 0)
  AND id >= (SELECT id FROM folders Where id <= 3 AND parent_id IS NULL Order by ID desc LIMIT 1);

ИЛИ ЖЕ

Вы не получите ваш проходной идентификатор наверху у родителей, тогда, пожалуйста, перейдите по ссылке, как показано ниже. http://www.sqlfiddle.com/#!9/b40b8/194 (при передаче id = 3)
http://www.sqlfiddle.com/#!9/b40b8/208 (при передаче id = 6)

SELECT 
      * 
FROM 
     folders f
WHERE 
    id = 3 OR Parent_id <=3 
    OR (id <= 3  AND IFNULL(Parent_id,0) = 0);