enhancement: improve plan cache support for prepared statements with very large IN lists
- Dominant language
- Go
- Stars
- 40.5k
- Forks
- 6.2k
- PR merge metrics
- PR metrics pending
Description
# Summary
TiDB currently performs poorly for prepared statements with very large `IN (...)` lists, especially when the SQL text shape changes with the number of placeholders or when the optimizer still needs to rebuild expensive range / selectivity / access-path structures on each execution.
In production workloads, this can lead to:
- high `Prepare` / compile frequency
- heavy optimizer CPU in `GetPlanFromPlanCache -> generateNewPlan -> ranger -> Selectivity -> fillIndexPath`
- excessive allocations and GC pressure
- visible TiDB CPU spikes even when the statements themselves are not the slowest by execution latency
This issue proposes an enhancement for better plan cache support for "large IN-list statements".
# Background
We observed a production incident where a batch of statements like the following repeatedly stressed TiDB:
```sql
SELECT a.key_id, a.partition_key
FROM account_table a
WHERE a.type IN (?, ?, ?, ?, ?, ?, ?)
AND a.lookup_key IN (?, ?, ?, ..., ?)
```
and
```sql
SELECT a.id, a.partition_key, a.key_id, ...
FROM account_table a
JOIN ext_table_1 b ON ...
JOIN ext_table_2 c ON ...
JOIN ext_table_3 d ON ...
JOIN ext_table_4 e ON ...
LEFT JOIN dim_table p ON ...
WHERE a.partition_key = ?
AND a.key_id IN (?, ?, ?, ..., ?)
```
In this workload, the `IN` list could contain around 2,000 values.
From CPU profiles, the hottest stacks were consistently around:
- `github.com/pingcap/tidb/pkg/planner/core.GetPlanFromPlanCache`
- `github.com/pingcap/tidb/pkg/planner/core.generateNewPlan`
- `github.com/pingcap/tidb/pkg/planner/core.fillIndexPath`
- `github.com/pingcap/tidb/pkg/planner/cardinality.Selectivity`
- `github.com/pingcap/tidb/pkg/util/ranger.DetachCondAndBuildRangeForIndex`
- `runtime.mallocgc`
- `runtime.newobject`
- `runtime.scanobject`
- `runtime.gcDrain`
From slow query data over a week, a small family of large-`IN` statements accounted for a very large portion of total compile time, while representing only a small fraction of total statement count.
# Problem
For large `IN (...)` statements, TiDB plan cache is currently not effective enough to protect the system from repeated planning overhead.
Typical pain points are:
1. The SQL text shape is sensitive to the number of placeholders in the `IN` list, so statements with different list sizes naturally fall into different digests / cache entries.
2. Even when the statement is prepared, the optimizer may still spend significant CPU rebuilding ranges, estimating selectivity, and enumerating index access paths.
3. For very large `IN` lists, the planning overhead itself becomes a major source of CPU, allocations, and GC pressure.
As a result, these queries can become a system-level problem, not just a single-query latency problem.
# Expected Enhancement
TiDB could consider adding a dedicated optimization path for large `IN` predicates so that prepared statements of this kind can benefit more from plan cache and avoid repeated heavy planning work.
Possible directions:
1. Improve plan cache reuse for parameterized large-`IN` statements.
- Even when the number of values changes, TiDB could try to normalize the shape better, or provide a more reusable internal representation for large membership predicates.
2. Add a planner fast path for large `IN` predicates.
- Avoid rebuilding the full range / access-path search cost for every execution when the logical statement shape is stable.
3. Consider special handling once the `IN` list exceeds some threshold.
- For example, switch to an alternate internal representation instead of expanding and reprocessing a very large list element-by-element inside the optimizer.
4. Consider a cacheable "membership input" abstraction.
- This could be backed by a temporary internal structure or another execution-time-only representation, so that planning does not depend so heavily on the exact list cardinality.
# Prior Art / References
## PostgreSQL
PostgreSQL supports prepared statements and can choose between custom plans and generic plans for parameterized statements:
- `PREPARE` documentation:
- https://www.postgresql.org/docs/15/sql-prepare.html
- Row and array comparisons, including `expression operator ANY (array expression)`:
- https://www.postgresql.org/docs/current/functions-comparisons.html
Relevant points:
- PostgreSQL explicitly supports generic-plan reuse for prepared statements.
- PostgreSQL also supports expressing membership predicates as `col = ANY($1)` where `$1` is an array parameter, which gives applications a way to avoid generating a different SQL text for every `IN` list length.
Example:
```sql
PREPARE q(text[]) AS
SELECT key_id, partition_key
FROM account_table
WHERE lookup_key = ANY($1);
```
This is not necessarily a perfect one-to-one design to copy, but it is a useful reference for a "stable statement shape + array parameter" approach.
## Db2
Db2 documents parameter markers as a core mechanism for statement reuse:
- Parameter markers:
- https://www.ibm.com/docs/en/db2/11.1?topic=design-parameters-markers
Db2 also supports array-related SQL features and array parameters in procedures:
- Array parameters in Db2 SQL procedures:
- https://www.ibm.com/docs/en/db2/11.1?topic=procedures-array-parameters-in-db2-sql
- Array element / array predicate references:
- https://www.ibm.com/docs/en/db2/11.5.x?topic=expressions-array-element-specification
- https://www.ibm.com/docs/en/db2/11.1?topic=predicates-array-exists-predicate
The main point here is not that TiDB should exactly copy Db2 syntax, but that other systems expose more structured ways to pass a collection of values without forcing the optimizer to repeatedly handle a huge ad hoc `IN (?, ?, ..., ?)` shape.
# Why this matters
This enhancement would help reduce:
- optimizer CPU spikes
- allocation / GC amplification
- instability caused by repeated compile/prepare on batch lookup workloads
It would also make TiDB more robust for common application patterns such as:
- batch lookup by many external keys
- batch lookup by many IDs
- repeated API requests that naturally carry hundreds or thousands of keys
# Possible Acceptance Criteria
Any of the following would be meaningful progress:
1. A documented and supported way to make very large membership predicates plan-cache-friendly.
2. A planner / plan-cache enhancement that materially reduces repeated compile CPU for large `IN` prepared statements.
3. Benchmarks or tests showing reduced planning CPU / allocation / GC for repeated executions of large-`IN` statements.
# Additional Notes
This is an enhancement request, not a correctness bug.
Even if the final solution is not "support plan cache for arbitrary huge `IN` directly", a documented recommended pattern or a TiDB-native equivalent of array-parameter membership would already be very helpful.
Contributor guide
Assessment
This issue has not been assessed yet.