web-dev-qa-db-fra.com

Manière optimale d'ignorer les insertions en double?

Contexte

Ce problème concerne l'ignorance des insertions en double à l'aide de PostgreSQL 9.2 ou version ultérieure. La raison pour laquelle je demande est à cause de ce code:

  -- 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
  );

Le code n'est pas encombré par les vérifications des valeurs existantes. (Dans cette situation particulière, l'utilisateur ne se soucie pas des erreurs d'insertion de doublons - l'insertion doit "simplement fonctionner".) L'ajout de code dans cette situation pour tester explicitement les doublons entraîne des complications.

Problème

Dans PostgreSQL, j'ai trouvé plusieurs façons d'ignorer les insertions en double.

Ignorer les doublons # 1

Créez une transaction qui détecte les violations de contraintes uniques, sans rien faire:

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

Ignorer les doublons # 2

Créez une règle pour ignorer les doublons sur une table donnée:

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;

Des questions

Mes questions sont principalement académiques:

  • Quelle méthode est la plus efficace?
  • Quelle méthode est la plus maintenable et pourquoi?
  • Quelle est la méthode standard pour ignorer les erreurs de duplication d'insertion avec PostgreSQL?
  • Existe-t-il un moyen techniquement plus efficace d'ignorer les insertions en double; si oui, c'est quoi?

Je vous remercie!

33
Dave Jarvis

Comme le mentionnent les réponses à l'autre question (dont celle-ci est considérée comme un doublon), il existe (depuis la version 9.5) une fonctionnalité native UPSERT . Pour les versions plus anciennes, continuez à lire :)

J'ai mis en place un test pour vérifier les options. Je vais inclure le code ci-dessous, qui peut être exécuté dans psql sur une boîte linux/Unix (simplement parce que pour plus de clarté dans les résultats, j'ai canalisé la sortie des commandes de configuration vers /dev/null - sur une boîte Windows, on peut choisir un fichier journal à la place).

J'ai essayé de rendre des résultats différents comparables en utilisant plus d'un (c'est-à-dire 100) INSERT par type, exécuté à partir d'une boucle à l'intérieur d'une procédure stockée plpgsql. De plus, avant chaque exécution, la table est réinitialisée en tronquant et en réinsérant les données d'origine.

En vérifiant quelques essais, il ressemble à cela en utilisant la règle et en ajoutant explicitement WHERE NOT EXISTS ou l'instruction INSERT passent un temps similaire, tandis que la génération d'une exception prend beaucoup plus de temps.

Ce dernier n'est pas ça surprenant :

Conseil: Un bloc contenant une clause EXCEPTION est beaucoup plus cher à entrer et à quitter qu'un bloc sans un. Par conséquent, n'utilisez pas EXCEPTION sans besoin.

Personnellement, en raison de la lisibilité et de la maintenabilité, je préfère ajouter le WHERE NOT EXISTS bit aux INSERT eux-mêmes. Tout comme avec les déclencheurs (qui pourraient également être testés ici), le débogage (ou simplement le suivi du comportement de INSERT) est plus compliqué avec les règles présentes.

Et le code que j'ai utilisé (n'hésitez pas à signaler des idées fausses ou d'autres problèmes):

\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