51

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,030
  • 21
  • 100
  • 124
Anthony Kong
  • 33,453
  • 37
  • 154
  • 277
  • 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

9 Answers9

39

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
  • 36,887
  • 10
  • 71
  • 115
Jason Damiani
  • 411
  • 4
  • 4
  • 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
36

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,030
  • 21
  • 100
  • 124
dwanderson
  • 2,565
  • 2
  • 23
  • 38
10

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,030
  • 21
  • 100
  • 124
Conrad
  • 101
  • 1
  • 2
7

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,069
  • 16
  • 14
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,030
  • 21
  • 100
  • 124
Gary van der Merwe
  • 8,627
  • 3
  • 44
  • 77
  • 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
3

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,030
  • 21
  • 100
  • 124
Ido S
  • 1,204
  • 9
  • 10
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
  • 344
  • 3
  • 13
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,030
  • 21
  • 100
  • 124
susodapop
  • 302
  • 3
  • 10
-2

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,030
  • 21
  • 100
  • 124
abhishek
  • 287
  • 1
  • 5
  • 28