539

我正在使用 Python 写入 postgres 数据库:

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

但是因为我的某些行是相同的,所以我收到以下错误:

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

如何编写“插入,除非该行已经存在”的 SQL 语句?

我见过这样的复杂语句推荐:

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

但首先,这是否符合我的需要,其次,我怎样才能将其中一个作为简单的字符串执行?

4

18 回答 18

857

Postgres 9.5(自 2016-01-07 发布)提供了一个“upsert”命令,也称为INSERT 的 ON CONFLICT 子句

INSERT ... ON CONFLICT DO NOTHING/UPDATE

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

于 2015-07-31T09:33:23.477 回答
432

如何编写“插入,除非该行已经存在”的 SQL 语句?

在 PostgreSQL 中有一个很好的方法来进行条件插入:

INSERT INTO example_table
    (id, name)
SELECT 1, 'John'
WHERE
    NOT EXISTS (
        SELECT id FROM example_table WHERE id = 1
    );

CAVEAT但是,这种方法对于并发写入操作并不是 100% 可靠的。SELECTNOT EXISTS反半连接和INSERT自身之间有一个非常小的竞争条件。在这种情况下它可能会失败。

于 2012-11-12T10:27:16.060 回答
60

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

那么高的水平。我假设在我的示例中所有三列都是不同的,因此对于第 3 步,将 NOT EXITS 连接更改为仅连接 100 表中的唯一列。

  1. 创建临时表。请参阅此处的文档。

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

    INSERT INTO temp_data(name, name_slug, status); 
    
  3. 将任何索引添加到临时表。

  4. 做主表插入。

    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
        );
    
于 2010-11-01T15:36:30.103 回答
19

不幸的是,PostgreSQL既不支持MERGE也不支持ON DUPLICATE KEY UPDATE,所以你必须在两个语句中做到这一点:

UPDATE  invoices
SET     billed = 'TRUE'
WHERE   invoices = '12345'

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

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

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

并称之为:

SELECT  fn_upd_invoices('12345', 'TRUE')
于 2010-11-01T14:28:27.543 回答
16

这正是我面临的问题,我的版本是 9.5

我用下面的 SQL 查询来解决它。

INSERT INTO example_table (id, name)
SELECT 1 AS id, 'John' AS name FROM example_table
WHERE NOT EXISTS(
            SELECT id FROM example_table WHERE id = 1
    )
LIMIT 1;

希望这将帮助那些与版本> = 9.5有相同问题的人。

谢谢阅读。

于 2019-05-16T06:12:39.933 回答
14

您可以使用 VALUES - 在 Postgres 中可用:

INSERT INTO person (name)
    SELECT name FROM person
    UNION 
    VALUES ('Bob')
    EXCEPT
    SELECT name FROM person;
于 2012-03-30T09:02:24.717 回答
9

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

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 插入。例如:

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

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

于 2012-05-21T15:32:19.400 回答
7

使用 WITH 查询在 PostgreSQL 中进行条件 INSERT 有一个很好的方法:例如:

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 
于 2017-10-14T04:26:56.520 回答
5

INSERT .. WHERE NOT EXISTS 是一个好方法。并且可以通过事务“信封”来避免竞争条件:

BEGIN;
LOCK TABLE hundred IN SHARE ROW EXCLUSIVE MODE;
INSERT ... ;
COMMIT;
于 2015-03-26T12:56:12.163 回答
5

我们可以使用 upsert 简化查询

insert into invoices (invoiceid, billed) 
  values ('12345', 'TRUE') 
  on conflict (invoiceid) do 
    update set billed=EXCLUDED.billed;
于 2021-03-26T17:12:26.690 回答
2

使用规则很容易:

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

但它因并发写入而失败......

于 2016-07-12T08:52:16.973 回答
1

psycopgs 游标类具有属性rowcount

此只读属性指定最后一次执行 *() 产生的行数(对于 DQL 语句,如 SELECT)或受影响的行数(对于 DML 语句,如 UPDATE 或 INSERT)。

因此,您可以先尝试 UPDATE 并仅在 rowcount 为 0 时才尝试 INSERT。

但是根据数据库中的活动级别,您可能会遇到 UPDATE 和 INSERT 之间的竞争条件,其中另一个进程可能会在此期间创建该记录。

于 2010-11-01T14:59:26.887 回答
1

投票最多的方法(来自 John Doe)确实对我有用,但在我的情况下,从预期的 422 行中我只得到 180 行。我找不到任何错误并且根本没有错误,所以我寻找了不同的简单的方法。

IF NOT FOUND THEN在 a 之后使用SELECT对我来说非常有效。

(在PostgreSQL 文档中描述)

文档中的示例:

SELECT * INTO myrec FROM emp WHERE empname = myname;
IF NOT FOUND THEN
  RAISE EXCEPTION 'employee % not found', myname;
END IF;
于 2013-11-23T11:29:38.457 回答
1

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

我建议你插入一个 id 作为序列类型来处理主键

于 2018-08-30T12:34:05.447 回答
1

如果您说您的许多行是相同的,您将多次检查。您可以发送它们,数据库将使用 ON CONFLICT 子句确定是否插入它,如下所示

  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);
于 2019-02-21T03:18:38.643 回答
0

我一直在寻找类似的解决方案,试图找到在 PostgreSQL 和 HSQLDB 中工作的 SQL。(HSQLDB 使这变得困难。)以您的示例为基础,这是我在其他地方找到的格式。

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 );"
于 2014-09-26T06:53:00.883 回答
-1

这是一个通用的 python 函数,它给定一个表名、列和值,为 postgresql 生成等效的 upsert。

导入json

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)
于 2016-07-25T23:44:37.613 回答
-13

解决方案很简单,但不是立即解决。
如果要使用此指令,则必须对 db 进行一次更改:

ALTER USER user SET search_path to 'name_of_schema';

在这些更改之后,“INSERT”将正常工作。

于 2016-11-08T11:00:38.770 回答