cockroachdb / cockroachdb/cockroach

sql/catalog: ResolveFunction has no per-statement cache; redundant search_path walks during planning

Open
#172,125 0 comments 0 reactions 1 assignee Claimed by @DrewKimball View on GitHub
A-schema-descriptors A-sql-name-resolution C-performance O-agent O-support T-sql-queries
Dominant language
Go
Stars
32.5k
Forks
4.1k
PR merge metrics
PR metrics pending

Description

### Describe the problem

`schemaResolver.ResolveFunction` ([`pkg/sql/schema_resolver.go:481`](https://github.com/cockroachdb/cockroach/blob/master/pkg/sql/schema_resolver.go#L481)) has no per-statement memoization. Every call walks the entire session search_path via `maybeLookupRoutine`, performing `LookupSchema(db, scName)` for each path entry — even when the same name was just resolved a moment earlier in the same statement.

Each `LookupSchema` does at least two descriptor-cache accesses (Database + Schema). For code that resolves the same function name N times within a single planning cycle, the planner pays `N × 2 × len(search_path)` descriptor cache accesses, even though the result is deterministic for the same `(name, search_path)` within the statement.

For **built-in functions** specifically — which dominate the planner's resolution traffic — the work is doubly wasteful: the answer is always the same regardless of search_path entries (since `pg_catalog` is implicitly always available and the built-in lives there), yet the per-path-entry walk still fires.

### Why the existing caching layers don't cover this

CRDB has several caches in this area; none plug this gap.

| Layer | Covers | Why it doesn't help |
|---|---|---|
| `descs.Collection` | descriptor lookups by ID | Descriptor cache hits cleanly. The problem is the layer above calls into it repeatedly. |
| Lease manager | leased descriptor versions | Feeds `descs.Collection`; same story. |
| Query plan cache | optimized plans by query digest | Doesn't help when queries have unique literals (custom plan type). |
| `resolvedTypesByOID` | hydrated UDTs by OID, per-statement | Only covers type-by-OID resolution, not function-by-name. |
| `pgClassOIDCache` | `pg_class` OID → name/schema, per-statement | Different concern. |

The missing piece is a per-statement memoization of `(function name, search_path) → ResolvedFunctionDefinition`, directly analogous to what was added for types in `resolvedTypesByOID`.

### When it bites

Most queries resolve each function name a handful of times — the overhead is invisible. But certain planner paths can hit `ResolveFunction` thousands of times for a single statement (e.g. constant-folding the same built-in expression over many input values, or any path that constructs many similar scalar expressions during exploration). In those cases the redundant work can dominate planning latency.

### Suggested fix

Add a per-statement function-resolution cache on `schemaResolver`, mirroring `resolvedTypesByOID`:

- New field: `resolvedFunctionsByName map[string]cachedFunction`, where `cachedFunction` holds the resolved definition plus the lease generation captured at insert time
- Cache use gated on `!sr.skipDescriptorCache && sr.typeResolutionDbID == descpb.InvalidID` (same gate as the UDT cache; respects `runWithOptions` callers that require fresh reads)
- Validate entries against `descs.Collection.GetLeaseGeneration()` on lookup, evicting on mismatch so in-statement DDL on functions is reflected correctly
- Cleared per statement in `planner.resetPlanner`

Estimated change: ~30 lines, structurally identical to the existing `resolvedTypesByOID` pattern. The same `cachedFunction`/lease-gen pattern is worth retrofitting onto the existing `resolvedTypesByOID` cache (which has the same in-statement-DDL gap) and onto `pgClassOIDCache` for defense in depth.

### Related

- #171543 — uncached negative namespace lookups (different mechanism, same family of "expensive name resolution during planning")

Jira issue: CRDB-65266

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.