@@ -9,10 +9,12 @@ import (
99 "context"
1010 "fmt"
1111 "math/bits"
12+ "math/rand"
1213 "strings"
1314
1415 "github.com/cockroachdb/cockroach/pkg/clusterversion"
1516 "github.com/cockroachdb/cockroach/pkg/security/username"
17+ "github.com/cockroachdb/cockroach/pkg/settings"
1618 "github.com/cockroachdb/cockroach/pkg/sql/catalog"
1719 "github.com/cockroachdb/cockroach/pkg/sql/catalog/multiregion"
1820 "github.com/cockroachdb/cockroach/pkg/sql/catalog/typedesc"
@@ -46,6 +48,44 @@ type routineDep struct {
4648 invocationTypes []* types.T
4749}
4850
51+ // canaryFraction controls the probabilistic sampling rate for queries
52+ // participating in the canary statistics rollout feature.
53+ //
54+ // This cluster-level setting determines what fraction of queries will use
55+ // "canary statistics" (newly collected stats within their canary window)
56+ // versus "stable statistics" (previously proven stats). For example, a value
57+ // of 0.2 means 20% of queries will test canary stats while 80% use stable stats.
58+ //
59+ // The selection is atomic per query: if a query is chosen for canary evaluation,
60+ // it will use canary statistics for ALL tables it references (where available).
61+ // A query never uses a mix of canary and stable statistics.
62+ var canaryFraction = settings .RegisterFloatSetting (
63+ settings .ApplicationLevel ,
64+ "sql.stats.canary_fraction" ,
65+ "probability that a query will use canary statistics instead of stable statistics (0.0-1.0)" ,
66+ 0.1 ,
67+ settings .FloatInRange (0 , 1 ),
68+ )
69+
70+ // canaryRollDice performs the probabilistic check to determine if a query
71+ // should use the "canary path" for statistics.
72+ // This selection is atomic per query.
73+ func canaryRollDice (evalCtx * eval.Context ) bool {
74+ threshold := canaryFraction .Get (& evalCtx .Settings .SV )
75+
76+ // If the fraction is 0, never use canary stats.
77+ if threshold == 0 {
78+ return false
79+ }
80+ // If the fraction is 1, always use canary stats.
81+ if threshold == 1 {
82+ return true
83+ }
84+
85+ actual := rand .Float64 ()
86+ return actual < threshold
87+ }
88+
4989// Metadata assigns unique ids to the columns, tables, and other metadata used
5090// for global identification within the scope of a particular query. These ids
5191// tend to be small integers that can be efficiently stored and manipulated.
@@ -153,12 +193,35 @@ type Metadata struct {
153193 depDigest cat.DependencyDigest
154194 }
155195
196+ // useCanaryStats indicates whether this query participates in the canary
197+ // statistics rollout feature. When set to true, the optimizer attempts to use
198+ // "canary statistics" for all tables referenced by the query.
199+ //
200+ // This flag is determined probabilistically during query planning based on the
201+ // sql.stats.canary_fraction cluster setting. The selection is atomic per query:
202+ // either all tables use canary stats (when available) or all use stable stats.
203+ //
204+ // Canary statistics are newly collected table statistics that are still within
205+ // their configured "canary window" (canary_stats_window storage parameter).
206+ // These stats provide a controlled way to gradually roll out new statistics
207+ // before promoting them to stable, allowing for manual intervention if
208+ // performance regressions are detected.
209+ //
210+ // Stable statistics are the previously established statistics that have either
211+ // been promoted from canary status or were collected before canary mode was
212+ // enabled for the table.
213+ //
214+ // Fallback behavior: If a table lacks distinct canary statistics (e.g., only
215+ // one statistics version exists, or canary stats have expired), the optimizer
216+ // will use the available stable statistics even when this flag is true.
217+ useCanaryStats bool
218+
156219 // NOTE! When adding fields here, update Init (if reusing allocated
157220 // data structures is desired), CopyFrom and TestMetadata.
158221}
159222
160223// Init prepares the metadata for use (or reuse).
161- func (md * Metadata ) Init () {
224+ func (md * Metadata ) Init (evalCtx * eval. Context ) {
162225 // Clear the metadata objects to release memory (this clearing pattern is
163226 // optimized by Go).
164227 schemas := md .schemas
@@ -239,6 +302,11 @@ func (md *Metadata) Init() {
239302 md .objectRefsByName = objectRefsByName
240303 md .privileges = privileges
241304 md .builtinRefsByName = builtinRefsByName
305+ // TODO(janexing): figure out if this gate can overkill. Can any routines
306+ // or builtin functions access tables as an internal query?
307+ if ! evalCtx .SessionData ().Internal {
308+ md .useCanaryStats = canaryRollDice (evalCtx )
309+ }
242310}
243311
244312// CopyFrom initializes the metadata with a copy of the provided metadata.
@@ -337,6 +405,7 @@ func (md *Metadata) CopyFrom(from *Metadata, copyScalarFn func(Expr) Expr) {
337405 md .withBindings = nil
338406
339407 md .rlsMeta = from .rlsMeta .Copy ()
408+ md .useCanaryStats = from .useCanaryStats
340409}
341410
342411// MDDepName stores either the unresolved DataSourceName or the StableID from
0 commit comments