Postgres:如果尚不存在,请插入


361

我正在使用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

但是,首先,这是否满足我的需要,其次,如何将其中一个作为简单的字符串执行?


56
无论您如何解决此问题,都不应像这样生成查询。在查询中使用参数,并分别传递值;看到stackoverflow.com/questions/902408/...
托马斯武泰

3
为什么不捕获异常并忽略它?
马修·米切尔

5
从Posgres 9.5(当前在beta2版本开始)开始,有一个类似upsert的新功能,请参见:postgresql.org/docs/9.5/static/sql-insert.html#SQL-ON-CONFLICT
Ezequiel Moreno

2
您是否考虑过接受答案?=]
Relequestual,2015年

Answers:


512

Postgres 9.5(自2016年1月7日发布)提供了一个“ upsert”命令,也称为INSERTON CONFLICT子句

INSERT ... ON CONFLICT DO NOTHING/UPDATE

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


14
9.5发布了。
luckydonald

2
在PostgreSQL 9.5之前的@TusharJain中,您可以执行“老式的” UPSERT(带有CTE),但是您可能会遇到种族问题,并且不能像9.5样式一样有效。此博客上有一个关于upsert的很好的详细信息(在底部的更新区域中),如果您想了解更多有关详细信息的信息,则包括一些链接。
Skyguard'4

16
对于那些需要的东西,这里有两个简单的例子。(1)INSERT如果不存在别的什么- INSERT INTO distributors (did, dname) VALUES (7, 'Redline GmbH') ON CONFLICT (did) DO NOTHING;(2)INSERT如果不存在其他UPDATE - INSERT INTO distributors (did, dname) VALUES (5, 'Gizmo Transglobal'), (6, 'Associated Computing, Inc') ON CONFLICT (did) DO UPDATE SET dname = EXCLUDED.dname;这些实施例是从手动- postgresql.org/docs/9.5/static/sql-insert.html
AnnieFromTaiwan

13
有一个警告/副作用。在具有序列列(串行或大序列)的表中,即使未插入任何行,该序列也会在每次插入尝试时递增。
Grzegorz Luczywo

2
最好链接到INSERT文档,而不要指向发行版。文档链接:postgresql.org/docs/9.5/static/sql-insert.html
borjagvo

379

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

有一种在PostgreSQL中进行条件INSERT的好方法:

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本身。它可以在这样的条件下可能会失败。


假设“名称”字段具有唯一约束,这有多安全?它会因独特违规而失败吗?
agnsaft 2012年

2
这很好。唯一的问题是耦合,我猜:如果修改表以使更多列是唯一的,该怎么办。在这种情况下,必须修改所有脚本。如果有更通用的方法可以做到这一点,那就太好了……
Willem Van Onsem 2014年

1
是否可以将其与RETURNS id例如获取id是否已插入一起使用?
奥利维尔·庞斯

2
@OlivierPons是的,有可能。RETURNING id在查询的和处添加,如果未插入任何行,它将返回新的行ID或不返回任何内容。
AlexM '16

4
我发现这是不可靠的。似乎Postgres有时会在执行选择之前执行插入操作,即使记录尚未插入,我也会遇到重复的键冲突。尝试对ON CONFLICT使用version => 9.5。
Michael Silver

51

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

如此高的水平。我假设示例中的所有三个列都是不同的,因此对于步骤3,将NOT EXITS联接更改为仅联接百表中的唯一列。

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

3
当我不知道行是否已存在时,这是我发现进行批量插入的最快方法。
nate c

选择“ X”?有人可以澄清吗?这只是权利的选择陈述:SELECT name,name_slug,status*
roberthuttinger 2014年

3
查找相关的子查询。“ X”可以更改为1甚至“ SadClown”。SQL要求有一些东西,并且使用'X'是很常见的事情。它很小,并且很明显正在使用相关的子查询,并且满足SQL要求的要求。
Kuberchaun 2014年

您提到“将所有数据插入(假设临时表)并进行与之不同的选择”。在那种情况下,不是SELECT DISTINCT name, name_slug, status FROM temp_data吗?
gibbz00

17

不幸的是,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')

1
实际上,这是行不通的:我可以INSERT INTO hundred (name, name_slug, status) SELECT 'Chichester', 'chichester', NULL WHERE 'Chichester' NOT IN (SELECT NAME FROM hundred);多次拨打电话,并且一直在插入行。
AP257 2011年

1
@ AP257 :CREATE TABLE hundred (name TEXT, name_slug TEXT, status INT); INSERT INTO hundred (name, name_slug, status) SELECT 'Chichester', 'chichester', NULL WHERE 'Chichester' NOT IN (SELECT NAME FROM hundred); INSERT INTO hundred (name, name_slug, status) SELECT 'Chichester', 'chichester', NULL WHERE 'Chichester' NOT IN (SELECT NAME FROM hundred); SELECT * FROM hundred。有一个记录。
Quassnoi

12

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

INSERT INTO person (name)
    SELECT name FROM person
    UNION 
    VALUES ('Bob')
    EXCEPT
    SELECT name FROM person;

12
SELECT name FROM Person <---如果一行中有十亿行怎么办?
Henley Chiu

1
我认为这是解决问题的一种不错的快速方法,但是仅当您确定源表永远不会变得很大时才可以。我有一个表,该表永远不会超过1000行,因此可以使用此解决方案。
伦纳德

哇,这正是我所需要的。我担心我需要创建一个函数或一个临时表,但这排除了所有这些—谢谢!
Amalgovinus

8

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

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已经提到的,上面的代码利用了参数,而不是连接字符串。


如果其他人也想知道,请从文档中获取信息:“行级触发器在BEFORE之前可以返回null,以指示触发器管理器跳过该行的其余操作(即,不触发后续触发器,并且INSERT / UPDATE / DELETE不会在此行发生。如果返回非空值,则操作将使用该行值。”
皮特

我一直在寻找这个答案。使用函数+触发器而不是select语句来清理代码。+1
Jacek Krawczyk

我喜欢这个答案,使用功能和触发器。现在,我找到了使用函数和触发器打破僵局的另一种方法……
Sukma Saputra,

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 

7

这正是我面临的问题,我的版本是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有相同问题的人有所帮助。

谢谢阅读。



2

使用规则很容易:

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

但是它并发写入失败...


1

投票最多的方法(来自John Doe)对我有用,但是在我的情况下,从预期的422行中我只有180行。我找不到任何错误,也没有任何错误,因此我寻找了一个不同的方法简单的方法。

使用IF NOT FOUND THEN后一个SELECT对我来说非常合适。

(在PostgreSQL文档中描述)

文档中的示例:

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

1

psycopgs游标类具有属性rowcount

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

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

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


大概将这些查询包装在事务中将缓解竞争状况。
丹尼尔·里昂斯2012年

谢谢,真正简单,干净的解决方案
Alexander Malfait 2013年

1

您的“一百”列似乎已定义为主键,因此必须是唯一的(不是)。问题不在于此,而在于您的数据。

我建议您插入一个ID作为序列类型以方便主键


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

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

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

-8

解决方法简单,但不立即。
如果要使用此指令,则必须对数据库进行一次更改:

ALTER USER user SET search_path to 'name_of_schema';

这些更改后,“插入”将正常工作。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.