2013-02-02 20 views
6

Ho visto this answer e spero che non sia corretto, proprio come qualcuno non ha detto correttamente che le chiavi primarie sono su una colonna e non posso impostarlo su più colonne.MySQL ricorsivo seleziona?

Ecco il mio tavolo

create table Users(id INT primary key AUTO_INCREMENT, 
    parent INT, 
    name TEXT NOT NULL, 
    FOREIGN KEY(parent) 
    REFERENCES Users(id) 
); 


+----+--------+---------+ 
| id | parent | name | 
+----+--------+---------+ 
| 1 | NULL | root | 
| 2 |  1 | one  | 
| 3 |  1 | 1down | 
| 4 |  2 | one_a | 
| 5 |  4 | one_a_b | 
+----+--------+---------+ 

mi piacerebbe selezionare id utente 2 e ricorsivamente in modo da ottenere tutta la sua bambina diretta e indiretta (in modo id 4 e 5).

Come si scrive in modo che funzioni? Ho visto la ricorsione in postgresql e sqlserver.

+0

Bill Karwin è corretto. MySQL non ha alcuna funzione per le query ricorsive proprio come 'SQL Server' in quanto ha' CTE'. ma il comportamento della ricorsione può ancora essere simulato. ': D' –

+0

Non penso che si possa fare la ricorsione in MySQL con una singola query, ma ho fatto una gerarchia genitoriale simile interrogando tramite stored procedure che continuano a cercare il livello genitore finché non sono state trovate più voci genitoriali ... ma fatte tramite una tabella temporanea che viene cancellata al termine ... funzionerebbe per te? – DRapp

+0

@DRapp: Probabilmente sarebbe accettabile. Sarebbe divertente imparare in entrambi i modi –

risposta

14
CREATE DEFINER = 'root'@'localhost' 
PROCEDURE test.GetHierarchyUsers(IN StartKey INT) 
BEGIN 
    -- prepare a hierarchy level variable 
    SET @hierlevel := 00000; 

    -- prepare a variable for total rows so we know when no more rows found 
    SET @lastRowCount := 0; 

    -- pre-drop temp table 
    DROP TABLE IF EXISTS MyHierarchy; 

    -- now, create it as the first level you want... 
    -- ie: a specific top level of all "no parent" entries 
    -- or parameterize the function and ask for a specific "ID". 
    -- add extra column as flag for next set of ID's to load into this. 
    CREATE TABLE MyHierarchy AS 
    SELECT U.ID 
     , U.Parent 
     , U.`name` 
     , 00 AS IDHierLevel 
     , 00 AS AlreadyProcessed 
    FROM 
    Users U 
    WHERE 
    U.ID = StartKey; 

    -- how many rows are we starting with at this tier level 
    -- START the cycle, only IF we found rows... 
    SET @lastRowCount := FOUND_ROWS(); 

    -- we need to have a "key" for updates to be applied against, 
    -- otherwise our UPDATE statement will nag about an unsafe update command 
    CREATE INDEX MyHier_Idx1 ON MyHierarchy (IDHierLevel); 


    -- NOW, keep cycling through until we get no more records 
    WHILE @lastRowCount > 0 
    DO 

    UPDATE MyHierarchy 
    SET 
     AlreadyProcessed = 1 
    WHERE 
     IDHierLevel = @hierLevel; 

    -- NOW, load in all entries found from full-set NOT already processed 
    INSERT INTO MyHierarchy 
    SELECT DISTINCT U.ID 
        , U.Parent 
        , U.`name` 
        , @hierLevel + 1 AS IDHierLevel 
        , 0 AS AlreadyProcessed 
    FROM 
     MyHierarchy mh 
    JOIN Users U 
    ON mh.Parent = U.ID 
    WHERE 
     mh.IDHierLevel = @hierLevel; 

    -- preserve latest count of records accounted for from above query 
    -- now, how many acrual rows DID we insert from the select query 
    SET @lastRowCount := ROW_COUNT(); 


    -- only mark the LOWER level we just joined against as processed, 
    -- and NOT the new records we just inserted 
    UPDATE MyHierarchy 
    SET 
     AlreadyProcessed = 1 
    WHERE 
     IDHierLevel = @hierLevel; 

    -- now, update the hierarchy level 
    SET @hierLevel := @hierLevel + 1; 

    END WHILE; 


    -- return the final set now 
    SELECT * 
    FROM 
    MyHierarchy; 

-- and we can clean-up after the query of data has been selected/returned. 
-- drop table if exists MyHierarchy; 


END 

Potrebbe sembrare ingombrante, ma di utilizzare questo, do

call GetHierarchyUsers(5); 

(o qualsiasi altra cosa ID chiave che si desidera trovare l'albero gerarchico per).

La premessa è iniziare con la CHIAVE con cui si sta lavorando. Quindi, usalo come base per unirti alla tabella degli utenti, ma in base all'ID del genitore della prima voce. Una volta trovato, aggiorna la tabella temporale per non tentare di unirsi a quella chiave di nuovo nel ciclo successivo. Quindi continua fino a quando non è possibile trovare più chiavi ID "parent".

Ciò restituirà l'intera gerarchia di record al padre, indipendentemente dalla profondità del nesting. Tuttavia, se vuoi solo il genitore FINALE, puoi usare la variabile @hierlevel per restituire solo l'ultima nel file aggiunto, o ORDER BY e LIMIT 1

+0

wow che è gentile di molto: | +1 e accettato –

+0

Questa è una buona soluzione, ma se la query viene eseguita una seconda volta prima del completamento della prima esecuzione, la tabella MyHeirarchy temporanea verrà eliminata e la prima query avrà esito negativo. La creazione della tabella temporanea con un timestamp nel nome e l'eliminazione al termine della procedura (anziché all'inizio) risolverà il problema. –

+2

@andrewlorien, sì, è vero, ma allora hai a che fare con dynamic-sql. Un'altra alternativa è usare le tabelle #tempTable names (o ## temp) che sono uniche per connessione e/o utente. Ciò impedirebbe l'eliminazione accidentale da un utente a un altro. – DRapp

4

So che probabilmente c'è una risposta migliore e più efficiente sopra ma questo frammento fornisce un approccio leggermente diverso e fornisce sia antenati che figli.

L'idea è di inserire costantemente rowIds relativi nella tabella temporanea, quindi recuperare una riga per cercare i suoi parenti, risciacquare ripetizione fino a quando tutte le righe vengono elaborate. La query può essere probabilmente ottimizzata per utilizzare solo 1 tabella temporanea.

Questo è un esempio operativo sqlfiddle.

CREATE TABLE Users 
     (`id` int, `parent` int,`name` VARCHAR(10))// 

    INSERT INTO Users 
     (`parent`, `name`) 
    VALUES 
     (1, NULL, 'root'), 
     (2, 1, 'one'), 
     (3, 1, '1down'), 
     (4, 2, 'one_a'), 
     (5, 4, 'one_a_b')// 

    CREATE PROCEDURE getAncestors (in ParRowId int) 
    BEGIN 
     DECLARE tmp_parentId int; 
     CREATE TEMPORARY TABLE tmp (parentId INT NOT NULL); 
     CREATE TEMPORARY TABLE results (parentId INT NOT NULL); 
     INSERT INTO tmp SELECT ParRowId; 
     WHILE (SELECT COUNT(*) FROM tmp) > 0 DO 
     SET tmp_parentId = (SELECT MIN(parentId) FROM tmp); 
     DELETE FROM tmp WHERE parentId = tmp_parentId; 
     INSERT INTO results SELECT parent FROM Users WHERE id = tmp_parentId AND parent IS NOT NULL; 
     INSERT INTO tmp SELECT parent FROM Users WHERE id = tmp_parentId AND parent IS NOT NULL; 
     END WHILE; 
     SELECT * FROM Users WHERE id IN (SELECT * FROM results); 
    END// 

    CREATE PROCEDURE getChildren (in ParRowId int) 
    BEGIN 
     DECLARE tmp_childId int; 
     CREATE TEMPORARY TABLE tmp (childId INT NOT NULL); 
     CREATE TEMPORARY TABLE results (childId INT NOT NULL); 
     INSERT INTO tmp SELECT ParRowId; 
     WHILE (SELECT COUNT(*) FROM tmp) > 0 DO 
     SET tmp_childId = (SELECT MIN(childId) FROM tmp); 
     DELETE FROM tmp WHERE childId = tmp_childId; 
     INSERT INTO results SELECT id FROM Users WHERE parent = tmp_childId; 
     INSERT INTO tmp SELECT id FROM Users WHERE parent = tmp_childId; 
     END WHILE; 
     SELECT * FROM Users WHERE id IN (SELECT * FROM results); 
    END// 

Usage:

CALL getChildren(2); 

    -- returns 
    id parent name 
    4 2 one_a 
    5 4 one_a_b 


CALL getAncestors(5); 

    -- returns 
    id parent name 
    1 (null) root 
    2 1 one 
    4 2 one_a