2013-02-02 7 views
6

私はthis answerを見ましたが、彼が間違っていることを願っています。誰かが間違ってプライマリキーが列にあり、複数の列に設定できないように思います。ここで再帰的なmysqlを選択しますか?

が私のテーブルが

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

である私は、私はそのすべての直接および間接の子を取得します(そうID 4および5)ユーザーID 2を選択して、再帰的にしたいと思います。

これはどのように動作しますか?私はpostgresqlとsqlserverで再帰を見た。

+0

ビルKarwinが正しいです。 MySQLには 'CTE'を持っているので' SQL Server'のような再帰的なクエリのための関数はありません。再帰の振る舞いはまだシミュレートできます。 ':D' –

+0

私はあなたが単一のクエリでMySQLで再帰を行うことはできないと思いますが、親エントリが見つからなくなるまで親レベルを探し続けるストアドプロシージャを使って同様の親階層クエリを実行しました。終了時に削除される一時テーブルを介して...それはあなたのために働くだろうか? – DRapp

+0

@DRapp:それはおそらく受け入れられるでしょう。いずれかの方法を学ぶことは楽しいだろう –

答えて

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 

それは面倒表示されることがありますが、これを使用するためには、

call GetHierarchyUsers(5); 

を行う(または任意のキーIDあなたがのために階層ツリーをUP見つけたいです)。

前提は、使用している1つのキーから始めることです。その後、それをユーザーテーブルAGAINに参加するための基礎として使用しますが、最初のエントリのPARENT IDに基づいて行います。見つかったら、テンポラリテーブルを更新して、次のサイクルで再びそのキーの試行と参加をしないようにします。その後、親のIDキーが見つからなくなるまで続けてください。

これは、ネストの深さにかかわらず、親レコードまでの階層全体を返します。しかし、FINALの親だけが必要な場合は、@hierlevel変数を使って、追加されたファイル、またはORDER BYとLIMITの中の最新のものだけを返すことができます1

+0

うわーは親切ですたくさんの:| +1&accepted –

+0

これは良い解決策ですが、最初の実行が完了する前にクエリを2回実行すると、一時的なMyHeirarchyテーブルが削除され、最初のクエリが失敗します。テンポラリテーブルを名前にタイムスタンプを付けて作成し、プロシージャの最後に(開始時ではなく)削除すると、その問題が解決されます。 –

+2

@andrewlorien、はい、それは本当ですが、あなたはdynamic-sqlを扱っています。もう1つの方法は、#tempTableの名前(または## temp)テーブルを使用することです。このテーブルは、接続および/またはユーザーごとに一意です。これは、あるユーザーから別のユーザーへの誤った削除を防ぎます。 – DRapp

4

私はおそらくもっと良い、このスニペットはわずかに異なるアプローチを提供し、祖先と子供の両方を提供します。

考えられるのは、常に相対的なrowIdsをテンポラリテーブルに挿入し、親を探す行をフェッチし、すべての行が処理されるまで繰り返しリンスすることです。おそらく1つのテンポラリテーブルのみを使用するようにクエリを最適化できます。

ここには、作業中の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// 

使用法:

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