Hi all, would like to raise a problem and see if it echos with everyone else.
Problem
Django’s In lookup currently compiles to WHERE col IN (%s, %s, …, %s) with N placeholders, on every backend including PostgreSQL. For large __in lists this becomes an O(N) Python cost in the driver, independent of how fast Postgres executes the query.
A concrete case my team hit this week: a filter(id__in=ids) call with ~42,000 UUIDs. The generated SQL is ~170 KB. Postgres executes it in ~99 ms (verified via EXPLAIN ANALYZE), but the request spends ~3.2 s (41.9% of total request time) inside psycopg’s _split_quer.
This cost is the same on both psycopg2 and psycopg3, and on both client-side and server-side binding modes:
- psycopg2’s
_split_queryregex-scans the SQL to substitute%sclient-side. - psycopg3’s
Cursor(server-side binding, extended query protocol) still has to rewrite%s→$1, $2, …, $Nbecause that’s what the protocol speaks. Same O(N) scan. (source) - psycopg3’s
_query2pglru_cacheis bypassed whenlen(query) > 4096orlen(vars) > 50— a 42k IN clause blows past both on every call.
Proposal
Add In.as_postgresql that emits col = ANY(%s) (with appropriate type cast where needed, e.g. = ANY(%s::uuid[])) and binds the values as a single list parameter. psycopg2 and psycopg3 both adapt Python list to a PG array natively, so no driver changes are required.
This is the same shape of optimization as PR #18847: collapse N placeholders to one bound array param to skip per-placeholder driver work and let prepared-statement caching kick in.
Scope:
- PostgreSQL only. SQLite/MySQL/Oracle unchanged.
- Literal-iterable RHS only.
filter(field__in=queryset)andfilter(field__in=Subquery(...))must still compile toIN (SELECT …)— the existingIn.process_rhsbranch already separates these. - Preserve the empty-list shortcut.
- The PG planner is array-aware:
col = ANY(array_literal)andcol IN (list)produce equivalent plans on indexed columns.
Open questions for the proposal
- Type cast: bare
= ANY(%s)occasionally trips PG planner type resolution for certain field types (UUID notably). Emitting= ANY(%s::<db_type>[])usingfield.db_type(connection)seems to be the safe choice. Anyone seen a case where this falls down?
Let’s see if others face the same issue with postgres and may want a way out too!