Sélectionnez des produits dont la catégorie appartient à une catégorie de la hiérarchie.

StackOverflow https://stackoverflow.com/questions/197362

Question

J'ai une table de produits contenant un FK pour une catégorie. La table de catégories est créée de manière à ce que chaque catégorie puisse avoir une catégorie parente, exemple:

Computers
    Processors
        Intel
            Pentium
            Core 2 Duo
        AMD
            Athlon

Je dois effectuer une requête de sélection sur le fait que, si la catégorie sélectionnée est Processeurs, les produits au format Intel, Pentium, Core 2 Duo, Amd, etc. seront renvoyés.

J'ai pensé créer une sorte de " cache " qui stockera toutes les catégories dans la hiérarchie pour chaque catégorie de la base de données et inclura le symbole "IN". dans la clause where. Est-ce la meilleure solution?

Était-ce utile?

La solution

La meilleure solution à cela est au stade de la conception de la base de données. Votre table de catégories doit être un ensemble imbriqué . L'article La gestion des données hiérarchiques dans MySQL n'est pas spécifique à MySQL ( malgré le titre) et donne un bon aperçu des différentes méthodes de stockage d’une hiérarchie dans une table de base de données.

Résumé:

Ensembles imbriqués

  • Les sélections sont faciles pour toutes les profondeurs
  • Les insertions et les suppressions sont difficiles

Hiérarchie standard basée sur parent_id

  • Les sélections sont basées sur les jointures internes (donc soyez poilu rapidement)
  • Les insertions et les suppressions sont faciles

Donc, selon votre exemple, si votre table hiérarchique était un jeu imbriqué, votre requête ressemblerait à ceci:

SELECT * FROM products 
   INNER JOIN categories ON categories.id = products.category_id 
WHERE categories.lft > 2 and categories.rgt < 11

Les 2 et 11 sont respectivement les côtés gauche et droit de l’enregistrement Processors .

Autres conseils

Ressemble à un travail pour une expression de table commune .. quelque chose dans la lignée de:

with catCTE (catid, parentid)
as
(
select cat.catid, cat.catparentid from cat where cat.name = 'Processors'
UNION ALL
select cat.catid, cat.catparentid from cat inner join catCTE on cat.catparentid=catcte.catid
)
select distinct * from catCTE

Cela devrait sélectionner la catégorie dont le nom est "Processors" et l'un de ses descendants, devrait pouvoir l'utiliser dans une clause IN pour récupérer les produits.

J'ai déjà fait des choses similaires dans le passé, en recherchant d'abord les identifiants de catégorie, puis en recherchant les produits "IN". ces catégories. Obtenir les catégories est difficile, et vous avez quelques options:

  • Si le niveau d'imbrication des catégories est connu ou si vous pouvez trouver une limite supérieure: construisez un SELECT d'apparence horrible avec beaucoup de JOIN. C’est rapide, mais moche et vous devez définir une limite pour les niveaux de la hiérarchie.
  • Si vous avez un nombre relativement petit de catégories au total, interrogez-les toutes (identifiants simplement, parents), collectez les identifiants de ceux qui vous intéressent et effectuez un SELECT ... IN pour les produits. C’était l’option appropriée pour moi.
  • Interrogez la hiérarchie vers le haut ou le bas à l'aide d'une série de commandes SELECT. Simple mais relativement lent.
  • Je pense que les versions récentes de SQLServer prennent en charge certaines requêtes récursives, mais ne les ont pas utilisées moi-même.

Les procédures stockées peuvent vous aider si vous ne souhaitez pas utiliser cette application.

Ce que vous voulez trouver est la fermeture transitive de la catégorie "parent". relation. Je suppose qu'il n'y a pas de limite à la profondeur de la hiérarchie des catégories, vous ne pouvez donc pas formuler une seule requête SQL qui trouve toutes les catégories. Voici ce que je ferais (en pseudocode):

categoriesSet = empty set
while new.size > 0:
  new = select * from categories where parent in categoriesSet
  categoriesSet = categoriesSet+new

Continuez donc à interroger les enfants jusqu'à ce qu'ils n'en trouvent plus. Cela se comporte bien en termes de vitesse, sauf si vous avez une hiérarchie dégénérée (par exemple, 1000 catégories, chacune étant l'enfant d'un autre), ou un grand nombre de catégories totales. Dans le second cas, vous pouvez toujours utiliser des tables temporaires pour limiter les transferts de données entre votre application et la base de données.

Peut-être quelque chose comme:

select *
from products
where products.category_id IN
  (select c2.category_id 
   from categories c1 inner join categories c2 on c1.category_id = c2.parent_id
   where c1.category = 'Processors'
   group by c2.category_id)

[EDIT] Si la profondeur de la catégorie est supérieure à 1, cela formerait votre requête la plus interne. Je soupçonne que vous pourriez concevoir une procédure stockée permettant d’explorer la table jusqu’à ce que les ID renvoyés par la requête interne n’aient pas d’enfants - il est probablement préférable d’avoir un attribut qui marque une catégorie en tant que nœud terminal dans la hiérarchie - puis effectuer la requête externe sur ces identifiants.

CREATE TABLE #categories (id INT NOT NULL, parentId INT, [name] NVARCHAR(100))
INSERT INTO #categories
    SELECT 1, NULL, 'Computers'
    UNION
SELECT 2, 1, 'Processors'
    UNION
SELECT 3, 2, 'Intel'
    UNION
SELECT 4, 2, 'AMD'
    UNION
SELECT 5, 3, 'Pentium'
    UNION
SELECT 6, 3, 'Core 2 Duo'
    UNION
SELECT 7, 4, 'Athlon'
SELECT * 
    FROM #categories
DECLARE @id INT
    SET @id = 2
            ; WITH r(id, parentid, [name]) AS (
    SELECT id, parentid, [name] 
        FROM #categories c 
        WHERE id = @id
        UNION ALL
    SELECT c.id, c.parentid, c.[name] 
        FROM #categories c  JOIN r ON c.parentid=r.id
    )
SELECT * 
    FROM products 
    WHERE p.productd IN
(SELECT id 
    FROM r)
DROP TABLE #categories   

La dernière partie de l'exemple ne fonctionne pas réellement si vous l'exécutez comme ceci. Supprimez simplement la sélection parmi les produits et remplacez-la par un simple SELECT * FROM r

Ceci devrait récapituler toutes les catégories "enfants" à partir d'une catégorie donnée.

DECLARE @startingCatagoryId int
DECLARE @current int
SET @startingCatagoryId = 13813 -- or whatever the CatagoryId is for 'Processors'

CREATE TABLE #CatagoriesToFindChildrenFor
(CatagoryId int)

CREATE TABLE #CatagoryTree
(CatagoryId int)

INSERT INTO #CatagoriesToFindChildrenFor VALUES (@startingCatagoryId)

WHILE (SELECT count(*) FROM #CatagoriesToFindChildrenFor) > 0
BEGIN
    SET @current = (SELECT TOP 1 * FROM #CatagoriesToFindChildrenFor)

    INSERT INTO #CatagoriesToFindChildrenFor
    SELECT ID FROM Catagory WHERE ParentCatagoryId = @current AND Deleted = 0

    INSERT INTO #CatagoryTree VALUES (@current)
    DELETE #CatagoriesToFindChildrenFor WHERE CatagoryId = @current
END

SELECT * FROM #CatagoryTree ORDER BY CatagoryId

DROP TABLE #CatagoriesToFindChildrenFor
DROP TABLE #CatagoryTree

j’aime utiliser une table temporaire de pile pour les données hiérarchiques. voici un exemple approximatif -

-- create a categories table and fill it with 10 rows (with random parentIds)
CREATE TABLE Categories ( Id uniqueidentifier, ParentId uniqueidentifier )
GO

INSERT
INTO   Categories
SELECT NEWID(),
       NULL 
GO

INSERT
INTO   Categories
SELECT   TOP(1)NEWID(),
         Id
FROM     Categories
ORDER BY Id
GO 9


DECLARE  @lvl INT,            -- holds onto the level as we move throught the hierarchy
         @Id Uniqueidentifier -- the id of the current item in the stack

SET @lvl = 1

CREATE TABLE #stack (item UNIQUEIDENTIFIER, [lvl] INT)
-- we fill fill this table with the ids we want
CREATE TABLE #tmpCategories (Id UNIQUEIDENTIFIER)

-- for this example we’ll just select all the ids 
-- if we want all the children of a specific parent we would include it’s id in
-- this where clause
INSERT INTO #stack SELECT Id, @lvl FROM Categories WHERE ParentId IS NULL

WHILE @lvl > 0
BEGIN -- begin 1

      IF EXISTS ( SELECT * FROM #stack WHERE lvl = @lvl )
      BEGIN -- begin 2

      SELECT @Id = [item]
      FROM #stack
      WHERE lvl = @lvl

      INSERT INTO #tmpCategories
      SELECT @Id

      DELETE FROM #stack
      WHERE lvl = @lvl
      AND item = @Id

      INSERT INTO #stack
      SELECT Id, @lvl + 1
      FROM   Categories
      WHERE  ParentId = @Id

      IF @@ROWCOUNT > 0
      BEGIN -- begin 3
         SELECT @lvl = @lvl + 1
      END -- end 3
   END -- end 2
   ELSE
   SELECT @lvl = @lvl - 1

END -- end 1

DROP TABLE #stack

SELECT * FROM #tmpCategories
DROP TABLE #tmpCategories
DROP TABLE Categories

il existe une bonne explication ici texte du lien

Ma réponse à une autre question d'il y a quelques jours s'applique ici ... récursivité en SQL

Il existe dans le livre des méthodes que je vous ai associées et qui devraient bien couvrir votre situation.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top