From 430c82407fa12d49fcf0825c585a214003d2ec4e Mon Sep 17 00:00:00 2001 From: Michael Erickson Date: Fri, 12 Aug 2022 22:31:17 -0700 Subject: [PATCH] sql: show forecasted stats time in EXPLAIN When using statistics forecasts, add the forecast time (which could be in the future) to EXPLAIN output. This both indicates that forecasts are in use, and gives us an idea of how up-to-date / ahead they are. Assists: #79872 Release note: None --- pkg/sql/opt/cat/table.go | 3 +++ pkg/sql/opt/exec/execbuilder/relational.go | 12 +++++++++ pkg/sql/opt/exec/explain/emit.go | 25 +++++++++++++++++-- pkg/sql/opt/exec/factory.go | 6 +++++ pkg/sql/opt/testutils/testcat/BUILD.bazel | 1 + pkg/sql/opt/testutils/testcat/test_catalog.go | 6 +++++ pkg/sql/opt_catalog.go | 6 +++++ 7 files changed, 57 insertions(+), 2 deletions(-) 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 {