关于python:postgres:insert if不存在

Postgres: INSERT if does not exist already

我正在使用python写入Postgres数据库:

1
2
3
sql_string ="INSERT INTO hundred (name,name_slug,status) VALUES ("
sql_string += hundred +", '" + hundred_slug +"'," + status +");"
cursor.execute(sql_string)

但是由于我的一些行是相同的,所以我得到以下错误:

1
2
psycopg2.IntegrityError: duplicate key value  
  violates unique constraint"hundred_pkey"

如何编写"除非此行已存在,否则插入"SQL语句?

我见过这样复杂的陈述建议:

1
2
3
4
5
IF EXISTS (SELECT * FROM invoices WHERE invoiceid = '12345')
UPDATE invoices SET billed = 'TRUE' WHERE invoiceid = '12345'
ELSE
INSERT INTO invoices (invoiceid, billed) VALUES ('12345', 'TRUE')
END IF

但首先,这是否是为了满足我的需要而造成的过度杀伤力,其次,我如何才能将其中一个作为简单的字符串执行呢?


How can I write an 'INSERT unless this row already exists' SQL statement?

PostgreSQL中有一种很好的条件插入方法:

1
2
3
4
5
6
7
INSERT INTO example_table
    (id, name)
SELECT 1, 'John'
WHERE
    NOT EXISTS (
        SELECT id FROM example_table WHERE id = 1
    );

但是,这种方法对于并发写操作并不是100%可靠。在NOT EXISTS反半连接中的SELECTINSERT本身之间有一个非常小的竞争条件。在这种情况下,它可能会失效。


Postgres 9.5(自2016-01-07发布)提供了一个"upsert"命令,也称为"on conflict"条款,插入:

1
INSERT ... ON CONFLICT DO NOTHING/UPDATE

它解决了在使用并发操作时可能遇到的许多微妙问题,其他一些答案也提出了这一点。


一种方法是创建一个非约束(没有唯一索引)表,将所有数据插入其中,并进行一个与之不同的选择,以将数据插入到一百个表中。

那么高的水平就是。我假设在我的示例中这三列都是不同的,所以对于步骤3,将not exits join更改为只在一百表中的唯一列上联接。

  • 创建临时表。参见这里的文档。

    1
    CREATE TEMPORARY TABLE temp_data(name, name_slug, status);
  • 将数据插入临时表。

    1
    INSERT INTO temp_data(name, name_slug, status);
  • 向临时表添加任何索引。

  • 插入主表。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    INSERT INTO hundred(name, name_slug, status)
        SELECT DISTINCT name, name_slug, status
        FROM hundred
        WHERE NOT EXISTS (
            SELECT 'X'
            FROM temp_data
            WHERE
                temp_data.name          = hundred.name
                AND temp_data.name_slug = hundred.name_slug
                AND temp_data.status    = status
        );

  • 不幸的是,PostgreSQL既不支持MERGE也不支持ON DUPLICATE KEY UPDATE,因此您必须在两个声明中进行:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    UPDATE  invoices
    SET     billed = 'TRUE'
    WHERE   invoices = '12345'

    INSERT
    INTO    invoices (invoiceid, billed)
    SELECT  '12345', 'TRUE'
    WHERE   '12345' NOT IN
            (
            SELECT  invoiceid
            FROM    invoices
            )

    您可以将其包装成一个函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    CREATE OR REPLACE FUNCTION fn_upd_invoices(id VARCHAR(32), billed VARCHAR(32))
    RETURNS VOID
    AS
    $$
            UPDATE  invoices
            SET     billed = $2
            WHERE   invoices = $1;

            INSERT
            INTO    invoices (invoiceid, billed)
            SELECT  $1, $2
            WHERE   $1 NOT IN
                    (
                    SELECT  invoiceid
                    FROM    invoices
                    );
    $$
    LANGUAGE 'sql';

    就这么叫吧:

    1
    SELECT  fn_upd_invoices('12345', 'TRUE')


    您可以使用postgres中的值:

    1
    2
    3
    4
    5
    6
    INSERT INTO person (name)
        SELECT name FROM person
        UNION
        VALUES ('Bob')
        EXCEPT
        SELECT name FROM person;


    在PostgreSQL中使用with query执行条件插入有一种很好的方法:像:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    WITH a as(
    select
     id
    from
     schema.table_name
    where
     column_name = your_identical_column_value
    )
    INSERT into
     schema.table_name
    (col_name1, col_name2)
    SELECT
        (col_name1, col_name2)
    WHERE NOT EXISTS (
         SELECT
             id
         FROM
             a
            )
      RETURNING id


    我知道这个问题是不久前提出的,但我想这可能会对某人有所帮助。我认为最简单的方法是通过触发器。例如。:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    Create Function ignore_dups() Returns Trigger
    As $$
    Begin
        If Exists (
            Select
                *
            From
                hundred h
            Where
                -- Assuming all three fields are primary key
                h.name = NEW.name
                And h.hundred_slug = NEW.hundred_slug
                And h.status = NEW.status
        ) Then
            Return NULL;
        End If;
        Return NEW;
    End;
    $$ Language plpgsql;

    Create Trigger ignore_dups
        Before Insert On hundred
        For Each Row
        Execute Procedure ignore_dups();

    从psql提示符执行此代码(或者您希望直接在数据库上执行查询)。然后您可以从python正常插入。例如。:

    1
    2
    sql ="Insert Into hundreds (name, name_slug, status) Values (%s, %s, %s)"
    cursor.execute(sql, (hundred, hundred_slug, status))

    注意,正如@thomas_uters已经提到的,上面的代码利用参数而不是连接字符串。


    插入…不存在的地方是好方法。交易"信封"可以避免竞争条件:

    1
    2
    3
    4
    BEGIN;
    LOCK TABLE hundred IN SHARE ROW EXCLUSIVE MODE;
    INSERT ... ;
    COMMIT;

    规则很简单:

    1
    2
    CREATE RULE file_insert_defer AS ON INSERT TO file
    WHERE (EXISTS ( SELECT * FROM file WHERE file.id = new.id)) DO INSTEAD NOTHING

    但同时写入失败了…


    psycopgs cursor类具有rowcount属性。

    This read-only attribute specifies the number of rows that the last
    execute*() produced (for DQL statements like SELECT) or affected (for
    DML statements like UPDATE or INSERT).

    因此,您可以尝试首先更新,然后仅在行数为0时插入。

    但是,根据数据库中的活动级别,您可能会遇到更新和插入之间的竞争情况,在此情况下,另一个进程可能会在临时创建该记录。


    投票最多的方法(来自JohnDoe)在某种程度上对我有用,但在我的例子中,预期422行中我只有180行。我找不到任何错误,也没有任何错误,所以我寻找了一种不同的简单方法。

    在使用SELECT之后使用IF NOT FOUND THEN,对我来说非常好。

    (在PostgreSQL文档中描述)

    文档示例:

    1
    2
    3
    4
    SELECT * INTO myrec FROM emp WHERE empname = myname;
    IF NOT FOUND THEN
      RAISE EXCEPTION 'employee % not found', myname;
    END IF;


    您的列"一百"似乎被定义为主键,因此必须是唯一的,而事实并非如此。问题不在于,在于你的数据。

    我建议您插入一个ID作为串行类型来手动输入主键


    如果您说您的许多行是相同的,那么您将结束多次检查。您可以发送它们,数据库将按照下面的on conflict子句确定是否插入它

    1
    2
    3
      INSERT INTO Hundred (name,name_slug,status) VALUES ("sql_string += hundred  
      +"
    ,'" + hundred_slug +"'," + status +") ON CONFLICT ON CONSTRAINT
      hundred_pkey DO NOTHING;" cursor.execute(sql_string);

    我在寻找一个类似的解决方案,试图找到可以在PostgreSQL和HSQLDB中工作的SQL。(hsqldb是造成这种困难的原因。)以您的示例为基础,这是我在其他地方找到的格式。

    1
    2
    3
    4
    5
    sql ="INSERT INTO hundred (name,name_slug,status)"
    sql +=" ( SELECT" + hundred +", '" + hundred_slug +"'," + status
    sql +=" FROM hundred"
    sql +=" WHERE name =" + hundred +" AND name_slug = '" + hundred_slug +"' AND status =" + status
    sql +=" HAVING COUNT(*) = 0 );"

    这里是一个通用的python函数,它给出了表名、列和值,生成了postgresql的upsert等价物。

    导入JSON

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    def upsert(table_name, id_column, other_columns, values_hash):

        template ="""
        WITH new_values ($$ALL_COLUMNS$$) as (
          values
             ($$VALUES_LIST$$)
        ),
        upsert as
        (
            update $$TABLE_NAME$$ m
                set
                    $$SET_MAPPINGS$$
            FROM new_values nv
            WHERE m.$$ID_COLUMN$$ = nv.$$ID_COLUMN$$
            RETURNING m.*
        )
        INSERT INTO $$TABLE_NAME$$ ($$ALL_COLUMNS$$)
        SELECT $$ALL_COLUMNS$$
        FROM new_values
        WHERE NOT EXISTS (SELECT 1
                          FROM upsert up
                          WHERE up.$$ID_COLUMN$$ = new_values.$$ID_COLUMN$$)
       """


        all_columns = [id_column] + other_columns
        all_columns_csv =",".join(all_columns)
        all_values_csv = ','.join([query_value(values_hash[column_name]) for column_name in all_columns])
        set_mappings =",".join([ c+" = nv." +c for c in other_columns])

        q = template
        q = q.replace("$$TABLE_NAME$$", table_name)
        q = q.replace("$$ID_COLUMN$$", id_column)
        q = q.replace("$$ALL_COLUMNS$$", all_columns_csv)
        q = q.replace("$$VALUES_LIST$$", all_values_csv)
        q = q.replace("$$SET_MAPPINGS$$", set_mappings)

        return q


    def query_value(value):
        if value is None:
            return"NULL"
        if type(value) in [str, unicode]:
            return"'%s'" % value.replace("'","''")
        if type(value) == dict:
            return"'%s'" % json.dumps(value).replace("'","''")
        if type(value) == bool:
            return"%s" % value
        if type(value) == int:
            return"%s" % value
        return value


    if __name__ =="__main__":

        my_table_name = 'mytable'
        my_id_column = 'id'
        my_other_columns = ['field1', 'field2']
        my_values_hash = {
            'id': 123,
            'field1':"john",
            'field2':"doe"
        }
        print upsert(my_table_name, my_id_column, my_other_columns, my_values_hash)

    解决方案很简单,但不是直接的。如果要使用此指令,必须对数据库进行一次更改:

    1
    ALTER USER user SET search_path to 'name_of_schema';

    这些更改之后,"插入"将正常工作。