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%可靠。在
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 ); |
不幸的是,
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行。我找不到任何错误,也没有任何错误,所以我寻找了一种不同的简单方法。
在使用
(在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'; |
这些更改之后,"插入"将正常工作。