У меня есть таблица MySQL, которая выглядит следующим образом:
id | name | parent_id
19 | category1 | 0
20 | category2 | 19
21 | category3 | 20
22 | category4 | 21
......
Теперь я хочу иметь один запрос MySQL, к которому я просто поставляю идентификатор [например, say 'id = 19'], тогда я должен получить все его дочерние идентификаторы [т. результат должен иметь идентификаторы '20, 21,22 '].... Кроме того, иерархия детей неизвестна, она может меняться....
Кроме того, у меня уже есть решение, использующее цикл for... Дайте мне знать, как достичь того же, используя один MySQL-запрос, если это возможно.
Если вы используете 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
псевдонимов from
. Таким образом, именно здесь ставится условие включения только тех записей, для которых родительский объект уже был идентифицирован как находящийся в дереве потомков (все потомки первичного родителя постепенно добавляются в @pv
).where
оцениваются по порядку, и оценка прерывается, как только общий результат определен. Поэтому второе условие должно быть на втором месте, так как оно добавляет id
в родительский список, и это должно происходить, только если id
проходит первое условие. Функция length
вызывается только для того, чтобы убедиться, что это условие всегда выполняется, даже если строка pv
по какой-то причине приведет к ложному значению.В целом, эти предположения могут оказаться слишком рискованными, чтобы на них можно было положиться. Документация предупреждает:
вы можете получить ожидаемые результаты, но это не гарантируется [...] порядок вычисления для выражений с участием пользовательских переменных не определен.
Таким образом, даже несмотря на то, что он работает в соответствии с вышеуказанным запросом, порядок оценки может все еще изменяться, например, когда вы добавляете условия или используете этот запрос в качестве представления или подзапроса в большем запросе. Это "особенность", которая будет удалена в будущем выпуске MySQL:
Предыдущие выпуски MySQL позволяли присваивать значение пользовательской переменной в операторах, отличных от
SET
. Эта функциональность поддерживается в MySQL 8.0 для обратной совместимости, но подлежит удалению в будущем выпуске MySQL.
Как указано выше, начиная с MySQL 8.0, вы должны использовать рекурсив with
синтаксисом.
Для очень больших наборов данных это решение может быть медленным, так как операция find_in_set
- не самый идеальный способ найти число в списке, конечно, не в списке, размер которого достигает такого же порядка, как число возвращаемых записей.,
with recursive
connect by
Все больше и больше баз данных реализуют стандарт SQL: 1999 ISO с синтаксисом 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
, доступное в Oracle, DB2, Informix, CUBRID и других базах данных.
MySQL версии 5.7 не предлагает такую функцию. Когда ваш движок базы данных предоставляет этот синтаксис или вы можете перейти на тот, который это делает, тогда это, безусловно, лучший вариант. Если нет, то также рассмотрите следующие альтернативы.
Все станет намного проще, если вы назначите значения 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/%'
Если вы знаете верхний предел того, насколько глубоким может стать ваше дерево иерархии, вы можете использовать стандартный 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
указывает, для какого из родителей вы хотите получить потомков. Вы можете расширить этот запрос с большим количеством уровней по мере необходимости.
Из блога Управление иерархическими данными в 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
Попробуйте следующее:
Определение таблицы:
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 |
+----+-----------+-----------------------------------------+
(20, 'category2', 19), (21, 'category3', 20), (22, 'category4', 20),
То же самое для другого вопроса здесь
Mysql select recursive получить все дочерние с несколькими уровнями
Запрос будет выглядеть следующим образом:
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;
SELECT idFolder, (SELECT GROUP_CONCAT(lv SEPARATOR ',') FROM ( SELECT @pv:=(SELECT GROUP_CONCAT(idFolder SEPARATOR ',') FROM Folder WHERE idFolderParent IN (@pv)) AS lv FROM Folder JOIN (SELECT @pv:= F1.idFolder )tmp WHERE idFolderParent IN (@pv)) a) from folder F1 where id > 10
; Я не могу сослаться на F1.idFolder для @pv
Лучший подход, который я придумал, - это
Линейный подход 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)
Надеюсь, это поможет кому-то :)
Если вам нужна быстрая скорость чтения, лучшим вариантом является использование таблицы закрытия. Таблица закрытия содержит строку для каждой пары предков/потомков. Поэтому в вашем примере таблица закрытия будет выглядеть как
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
Конечно, есть большой недостаток, когда вы используете денормализованные данные, подобные этому. Вам нужно поддерживать таблицу закрытия рядом с таблицей категорий. Лучший способ, вероятно, использовать триггеры, но несколько сложно правильно отслеживать вставки/обновления/удаления для таблиц закрытия. Как и в любом случае, вам нужно посмотреть на свои требования и решить, какой подход лучше всего подходит вам.
Изменить: см. вопрос Каковы параметры хранения иерархических данных в реляционной базе данных? для получения дополнительных параметров. Существуют различные оптимальные решения для разных ситуаций.
Простой запрос для списка дочернего элемента первой рекурсии:
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 и просмотрите все результаты.
Вы легко можете сделать это в других базах данных с помощью рекурсивного запроса (YMMV на производительность).
Другой способ сделать это - сохранить два дополнительных бита данных, левое и правое значение. Левое и правое значение получаются из предзакатного обхода древовидной структуры, которую вы представляете.
Это известно как измененный обход дерева предзаказов и позволяет запускать простой запрос, чтобы сразу получить все родительские значения. Он также называется именем "вложенный набор".
Просто используйте BlueM/tree php class для создания дерева таблицы самозависимости в mysql.
Дерево и дерево \ Node - это классы PHP для обработки данных, иерархически структурированных с использованием ссылок на родительские идентификаторы. Типичным примером является таблица в реляционной базе данных, где каждая запись "родительского" поля ссылается на первичный ключ другой записи. Конечно, Tree может не только использовать данные, происходящие из базы данных, но и все: вы предоставляете данные, а Tree использует их независимо от того, откуда поступают данные и как они были обработаны. читать дальше
Вот пример использования BlueM/tree:
<?php
require '/path/to/vendor/autoload.php'; $db = new PDO(...); // Set up your database connection
$stm = $db->query('SELECT id, parent, title FROM tablename ORDER BY title');
$records = $stm->fetchAll(PDO::FETCH_ASSOC);
$tree = new BlueM\Tree($records);
...
Это немного сложно, проверьте, работает ли он для вас
select a.id,if(a.parent = 0,@varw:=concat(a.id,','),@varw:=concat(a.id,',',@varw)) as list from (select * from recursivejoin order by if(parent=0,id,parent) asc) a left join recursivejoin b on (a.id = b.parent),(select @varw:='') as c having list like '%19,%';
Ссылка на скрипт SQL http://www.sqlfiddle.com/#!2/e3cdf/2
Замените имя поля и таблицы соответствующим образом.
Это таблица категорий.
SELECT id,
NAME,
parent_category
FROM (SELECT * FROM category
ORDER BY parent_category, id) products_sorted,
(SELECT @pv := '2') initialisation
WHERE FIND_IN_SET(parent_category, @pv) > 0
AND @pv := CONCAT(@pv, ',', id)
Выход ::
Это работает для меня, надеюсь, это будет работать и для вас. Это даст вам Record set Root to Child для любого определенного меню. Измените имя поля в соответствии с вашими требованиями.
SET @id:= '22';
SELECT Menu_Name, (@id:=Sub_Menu_ID ) as Sub_Menu_ID, Menu_ID
FROM
( SELECT Menu_ID, Menu_Name, Sub_Menu_ID
FROM menu
ORDER BY Sub_Menu_ID DESC
) AS aux_table
WHERE Menu_ID = @id
ORDER BY Sub_Menu_ID;
Что-то, о чем здесь не упоминалось, хотя и немного похоже на вторую альтернативу принятого ответа, но разные и низкие затраты на большой иерархический запрос и простые (вставлять обновляемые элементы), добавляет столбец постоянного пути для каждого элемента.
некоторые вроде:
id | name | path
19 | category1 | /19
20 | category2 | /19/20
21 | category3 | /19/20/21
22 | category4 | /19/20/21/22
Пример:
-- get children of category3:
SELECT * FROM my_table WHERE path LIKE '/19/20/21%'
-- Reparent an item:
UPDATE my_table SET path = REPLACE(path, '/19/20', '/15/16') WHERE path LIKE '/19/20/%'
Оптимизируйте длину пути и путь ORDER BY path
используя кодировку base36, вместо этого действительный числовой идентификатор пути
// base10 => base36
'1' => '1',
'10' => 'A',
'100' => '2S',
'1000' => 'RS',
'10000' => '7PS',
'100000' => '255S',
'1000000' => 'LFLS',
'1000000000' => 'GJDGXS',
'1000000000000' => 'CRE66I9S'
https://en.wikipedia.org/wiki/Base36
Подавление разделителя '/' с помощью фиксированной длины и заполнения для кодированного идентификатора
Подробное объяснение оптимизации здесь: https://bojanz.wordpress.com/2014/04/25/storing-hierarchical-data-materialized-path/
СДЕЛАТЬ
построение функции или процедуры для разделения пути для возвращающихся предков одного элемента
Мне было легче:
1) создайте функцию, которая будет проверять, находится ли элемент в родительской иерархии другого. Что-то вроде этого (я не буду писать функцию, сделаю ее с WHILE DO):
is_related(id, parent_id);
в вашем примере
is_related(21, 19) == 1;
is_related(20, 19) == 1;
is_related(21, 18) == 0;
2) используйте подвыбор, что-то вроде этого:
select ...
from table t
join table pt on pt.id in (select i.id from table i where is_related(t.id,i.id));
Я сделал запрос для вас. Это даст вам рекурсивную категорию с одним запросом:
SELECT id,NAME,'' AS subName,'' AS subsubName,'' AS subsubsubName FROM Table1 WHERE prent is NULL
UNION
SELECT b.id,a.name,b.name AS subName,'' AS subsubName,'' AS subsubsubName FROM Table1 AS a LEFT JOIN Table1 AS b ON b.prent=a.id WHERE a.prent is NULL AND b.name IS NOT NULL
UNION
SELECT c.id,a.name,b.name AS subName,c.name AS subsubName,'' AS subsubsubName FROM Table1 AS a LEFT JOIN Table1 AS b ON b.prent=a.id LEFT JOIN Table1 AS c ON c.prent=b.id WHERE a.prent is NULL AND c.name IS NOT NULL
UNION
SELECT d.id,a.name,b.name AS subName,c.name AS subsubName,d.name AS subsubsubName FROM Table1 AS a LEFT JOIN Table1 AS b ON b.prent=a.id LEFT JOIN Table1 AS c ON c.prent=b.id LEFT JOIN Table1 AS d ON d.prent=c.id WHERE a.prent is NULL AND d.name IS NOT NULL
ORDER BY NAME,subName,subsubName,subsubsubName
Вот скрипка.