【问题标题】:Postgres: INSERT if does not exist alreadyPostgres:如果不存在则插入
【发布时间】:2011-05-03 10:52:50
【问题描述】:

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

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

【问题讨论】:

  • 不管你如何解决这个问题,你都不应该这样生成你的查询。在查询中使用参数并分别传递值;见stackoverflow.com/questions/902408/…
  • 为什么不捕获异常并忽略它?
  • Posgres 9.5(目前在 beta2 上)有一个类似 upsert 的新功能,请参阅:postgresql.org/docs/9.5/static/sql-insert.html#SQL-ON-CONFLICT
  • 您是否考虑过接受这个答案? =]
  • @AP257 为什么您还没有接受任何答案?例如,Arie 的回答非常有用且获得了高度评价。

标签: postgresql sql-insert upsert


【解决方案1】:

Postgres 9.5(自 2016 年 1 月 7 日发布)提供了一个 "upsert" 命令,也称为 ON CONFLICT clause to INSERT

INSERT ... ON CONFLICT DO NOTHING/UPDATE

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

【讨论】:

  • @TusharJain 在 PostgreSQL 9.5 之前,您可以执行“老式”UPSERT(使用 CTE),但您可能会遇到竞争条件问题,并且它的性能不如 9.5 样式。 blog(在底部的更新区域中)有关于 upsert 的详细信息,包括一些链接,如果您想了解更多关于详细信息的信息。
  • 对于那些需要的,这里有两个简单的例子。 (1) 如果不存在则插入,否则没有任何东西 - INSERT INTO distributors (did, dname) VALUES (7, 'Redline GmbH') ON CONFLICT (did) DO NOTHING; (2) 如果不存在,则插入,否则更新 - 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
  • 有一个警告/副作用。在具有序列列(序列或大序列)的表中,即使没有插入任何行,序列也会在每次插入尝试时递增。
  • 最好链接到 INSERT 文档而不是指向发布。文档链接:postgresql.org/docs/9.5/static/sql-insert.html
  • 如果您需要ON CONFLICT DO NOTHING RETURNING id,请阅读此答案stackoverflow.com/a/42217872/368691
【解决方案2】:

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

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

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

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

【讨论】:

  • 假设“名称”字段具有唯一约束,这有多安全?它会因违反唯一性而失败吗?
  • 这很好用。唯一的问题是我猜想的耦合:如果修改表以使更多列是唯一的怎么办。在这种情况下,必须修改所有脚本。如果有更通用的方法来做到这一点,那就太好了......
  • 是否可以将它与RETURNS id 一起使用,例如获取id 是否已插入?
  • @OlivierPons 是的,这是可能的。在查询的 and 处添加RETURNING id,如果没有插入行,它将返回一个新的行 id 或不返回任何内容。
  • 我发现这不可靠。似乎 Postgres 有时会在执行选择之前执行插入,即使尚未插入记录,我也会遇到重复的键冲突。尝试使用版本 => 9.5 和 ON CONFLICT。
【解决方案3】:

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

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

  1. 创建临时表。请参阅文档here

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

【讨论】:

  • 当我不知道行是否已经存在时,这是我发现的进行批量插入的最快方法。
  • 选择“X”?有人可以澄清吗?这只是一个选择语句吧:SELECT name,name_slug,status*
  • 查找相关子查询。 'X' 可以变成 1 甚至是 'SadClown'。 SQL 要求有一些东西,'X' 是一个常用的东西。它很小,很明显正在使用相关子查询并满足 SQL 的要求。
  • 您提到“将所有数据插入(假设临时表)并执行与该不同的选择”。那样的话,不应该是SELECT DISTINCT name, name_slug, status FROM temp_data吗?
  • 此解决方案对于并发写入操作是否可靠?我们不会期待子查询中INSERTSELECT 之间的竞争条件吗?
【解决方案4】:

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

【讨论】:

  • 其实这行不通:我可以调用INSERT INTO hundred (name, name_slug, status) SELECT 'Chichester', 'chichester', NULL WHERE 'Chichester' NOT IN (SELECT NAME FROM hundred);任意次数,它一直在插入行。
  • @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。有一条记录。
  • Postgres 确实支持ON DUPLICATE KEY UPDATE。它叫ON CONFLICT (column) DO UPDATE SET
  • @kolypto:当然,自 2016 年以来。请随意编辑答案。
【解决方案5】:

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

感谢阅读。

【讨论】:

  • 此答案与@John Doe 相同,其中指出了并发写入操作的警告。
  • @RianLauw:是的,没错,大约 80% ~ 90% 是一样的。但是您可以同时使用这两种方法来为您找到更好的答案。
【解决方案6】:

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

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

【讨论】:

  • SELECT name FROM Person
  • 我认为这是解决问题的好方法,但前提是您确定源表永远不会变大。我有一个永远不会超过 1000 行的表,所以我可以使用这个解决方案。
  • 哇,这正是我所需要的。我担心我需要创建一个函数或一个临时表,但这排除了所有这些——谢谢!
  • @HenleyChiu 有一个很好的观点。由于这些选择是针对现有表,也许我们可以为每个选择添加一个 where 子句,以确保我们只选择“Bob”的行?
【解决方案7】:

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

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

【讨论】:

  • 如果其他人也想知道,来自docs:“触发的行级触发器 BEFORE 可以返回 null 以指示触发器管理器跳过该行的其余操作(即后续触发器不会被触发,并且该行不会发生 INSERT/UPDATE/DELETE)。如果返回非空值,则操作将继续使用该行值。"
  • 正是我正在寻找的这个答案。干净的代码,使用函数 + 触发器而不是 select 语句。 +1
  • 我喜欢这个答案,使用函数和触发器。现在我找到了另一种使用函数和触发器来打破僵局的方法......
【解决方案8】:

在 PostgreSQL 中使用 WITH 查询有一个很好的方法来进行条件插入: 喜欢:

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 

【讨论】:

【解决方案9】:

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

BEGIN;
LOCK TABLE hundred IN SHARE ROW EXCLUSIVE MODE;
INSERT ... ;
COMMIT;

【讨论】:

    【解决方案10】:

    我们可以使用 upsert 来简化查询

    insert into invoices (invoiceid, billed) 
      values ('12345', 'TRUE') 
      on conflict (invoiceid) do 
        update set billed=EXCLUDED.billed;
    

    【讨论】:

      【解决方案11】:

      规则很简单:

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

      但是并发写入失败...

      【讨论】:

        【解决方案12】:

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

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

        (在PostgreSQL Documentation中描述)

        文档示例:

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

        【讨论】:

          【解决方案13】:

          psycopgs 游标类具有属性rowcount

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

          因此,您可以先尝试 UPDATE,然后仅当 rowcount 为 0 时才尝试 INSERT。

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

          【讨论】:

          • 大概将这些查询包装在事务中会缓解竞争条件。
          【解决方案14】:

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

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

          【讨论】:

            【解决方案15】:

            如果您说您的许多行是相同的,您将结束检查很多次。您可以发送它们,数据库将使用 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);
            

            【讨论】:

              【解决方案16】:

              我一直在寻找类似的解决方案,试图找到在 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 );"
              

              【讨论】:

                【解决方案17】:

                这是一个通用的 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)
                

                【讨论】:

                  【解决方案18】:

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

                  ALTER USER user SET search_path to 'name_of_schema';
                  

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

                  【讨论】:

                    猜你喜欢
                    • 2013-02-18
                    • 2018-09-17
                    • 1970-01-01
                    • 1970-01-01
                    • 2014-06-11
                    • 1970-01-01
                    • 2017-04-24
                    • 1970-01-01
                    • 2016-05-15
                    相关资源
                    最近更新 更多