Domanda

Ho una tabella di prodotti che contiene un FK per una categoria, la tabella Categorie viene creata in modo tale che ogni categoria possa avere una categoria padre, ad esempio:

Computers
    Processors
        Intel
            Pentium
            Core 2 Duo
        AMD
            Athlon

Devo fare una query di selezione che, se la categoria selezionata è Processori, restituirà prodotti che sono in Intel, Pentium, Core 2 Duo, Amd, ecc ...

Ho pensato di creare una sorta di " cache " che memorizzerà tutte le categorie nella gerarchia per ogni categoria nel db e includerà " IN " nella clausola where. Questa è la soluzione migliore?

È stato utile?

Soluzione

La migliore soluzione per questo è in fase di progettazione del database. La tabella delle categorie deve essere un set nidificato . L'articolo Gestione dei dati gerarchici in MySQL non è specifico di MySQL ( nonostante il titolo) e offre una grande panoramica dei diversi metodi di memorizzazione di una gerarchia in una tabella di database.

Riepilogo esecutivo:

Set nidificati

  • Le selezioni sono facili per qualsiasi profondità
  • Inserimenti ed eliminazioni sono difficili

Gerarchia basata su parent_id standard

  • Le selezioni si basano su join interni (quindi diventa peloso in fretta)
  • Inserimenti ed eliminazioni sono facili

Quindi, in base al tuo esempio, se la tua tabella gerarchica fosse un set nidificato, la tua query sarebbe simile a questa:

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

il 2 e l'11 sono rispettivamente a sinistra e a destra del record Processors .

Altri suggerimenti

Sembra un lavoro per un'espressione di tabella comune ... qualcosa sulla falsariga di:

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

Questo dovrebbe selezionare la categoria il cui nome è 'Processori' e uno qualsiasi dei suoi discendenti, dovrebbe essere in grado di usarlo in una clausola IN per ritirare i prodotti.

Ho fatto cose simili in passato, prima facendo una query per gli ID di categoria, quindi facendo una query per i prodotti " IN " quelle categorie. Ottenere le categorie è la cosa più difficile e hai alcune opzioni:

  • Se il livello di annidamento delle categorie è noto o è possibile trovare un limite superiore: crea un SELECT dall'aspetto orribile con molti JOIN. Questo è veloce, ma brutto e devi impostare un limite ai livelli della gerarchia.
  • Se hai un numero relativamente piccolo di categorie totali, interrogale tutte (solo ID, genitori), raccogli gli ID di quelli a cui tieni e fai un SELEZIONA .... IN per i prodotti. Questa è stata l'opzione appropriata per me.
  • Esegui query su / giù nella gerarchia utilizzando una serie di SELECT. Semplice, ma relativamente lento.
  • Credo che le versioni recenti di SQLServer abbiano un po 'di supporto per le query ricorsive, ma non le ho usate io.

Le stored procedure possono essere utili se non si desidera eseguire questo lato app.

Quello che vuoi trovare è la chiusura transitiva della categoria "parent" relazione. Suppongo che non ci siano limiti alla profondità della gerarchia di categorie, quindi non è possibile formulare una singola query SQL che trovi tutte le categorie. Quello che vorrei fare (in pseudocodice) è questo:

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

Quindi continua a fare query per i bambini fino a quando non ne vengono più trovati. Questo si comporta bene in termini di velocità a meno che tu non abbia una gerarchia degenerata (diciamo, 1000 categorie, ognuna figlia di un'altra) o un gran numero di categorie totali. Nel secondo caso, puoi sempre lavorare con tabelle temporanee per mantenere piccolo il trasferimento di dati tra la tua app e il database.

Forse qualcosa del tipo:

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)

[MODIFICA] Se la profondità della categoria è maggiore di una, ciò costituirebbe la tua query più interna. Ho il sospetto che tu possa progettare una procedura memorizzata che eseguirà il drill down nella tabella fino a quando gli ID restituiti dalla query interna non avessero figli - probabilmente è meglio avere un attributo che contrassegna una categoria come nodo terminale nella gerarchia - quindi eseguire la query esterna su tali ID.

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   

L'ultima parte dell'esempio in realtà non funziona se la stai eseguendo in questo modo. Basta rimuovere la selezione dai prodotti e sostituirla con un semplice SELEZIONA * DA r

Questo dovrebbe richiamare tutte le categorie "figlio" a partire da una data categoria.

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

Mi piace usare una tabella temporanea dello stack per i dati gerarchici. ecco un esempio approssimativo -

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

c'è una buona spiegazione qui testo del link

La mia risposta a un'altra domanda di un paio di giorni fa si applica qui ... ricorsione in SQL

Ci sono alcuni metodi nel libro che ho collegato che dovrebbero coprire bene la tua situazione.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top