Теперь, когда MySQL 8.0 близится к выпуску, все популярные базы данных SQL будут поддерживать рекурсивные запросы в стандартном синтаксисе.
WITH RECURSIVE MyTree AS (
SELECT * FROM MyTable WHERE ParentId IS NULL
UNION ALL
SELECT m.* FROM MyTABLE AS m JOIN MyTree AS t ON m.ParentId = t.Id
)
SELECT * FROM MyTree;
Я тестировал рекурсивные запросы в MySQL 8.0 в своей презентации Рекурсивный сброс запросов в 2017 году.
Ниже мой первоначальный ответ с 2008 года:
Существует несколько способов хранения древовидных данных в реляционной базе данных. То, что вы показываете в своем примере, использует два метода:
- Список смежности (столбец «родитель») и
- Перечисление пути (точечные числа в столбце вашего имени).
Другое решение называется Nested Sets , и оно также может храниться в той же таблице. Прочитайте " Деревья и иерархии в SQL для умных людей " Джо Селко, чтобы узнать больше об этих проектах.
Я обычно предпочитаю проект под названием Closure Table (он же «Соотношение смежности») для хранения древовидных данных. Это требует другой таблицы, но тогда запросить деревья довольно просто.
Я рассматриваю таблицу закрытия в своей презентации Модели для иерархических данных с использованием SQL и PHP и в моей книге Антипаттерны SQL: предотвращение ошибок при программировании базы данных .
CREATE TABLE ClosureTable (
ancestor_id INT NOT NULL REFERENCES FlatTable(id),
descendant_id INT NOT NULL REFERENCES FlatTable(id),
PRIMARY KEY (ancestor_id, descendant_id)
);
Сохраните все пути в таблице закрытия, где существует прямое происхождение от одного узла к другому. Включите строку для каждого узла, чтобы ссылаться на себя. Например, используя набор данных, который вы указали в своем вопросе:
INSERT INTO ClosureTable (ancestor_id, descendant_id) VALUES
(1,1), (1,2), (1,4), (1,6),
(2,2), (2,4),
(3,3), (3,5),
(4,4),
(5,5),
(6,6);
Теперь вы можете получить дерево, начиная с узла 1, например:
SELECT f.*
FROM FlatTable f
JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1;
Вывод (в клиенте MySQL) выглядит следующим образом:
+----+
| id |
+----+
| 1 |
| 2 |
| 4 |
| 6 |
+----+
Другими словами, узлы 3 и 5 исключаются, поскольку они являются частью отдельной иерархии, а не нисходящими из узла 1.
Re: комментарий от e-satun о непосредственных детях (или ближайших родителях). Вы можете добавить столбец «path_length
» к ClosureTable
, чтобы упростить запрос специально для непосредственного ребенка или родителя (или любого другого расстояния).
INSERT INTO ClosureTable (ancestor_id, descendant_id, path_length) VALUES
(1,1,0), (1,2,1), (1,4,2), (1,6,1),
(2,2,0), (2,4,1),
(3,3,0), (3,5,1),
(4,4,0),
(5,5,0),
(6,6,0);
Затем вы можете добавить в свой запрос термин для запроса непосредственных потомков данного узла. Это потомки, у которых path_length
равно 1.
SELECT f.*
FROM FlatTable f
JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1
AND path_length = 1;
+----+
| id |
+----+
| 2 |
| 6 |
+----+
Комментарий от @ashraf: «Как насчет сортировки всего дерева [по имени]?»
Вот пример запроса, который возвращает все узлы, которые являются потомками узла 1, присоединяет их к FlatTable, который содержит другие атрибуты узла, такие как name
, и сортирует по имени.
SELECT f.name
FROM FlatTable f
JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1
ORDER BY f.name;
Комментарий от @Nate:
SELECT f.name, GROUP_CONCAT(b.ancestor_id order by b.path_length desc) AS breadcrumbs
FROM FlatTable f
JOIN ClosureTable a ON (f.id = a.descendant_id)
JOIN ClosureTable b ON (b.descendant_id = a.descendant_id)
WHERE a.ancestor_id = 1
GROUP BY a.descendant_id
ORDER BY f.name
+------------+-------------+
| name | breadcrumbs |
+------------+-------------+
| Node 1 | 1 |
| Node 1.1 | 1,2 |
| Node 1.1.1 | 1,2,4 |
| Node 1.2 | 1,6 |
+------------+-------------+
Пользователь предложил изменить сегодня. ТАК модераторы одобрили редактирование, но я его отменяю.
Редактирование предполагает, что ORDER BY в последнем запросе должен быть ORDER BY b.path_length, f.name
, вероятно, чтобы убедиться, что порядок соответствует иерархии. Но это не работает, потому что он будет заказывать «Node 1.1.1» после «Node 1.2».
Если вы хотите, чтобы порядок соответствовал иерархии разумным образом, это возможно, но не просто путем упорядочения по длине пути. Например, см. Мой ответ на Иерархическая база данных таблицы закрытия MySQL - Как извлечь информацию в правильном порядке .