Postgres:INSERT如果不存在
我正在使用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"
我怎样才能写一个“INSERT,除非这行已经存在”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
但首先,这是为我所需要的矫枉过正,其次,我怎么能执行其中的一个作为一个简单的string?
我怎样才能写一个“INSERT,除非这行已经存在”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%可靠的。 NOT EXISTS
反半连接中的SELECT
和INSERT
本身之间有一个非常小的竞争条件。 在这种情况下可能会失败。
Postgres 9.5(自2016-01-07发布)提供了一个“upsert”命令:
INSERT ... ON CONFLICT DO NOTHING/UPDATE
它解决了在使用并发操作时可能遇到的许多微妙的问题,其他一些答案提出。
一种方法是创build一个非约束(没有唯一索引)表来插入所有的数据,并做一个select不同,做你的插入到你的一百表。
如此高的水平将是。 我假设在我的例子中,所有三列都是不同的,所以对于step3,将NOT EXITS连接更改为只连接百个表中的唯一列。
-
创build临时表。 在这里看到文档。
CREATE TEMPORARY TABLE temp_data(name, name_slug, status);
-
将数据插入临时表。
INSERT INTO temp_data(name, name_slug, status);
-
将任何索引添加到临时表中。
-
做主表插入。
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
,所以你必须用两条语句来完成:
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 invoices (invoiceid, billed) SELECT '12345', 'TRUE' WHERE NOT EXISTS (SELECT 1 FROM invoices WHERE invoiceid = '12345')
你可以使用VALUES – 在Postgres中可用:
INSERT INTO person (name) SELECT name FROM person UNION VALUES ('Bob') EXCEPT SELECT name FROM person;
我知道这个问题是来自不久前,但认为这可能有助于某人。 我认为最简单的方法是通过触发器。 例如:
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已经提到的,上面的代码利用了参数,而不是连接string。
INSERT .. WHERE不存在是很好的方法。 交易“信封”可以避免竞争条件:
BEGIN; LOCK TABLE hundred IN SHARE ROW EXCLUSIVE MODE; INSERT ... ; COMMIT;
(John Doe)最有帮助的方法对我来说确实有效,但在我的情况下,从预期的422行我只得到180.我找不到任何错误,没有任何错误,所以我寻找一个不同的简单的方法。
在SELECT
之后使用IF NOT FOUND THEN
完全适合我。
(在PostgreSQL文档中描述)
文档示例:
SELECT * INTO myrec FROM emp WHERE empname = myname; IF NOT FOUND THEN RAISE EXCEPTION 'employee % not found', myname; END IF;
psycopgs游标类有属性rowcount 。
此只读属性指定最后一次执行*()所产生(对于DQL语句(如SELECT))或受影响(对于DML语句(如UPDATE或INSERT))的行数。
所以你可以先尝试更新和INSERT只有当rowcount是0。
但根据数据库中的活动级别,您可能会遇到UPDATE和INSERT之间的争用情况,在这种情况下,另一个进程可能会在此期间创build该logging。
规则很简单:
CREATE RULE file_insert_defer AS ON INSERT TO file WHERE (EXISTS ( SELECT * FROM file WHERE file.id = new.id)) DO INSTEAD NOTHING
但它并发写入失败…
我正在寻找一个类似的解决scheme,试图find在PostgreSQL中工作的SQL以及HSQLDB。 (HSQLDB是这样做的困难。)以您的示例为基础,这是我在其他地方find的格式。
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
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)
有一个很好的方式来使用WITH查询在PostgreSQL中执行条件INSERT:Like:
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
简单的解决scheme,但不立即。
如果你想使用这个指令,你必须对db进行一次修改:
ALTER USER user SET search_path to 'name_of_schema';
经过这些更改后,“INSERT”将正常工作。