cockroachdb / cockroachdb/cockroach

sql: routine body statements do not benefit from the query cache

Open
#166,781 1 comment 0 reactions 0 assignees View on GitHub
A-sql-optimizer A-sql-routine C-enhancement C-performance O-support P-3 T-sql-queries
Dominant language
Go
Stars
32.5k
Forks
4.1k
PR merge metrics
PR metrics pending

Description

Currently, statements inside UDF and stored procedure bodies bypass the query
cache entirely. Each invocation of a routine re-optimizes every body statement
from scratch.

### How it works today

At build time, `buildUDF` (`pkg/sql/opt/exec/execbuilder/scalar.go:977`)
calls `buildRoutinePlanGenerator` (`scalar.go:1145`), which returns a
`RoutinePlanGenerator` closure stored in `RoutineExpr.ForEachPlan`
(`pkg/sql/sem/tree/routine.go:76`).

At execution time, for each row that evaluates the UDF:
- `EvalRoutineExpr` (`pkg/sql/routine.go:113`) is called
- It calls `routineGenerator.Start` → `startInternal` (`routine.go:298`)
- `startInternal` invokes `g.expr.ForEachPlan(...)` (`routine.go:324`),
which calls the `planGen` closure
- The closure does the following for **every body statement, every invocation**:
1. `o.Init(ctx, ...)` — reinitialize a fresh optimizer (`scalar.go:1236`)
2. `f.CopyAndReplace(originalMemo, stmt, ...)` — copy the normalized (but
unoptimized) expression from the outer memo, substituting parameter
variables with constant datum values (`scalar.go:1280`). Normalization
rules re-fire during the copy.
3. `o.Optimize()` — full exploration + cost-based optimization (`scalar.go:1288`)
4. `eb.Build()` — exec-build the plan (`scalar.go:1327`)

### How direct queries differ

A direct query goes through `buildExecMemo` (`pkg/sql/plan_opt.go:908`),
which consults the `QueryCache` (`plan_opt.go:927-949`). On cache hit, the
already-optimized memo is reused via `reuseMemo` — skipping steps 1-3
entirely. On cache miss, the optimized result is stored back into the cache
(`plan_opt.go:996-1004`) for future executions.

The routine body path has **no query cache lookup and no cache store**.

Additionally, `AllowStableFolds()` is called for direct queries
(`plan_opt.go:959`) but never in the routine body path, so stable
expressions like `now()` are not constant-folded in UDF bodies.

### Impact

For UDFs called repeatedly (e.g., `SELECT my_udf(x) FROM large_table`),
the repeated optimization cost can be significant — every row triggers a
full optimize + exec-build cycle for every statement in the UDF body.

### Relation to other issues

This is related to but distinct from #165832, which tracks enabling generic
query plans for routine body statements. Even without GQP, routine bodies
could benefit from the regular query cache for statements that don't
reference routine parameters (or that have had parameters substituted to
produce a cacheable SQL key).

Epic: CRDB-63752

Jira issue: CRDB-62083

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.