2010-09-28 2 views
4

L'idée est simple: j'ai deux tables, catégories et produits.Gestion de l'arborescence dans une procédure MySQL

Catégories:

id | parent_id | name    | count 
1 NULL  Literature   6020 
2 1   Interesting books 1000 
3 1   Horrible books  5000 
4 1   Books to burn  20 
5 NULL  Motorized vehicles 1000 
6 5   Cars     999 
7 5   Motorbikes   1 
... 

Produits:

id | category_id | name 
1 1    Cooking for dummies 
2 3    Twilight saga 
3 5    My grandpa's car 
... 

maintenant lors de l'affichage, la catégorie parente contient tous les produits de toutes les catégories d'enfants. Toute catégorie peut avoir des catégories enfants. Le champ de compte dans la structure de table contient (ou au moins je veux qu'il contienne) le compte de tous les produits affichés dans cette catégorie particulière. Sur le front-end, je sélectionne toutes les sous-catégories avec une simple fonction récursive, mais je ne suis pas sûr de savoir comment le faire dans une procédure SQL (oui, il doit s'agir d'une procédure SQL ). catégories de toute nature et il existe plus de 100 000 produits.
Des idées?

Répondre

3

Jetez un oeil à this article sur la gestion des arbres heirachical dans MySQL.

Il explique les inconvénients de votre méthode actuelle et certaines solutions plus optimales.

Voir en particulier la section vers la fin intitulée «Fonctions agrégées dans un ensemble imbriqué».

+0

C'est vraiment sympa, mais je ne peux pas changer la méthode de stockage des données, et aussi, après un rapide coup d'œil, cela n'explique pas quoi faire avec une profondeur inconnue d'un arbre. – cypher

+0

Pas besoin de s'inquiéter de la profondeur avec la ressource d'en haut. Ceci est géré de la façon dont vous décrivez la hiérarchie. – DrColossos

+0

+1 BTW pour pointer vers un de mes articles préférés sur la hiérarchie – DrColossos

0

Ce que vous voulez est une expression de table commune. Malheureusement, il semble que mysql ne les supporte pas. Au lieu de cela, vous aurez probablement besoin d'utiliser une boucle pour continuer à sélectionner des arbres plus profonds.

Je vais essayer un exemple. Pour clarifier, vous cherchez à pouvoir appeler la procédure avec une entrée de dire '1' et récupérer toutes les sous-catégories et sous-catégories (et ainsi de suite) avec 1 comme racine éventuelle? comme

id parent 
1 null 
2 1 
3 1 
4 2 

?

Modifié:

C'est ce que je suis venu avec, il semble fonctionner. Malheureusement, je n'ai pas mysql, donc j'ai dû utiliser SQL Server. J'ai essayé de vérifier everythign pour m'assurer qu'il fonctionnera avec mysql mais il y a peut-être encore des problèmes.

declare @input int 
set @input = 1 

--not needed, but informative 
declare @depth int 
set @depth = 0 

--for breaking out of the loop 
declare @break int 
set @break = 0 

--my table '[recursive]' is pretty simple, the results table matches it 
declare @results table 
(
    id int, 
    parent int, 
    depth int 
) 

--Seed the results table with the root node 
insert into @results 
select id, parent, @depth from [recursive] 
where ID = @input 

--Loop through, adding notes as we go 
set @break = 1 
while (@break > 0) 
begin 
    set @[email protected]+1 --Increase the depth counter each loop 

    --This checks to see how many rows we are about to add to the table. 
    --If we don't add any rows, we can stop looping 
    select @break = count(id) from [recursive] 
    where parent in 
    (
     select id from @results 
    ) 
    and id not in --Don't add rows that are already in the results 
    (
     select id from @results 
    ) 

    --Here we add the rows to the results table 
    insert into @results 
    select id, parent, @depth from [recursive] 
    where parent in 
    (
     select id from @results 
    ) 
    and id not in --Don't add rows that are already in the results 
    (
     select id from @results 
    ) 
end 

--Select the results and return 
select * from @results 
+0

Oui, cela semble à peu près exact, malheureusement je ne sais pas quelle profondeur d'arbre puis-je espérer. Il peut être trois, il peut être trois hundread ... – cypher

+0

Edité avec une boucle qui pourrait fonctionner – david

5

Bill Karwin made some nice slides about hierachical data, et le modèle actuel contiguïté certainement comme les pros, mais ce n'est pas très adapté pour cela (obtenir un sous-arbre entier). Pour mes tables adjacentes, je le résous en stockant/mettant en cache le chemin (éventuellement dans un script, ou dans un 'before update trigger'), lors du changement d'identifiant parent_id, une nouvelle chaîne est créée.Votre table actuelle ressemblerait à ceci:

id | parent_id | path | name    | count 
1 NULL  1   Literature   6020 
2 1   1:2  Interesting books 1000 
3 1   1:3  Horrible books  5000 
4 1   1:4  Books to burn  20 
5 NULL  5   Motorized vehicles 1000 
6 5   5:6  Cars     999 
7 5   5:7  Motorbikes   1 

(choisir tout delimiter pas trouvé dans l'ID que vous aimez)

Donc, maintenant pour obtenir tous les produits d'une catégorie + sous-catégories:

SELECT p.* 
FROM categories c_main 
JOIN categories c_subs 
ON c_subs.id = c_main.id 
    OR c_subs.path LIKE CONCAT(c_main,':%') 
JOIN products p 
ON p.category_id = c_subs.id 
WHERE c_main.id = <id> 
+0

Cela semble tout à fait raisonnable, malheureusement aucune de ces réponses a répondu à ma question initiale, donc il a été attribué +50. Merci à tous. – cypher

+0

Eh bien, ce _can_ va dans une procédure, mais je vois votre point, hélas une seule méthode non récursive n'est pas possible avec votre modèle Adjacency pur AFAIK, d'où cette réponse avec un mélange de contiguïté et de chemin énumération (une réponse terriblement épelée pourrait ajouter, en effet posté tard dans la nuit). – Wrikken

3

Il y a un chapitre entier dans "Antipatterns SQL évitant les pièges de la programmation de base de données" par Bill Karwin sur la gestion des données hiérarchiques en SQL.

alt text

+2

Dommage que MySQL ne supporte pas les requêtes récursives –

+1

@MDSayemAhmed, MySQL 8, qui est actuellement en version bêta, va * enfin * supporter les requêtes récursives. –

0

Essayez de se débarrasser de la hiérarchie qui est mise en œuvre de cette façon. La récursion dans les procédures stockées n'est pas agréable, et par exemple, sur MS SQL, ils échouent après le 64ème niveau. De plus, pour obtenir par exemple tout de certaines catégories et sous-catégories, vous devrez descendre de façon récursive, ce qui n'est pas pratique pour SQL - mais pour dire lent.

À la place, utilisez ceci; créer champ category_path, et la faire ressembler à:

category_path name 
1/   literature 
1/2/   Interesting books 
1/3/   Horrible books 
1/4/   Books to burn 
5/   Motorized vehicles 
5/6/   Cars 
5/7/   Motorbikes 

En utilisant cette méthode, vous pourrez sélectionner les catégories et sous-catégories très rapide. Les mises à jour seront lentes, mais je suppose qu'elles peuvent être lentes. En outre, vous pouvez conserver vos anciens champs de relation parent-enfant pour vous aider à conserver votre structure arborescente.

Par exemple, obtenir toutes les voitures, sans récursion, sera:

SELECT * FROM ttt WHERE category_path LIKE '5/%' 
1

Comme vous ne l'avez accepté une réponse mais je pensais que je posterai ma méthode pour le traitement des arbres dans mysql et php. (Seul appel db à sproc non récursive)

script complet ici: http://pastie.org/1252426 ou voir ci-dessous ...

Hope this helps :)

PHP

<?php 
$conn = new mysqli("localhost", "foo_dbo", "pass", "foo_db", 3306); 

$result = $conn->query(sprintf("call product_hier(%d)", 3)); 

echo "<table border='1'> 
     <tr><th>prod_id</th><th>prod_name</th><th>parent_prod_id</th> 
     <th>parent_prod_name</th><th>depth</th></tr>"; 

while($row = $result->fetch_assoc()){ 
    echo sprintf("<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>", 
     $row["prod_id"],$row["prod_name"],$row["parent_prod_id"], 
     $row["parent_prod_name"],$row["depth"]); 
} 
echo "</table>"; 
$result->close(); 
$conn->close(); 
?> 

SQL

drop table if exists product; 

create table product 
(
prod_id smallint unsigned not null auto_increment primary key, 
name varchar(255) not null, 
parent_id smallint unsigned null, 
key (parent_id) 
)engine = innodb; 


insert into product (name, parent_id) values 
('Products',null), 
    ('Systems & Bundles',1), 
    ('Components',1), 
     ('Processors',3), 
     ('Motherboards',3), 
     ('AMD',5), 
     ('Intel',5), 
      ('Intel LGA1366',7); 


delimiter ; 

drop procedure if exists product_hier; 

delimiter # 

create procedure product_hier 
(
in p_prod_id smallint unsigned 
) 
begin 

declare v_done tinyint unsigned default 0; 
declare v_depth smallint unsigned default 0; 

create temporary table hier(
parent_id smallint unsigned, 
prod_id smallint unsigned, 
depth smallint unsigned default 0 
)engine = memory; 

insert into hier select parent_id, prod_id, v_depth from product where prod_id = p_prod_id; 

/* http://dev.mysql.com/doc/refman/5.0/en/temporary-table-problems.html */ 

create temporary table tmp engine=memory select * from hier; 

while not v_done do 

    if exists(select 1 from product p inner join hier on p.parent_id = hier.prod_id and hier.depth = v_depth) then 

     insert into hier 
      select p.parent_id, p.prod_id, v_depth + 1 from product p 
      inner join tmp on p.parent_id = tmp.prod_id and tmp.depth = v_depth; 

     set v_depth = v_depth + 1;   

     truncate table tmp; 
     insert into tmp select * from hier where depth = v_depth; 

    else 
     set v_done = 1; 
    end if; 

end while; 

select 
p.prod_id, 
p.name as prod_name, 
b.prod_id as parent_prod_id, 
b.name as parent_prod_name, 
hier.depth 
from 
hier 
inner join product p on hier.prod_id = p.prod_id 
inner join product b on hier.parent_id = b.prod_id 
order by 
hier.depth, hier.prod_id; 

drop temporary table if exists hier; 
drop temporary table if exists tmp; 

end # 

delimiter ; 

call product_hier(3); 

call product_hier(5); 
Questions connexes