pingcap / pingcap/tidb

enhancement: improve plan cache support for prepared statements with very large IN lists

Open
#67,681 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.