67

I am using this SQL for performance reasons:

 sql_tmpl = """delete from Data where id_data in (:iddata) """
 params = {
                    'iddata':[1, 2,3 4],
                    }

 # 'session' is a session object from SQLAlchemy
 self.session.execute(text(sql_tmpl), params)

However, I got an exception:

NotSupportedError: (NotSupportedError) ('Python type list not supported.  param=1', 'HY097')

Is there a workaround that can allow me to bind a list to the parameter of the 'in' clause?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Anthony Kong
  • 37,791
  • 46
  • 172
  • 304
  • The values passed to an IN are still discrete, rendered values in SQL, so the SQL statement itself must still be formed to include a separate bound parameter. If you are using literal SQL you need to do a string concatenation of ":iddata1, :iddata2, .." etc. Gary's answer below is the correct one. – zzzeek Nov 02 '12 at 14:42

11 Answers11

68

A new approach to this problem that works for any database (not just relying on psycopg2's type adaptation) uses expanding bind parameters:

sql_tmpl = """delete from Data where id_data in :iddata"""
params = { 'iddata': [1, 2, 3, 4], }
# session is a session object from sqlalchemy
t = text(sql_tmpl)
t = t.bindparams(bindparam('iddata', expanding=True))
self.session.execute(t, params)
snakecharmerb
  • 47,570
  • 11
  • 100
  • 153
Jason Damiani
  • 701
  • 5
  • 5
  • With `pymysql` and SQLAlchemy 1.2 this was not needed. The `expanding` feature appears to be related to statement caching. More details here: https://github.com/sqlalchemy/sqlalchemy/issues/3953 – We Are All Monica Aug 27 '19 at 07:27
  • 4
    @jnylen That's because pymysql supports list adaptation. Expanding bindparams is handy for more than just baked (cached) queries, since not all DB-API drivers support list adaptation, in which case you'd have to manually form and format the required placeholders into a query. It's also significantly faster in certain queries, such as passing a huge list to `in_()`: https://stackoverflow.com/questions/57829682/new-override-sqlalchemy-operator-compiler-output – Ilja Everilä Dec 21 '19 at 07:50
  • 4
    to help anyone this imports are needed: from sqlalchemy import text from sqlalchemy import bindparam – Luciano Marqueto Jun 21 '22 at 22:05
  • Why do we have to include ¡¡ – MadPhysicist Mar 06 '23 at 12:57
  • 2
    Why do we have to include `params` in this line `self.session.execute(t, params)`? Didn't we already bind them in the previous line? What does this line do then? `t = t.bindparams(bindparam('iddata', expanding=True))` – MadPhysicist Mar 06 '23 at 12:58
  • 2
    @MadPhysicist `params` has the actual parameter value that needs to be added to the query. `bindparam('iddata', expanding=True)` only flags the key `iddata` as 'to-be-expanded'. – Sander van Leeuwen May 03 '23 at 09:02
  • Using `bindparams` allowed me to work around mixed behavior from MySQL and Snowflake SqlAlchemy drivers. MySQL wanted no parens around the `:list_param` in the query, whereas Snowflake needed parens ( https://github.com/snowflakedb/snowflake-sqlalchemy/issues/326 ). – markfickett Aug 02 '23 at 15:31
47

psycopg2 now supports type adaptation, which allows, among other things, the ability to pass a list into a single parameterized value in the query. This also works in SQLAlchemy, at the very least for raw-SQL-esque queries to a PostgreSQL database (I don't have access to other database types, so I don't know if sqlalchemy will respect this convention for other databases, but my inclinationcitation needed is that it will work).

some_ids = [1, 2, 3, 4]
query = "SELECT * FROM my_table t WHERE t.id = ANY(:ids);"
conn.execute(sqlalchemy.text(query), ids=some_ids)
## runs just fine

I found that without the wrapper call to sqlalchemy.text, it gave a ProgrammingError: syntax error at or near ":".

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
dwanderson
  • 2,775
  • 2
  • 25
  • 40
12

Try it without the parentheses around, :iddata. That has been working for me.

sql_tmpl = """delete from Data where id_data in :iddata """
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Conrad
  • 121
  • 1
  • 2
12

Use a tuple instead of a list and you don't need parentheses around the parameter in the query:

sql_tmpl = "delete from Data where id_data in :iddata"
params = {  
   'iddata':(1, 2, 3, 4),
}
self.session.execute(text(sql_tmpl), params)     
k26dr
  • 1,229
  • 18
  • 15
4

Adding to dwanderson's answer: using SQLAlchemy, you can use the func method to add the "any" function to a query. This works for me using SQLAlchemy 1.0.9 and a PostgreSQL database.

Generic example:

from sqlalchemy import func

# some list
id_list = [1, 2, 3]

# assuming you have created a session
query = session.query(Table.user_name, Table.user_id).\
    filter(Table.user_id == func.any(id_list))

# one way of running the query
query.all()

You can verify that the list is passed as a single parameter (as opposed to a parameter per object in the list).

print(query.statement)

SELECT user_id, user_name FROM table WHERE table.user_id = any(:any_1)

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Ido S
  • 1,304
  • 10
  • 11
3

As far as I'm aware, none of the SQL engines allow passing in an array parameter. The way that SQLAlchemy handles this is to pass in a parameter for each item in the array.

>>> from sqlalchemy.sql import table, column
>>> print(table('Data').delete(column('id_data').in_([5, 6, 7,])))
DELETE FROM "Data" WHERE id_data IN (:id_data_1, :id_data_2, :id_data_3)

If you don't use the SQLAlchemy expression constructs, you would need to do this manually.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Gary van der Merwe
  • 9,134
  • 3
  • 49
  • 80
  • 4
    I don't know if this was the case when the answer was written, but now `psycopg2` supports a [bunch of coercions, including arrays](http://initd.org/psycopg/docs/usage.html#python-types-adaptation). See for example [Tuples adaptation](http://initd.org/psycopg/docs/usage.html#tuples-adaptation). Unfortunately, it seems that SQL Alchemy uses its own logic so that this feature is only available if using psycopg2 directly. – vicvicvic Jul 06 '15 at 10:06
1

You can generate a where clause using a loop and use ** to break out the list in the query.execute parameters. Here's an example: https://gist.github.com/pawl/555e5eecce77d4de0ada

pawl
  • 354
  • 4
  • 14
0

In Microsoft SQL Server, you can use Table Valued Parameters to accomplish the same thing.

SELECT * FROM table_name WHERE customer_id in (SELECT * FROM @MyTVP)

TVP's are currently only supported with PyTDS, not PyODBC. The expanding=True flag detailed by Jason Damiani is probably the best way to do this if you have a newer version of SQLAlchemy. But TVP's will do in a pinch.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
susodapop
  • 402
  • 4
  • 10
0

Using this example from above:

from sqlalchemy import func

# some list
id_list = [1, 2, 3]

# assuming you have created a session
query = session.query(Table.user_name, Table.user_id).\
    filter(Table.user_id == func.any(id_list))

# one way of running the query
query.all()

I had to make change func.any, to func.all and then it worked! Good luck

0

Jason Damiani's answer works when you are performing an execution. But if you want to get the stringified form of the text clause, you need to use bindparam() and apply expanding=True.

Example:

mylist = [1,2,3]
query = text("mycolumn IN :arr")
  .bindparams(bindparam(key="arr", value=mylist, expanding=True))
  .compile(compile_kwargs={"literal_binds": True})
print(query)
Cardin
  • 5,148
  • 5
  • 36
  • 37
-5

In case you are dealing raw SQL, you can go with this approach:

ids = [1000032, 1000048]
sql = 'SELECT CompNo, CompName, CompType FROM Component WHERE DeptID IN %(ids)s' % {"ids": tuple(ids)}
cursor.execute(sql)
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
abhishek
  • 301
  • 1
  • 5
  • 29