Updated 2026-07-16: clarified the current behavior and cited the SQLAlchemy docs.
Before SQLAlchemy 1.2.0, an empty list passed to in_() emitted SQL that queried your entire table. Since 1.2.0 this is handled: an empty in_() renders a backend-specific empty-set expression that matches no rows, and an empty notin_() one that matches all rows, so neither scans the table. If you are stuck on an older version, upgrading is the fix.
The gist below shows the old behavior and the query it produced:
For example, making an API that throws errors when an unexpected parameter is provided is a bad idea. What if you need to make changes to the client to add the new parameter? You will need to make sure you deploy the code on the server side first, otherwise it will cause errors.
When using joined eager loading, if the query contains a modifier that impacts the rows returned externally to the joins, such as when using DISTINCT, LIMIT, OFFSET or equivalent, the completed statement is first wrapped inside a subquery, and the joins used specifically for joined eager loading are applied to the subquery. SQLAlchemy’s joined eager loading goes the extra mile, and then ten miles further, to absolutely ensure that it does not affect the end result of the query, only the way collections and related objects are loaded, no matter what the format of the query is.
I made an example to illustrate this:
On MySQL this can be responsible for some really poor query performance, because it can cause it to use temporary tables and filesort.
The best way I’ve found to prevent the subqueries is by first querying for the ids only, then running another query that includes all relations:
The database was set up behind behind an AWS ELB and HAProxy.
The idle connection timeout on the ELB was set to 60 mins.
All the relevant timeouts on HAProxy seemed to be set to 60 mins too.
The pool_recycle in SQLAlchemy was set to 30 mins.
I was still seeing the occasional “Lost connection to MySQL server during query” when small queries were running after the connection had some time to sit around.
The solution ended up being setting my pool_recycle down to 5 mins, but I’m still not sure what was causing connections to time out after 5 mins.
Most of this also applies for “MySQL server has gone away”.
You should also make sure your pool_recycle is set lower than your ‘interactive_timeout’ and ‘wait_timeout’ properties in the mysql config file to the values you need.
If you’re seeing unexpected lazy loading on a lazy=”joined” relationship in SQLAlchemy, it might be because you’re accessing those relationships after you’ve already run session.commit(). By default, session.commit() will expire the data on your relationships, meaning it will try to fetch it again next time you try to access those attributes.
By default, the Session also expires all database loaded state on all ORM-managed attributes after transaction commit. This so that subsequent operations load the most recent data from the database. This behavior can be disabled using the expire_on_commit=False option to sessionmaker or the Session constructor.
In Python 2.7, it knows to switch to different thread whenever it starts waiting on IO. And, it will check if it needs to switch threads every 100 bytecode instructions.
Updated 2026-07-16: updated the answer and links for SQLAlchemy 2.0.
Session.execute runs its statement within the current transaction, and SQLAlchemy’s autobegin starts that transaction on first use.
So if the statement writes (INSERT, UPDATE, DELETE), yes, you need to commit(), or the transaction rolls back when the session closes. A read-only SELECT changes nothing, so there is nothing to commit.
Here’s a code example along with the output from echo=True, showing the session begins a transaction but doesn’t end it until you run session.commit():
Before SQLAlchemy 1.1.4, if you wanted to stream your MySQL query using server side cursors, you would need to run your query using execution_options(stream_results=True) and pass SSCursor into create_engine’s connect_args. Now, all you need to do is pass server_side_cursors=True into create_engine and it will automatically stream the results for your select queries. SQLAlchemy 1.4 deprecated server_side_cursors in favor of stream_results=True.