Sélectionnez des produits dont la catégorie appartient à une catégorie de la hiérarchie.
-
10-07-2019 - |
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?
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.