diff --git a/pkg/sql/opt/cat/table.go b/pkg/sql/opt/cat/table.go index 2f7e0b674290..ef24aa2ef8d7 100644 --- a/pkg/sql/opt/cat/table.go +++ b/pkg/sql/opt/cat/table.go @@ -201,6 +201,9 @@ type TableStatistic interface { // HistogramType returns the type that the histogram was created on. For // inverted index histograms, this will always return types.Bytes. HistogramType() *types.T + + // IsForecast returns true if this statistic is a forecast. + IsForecast() bool } // HistogramBucket contains the data for a single histogram bucket. Note diff --git a/pkg/sql/opt/exec/execbuilder/relational.go b/pkg/sql/opt/exec/execbuilder/relational.go index b0c07e34b673..d0d40c179cd6 100644 --- a/pkg/sql/opt/exec/execbuilder/relational.go +++ b/pkg/sql/opt/exec/execbuilder/relational.go @@ -400,6 +400,18 @@ func (b *Builder) maybeAnnotateWithEstimates(node exec.Node, e memo.RelExpr) { } val.TableStatsCreatedAt = stat.CreatedAt() val.LimitHint = scan.RequiredPhysical().LimitHint + val.Forecast = stat.IsForecast() + if val.Forecast { + val.ForecastAt = stat.CreatedAt() + // Find the first non-forecast stat. + for i := 0; i < tab.StatisticCount(); i++ { + nextStat := tab.Statistic(i) + if !nextStat.IsForecast() { + val.TableStatsCreatedAt = nextStat.CreatedAt() + break + } + } + } } } ef.AnnotateNode(node, exec.EstimatedStatsID, &val) diff --git a/pkg/sql/opt/exec/explain/emit.go b/pkg/sql/opt/exec/explain/emit.go index cb0c9e24143d..e16d2384192c 100644 --- a/pkg/sql/opt/exec/explain/emit.go +++ b/pkg/sql/opt/exec/explain/emit.go @@ -475,10 +475,31 @@ func (e *emitter) emitNodeAttributes(n *Node) error { } duration = string(humanizeutil.LongDuration(timeSinceStats)) } + + var forecastStr string + if s.Forecast { + if e.ob.flags.Redact.Has(RedactVolatile) { + forecastStr = "; using stats forecast" + } else { + timeSinceStats := timeutil.Since(s.ForecastAt) + if timeSinceStats >= 0 { + forecastStr = fmt.Sprintf( + "; using stats forecast for %s ago", humanizeutil.LongDuration(timeSinceStats), + ) + } else { + timeSinceStats *= -1 + forecastStr = fmt.Sprintf( + "; using stats forecast for %s in the future", + humanizeutil.LongDuration(timeSinceStats), + ) + } + } + } + e.ob.AddField("estimated row count", fmt.Sprintf( - "%s (%s%% of the table; stats collected %s ago)", + "%s (%s%% of the table; stats collected %s ago%s)", estimatedRowCountString, percentageStr, - duration, + duration, forecastStr, )) } else { e.ob.AddField("estimated row count", estimatedRowCountString) diff --git a/pkg/sql/opt/exec/factory.go b/pkg/sql/opt/exec/factory.go index b3acbaa4abad..5fa4d4435bca 100644 --- a/pkg/sql/opt/exec/factory.go +++ b/pkg/sql/opt/exec/factory.go @@ -310,6 +310,12 @@ type EstimatedStats struct { // LimitHint is the "soft limit" of the number of result rows that may be // required. See physical.Required for details. LimitHint float64 + // Forecast is set only for scans; it is true if the stats for the scan were + // forecasted rather than collected. + Forecast bool + // ForecastAt is set only for scans with forecasted stats; it is the time the + // forecast was for (which could be in the past, present, or future). + ForecastAt time.Time } // ExecutionStats contain statistics about a given operator gathered from the diff --git a/pkg/sql/opt/testutils/testcat/BUILD.bazel b/pkg/sql/opt/testutils/testcat/BUILD.bazel index 8a6a47a7c08a..8df7a6e10b7e 100644 --- a/pkg/sql/opt/testutils/testcat/BUILD.bazel +++ b/pkg/sql/opt/testutils/testcat/BUILD.bazel @@ -23,6 +23,7 @@ go_library( deps = [ "//pkg/config/zonepb", "//pkg/geo/geoindex", + "//pkg/jobs/jobspb", "//pkg/roachpb", "//pkg/security/username", "//pkg/settings/cluster", diff --git a/pkg/sql/opt/testutils/testcat/test_catalog.go b/pkg/sql/opt/testutils/testcat/test_catalog.go index 285a4151a7d1..f337fe857927 100644 --- a/pkg/sql/opt/testutils/testcat/test_catalog.go +++ b/pkg/sql/opt/testutils/testcat/test_catalog.go @@ -18,6 +18,7 @@ import ( "github.com/cockroachdb/cockroach/pkg/config/zonepb" "github.com/cockroachdb/cockroach/pkg/geo/geoindex" + "github.com/cockroachdb/cockroach/pkg/jobs/jobspb" "github.com/cockroachdb/cockroach/pkg/roachpb" "github.com/cockroachdb/cockroach/pkg/security/username" "github.com/cockroachdb/cockroach/pkg/settings/cluster" @@ -1230,6 +1231,11 @@ func (ts *TableStat) HistogramType() *types.T { return tree.MustBeStaticallyKnownType(colTypeRef) } +// IsForecast is part of the cat.TableStatistic interface. +func (ts *TableStat) IsForecast() bool { + return ts.js.Name == jobspb.ForecastStatsName +} + // TableStats is a slice of TableStat pointers. type TableStats []*TableStat diff --git a/pkg/sql/opt_catalog.go b/pkg/sql/opt_catalog.go index c274ee4b45e8..351bfe334e6e 100644 --- a/pkg/sql/opt_catalog.go +++ b/pkg/sql/opt_catalog.go @@ -18,6 +18,7 @@ import ( "github.com/cockroachdb/cockroach/pkg/clusterversion" "github.com/cockroachdb/cockroach/pkg/config" "github.com/cockroachdb/cockroach/pkg/geo/geoindex" + "github.com/cockroachdb/cockroach/pkg/jobs/jobspb" "github.com/cockroachdb/cockroach/pkg/keys" "github.com/cockroachdb/cockroach/pkg/kv" "github.com/cockroachdb/cockroach/pkg/roachpb" @@ -1685,6 +1686,11 @@ func (os *optTableStat) HistogramType() *types.T { return os.stat.HistogramData.ColumnType } +// IsForecast is part of the cat.TableStatistic interface. +func (os *optTableStat) IsForecast() bool { + return os.stat.Name == jobspb.ForecastStatsName +} + // optFamily is a wrapper around descpb.ColumnFamilyDescriptor that keeps a // reference to the table wrapper. type optFamily struct {