sql >> データベース >  >> RDS >> PostgreSQL

関数内のSELECTまたはINSERTは競合状態になりやすいですか?

    これは、 SELECTの繰り返しの問題です。 またはINSERT UPSERTに関連する(ただし異なる)同時書き込み負荷の可能性がある場合 (これは INSERT またはUPDATE

    このPL/pgSQL関数はUPSERT(INSERT ... ON CONFLICT .. DO UPDATE )からINSERT またはSELECT 単一行

    CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT _tag_id int)
      LANGUAGE plpgsql AS
    $func$
    BEGIN
       SELECT tag_id  -- only if row existed before
       FROM   tag
       WHERE  tag = _tag
       INTO   _tag_id;
    
       IF NOT FOUND THEN
          INSERT INTO tag AS t (tag)
          VALUES (_tag)
          ON     CONFLICT (tag) DO NOTHING
          RETURNING t.tag_id
          INTO   _tag_id;
       END IF;
    END
    $func$;
    

    競合状態の小さなウィンドウがまだあります。 絶対に確実にする IDを取得します:

    CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT _tag_id int)
      LANGUAGE plpgsql AS
    $func$
    BEGIN
       LOOP
          SELECT tag_id
          FROM   tag
          WHERE  tag = _tag
          INTO   _tag_id;
    
          EXIT WHEN FOUND;
    
          INSERT INTO tag AS t (tag)
          VALUES (_tag)
          ON     CONFLICT (tag) DO NOTHING
          RETURNING t.tag_id
          INTO   _tag_id;
    
          EXIT WHEN FOUND;
       END LOOP;
    END
    $func$;
    

    db<>ここでフィドル

    これは、いずれかのINSERTまでループし続けます またはSELECT 成功します。電話:

    SELECT f_tag_id('possibly_new_tag');
    

    後続のコマンドが同じトランザクション内の場合 行の存在に依存しており、他のトランザクションが同時に行を更新または削除する可能性がある場合は、SELECTで既存の行をロックできます。 FOR SHAREを含むステートメント 。
    代わりに行が挿入された場合、トランザクションが終了するまで、行はロックされます(または他のトランザクションでは表示されません)。

    一般的なケースから始めます(INSERT vs SELECT )高速化する。

    関連:

    • 条件付きINSERTからIDを取得
    • RETURNING from INSERT ...ONCONFLICTに除外された行を含める方法

    INSERTに関連する(純粋なSQL)ソリューション またはSELECT 複数の行 (セット)一度に:

    • PostgreSQLでONCONFLICTを使用してRETURNINGを使用するにはどうすればよいですか?

    これの何が問題になっていますか 純粋なSQLソリューション?

    CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT _tag_id int)
      LANGUAGE sql AS
    $func$
    WITH ins AS (
       INSERT INTO tag AS t (tag)
       VALUES (_tag)
       ON     CONFLICT (tag) DO NOTHING
       RETURNING t.tag_id
       )
    SELECT tag_id FROM ins
    UNION  ALL
    SELECT tag_id FROM tag WHERE tag = _tag
    LIMIT  1;
    $func$;
    

    完全に間違っているわけではありませんが、@ FunctorSaladがうまくいったように、抜け穴を塞ぐことはできません。並行トランザクションが同時に同じことを行おうとすると、関数は空の結果を出す可能性があります。マニュアル:

    すべてのステートメントは同じスナップショットで実行されます

    並行トランザクションが少し前に同じ新しいタグを挿入したが、まだコミットしていない場合:

    • 並行トランザクションが終了するのを待った後、UPSERT部分は空になります。 (同時トランザクションがロールバックする必要がある場合でも、新しいタグが挿入され、新しいIDが返されます。)

    • SELECT部分​​も空になります。これは、同じスナップショットに基づいており、(まだコミットされていない)同時トランザクションからの新しいタグが表示されていないためです。

    何も得られない 。意図したとおりではありません。これは素朴なロジックには直観に反します(そして私はそこで捕まりました)が、PostgresのMVCCモデルが機能する方法です-機能する必要があります。

    したがって、複数のトランザクションが同じタグを同時に挿入しようとする可能性がある場合は、これを使用しないでください。 または 実際に行を取得するまでループします。いずれにせよ、一般的な作業負荷でループがトリガーされることはほとんどありません。

    Postgres9.4以前

    この(少し簡略化された)テーブルを考えると:

    CREATE table tag (
      tag_id serial PRIMARY KEY
    , tag    text   UNIQUE
    );
    

    ほぼ100%安全 新しいタグを挿入/既存のタグを選択する関数は、次のようになります。

    CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT tag_id int)
      LANGUAGE plpgsql AS
    $func$
    BEGIN
       LOOP
          BEGIN
          WITH sel AS (SELECT t.tag_id FROM tag t WHERE t.tag = _tag FOR SHARE)
             , ins AS (INSERT INTO tag(tag)
                       SELECT _tag
                       WHERE  NOT EXISTS (SELECT 1 FROM sel)  -- only if not found
                       RETURNING tag.tag_id)       -- qualified so no conflict with param
          SELECT sel.tag_id FROM sel
          UNION  ALL
          SELECT ins.tag_id FROM ins
          INTO   tag_id;
    
          EXCEPTION WHEN UNIQUE_VIOLATION THEN     -- insert in concurrent session?
             RAISE NOTICE 'It actually happened!'; -- hardly ever happens
          END;
    
          EXIT WHEN tag_id IS NOT NULL;            -- else keep looping
       END LOOP;
    END
    $func$;
    

    db<>ここでフィドル
    古いsqlfiddle

    なぜ100%ではないのですか?関連するUPSERTのマニュアルの注記を検討してください 例:

    • https://www.postgresql.org/docs/current/plpgsql-control-structures.html#PLPGSQL-UPSERT-EXAMPLE

    説明

    • SELECTをお試しください 最初 。このようにして、かなり高価なを回避できます。 99.99%の確率で例外処理。

    • CTEを使用して、競合状態の(すでに小さい)タイムスロットを最小限に抑えます。

    • SELECT間の時間枠 およびINSERT 1つのクエリ内 超小型です。同時負荷が大きくない場合、または1年に1回例外が発生する可能性がある場合は、ケースを無視してSQLステートメントを使用できます。これはより高速です。

    • FETCH FIRST ROW ONLYは必要ありません (=LIMIT 1 )。タグ名は明らかにUNIQUE

    • FOR SHAREを削除します 私の例では、通常、DELETEを同時に実行しない場合 またはUPDATE テーブルのtag 。わずかなパフォーマンスのコストがかかります。

    • 言語名を引用しないでください:'plpgsql' plpgsql 識別子です 。引用は問題を引き起こす可能性があり、下位互換性のためにのみ許容されます。

    • idのようなわかりにくい列名は使用しないでください またはname 。いくつかのテーブルを結合するとき(これがあなたの仕事です リレーショナルDBの場合)複数の同一の名前になり、エイリアスを使用する必要があります。

    関数に組み込まれています

    この関数を使用すると、FOREACH LOOPを大幅に簡素化できます。 宛先:

    ...
    FOREACH TagName IN ARRAY $3
    LOOP
       INSERT INTO taggings (PostId, TagId)
       VALUES   (InsertedPostId, f_tag_id(TagName));
    END LOOP;
    ...
    

    ただし、unnest()を使用した単一のSQLステートメントとしてより高速です。 :

    INSERT INTO taggings (PostId, TagId)
    SELECT InsertedPostId, f_tag_id(tag)
    FROM   unnest($3) tag;
    

    ループ全体を置き換えます。

    代替ソリューション

    このバリアントは、UNION ALLの動作に基づいています。 LIMITを使用 句:十分な行が見つかるとすぐに、残りは実行されません:

    • 結果が得られるまで複数のSELECTを試す方法はありますか?

    これに基づいて、INSERTを外部委託できます。 別の関数に。そこでのみ、例外処理が必要です。最初の解決策と同じくらい安全です。

    CREATE OR REPLACE FUNCTION f_insert_tag(_tag text, OUT tag_id int)
      RETURNS int
      LANGUAGE plpgsql AS
    $func$
    BEGIN
       INSERT INTO tag(tag) VALUES (_tag) RETURNING tag.tag_id INTO tag_id;
    
       EXCEPTION WHEN UNIQUE_VIOLATION THEN  -- catch exception, NULL is returned
    END
    $func$;
    

    主な機能で使用されるもの:

    CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT _tag_id int)
       LANGUAGE plpgsql AS
    $func$
    BEGIN
       LOOP
          SELECT tag_id FROM tag WHERE tag = _tag
          UNION  ALL
          SELECT f_insert_tag(_tag)  -- only executed if tag not found
          LIMIT  1  -- not strictly necessary, just to be clear
          INTO   _tag_id;
    
          EXIT WHEN _tag_id IS NOT NULL;  -- else keep looping
       END LOOP;
    END
    $func$;
    
    • ほとんどの呼び出しにSELECTのみが必要な場合、これは少し安価です。 、INSERTを使用したより高価なブロックのため EXCEPTIONを含む 句が入力されることはめったにありません。クエリも簡単です。

    • FOR SHARE ここではできません(UNIONでは許可されていません クエリ)。

    • LIMIT 1 必要ありません(9.4ページでテスト済み)。 PostgresはLIMIT 1を導き出します INTO _tag_idから 最初の行が見つかるまでのみ実行されます。



    1. SQLServer比較演算子のリスト

    2. 行を連結するときにFORXMLPATH('')がどのように機能するか

    3. LOWER()がMariaDBでどのように機能するか

    4. SQL Serverの小数点以下の桁数を切り捨てる(丸めない)