711

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?

6
  • 87
    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/… Commented Nov 1, 2010 at 14:36
  • 3
    Why not catch the exception and ignore it? Commented Mar 31, 2015 at 19:42
  • 8
    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 Commented Nov 11, 2015 at 14:08
  • 5
    Have you considered accepting an answer for this? =] Commented Dec 2, 2015 at 14:34
  • @AP257 Why haven't you accepted any answer yet? For example Arie's answer is very useful and highly upvoted. Commented Oct 11, 2021 at 20:22

22 Answers 22

1205
+250

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.

Sign up to request clarification or add additional context in comments.

15 Comments

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.
@GrzegorzLuczywo is there anyway around the serial being incremented?
|
502

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.

12 Comments

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.
|
66

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 EXISTS join to only join on the unique columns in the hundred table.

  1. Create temporary table. See docs here.

     CREATE TEMPORARY TABLE temp_data(name, name_slug, status);
    
  2. INSERT Data into temp table.

     INSERT INTO temp_data(name, name_slug, status); 
    
  3. Add any indexes to the temp table.

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

7 Comments

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?
|
21

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.

2 Comments

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.
20

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

5 Comments

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.
Supports MERGE too since v15.
17

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;

4 Comments

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?
10

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.

3 Comments

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...
10

we can simplify the query using upsert

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

Comments

8

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 

1 Comment

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?
5

You can also use MERGE introduced in Postgres 15

Nevertheless, not as straightforward as INSERT … ON CONFLICT DO NOTHING

1 Comment

I don't think merge works in all cases. Firstly, I don't think you want to insert for all non-matches and secondly on an empty table the insert would still fail even though the id does not exist.
4

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;

Comments

3

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

Comments

1

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;

Comments

1

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.

1 Comment

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

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

Comments

1

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

Comments

1

In Postgres version 9.5 or higher you can use ON CONFLICT to avoid errors of contraints like @Arie mentioned above. To know more options related to this INSERT query refer to Postgres Docs. An alternative solution is by using try/catch to handle runtime errors

Comments

0

First and foremost: Use INSERT ... ON CONFLICT (PostgreSQL's idiomatic way)

This is the cleanest and safest solution.

Python Example (Safe and Clean)

import psycopg2

# Example variables

hundred = 'your-name'
hundred_slug = 'like-your-name'
status = 'active'

sql = """
INSERT INTO hundred (name, name_slug, status)
VALUES (%s, %s, %s)
ON CONFLICT (name) DO NOTHING;
"""
cursor.execute(sql, (hundred, hundred_slug, status))
 

Below are my reason to choose this way

  1. Prevents SQL injection

  2. Easy to read, review for others and understand.

  3. Properly handles data escaping

Comments

-1
INSERT INTO invoices (invoiceid, billed) (
    SELECT '12345','TRUE' WHERE NOT EXISTS (
        SELECT 1 FROM invoices WHERE invoiceid='12345' AND billed='TRUE'
        )
)

Comments

-2

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

Comments

-2

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)

Comments

-19

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.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.