ChatGPT解决这个技术问题 Extra ChatGPT

Postgres: INSERT if does not exist already

I'm using Python to write to a postgres database:

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

But because some of my rows are identical, I get the following error:

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

How can I write an 'INSERT unless this row already exists' SQL statement?

I've seen complex statements like this recommended:

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

But firstly, is this overkill for what I need, and secondly, how can I execute one of those as a simple string?

Regardless of how you solve this issue, you shouldn't generate your query like that. Use parameters in your query and pass the values separately; see stackoverflow.com/questions/902408/…
Why not catch the exception and ignore it?
As of Posgres 9.5(currently on beta2) there is a new upsert like feature, see: postgresql.org/docs/9.5/static/sql-insert.html#SQL-ON-CONFLICT
Have you considered accepting an answer for this? =]
@AP257 Why haven't you accepted any answer yet? For example Arie's answer is very useful and highly upvoted.

a
a_horse_with_no_name

Postgres 9.5 (released since 2016-01-07) offers an "upsert" command, also known as an ON CONFLICT clause to INSERT:

INSERT ... ON CONFLICT DO NOTHING/UPDATE

It solves many of the subtle problems you can run into when using concurrent operation, which some other answers propose.


@TusharJain prior to PostgreSQL 9.5 you can do an "old fashioned" UPSERT (with CTE) but you may experience problems with race conditions and it will not be performant as 9.5 style. There is a good detail about upsert on this blog (in the updated area at the bottom) including some links if you want to read more about the details.
For those needed, here's two simple examples. (1) INSERT if not exists else NOTHING - INSERT INTO distributors (did, dname) VALUES (7, 'Redline GmbH') ON CONFLICT (did) DO NOTHING; (2) INSERT if not exists else UPDATE - INSERT INTO distributors (did, dname) VALUES (5, 'Gizmo Transglobal'), (6, 'Associated Computing, Inc') ON CONFLICT (did) DO UPDATE SET dname = EXCLUDED.dname; These examples are from manual - postgresql.org/docs/9.5/static/sql-insert.html
There is one caveat/side effect. In a table with sequence column (serial or bigserial), even if no row is inserted the sequence is incremented at every insert attempt.
It'd be better linking to INSERT documentation instead of pointing to release. Doc link: postgresql.org/docs/9.5/static/sql-insert.html
If you need to ON CONFLICT DO NOTHING RETURNING id, then read this answer stackoverflow.com/a/42217872/368691.
B
Basil Bourque

How can I write an 'INSERT unless this row already exists' SQL statement?

There is a nice way of doing conditional INSERT in PostgreSQL:

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

CAVEAT This approach is not 100% reliable for concurrent write operations, though. There is a very tiny race condition between the SELECT in the NOT EXISTS anti-semi-join and the INSERT itself. It can fail under such conditions.


How safe is this assuming that the "name"-field has a UNIQUE constraint? Will it ever fail with unique-violation?
This works fine. The only problem is the coupling I guess: what if one modifies the table such that more columns are unique. In that case all the scripts must be modified. It would be nice if there was a more generic way to do this...
Is it possible to use it with RETURNS id for example to get the id whether is has been inserted or not?
@OlivierPons yes, it's possible. Add RETURNING id at the and of the query and it will return either a new row id or nothing, if no row has been inserted.
I have found this to be unreliable. It appears Postgres sometimes executes the insert before it has executed the select and I end up with a duplicate key violation even though the record hasn't been inserted yet. Try to use version =>9.5 with ON CONFLICT.
M
Mike T

One approach would be to create a non-constrained (no unique indexes) table to insert all your data into and do a select distinct from that to do your insert into your hundred table.

So high level would be. I assume all three columns are distinct in my example so for step3 change the NOT EXITS join to only join on the unique columns in the hundred table.

Create temporary table. See docs here. CREATE TEMPORARY TABLE temp_data(name, name_slug, status); INSERT Data into temp table. INSERT INTO temp_data(name, name_slug, status); Add any indexes to the temp table. Do main table insert. 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 );


This is the fastest way I have found to do mass inserts when I do not know if the row already exists.
select 'X'? can someone clarify? This is simply a select statement right: SELECT name,name_slug,status or *
Lookup up correlated subquery. 'X' could be change to a 1 or even 'SadClown'. SQL requires there to be something and 'X' is a common thing to use. It's small and it makes it obvious a correlated subquery is being used and meets the requirements of of what SQL requires.
You mentioned "insert all your data into(assuming temp table) and do a select distinct from that". In that case, should it not be SELECT DISTINCT name, name_slug, status FROM temp_data?
Would this solution be reliable for concurrent write operations? Won't we be expecting race conditions between the INSERT and the SELECT in the subquery?
Q
Quassnoi

Unfortunately, PostgreSQL supports neither MERGE nor ON DUPLICATE KEY UPDATE, so you'll have to do it in two statements:

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

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

You can wrap it into a function:

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

and just call it:

SELECT  fn_upd_invoices('12345', 'TRUE')

Actually, this doesn't work: I can call INSERT INTO hundred (name, name_slug, status) SELECT 'Chichester', 'chichester', NULL WHERE 'Chichester' NOT IN (SELECT NAME FROM hundred); any number of times, and it keeps inserting the row.
@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. There is one record.
Postgres does support ON DUPLICATE KEY UPDATE. It's called ON CONFLICT (column) DO UPDATE SET
@kolypto: sure, since 2016. Feel free to edit the answer.
t
tuanngocptn

This is exactly the problem I face and my version is 9.5

And I solve it with SQL query below.

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;

Hope that will help someone who has the same issue with version >= 9.5.

Thanks for reading.


This answer is the same as @John Doe, where the caveat of concurrent write operations has been pointed out.
@RianLauw: yeah right, That is the same about 80% ~ 90%. But you can use both way to find the better answer for you.
佚名

You can make use of VALUES - available in Postgres:

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

SELECT name FROM Person <--- what if there's a billion rows in person?
I think this is a nice quick way to solve the issue, but only when you're sure the source table will never grow huge. I've got a table which will never have more than 1000 rows, so I can use this solution.
WOW, this is exactly what I needed. I was worried I'd need to create a function or a temp table, but this precludes all that--thank you!
@HenleyChiu has quite a good point. Since those selects are agains existing table maybe we can add a where clause to each ensuring we only select 'Bob''s row?
k
ktr

I know this question is from a while ago, but thought this might help someone. I think the easiest way to do this is via a trigger. E.g.:

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

Execute this code from a psql prompt (or however you like to execute queries directly on the database). Then you can insert as normal from Python. E.g.:

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

Note that as @Thomas_Wouters already mentioned, the code above takes advantage of parameters rather than concatenating the string.


If anyone else was wondering too, from the docs: "Row-level triggers fired BEFORE can return null to signal the trigger manager to skip the rest of the operation for this row (i.e., subsequent triggers are not fired, and the INSERT/UPDATE/DELETE does not occur for this row). If a nonnull value is returned then the operation proceeds with that row value."
Exactly this answer I was looking for. Clean code, using function + trigger instead of select statement. +1
I love this answer, use function and trigger. Now I find another way to break the deadlock using functions and triggers...
Z
Zoey Hewll

There is a nice way of doing conditional INSERT in PostgreSQL using WITH query: 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 

This is essentially the same as the answer (here stackoverflow.com/a/13342031/5294065) by @John Doe, where the subquery is just a CTE, right?
B
Brian Burns

we can simplify the query using upsert

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

P
Pavel Francírek

INSERT .. WHERE NOT EXISTS is good approach. And race conditions can be avoided by transaction "envelope":

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

佚名

It's easy with rules:

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

But it fails with concurrent writes ...


C
Christoph Lösch

The approach with the most upvotes (from John Doe) does somehow work for me but in my case from expected 422 rows i get only 180. I couldn't find anything wrong and there are no errors at all, so i looked for a different simple approach.

Using IF NOT FOUND THEN after a SELECT just works perfectly for me.

(described in PostgreSQL Documentation)

Example from documentation:

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

b
bcorso

psycopgs cursor class has the attribute 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).

So you could try UPDATE first and INSERT only if rowcount is 0.

But depending on activity levels in your database you may hit a race condition between UPDATE and INSERT where another process may create that record in the interim.


Presumably wrapping these queries in a transaction would alleviate the race condition.
B
Boodoo

Your column "hundred" seems to be defined as primary key and therefore must be unique which is not the case. The problem isn't with, it is with your data.

I suggest you insert an id as serial type to handly the primary key


o
opena

If you say that many of your rows are identical you will end checking many times. You can send them and the database will determine if insert it or not with the ON CONFLICT clause as follows

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

J
Jeff Fairley

I was looking for a similar solution, trying to find SQL that work work in PostgreSQL as well as HSQLDB. (HSQLDB was what made this difficult.) Using your example as a basis, this is the format that I found elsewhere.

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

P
Patrick

Here is a generic python function that given a tablename, columns and values, generates the upsert equivalent for postgresql.

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

e
el fuser

The solution in simple, but not immediatly. If you want use this instruction, you must make one change to the db:

ALTER USER user SET search_path to 'name_of_schema';

after these changes "INSERT" will work correctly.