web-dev-qa-db-ja.com

重複する挿入を無視する最適な方法は?

バックグラウンド

この問題は、PostgreSQL 9.2以降を使用した重複挿入の無視に関連しています。私が尋ねる理由はこのコードのためです:

  -- Ignores duplicates.
  INSERT INTO
    db_table (tbl_column_1, tbl_column_2)
  VALUES (
    SELECT
      unnseted_column,
      param_association
    FROM
      unnest( param_array_ids ) AS unnested_column
  );

コードは、既存の値のチェックによって妨げられません。 (この特定の状況では、ユーザーは重複の挿入によるエラーを気にしません-挿入は「うまくいく」はずです。)重複を明示的にテストするためにこの状況でコードを追加すると、複雑になります。

問題

PostgreSQLでは、重複した挿入を無視する方法をいくつか見つけました。

重複を無視#1

アクションを実行せずに、一意の制約違反をキャッチするトランザクションを作成します。

  BEGIN
    INSERT INTO db_table (tbl_column) VALUES (v_tbl_column);
  EXCEPTION WHEN unique_violation THEN
    -- Ignore duplicate inserts.
  END;

重複を無視する#2

特定のテーブルの重複を無視するルールを作成します。

CREATE OR REPLACE RULE db_table_ignore_duplicate_inserts AS
    ON INSERT TO db_table
   WHERE (EXISTS ( SELECT 1
           FROM db_table
          WHERE db_table.tbl_column = NEW.tbl_column)) DO INSTEAD NOTHING;

ご質問

私の質問はほとんどが学術的です:

  • どの方法が最も効率的ですか?
  • 最も保守しやすい方法とその理由は?
  • PostgreSQLで挿入重複エラーを無視する標準的な方法は何ですか?
  • 重複する挿入を無視するための技術的により効率的な方法はありますか?もしそうなら、それは何ですか?

ありがとうございました!

33
Dave Jarvis

他の質問(これは重複と見なされます)に対する回答として、(バージョン9.5以降)ネイティブ UPSERT 機能があります。古いバージョンについては、読み続けてください:)

オプションを確認するためのテストを設定しました。以下のコードを含めます。これは、Linux/Unixボックスのpsqlで実行できます(結果を明確にするために、セットアップコマンドの出力をパイプラインで/dev/nullにパイプしました。 Windowsボックスでは、代わりにログファイルを選択できます)。

INSERTストアドプロシージャ内のループから実行する、タイプごとに複数(つまり100)のplpgsqlを使用して、異なる結果を比較できるようにしました。さらに、各実行前に、元のデータを切り捨てて再挿入することにより、テーブルがリセットされます。

いくつかのテストの実行を確認すると、ルールを使用してWHERE NOT EXISTSINSERTステートメントを明示的に追加すると、同様の時間が費やされますが、例外のキャッチにはかなり長い時間がかかります。

後者はそうではありません 意外

ヒント:EXCEPTION句を含むブロックは、EXCEPTION句を含まないブロックに比べて、出入りにかなりの費用がかかります。したがって、必要がない場合はEXCEPTIONを使用しないでください。

個人的には、読みやすさと保守性のため、INSERTs自体にWHERE NOT EXISTSビットを追加することを好みます。トリガー(ここでもテストできます)と同様に、デバッグ(または単にINSERT動作をトレースする)は、ルールが存在する場合はさらに複雑になります。

そして、私が使用したコード(誤解やその他の問題を自由に指摘してください):

\o /dev/null
\timing off

-- set up data
DROP TABLE IF EXISTS insert_test;

CREATE TABLE insert_test_base_data (
    id integer PRIMARY KEY,
    col1 double precision,
    col2 text
);

CREATE TABLE insert_test (
    id integer PRIMARY KEY,
    col1 double precision,
    col2 text
);

INSERT INTO insert_test_base_data
SELECT i, (SELECT random() AS r WHERE s.i = s.i)
FROM 
    generate_series(2, 200, 2) s(i)
;

UPDATE insert_test_base_data
SET col2 = md5(col1::text)
;

INSERT INTO insert_test
SELECT *
FROM insert_test_base_data
;



-- function with exception block to be called later
CREATE OR REPLACE FUNCTION f_insert_test_insert(
    id integer,
    col1 double precision,
    col2 text
)
RETURNS void AS
$body$
BEGIN
    INSERT INTO insert_test
    VALUES ($1, $2, $3)
    ;
EXCEPTION
    WHEN unique_violation
    THEN NULL;
END;
$body$
LANGUAGE plpgsql;



-- function running plain SQL ... WHERE NOT EXISTS ...
CREATE OR REPLACE FUNCTION insert_test_where_not_exists()
RETURNS void AS
$body$
BEGIN
    FOR i IN 1 .. 100
    LOOP
        INSERT INTO insert_test
        SELECT i, rnd, md5(rnd::text)
        FROM (SELECT random() AS rnd) r
        WHERE NOT EXISTS (
            SELECT 1
            FROM insert_test
            WHERE id = i
        )
        ;
    END LOOP;
END;
$body$
LANGUAGE plpgsql;



-- call a function with exception block
CREATE OR REPLACE FUNCTION insert_test_function_with_exception_block()
RETURNS void AS
$body$
BEGIN
    FOR i IN 1 .. 100
    LOOP
        PERFORM f_insert_test_insert(i, rnd, md5(rnd::text))
        FROM (SELECT random() AS rnd) r
        ;
    END LOOP;
END;
$body$
LANGUAGE plpgsql;



-- leave checking existence to a rule
CREATE OR REPLACE FUNCTION insert_test_rule()
RETURNS void AS
$body$
BEGIN
    FOR i IN 1 .. 100
    LOOP
        INSERT INTO insert_test
        SELECT i, rnd, md5(rnd::text)
        FROM (SELECT random() AS rnd) r
        ;
    END LOOP;
END;
$body$
LANGUAGE plpgsql;



\o
\timing on


\echo 
\echo 'check before INSERT'

SELECT insert_test_where_not_exists();

\echo 



\o /dev/null

\timing off

TRUNCATE insert_test;

INSERT INTO insert_test
SELECT *
FROM insert_test_base_data
;

\timing on

\o

\echo 'catch unique-violation'

SELECT insert_test_function_with_exception_block();

\echo 
\echo 'implementing a RULE'

\o /dev/null
\timing off

TRUNCATE insert_test;

INSERT INTO insert_test
SELECT *
FROM insert_test_base_data
;

CREATE OR REPLACE RULE db_table_ignore_duplicate_inserts AS
    ON INSERT TO insert_test
    WHERE EXISTS ( 
        SELECT 1
        FROM insert_test
        WHERE id = NEW.id
    ) 
    DO INSTEAD NOTHING;

\o 
\timing on

SELECT insert_test_rule();
22
dezso