web-dev-qa-db-ja.com

2つのテーブル間の関係の深さを検出するためのPL / pgSQL関数またはrCTE

関係の深さをチェックするためにPostgreSQL関数を実行する必要があります。テーブルaからbへの循環バインディングがあり、a内の別の要素に戻り、場合によっては再びbに戻ります。これらの関係は数回可能です。これらの関係を要求する関数を作成しようとしています。

単純なSQLクエリを作成しようとしましたが、メモリオーバーフローエラーが発生し、1億行を超える結果テーブルが作成されました。 1000万行を超えるテーブル全体をチェックする場合。そこで、各ループの後にストレージを拒否するFORループを試します。

問題は、ループが関係1にカウントされるが、それ以上カウントされない最初の要素まで実行されることです。それ以降のすべてのリレーションはリレーションカウント0を取得し、bstidは単一の値のままです。

デバッグ制御を取得するために、いくつかのRETURNNEXTステートメントを配置しました。ネストされたIF-ELSEステートメントに何か問題がありますか?

CREATE OR REPLACE FUNCTION bst_ebenen() RETURNS SETOF varchar(1000) AS 
$BODY$ 
DECLARE 
    --an_array varchar[bstobjid][id];
    bstobjid varchar;
    loopid bigint;
    bstid bigint;
    relation varchar;
    searchsql text := '';
    ebenen integer := 0;
    message varchar := '';

BEGIN
    searchsql = 'SELECT objid AS bstobjid FROM buchstelle';

    FOR bstobjid IN EXECUTE(searchsql) LOOP

        ebenen = 0;

        --1. Relationsebene abfragen
        loopid = (SELECT bst.id FROM buchstelle bst WHERE bst.objid = bstobjid);

        IF loopid NOT IN (SELECT rid FROM buchstelle__relation LIMIT 1) 
            THEN ebenen = 0;                    
        ELSE 
        ebenen = 1;

            relation = (SELECT rel.relation FROM buchstelle__relation rel
                        WHERE rel.rid = loopid LIMIT 1);

            RETURN NEXT relation;

            --2. Relationsebene abfragen

            bstid = (SELECT bst.id FROM buchstelle bst WHERE bst.objid = relation LIMIT 1);
            RETURN NEXT bstid;

            IF bstid NOT IN (SELECT rid FROM buchstelle__relation LIMIT 1)
            THEN ebenen = ebenen;
            ELSE 
                ebenen = 2; 
                relation = (SELECT rel.relation FROM buchstelle__relation rel
                            WHERE rel.rid = bstid LIMIT 1);

                message = (ebenen, 'Ebenen');
                RETURN NEXT message;
                RETURN NEXT relation;

                --3. Relationsebene abfragen

                bstid = (SELECT bst.id FROM buchstelle bst WHERE bst.objid = relation LIMIT 1);
                RETURN NEXT bstid;

                IF bstid NOT IN (SELECT rid FROM buchstelle__relation LIMIT 1)
                    THEN ebenen = ebenen;

                ELSE 
                    ebenen = 3; 
                    relation = (SELECT rel.relation FROM buchstelle__relation rel
                                WHERE rel.rid = bstid LIMIT 1);


                    --4. Relationsebene abfragen

                    bstid = (SELECT bst.id FROM buchstelle bst WHERE bst.objid = relation LIMIT 1);
                    RETURN NEXT bstid;

                    IF bstid NOT IN (SELECT rid FROM buchstelle__relation LIMIT 1)
                        THEN ebenen = ebenen;

                    ELSE 
                        ebenen = 4; 
                        relation = (SELECT rel.relation FROM buchstelle__relation rel
                                    WHERE rel.rid = bstid LIMIT 1);


                        --5. Relationsebene abfragen

                        bstid = (SELECT bst.id FROM buchstelle bst WHERE bst.objid = relation LIMIT 1);

                        IF bstid NOT IN (SELECT rid FROM buchstelle__relation LIMIT 1)
                            THEN ebenen = ebenen;

                        ELSE 
                            ebenen = 5; 
                            relation = (SELECT rel.relation FROM buchstelle__relation rel
                                        WHERE rel.rid = bstid LIMIT 1);

                        END IF;
                    END IF;
                END IF;     
            END IF;
        END IF;

        -- Ausgabestring für das jeweilige Objekt

        message = (bstobjid, loopid, ' mit ', ebenen, ' Relationsebenen');

        RETURN NEXT message;

    END LOOP;

-- Ausgabestring für die Gesamtprüfung  

message = (max(ebenen), ' ist die maximale Relationstiefe');

RETURN NEXT message;

END;
$BODY$
 LANGUAGE plpgsql;

[編集]

@horse_with_no_name:

はい、関数の道を歩く前に、これを試しました:

WITH RECURSIVE ebenen (objid_bst, rid_anrel, verweistauf_bst)
AS (

SELECT bst.objid AS objid_bst, anrel.rid As rid_anrel, anrel.relation AS verweistauf_bst
FROM buchstelle bst
LEFT JOIN buchstelle__relation anrel ON bst.id = anrel.rid 

UNION ALL

SELECT bst.objid, anrel.rid, anrel.relation
FROM ebenen, buchstelle bst
INNER JOIN buchstelle__relation anrel ON anrel.relation = bst.objid
WHERE bst.id IN (anrel.rid)
)

SELECT 
objid_bst, rid_anrel, verweistauf_bst
, count(rid_anrel) OVER (PARTITION BY objid_bst) AS relationen
 FROM ebenen
 ORDER BY relationen
--LIMIT 1000000;

そして最初に、いくつかのサブクエリを持つネストされたSQLクエリ。最初の2つのステップは、完全なデータセットに対してクエリを実行することによるメモリオーバーフローを伴いました。

[2014年3月7日編集]

私は自分のタスクの解決策を見つけるために循環有向グラフ関係を探しました。しかし、それらは単一のテーブル内の現実を扱います。 2つのテーブルの間の循環を処理する必要があります。ここでよりよく理解するために、発生する可能性のある単純化された関係。このパスでtab2からtab1に戻るたびに、1つの関係がマークされるため、カウントする必要があります。 CTEを関数FOR-Loopに入れることで、タスクを解決できるかもしれません。しかし、CTEが実際にどのように機能するかについてもっと理解する必要があります。

tab1
id          objid 
1            aaa
2            bbb
3            ccc
4            ddd  
5            eee 
6            fff           
7            ggg
8            hhh

tab2
id       rid         rel2tab1         
1         3            aaa  
2         4            aaa
3         7            hhh
4         8            ccc

relations for each element in tab1
1
2
3 --> 1(tab2) --> 1(tab1)
4 --> 2(tag2) --> 1(tab1)
5 
6
7 --> 3(tab2) --> 8(tab1) --> 4(tab2) --> 3(tab1) --> 1(tab2) --> 1(tab1)
8 --> 4(tab2) --> 3(tab1) --> 1(tab2) --> 1(tab1)

[編集:2014年8月19日]

数日後、私は再びタスクを引き受け、高性能を備えたはるかに簡単なソリューションを発見しました。たぶん、他の誰かが同様のタスクにそれを使用することができます:

SELECT CASE WHEN
(SELECT sum(t1.id)
FROM
    table1 t1
    LEFT JOIN table2 t2 ON t2.rid = t1.id
WHERE t2.rid IS NOT NULL) IS NULL THEN '0' 
WHEN
(SELECT sum(t1.id)
FROM
    table1 t1
    LEFT JOIN table2 t2 ON t2.rid = t1.id
WHERE t1.objid IN 
(
    SELECT t2.rel_t1
    FROM
    table1 t1
    LEFT JOIN table2 t2 ON t2.rid = t1.id
    WHERE t2.rid IS NOT NULL
)
AND t2.rel_t1 IS NOT NULL) IS NULL THEN '1'
WHEN
(SELECT  sum(t1.id)
FROM
table1 t1
LEFT JOIN table2 t2 ON t2.rid = t1.id
WHERE t1.objid IN (
    SELECT t2.rel_t1
    FROM
        table1 t1
        LEFT JOIN table2 t2 ON t2.rid = t1.id
    WHERE t1.objid IN 
    (
        SELECT t2.rel_t1
        FROM
        table1 t1
        LEFT JOIN table2 t2 ON t2.rid = t1.id
        WHERE t2.rid IS NOT NULL
    )
    AND t2.rel_t1 IS NOT NULL
)
AND t2.rel_t1 IS NOT NULL) IS NULL THEN '2'
WHEN
(SELECT sum(t1.id)
FROM
    table1 t1
    LEFT JOIN table2 t2 ON t2.rid = t1.id
WHERE t1.objid IN 
(
    SELECT t2.rel_t1
    FROM
        table1 t1
        LEFT JOIN table2 t2 ON t2.rid = t1.id
    WHERE t1.objid IN 
    (
        SELECT t2.rel_t1
        FROM
            table1 t1
            LEFT JOIN table2 t2 ON t2.rid = t1.id
        WHERE t1.objid IN 
        (
            SELECT t2.rel_t1
            FROM
                table1 t1
                LEFT JOIN table2 t2 ON t2.rid = t1.id
            WHERE t2.rid IS NOT NULL
        )
        AND t2.rel_t1 IS NOT NULL
    )
    AND t2.rel_t1 IS NOT NULL
)
AND t2.rel_t1 IS NOT NULL) IS NULL THEN '3'
WHEN
(SELECT sum(t1.id)
FROM
    table1 t1
    LEFT JOIN table2 t2 ON t2.rid = t1.id
WHERE t1.objid IN 
(
    SELECT t2.rel_t1
    FROM
        table1 t1
        LEFT JOIN table2 t2 ON t2.rid = t1.id
    WHERE t1.objid IN 
    (
        SELECT t2.rel_t1
        FROM
            table1 t1
            LEFT JOIN table2 t2 ON t2.rid = t1.id
        WHERE t1.objid IN 
        (
            SELECT t2.rel_t1
            FROM
                table1 t1
                LEFT JOIN table2 t2 ON t2.rid = t1.id
            WHERE t1.objid IN 
            (
                SELECT t2.rel_t1
                FROM
                    table1 t1
                    LEFT JOIN table2 t2 ON t2.rid = t1.id
                WHERE t2.rid IS NOT NULL
            )
            AND t2.rel_t1 IS NOT NULL
        )
        AND t2.rel_t1 IS NOT NULL
    )
    AND t2.rel_t1 IS NOT NULL
)
AND t2.rel_t1 IS NOT NULL) IS NULL THEN '4' 
WHEN
(SELECT sum(t1.id)
FROM
    table1 t1
    LEFT JOIN table2 t2 ON t2.rid = t1.id
WHERE t1.objid IN 
(
    SELECT t2.rel_t1
    FROM
        table1 t1
        LEFT JOIN table2 t2 ON t2.rid = t1.id
    WHERE t1.objid IN 
    (
        SELECT t2.rel_t1
        FROM
            table1 t1
            LEFT JOIN table2 t2 ON t2.rid = t1.id
        WHERE t1.objid IN 
        (
            SELECT t2.rel_t1
            FROM
                table1 t1
                LEFT JOIN table2 t2 ON t2.rid = t1.id
            WHERE t1.objid IN 
            (
                SELECT t2.rel_t1
                FROM
                    table1 t1
                    LEFT JOIN table2 t2 ON t2.rid = t1.id
                WHERE t1.objid IN 
                (
                    SELECT t2.rel_t1
                    FROM
                        table1 t1
                        LEFT JOIN table2 t2 ON t2.rid = t1.id
                    WHERE t2.rid IS NOT NULL
                )
                AND t2.rel_t1 IS NOT NULL
            )
            AND t2.rel_t1 IS NOT NULL
        )
        AND t2.rel_t1 IS NOT NULL
    )
    AND t2.rel_t1 IS NOT NULL
)
AND t2.rel_t1 IS NOT NULL) IS NULL THEN '5'
ELSE 'Error: reached the maximum testing depth'
END AS relations
3
nunatak

再帰的CTE が進むべき道のようです。
パスにサイクルなしがあると仮定します。それ以外の場合は、サイクルを検出するためにさらに作業が必要です。以下のアレイソリューションは簡単に適応できます。

テスト設定

この簡略化されたレイアウトに基づいて構築:

_CREATE TABLE t1 (t1_id int, objid text);
INSERT INTO t1 VALUES
 (1,'aaa')
,(2,'bbb')
,(3,'ccc')
,(4,'ddd')
,(5,'eee')
,(6,'fff')
,(7,'ggg')
,(8,'hhh');

CREATE TABLE t2 (t2_id int, t1_id int, objid text);
INSERT INTO t2 VALUES
 (1,3,'aaa')
,(2,4,'aaa')
,(3,7,'hhh')
,(4,8,'ccc');
_

2つの異なるソリューション:

パスとして文字列を使用したソリューション

_WITH RECURSIVE cte AS (
   SELECT t.t1_id AS start_id
        , t2.t2_id::text || '(t2)' || COALESCE(' ->' || t1.t1_id || '(t1)', '') AS path
        , t1.t1_id
   FROM   t1 t
   LEFT   JOIN t2 USING (t1_id)
   LEFT   JOIN t1 ON t1.objid = t2.objid

   UNION ALL
   SELECT c.start_id
        , c.path || ' ->' || t2.t2_id || '(t2)' || COALESCE(' ->' || t1.t1_id || '(t1)', '')
        , t1.t1_id
   FROM   cte c
   JOIN   t2      USING (t1_id)
   LEFT   JOIN t1 USING (objid)
   )
SELECT DISTINCT ON (start_id)
       start_id, path
FROM   cte
ORDER  BY start_id, path DESC;
_

結果:

_start_id   path
1          
2          
3          1(t2) ->1(t1)
4          2(t2) ->1(t1)
5          
6          
7          3(t2) ->8(t1) ->4(t2) ->3(t1) ->1(t2) ->1(t1)
8          4(t2) ->3(t1) ->1(t2) ->1(t1)
_

明らかに、テーブル名は冗長です。見栄えを良くするために追加しました。

逆配列のソリューション

右端の要素は最初に_t2_id_で、右から左に交互に繰り返します。

_WITH RECURSIVE cte AS (
   SELECT t.t1_id AS start_id, ARRAY[t1.t1_id, t2.t2_id] AS path
   FROM   t1 t
   LEFT   JOIN t2 USING (t1_id)
   LEFT   JOIN t1 ON t1.objid = t2.objid

   UNION ALL
   SELECT c.start_id, t1.t1_id || (t2.t2_id || path)
   FROM   cte c
   JOIN   t2 ON t2.t1_id = path[1]
   LEFT   JOIN t1 USING (objid)
   )
SELECT DISTINCT ON (start_id)
       start_id, array_remove(path, NULL) AS path
FROM   cte
ORDER  BY start_id, array_length(path, 1) DESC;
_

結果:

_start_id   path
1          {}
2          {}
3          {1,1}
4          {1,2}
5          {}
6          {}
7          {1,1,3,4,8,3}
8          {1,1,3,4}
_

array_remove() Postgres9.3以降が必要です。

必要な列が1つ少なくなるように配列を反転しました。最後の要素を最初に置くことで、次のステップで_path[1]_を参照できます。それが安いかどうかわからない、テストが必要になるでしょう...

コードは短くなりますが、おそらく遅くなります。配列の処理はもっと高価だと思います。サイクルを観察する必要がある場合は、適応が容易です。

SQLFiddle。

主なポイント

  • 2つのテーブルを交互に使用しています。
    これを再帰的にするには、ワンステップカバーする必要があります2つのホップ(_t1 -> t2_から_t2 -> t1_に戻る)。

  • 最初のSELECTは2x _LEFT JOIN_を使用して、例の結果のようにすべての行を含めます。
    一致するものが見つからない場合にループを停止する再帰部分JOIN。ホップバックは再び_LEFT JOIN_を使用します。

2