web-dev-qa-db-fra.com

Comment créer une requête récursive hiérarchique MySQL

J'ai une table MySQL qui est comme suit:

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

Maintenant, je veux avoir une seule requête MySQL à laquelle je fournis simplement l'identifiant [par exemple, dites 'id = 19'], puis je devrais obtenir tous ses identifiants enfants [i.e. le résultat devrait avoir les identifiants '20, 21,22 '] .... De plus, la hiérarchie des enfants n'est pas connue, elle peut varier ....

De plus, j'ai déjà la solution en utilisant la boucle for ..... Faites-moi savoir comment réaliser la même chose en utilisant une seule requête MySQL, si possible.

217
Tarun Parswani

Si vous êtes sur MySQL 8, utilisez la clause récursive 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;
_

La valeur spécifiée dans _parent_id = 19_ doit être définie sur id du parent dont vous souhaitez sélectionner tous les descendants.

Avant MySQL 8

Pour les versions de MySQL qui ne prennent pas en charge les expressions de table communes (jusqu'à la version 5.7), réalisez ceci avec la requête suivante:

_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))
_

Voici un violon .

Ici, la valeur spécifiée dans _@pv := '19'_ doit être définie sur id du parent dont vous souhaitez sélectionner tous les descendants.

Cela fonctionnera également si un parent a plusieurs enfants. Cependant, il est nécessaire que chaque enregistrement réponde à la condition _parent_id < id_, sinon les résultats ne seront pas complets.

Assignations de variables dans une requête

Cette requête utilise une syntaxe MySQL spécifique: les variables sont attribuées et modifiées lors de son exécution. Certaines hypothèses sont faites sur l'ordre d'exécution:

  • La clause from est évaluée en premier. C’est donc là que _@pv_ est initialisé.
  • La clause where est évaluée pour chaque enregistrement dans l'ordre d'extraction des alias from. Il s'agit donc d'une condition qui n'inclut que les enregistrements pour lesquels le parent a déjà été identifié comme étant dans l'arbre des descendants (tous les descendants du parent principal sont progressivement ajoutés à _@pv_).
  • Les conditions de cette clause where sont évaluées dans l'ordre et l'évaluation est interrompue une fois que le résultat total est certain. Par conséquent, la deuxième condition doit être à la deuxième place, car elle ajoute id à la liste parente, ce qui ne devrait se produire que si id réussit la première condition. La fonction length est uniquement appelée pour s'assurer que cette condition est toujours vraie, même si la chaîne pv devait pour une raison quelconque donner une valeur falsy.

Au total, on peut trouver ces hypothèses trop risquées. Le documentation avertit:

vous pouvez obtenir les résultats que vous attendez, mais cela n'est pas garanti [...] l'ordre d'évaluation des expressions impliquant des variables utilisateur n'est pas défini.

Ainsi, même si cela fonctionne de manière cohérente avec la requête ci-dessus, l'ordre d'évaluation peut toujours changer, par exemple lorsque vous ajoutez des conditions ou utilisez cette requête en tant que vue ou sous-requête dans une requête plus grande. C'est une "fonctionnalité" qui sera supprimée dans une prochaine version de MySQL :

Les versions précédentes de MySQL permettaient d’attribuer une valeur à une variable utilisateur dans des instructions autres que SET. Cette fonctionnalité est prise en charge dans MySQL 8.0 pour des raisons de compatibilité ascendante, mais peut être supprimée dans une version ultérieure de MySQL.

Comme indiqué ci-dessus, à partir de MySQL 8.0, vous devez utiliser la syntaxe récursive with.

Efficacité

Pour les très grands ensembles de données, cette solution peut être lente, car l'opération find_in_set n'est pas la méthode la plus idéale pour rechercher un nombre dans une liste, et certainement pas dans une liste atteignant une taille inférieure à. le même ordre de grandeur que le nombre d'enregistrements retournés.

Alternative 1: _with recursive_, _connect by_

De plus en plus de bases de données implémentent la syntaxe SQL: 1999 ISO standard _WITH [RECURSIVE]_ pour les requêtes récursives (par exemple, 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 + ). Et à partir de version 8.0, MySQL le supporte également . Voir le haut de cette réponse pour la syntaxe à utiliser.

Certaines bases de données ont une syntaxe alternative non standard pour les recherches hiérarchiques, telle que la clause _CONNECT BY_ disponible sur Oracle , DB2 , Informix , CUBRID et autres bases de données.

MySQL version 5.7 n'offre pas une telle fonctionnalité. Lorsque votre moteur de base de données fournit cette syntaxe ou que vous pouvez migrer vers une autre qui le fait, c'est certainement la meilleure option. Si non, alors considérez également les alternatives suivantes.

Alternative 2: identifiants de style de chemin

Les choses deviennent beaucoup plus faciles si vous affectez des valeurs id contenant les informations hiérarchiques: un chemin. Par exemple, dans votre cas, cela pourrait ressembler à ceci:

_ID       | NAME
19       | category1   
19/1     | category2  
19/1/1   | category3  
19/1/1/1 | category4  
_

Alors, votre select ressemblerait à ceci:

_select  id,
        name 
from    products
where   id like '19/%'
_

Alternative 3: auto-jointes répétées

Si vous connaissez une limite supérieure pour la profondeur de votre arborescence hiérarchique, vous pouvez utiliser une requête standard sql comme celle-ci:

_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;
_

Voir ceci violon

La condition where spécifie le parent dont vous voulez récupérer les descendants. Vous pouvez étendre cette requête avec plus de niveaux si nécessaire.

296
trincot

Depuis le blog Gestion des données hiérarchiques dans MySQL

Structure de la table

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

Requete:

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

Sortie

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

La plupart des utilisateurs, à un moment ou à un autre, ont traité des données hiérarchiques dans une base de données SQL et ont sans aucun doute appris que la gestion des données hiérarchiques n’était pas destinée à une base de données relationnelle. Les tables d'une base de données relationnelle ne sont pas hiérarchiques (comme XML), mais simplement une liste à plat. Les données hiérarchiques ont une relation parent-enfant qui n'est pas naturellement représentée dans une table de base de données relationnelle. en savoir plus

Référez-vous au blog pour plus de détails.

EDIT:

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

Sortie:

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

Référence: Comment faire la requête SELECT récursive dans Mysql?

78
Damodaran

Essayez ces:

Définition du tableau:

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;

Rangées expérimentales:

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

Procédure stockée récursive:

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 ;

Fonction wrapper pour la procédure stockée:

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 ;

Sélectionnez un exemple:

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

Sortie:

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

Filtrage des lignes avec certains chemins:

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

Sortie:

+----+-----------+-----------------------------------------+
| id | name      | path                                    |
+----+-----------+-----------------------------------------+
| 20 | category2 | category1/category2                     |
| 21 | category3 | category1/category2/category3           |
| 22 | category4 | category1/category2/category3/category4 |
+----+-----------+-----------------------------------------+
9
Fandi Susanto

A fait la même chose pour une autre question ici

Mysql select recursive obtient tous les enfants avec plusieurs niveaux

La requête sera:

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
Dheerendra Kulkarni

La meilleure approche que je suis venu avec est

  1. Utilisez la lignée pour stocker\sort\arbres de trace. C'est plus que suffisant et fonctionne des milliers de fois plus rapidement en lecture que toute autre approche. Cela permet également de rester sur ce modèle même si la base de données change (car N'IMPORTE QUELLE base de données autorise l'utilisation de ce modèle)
  2. Utilisez la fonction qui détermine le lignage pour un ID spécifique.
  3. Utilisez-le comme vous le souhaitez (dans les sélections, sur les opérations CUD, ou même par les travaux).

Approche de la lignée descr. peut être trouvé partout, par exemple Ici ou ici . En ce qui concerne la fonction - that c'est ce qui m'a inspiré.

En fin de compte - obtenu une solution plus ou moins simple, relativement rapide et SIMPLE.

Corps de la fonction

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

Et puis tu viens

select get_lineage(the_id)

J'espère que ça aide quelqu'un :)

7
Der Zinger

Si vous avez besoin d'une vitesse de lecture rapide, la meilleure option consiste à utiliser une table de fermeture. Une table de fermeture contient une ligne pour chaque couple ancêtre/descendant. Donc, dans votre exemple, la table de fermeture ressemblerait à

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

Une fois que vous avez cette table, les requêtes hiérarchiques deviennent très faciles et rapides. Pour obtenir tous les descendants de la catégorie 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

Bien sûr, il y a un gros inconvénient lorsque vous utilisez des données dénormalisées comme celle-ci. Vous devez maintenir la table de fermeture à côté de votre table de catégories. Le meilleur moyen est probablement d'utiliser des déclencheurs, mais il est assez complexe de suivre correctement les insertions/mises à jour/suppressions pour les tables de fermeture. Comme pour tout, vous devez examiner vos besoins et décider quelle approche vous convient le mieux.

Éditer : Voir la question Quelles sont les options pour stocker des données hiérarchiques dans une base de données relationnelle? pour plus d'options. Il existe différentes solutions optimales pour différentes situations.

5
Justin Howard

Requête simple pour lister l'enfant de la première récursivité:

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

Résultat:

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

... à gauche rejoindre:

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

La solution de @tincot pour lister tous les enfants:

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)

Testez-le en ligne avec Sql Fiddle et voyez tous les résultats.

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

4
lynx_74

Vous pouvez le faire comme ceci dans d'autres bases de données assez facilement avec une requête récursive (YMMV sur les performances).

L'autre façon de le faire est de stocker deux bits de données supplémentaires, une valeur gauche et droite. Les valeurs left et right sont dérivées d'une traversée pré-ordre de l'arborescence que vous représentez.

Cette opération est connue sous le nom de Traversée de l'arborescence des commandes en attente modifiée et vous permet d'exécuter une requête simple pour obtenir toutes les valeurs parent en même temps. Il porte également le nom "jeu imbriqué".

3
Phil John

C'est un peu délicat, vérifiez si cela fonctionne pour vous

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,%';

Lien de violon SQL http://www.sqlfiddle.com/#!2/e3cdf/2

Remplacez avec votre nom de champ et de table de manière appropriée.

1
senK

Il suffit d'utiliser BlueM/tree La classe php pour créer un arbre d'une table d'auto-relation dans mysql.

Tree et Tree\Node sont des classes PHP permettant de traiter des données structurées hiérarchiquement à l'aide de références d'ID parent. Un exemple typique est une table dans une base de données relationnelle où le champ "parent" de chaque enregistrement fait référence à la clé primaire d’un autre enregistrement. Bien sûr, Tree ne peut pas utiliser uniquement les données provenant d'une base de données, mais n'importe quoi: vous fournissez les données et Tree les utilise, indépendamment de l'origine des données et de la façon dont elles ont été traitées. en savoir plus

Voici un exemple d'utilisation de 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); 
...
1
Saleh Mosleh

enter image description here

C'est une table catégorie .

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)

Sortie :: enter image description here

1
Pradip Rupareliya

Quelque chose non mentionné ici, bien que légèrement similaire à la deuxième alternative de la réponse acceptée, mais différent et peu coûteux pour les requêtes de grande hiérarchie et les éléments faciles (insérer une suppression de mise à jour), consisterait à ajouter une colonne de chemin persistant pour chaque élément.

certains, comme:

id | name        | path
19 | category1   | /19
20 | category2   | /19/20
21 | category3   | /19/20/21
22 | category4   | /19/20/21/22

Exemple:

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

Optimise la longueur du chemin et ORDER BY path en utilisant un codage en base36 à la place d'un identificateur de chemin numérique

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

Supprimer également le séparateur de barre oblique '/' en utilisant une longueur fixe et un remplissage avec l'id codé

Explication détaillée de l'optimisation ici: https://bojanz.wordpress.com/2014/04/25/storing-hierarchical-data-materialized-path/

TODO

construction d'une fonction ou d'une procédure pour diviser le chemin pour retrouver les ancêtres d'un élément

0
MTK

Cela fonctionne pour moi, espérons que cela fonctionnera pour vous aussi. Il vous donnera un ensemble d’enregistrements Racine à Enfant pour tout menu spécifique. Changez le nom du champ selon vos besoins.

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;
0
Monzur

Je l'ai trouvé plus facilement pour:

1) créer une fonction qui vérifiera si un élément est n'importe où dans la hiérarchie parentale d'un autre. Quelque chose comme ça (je ne vais pas écrire la fonction, faites-la avec WHILE DO):

is_related(id, parent_id);

dans votre exemple

is_related(21, 19) == 1;
is_related(20, 19) == 1;
is_related(21, 18) == 0;

2) utilisez une sous-sélection, quelque chose comme ceci:

select ...
from table t
join table pt on pt.id in (select i.id from table i where is_related(t.id,i.id));
0
cripox