忽略重复插入的最佳方法?

Dav*_*vis 35 postgresql constraint insert duplication unique-constraint

背景

此问题与使用 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
  );
Run Code Online (Sandbox Code Playgroud)

代码不受检查现有值的影响。(在这种特殊情况下,用户并不关心插入重复项的错误——插入应该“正常工作”。)在这种情况下添加代码来显式测试重复项会带来复杂性。

问题

在 PostgreSQL 中,我找到了几种忽略重复插入的方法。

忽略重复 #1

创建一个捕获唯一约束违规的事务,不采取任何行动:

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

忽略重复 #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;
Run Code Online (Sandbox Code Playgroud)

问题

我的问题主要是学术性的:

  • 什么方法最有效?
  • 什么方法最容易维护,为什么?
  • 忽略 PostgreSQL 插入重复错误的标准方法是什么?
  • 是否有技术上更有效的方法来忽略重复插入;如果是,那是什么?

谢谢!

dez*_*zso 23

正如对另一个问题(这个问题被认为是重复的)的回答所提到的,有(从 9.5 版开始)一个本机UPSERT功能。对于旧版本,请继续阅读:)

我已经设置了一个测试来检查选项。我将包括下面的代码,它可以在psqllinux/Unix 机器上运行(仅仅是因为为了结果的清晰起见,我将设置命令的输出通过管道传输到/dev/null- 在 Windows 机器上,可以选择一个日志文件代替)。

我试图通过对INSERT每种类型使用多个(即 100 个),从plpgsql存储过程中的循环运行来使不同的结果具有可比性。此外,在每次运行之前,通过截断并重新插入原始数据来重置表。

检查一些测试运行,看起来使用规则并显式添加WHERE NOT EXISTSotINSERT语句花费相似的时间,而捕获异常需要更多的时间来完成。

后者并不令人惊讶

提示:包含 EXCEPTION 子句的块比没有 EXCEPTION 子句的块进入和退出的成本要高得多。因此,不要在不需要的情况下使用 EXCEPTION。

就个人而言,由于可读性和可维护性,我更喜欢将WHERE NOT EXISTS位添加到INSERTs 本身。就像触发器(也可以在这里测试)一样,调试(或简单地跟踪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();
Run Code Online (Sandbox Code Playgroud)

  • @DaveJarvis:在数据验证方面,重复和冗余并不是(必然)坏事 (4认同)