You need to use table EXCLUDED instead of value literals in your ON CONFLICT statement:

The SET and WHERE clauses in ON CONFLICT DO UPDATE have access to the existing row using the table's name (or an alias), and to the row proposed for insertion using the special excluded table.

You also don't need to re-set the conflicting values, only the rest.

INSERT INTO table (col1, col2, col3) 
VALUES 
    (value1, value2, value3), 
    (value4, value5, value6)
ON CONFLICT (col1) DO UPDATE 
SET (col2, col3) = (EXCLUDED.col2, EXCLUDED.col3);

For readability, you can format your in-line SQLs if you triple-quote your f-strings. I'm not sure if and which IDEs can detect it's an in-line SQL in Python and switch syntax highlighting, but I find indentation helpful enough.

upsert_statement = f"""
    INSERT INTO table (col1, col2, col3) 
    VALUES 
        ({value1}, {value2}, {value3}), 
        ({value4}, {value5}, {value6})
    ON CONFLICT (col1) DO UPDATE 
    SET (col2, col3) = (EXCLUDED.col2, EXCLUDED.col3)"""

Here's a test at db<>fiddle:

drop table if exists test_70066823 cascade;
create table test_70066823 (
    id integer primary key, 
    text_column_1 text, 
    text_column_2 text);
insert into test_70066823 values
  (1,'first','first')
 ,(2,'second','second') returning *;
id text_column_1 text_column_2
1 first first
2 second second
insert into test_70066823
values  (1, 'third','first'),
        (3, 'fourth','third'),
        (4, 'fifth','fourth'),
        (2, 'sixth','second')
on conflict (id) do update 
set text_column_1=EXCLUDED.text_column_1,
    text_column_2=EXCLUDED.text_column_2
returning *;
id text_column_1 text_column_2
1 third first
3 fourth third
4 fifth fourth
2 sixth second

You can refer to this for improved insert performance. Inserts with a simple string-based execute or execute_many are the top 2 slowest approaches mentioned there.

Answer from Zegarek on Stack Overflow
Top answer
1 of 1
3

You need to use table EXCLUDED instead of value literals in your ON CONFLICT statement:

The SET and WHERE clauses in ON CONFLICT DO UPDATE have access to the existing row using the table's name (or an alias), and to the row proposed for insertion using the special excluded table.

You also don't need to re-set the conflicting values, only the rest.

INSERT INTO table (col1, col2, col3) 
VALUES 
    (value1, value2, value3), 
    (value4, value5, value6)
ON CONFLICT (col1) DO UPDATE 
SET (col2, col3) = (EXCLUDED.col2, EXCLUDED.col3);

For readability, you can format your in-line SQLs if you triple-quote your f-strings. I'm not sure if and which IDEs can detect it's an in-line SQL in Python and switch syntax highlighting, but I find indentation helpful enough.

upsert_statement = f"""
    INSERT INTO table (col1, col2, col3) 
    VALUES 
        ({value1}, {value2}, {value3}), 
        ({value4}, {value5}, {value6})
    ON CONFLICT (col1) DO UPDATE 
    SET (col2, col3) = (EXCLUDED.col2, EXCLUDED.col3)"""

Here's a test at db<>fiddle:

drop table if exists test_70066823 cascade;
create table test_70066823 (
    id integer primary key, 
    text_column_1 text, 
    text_column_2 text);
insert into test_70066823 values
  (1,'first','first')
 ,(2,'second','second') returning *;
id text_column_1 text_column_2
1 first first
2 second second
insert into test_70066823
values  (1, 'third','first'),
        (3, 'fourth','third'),
        (4, 'fifth','fourth'),
        (2, 'sixth','second')
on conflict (id) do update 
set text_column_1=EXCLUDED.text_column_1,
    text_column_2=EXCLUDED.text_column_2
returning *;
id text_column_1 text_column_2
1 third first
3 fourth third
4 fifth fourth
2 sixth second

You can refer to this for improved insert performance. Inserts with a simple string-based execute or execute_many are the top 2 slowest approaches mentioned there.

๐ŸŒ
Billyfung
billyfung.com โ€บ posts โ€บ 2017-06-30-psycopg2-multiple-insert
Improving Multiple Inserts with Psycopg2 - Billy Fung
I'm using Python 3 and the Psycopg2 postgres driver. ... def insert_data(filename, date): sql = """ INSERT INTO test (a, b, c, d) VALUES (%s, %s, %s, %s) """ with open(filename) as csvfile, get_cursor() as c: reader = csv.reader(csvfile) header = next(reader) for row in reader: n = row[0][2:] values = (row[1], date, n, row[2]) c.execute(sql, values)
Discussions

python - Upsert multiple rows in PostgreSQL with psycopg2 and errors logging - Stack Overflow
I'm writing an application that connects to database and upserts multiple rows, it creates SAVEPOINT for every row, so I can rollback without breaking a transaction, if there is a mistake, and comm... More on stackoverflow.com
๐ŸŒ stackoverflow.com
August 5, 2017
python - psycopg2: insert multiple rows with one query - Stack Overflow
I need to insert multiple rows with one query (number of rows is not constant), so I need to execute query like this one: INSERT INTO t (a, b) VALUES (1, 2), (3, 4), (5, 6); The only way I know is... More on stackoverflow.com
๐ŸŒ stackoverflow.com
Postgres 9.5 upsert command in pandas or psycopg2? - Stack Overflow
Most of the examples I see are people inserting a single row into a database with the ON CONFLICT DO UPDATE syntax. Does anyone have any examples using SQLAlchemy or pandas.to_sql? 99% of my inse... More on stackoverflow.com
๐ŸŒ stackoverflow.com
python - psycopg2: Update multiple rows in a table with values from a tuple of tuples - Stack Overflow
I'm attempting to update several rows at once using a tuple of tuples. I figured out how to construct the sql statement from this post, but implementing it in psycopg2 has proven to be more challen... More on stackoverflow.com
๐ŸŒ stackoverflow.com
๐ŸŒ
Stack Overflow
stackoverflow.com โ€บ questions โ€บ 45523539 โ€บ upsert-multiple-rows-in-postgresql-with-psycopg2-and-errors-logging
python - Upsert multiple rows in PostgreSQL with psycopg2 and errors logging - Stack Overflow
August 5, 2017 - Switch to INSERT INTO ... ON CONFLICT UPDATE .... Your current model is always going to perform terribly, it's doing multiple round-trips per row. If you can't use 9.5 upsert support, then you can improve it by using a stored proc to at least reduce round trips.
Top answer
1 of 16
308

I built a program that inserts multiple lines to a server that was located in another city.

I found out that using this method was about 10 times faster than executemany. In my case tup is a tuple containing about 2000 rows. It took about 10 seconds when using this method:

args_str = ','.join(cur.mogrify("(%s,%s,%s,%s,%s,%s,%s,%s,%s)", x) for x in tup)
cur.execute("INSERT INTO table VALUES " + args_str) 

and 2 minutes when using this method:

cur.executemany("INSERT INTO table VALUES(%s,%s,%s,%s,%s,%s,%s,%s,%s)", tup)
2 of 16
261

As of 2026 Psycopg 3

The executemany() method is the suggested solution in the psycopg documentation. It is shown below in the execute_many function. The alternative, demonstrated in the execute_array function, is to make psycopg adapt lists to arrays and execute() as a single query, hopefully faster.

Given the table t as:

create table t (i int, s text)
import psycopg

def execute_many(cur, the_list):

   query = '''insert into t (i, s) values (%s, %s)'''
   print('execute_many queries:\n')
   print('\n'.join([
      psycopg.ClientCursor(conn).mogrify(query, params) for params in the_list
      ]))
   cur.executemany(query, params_seq=the_list)

def execute_array(cur, the_list):

   # must be passed as lists
   i, s = [list(e) for e in list(zip(*the_list))]
   query = '''
   insert into t (i, s)
   select i, s
   from unnest(%(i)s, %(s)b::text[]) s (i, s)
   '''
   print('execute_array query')
   print(psycopg.ClientCursor(conn).mogrify(query, dict(i=i,s=s)))
   cur.execute(query, dict(i=i,s=s))

the_list = [(1,'a'),(2,'b'),(3,'c,w"')]
conn = psycopg.connect('')
cur = conn.cursor()
execute_array(cur, the_list)
execute_many(cur, the_list)
conn.commit()
conn.close()

Output:

execute_array query

   insert into t (i, s)
   select i, s
   from unnest('{1,2,3}'::int2[],  E'{a,b,"c,w\\""}'::text[]) s (i, s)
   
execute_many queries:

   insert into t (i, s) values (1, 'a')   
   insert into t (i, s) values (2, 'b')   
   insert into t (i, s) values (3, 'c,w"')   

To use the array query the string list must be passed as binary to psycopg and hard adapted as a text array with %(s)b::text[]

Answer from 2015

New execute_values method in Psycopg 2.7:

data = [(1,'x'), (2,'y')]
insert_query = 'insert into t (a, b) values %s'
psycopg2.extras.execute_values (
    cursor, insert_query, data, template=None, page_size=100
)

The pythonic way of doing it in Psycopg 2.6:

data = [(1,'x'), (2,'y')]
records_list_template = ','.join(['%s'] * len(data))
insert_query = 'insert into t (a, b) values {}'.format(records_list_template)
cursor.execute(insert_query, data)

Explanation: If the data to be inserted is given as a list of tuples like in

data = [(1,'x'), (2,'y')]

then it is already in the exact required format as

  1. the values syntax of the insert clause expects a list of records as in

    insert into t (a, b) values (1, 'x'),(2, 'y')

  2. Psycopg adapts a Python tuple to a Postgresql record.

The only necessary work is to provide a records list template to be filled by psycopg

# We use the data list to be sure of the template length
records_list_template = ','.join(['%s'] * len(data))

and place it in the insert query

insert_query = 'insert into t (a, b) values {}'.format(records_list_template)

Printing the insert_query outputs

insert into t (a, b) values %s,%s

Now to the usual Psycopg arguments substitution

cursor.execute(insert_query, data)

Or just testing what will be sent to the server

print (cursor.mogrify(insert_query, data).decode('utf8'))

Output:

insert into t (a, b) values (1, 'x'),(2, 'y')
๐ŸŒ
PYnative
pynative.com โ€บ home โ€บ python โ€บ databases โ€บ python postgresql insert, update, and delete from a table using psycopg2
Python PostgreSQL Insert, Update, and Delete from a Table using Psycopg2
March 9, 2021 - Perform PostgreSQL CRUD operations from Python. Insert, Update and Delete single and multiple rows from PostgreSQL table using Python. use of cursor.executemany()
Top answer
1 of 3
2

Here is my code for bulk insert & insert on conflict update query for postgresql from pandas dataframe:

Lets say id is unique key for both postgresql table and pandas df and you want to insert and update based on this id.

import pandas as pd
from sqlalchemy import create_engine, text

engine = create_engine(postgresql://username:pass@host:port/dbname)
query = text(f""" 
                INSERT INTO schema.table(name, title, id)
                VALUES {','.join([str(i) for i in list(df.to_records(index=False))])}
                ON CONFLICT (id)
                DO  UPDATE SET name= excluded.name,
                               title= excluded.title
         """)
engine.execute(query)

Make sure that your df columns must be same order with your table.

2 of 3
0

FYI, this is the solution I am using currently.

It seems to work fine for my purposes. I had to add a line to replace null (NaT) timestamps with None though, because I was getting an error when I was loading each row into the database.

def create_update_query(table):
    """This function creates an upsert query which replaces existing data based on primary key conflicts"""
    columns = ', '.join([f'{col}' for col in DATABASE_COLUMNS])
    constraint = ', '.join([f'{col}' for col in PRIMARY_KEY])
    placeholder = ', '.join([f'%({col})s' for col in DATABASE_COLUMNS])
    updates = ', '.join([f'{col} = EXCLUDED.{col}' for col in DATABASE_COLUMNS])
    query = f"""INSERT INTO {table} ({columns}) 
                VALUES ({placeholder}) 
                ON CONFLICT ({constraint}) 
                DO UPDATE SET {updates};"""
    query.split()
    query = ' '.join(query.split())
    return query


def load_updates(df, table, connection):
    conn = connection.get_conn()
    cursor = conn.cursor()
    df1 = df.where((pd.notnull(df)), None)
    insert_values = df1.to_dict(orient='records')
    for row in insert_values:
        cursor.execute(create_update_query(table=table), row)
        conn.commit()
    row_count = len(insert_values)
    logging.info(f'Inserted {row_count} rows.')
    cursor.close()
    del cursor
    conn.close()
Find elsewhere
๐ŸŒ
Trvrm
trvrm.github.io โ€บ bulk-psycopg2-inserts.html
Efficient Postgres Bulk Inserts using Psycopg2 and Unnest
fastInsert() is my new approach, based on using unnest() to unroll a set of arrays passed in through psycopg2 ยท class Tester(): def __init__(self,count): execute(SETUP_SQL) self.count=count self.data=[ { 'text':'Some text', 'properties': {"key":"value"}, } for i in range(count) ] def slowInsert(self): ''' Creates a new connection for each insertion ''' for row in self.data: text=row['text'] properties=row['properties'] execute(SINGLE_INSERT,locals()) def insert(self): ''' One connection. Multiple queries.
Top answer
1 of 5
25

To quote from psycopg2's documentation:

Warning Never, never, NEVER use Python string concatenation (+) or string parameters interpolation (%) to pass variables to a SQL query string. Not even at gunpoint.

Now, for an upsert operation you can do this:

insert_sql = '''
    INSERT INTO tablename (col1, col2, col3, col4)
    VALUES (%s, %s, %s, %s)
    ON CONFLICT (col1) DO UPDATE SET
    (col2, col3, col4) = (EXCLUDED.col2, EXCLUDED.col3, EXCLUDED.col4);
'''
cur.execute(insert_sql, (val1, val2, val3, val4))

Notice that the parameters for the query are being passed as a tuple to the execute statement (this assures psycopg2 will take care of adapting them to SQL while shielding you from injection attacks).

The EXCLUDED bit allows you to reuse the values without the need to specify them twice in the data parameter.

2 of 5
8

Using:

INSERT INTO members (member_id, customer_id, subscribed, customer_member_id, phone, cust_atts) VALUES (%s, %s, %s, %s, %s, %s) ON CONFLICT (customer_member_id) DO UPDATE SET (phone) = (EXCLUDED.phone);

I received the following error:

psycopg2.errors.FeatureNotSupported: source for a multiple-column UPDATE item must be a sub-SELECT or ROW() expression
LINE 1: ...ICT (customer_member_id) DO UPDATE SET (phone) = (EXCLUDED.p...

Changing to:

INSERT INTO members (member_id, customer_id, subscribed, customer_member_id, phone, cust_atts) VALUES (%s, %s, %s, %s, %s, %s) ON CONFLICT (customer_member_id) DO UPDATE SET (phone) = ROW(EXCLUDED.phone);

Solved the issue.

๐ŸŒ
Naysan
naysan.ca โ€บ 2020 โ€บ 05 โ€บ 09 โ€บ pandas-to-postgresql-using-psycopg2-bulk-insert-performance-benchmark
Pandas to PostgreSQL using Psycopg2: Bulk Insert Performance Benchmark | Naysan Saran
import os import psycopg2 import numpy as np import psycopg2.extras as extras from io import StringIO def execute_many(conn, df, table): """ Using cursor.executemany() to insert the dataframe """ # Create a list of tupples from the dataframe values tuples = [tuple(x) for x in df.to_numpy()] # Comma-separated dataframe columns cols = ','.join(list(df.columns)) # SQL quert to execute query = "INSERT INTO %s(%s) VALUES(%%s,%%s,%%s)" % (table, cols) cursor = conn.cursor() try: cursor.executemany(query, tuples) conn.commit() except (Exception, psycopg2.DatabaseError) as error: print("Error: %s" % e
๐ŸŒ
PostgreSQL
postgresql.org โ€บ message-id โ€บ CA+mi_8bGfzhEr0+t2FjZGDRnQP45MC1E3C_djdBem_xZQXsD8A@mail.gmail.com
PostgreSQL: Re: Fastest way to insert/update many rows
August 12, 2014 - > I have to set one column in each row, is there a way to update cursors like in PL/pgSQL's > update <table> set ... where current of <cursor> > i.e. iterate through the rows in the most efficient way for the database.
๐ŸŒ
Reddit
reddit.com โ€บ r/python โ€บ using psycopg2. can i do multiple inserts with out having to open a connection to postgres for each insert?
r/Python on Reddit: Using psycopg2. Can I do multiple inserts with out having to open a connection to postgres for each insert?
May 22, 2014 -

Python super noob here.

This is my first python script that deals with a database. However, I am wondering if there is a way to do inserts with out having to open and close a postgres connection each time an insert is done. I understand I may be able to use a bulk insert but I am interested in individual inserts. Below is my code.

#!/usr/bin/python
import psycopg2
counter = 0
#Connect to DB
conn = psycopg2.connect("dbname=testing user=postgres")
cur = conn.cursor()
cur.execute("CREATE TABLE test (id serial NOT NULL PRIMARY KEY,   f_name varchar(20), l_name varchar(20))");
conn.commit()
cur.close()
conn.close()

while counter < 1000:
        conn = psycopg2.connect("dbname=testing user=postgres")
        cur = conn.cursor()
        cur.execute("INSERT INTO test (f_name, l_name) VALUES (%s, %s)", ("Bob", "Babaluba"))
        conn.commit()
        cur.close()
        conn.close()
        counter = counter + 1

Thank you folks :)

Top answer
1 of 1
1

Two ways to deal with this.

1) Do this:

alter <the_table> alter column id set default nextval('name_of_sequence').

Then you won't have specify the id value in the query. If the sequence is dedicated to that table.column then also:

alter sequence name_of_sequence owned by <table_name>.id;

That will create a dependency so that if the table is dropped the sequence will be also.

2)

Leave the sequence freestanding and just pull the values as needed using procedures below.

SQL schema

create table py_seq_test(id bigint, fld_1 varchar);
create sequence py_seq;

Python code

a) Regular execute

import psycopg2

con = psycopg2.connect("dbname=test host=localhost  user=postgres")
cur = con.cursor()

cur.execute("insert into py_seq_test values(nextval('py_seq'), %s)", ['test1'])
cur.execute("insert into py_seq_test values(nextval('py_seq'), '%s')", ['test2'])
con.commit()
cur.execute("select * from py_seq_test")
cur.fetchall()

[(1, 'test'), (2, 'test2')]

b) execute_values

from psycopg2.extras import execute_values

execute_values(cur, 
   "insert into py_seq_test values %s", 
   [('test3',), ('test4',) ], 
   template="(nextval('py_seq'),  %s)")

con.commit()

cur.execute("select * from py_seq_test")
cur.fetchall()

[(1, 'test'), (2, 'test2'), (3, 'test3'), (4, 'test4']


UPDATE

Example of how execute() compares to execute_values() over inserting 10000 values. This is done in ipython using the %%time cell magic.

large_list = [(val,) for val in range(10000)]

%%time
for i_val in large_list:
    cur.execute("insert into py_seq_test values(nextval('py_seq'), %s)", [i_val[0]])
con.commit()

CPU times: user 157 ms, sys: 103 ms, total: 260 ms
Wall time: 790 ms

%%time
execute_values(cur, 
   "insert into py_seq_test values %s", 
   large_list, 
   template="(nextval('py_seq'),  %s)")
con.commit()

CPU times: user 30.6 ms, sys: 320 ยตs, total: 30.9 ms
Wall time: 164 ms

๐ŸŒ
Medium
medium.com โ€บ @santhanu โ€บ batch-upsert-pyspark-dataframe-into-postgres-tables-with-error-handling-using-psycopg2-and-asyncpg-59f08aa020b0
How to batch upsert PySpark DataFrame into Postgres tables with error handling using psycopg2 and asyncpg | by Santhanu | Medium
March 1, 2022 - Currently, Spark DataFrameWriter does not support any relational database upserts. We can only overwrite or append to an existing table in the database. However, we can use spark foreachPartition in conjunction with python postgres database packages like psycopg2 or asyncpg and upsert data into postgres tables by applying a function to each spark DataFrame partition.
๐ŸŒ
GeeksforGeeks
geeksforgeeks.org โ€บ python โ€บ python-psycopg2-insert-multiple-rows-with-one-query
Python Psycopg2 - Insert multiple rows with one query - GeeksforGeeks
July 23, 2025 - # importing packages import psycopg2 # forming connection conn = psycopg2.connect( database="Classroom", user='postgres', password='pass', host='127.0.0.1', port='5432' ) conn.autocommit = True # creating a cursor cursor = conn.cursor() # list of rows to be inserted values = [(14, 'Ian', 78), (15, 'John', 88), (16, 'Peter', 92)] # cursor.mogrify() to insert multiple values args = ','.join(cursor.mogrify("(%s,%s,%s)", i).decode('utf-8') for i in values) # executing the sql statement cursor.execute("INSERT INTO classroom VALUES " + (args)) # select statement to display output sql1 = '''select * from classroom;''' # executing sql statement cursor.execute(sql1) # fetching rows for i in cursor.fetchall(): print(i) # committing changes conn.commit() # closing connection conn.close()
๐ŸŒ
Jacopo Farina's blog
jacopofarina.eu โ€บ posts โ€บ ingest-data-into-postgres-fast
Insert data into Postgres. Fast. - Jacopo Farina's blog
April 25, 2021 - Another option to speed up the repeated insert is the execute_values function in Psycopg2. This function is included in the psycopg2 extras and basically generates long SQL statements containing multiple values instead of separate ones, and ...
๐ŸŒ
Medium
medium.com โ€บ @kennethhughesa โ€บ optimization-of-upsert-methods-in-postgresql-python-ac11b8471494
Optimization of Upsert Methods in PostgreSQL/Python | by Kenny Hughes | Medium
June 5, 2022 - At this point I switched to a different method using the psycopg2 extras sub-module and executing the upsert using the extras.extract_values method: