github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/sql/create_stats.go (about)

     1  // Copyright 2017 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package sql
    12  
    13  import (
    14  	"context"
    15  	"fmt"
    16  
    17  	"github.com/cockroachdb/cockroach/pkg/jobs"
    18  	"github.com/cockroachdb/cockroach/pkg/jobs/jobspb"
    19  	"github.com/cockroachdb/cockroach/pkg/kv"
    20  	"github.com/cockroachdb/cockroach/pkg/server/telemetry"
    21  	"github.com/cockroachdb/cockroach/pkg/settings"
    22  	"github.com/cockroachdb/cockroach/pkg/settings/cluster"
    23  	"github.com/cockroachdb/cockroach/pkg/sql/catalog/resolver"
    24  	"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode"
    25  	"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror"
    26  	"github.com/cockroachdb/cockroach/pkg/sql/privilege"
    27  	"github.com/cockroachdb/cockroach/pkg/sql/rowcontainer"
    28  	"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
    29  	"github.com/cockroachdb/cockroach/pkg/sql/sqlbase"
    30  	"github.com/cockroachdb/cockroach/pkg/sql/sqltelemetry"
    31  	"github.com/cockroachdb/cockroach/pkg/sql/stats"
    32  	"github.com/cockroachdb/cockroach/pkg/sql/types"
    33  	"github.com/cockroachdb/cockroach/pkg/util"
    34  	"github.com/cockroachdb/cockroach/pkg/util/errorutil/unimplemented"
    35  	"github.com/cockroachdb/cockroach/pkg/util/hlc"
    36  	"github.com/cockroachdb/cockroach/pkg/util/log"
    37  	"github.com/cockroachdb/errors"
    38  	"google.golang.org/grpc/codes"
    39  	"google.golang.org/grpc/status"
    40  )
    41  
    42  // createStatsPostEvents controls the cluster setting for logging
    43  // automatic table statistics collection to the event log.
    44  var createStatsPostEvents = settings.RegisterPublicBoolSetting(
    45  	"sql.stats.post_events.enabled",
    46  	"if set, an event is logged for every CREATE STATISTICS job",
    47  	false,
    48  )
    49  
    50  func (p *planner) CreateStatistics(ctx context.Context, n *tree.CreateStats) (planNode, error) {
    51  	return &createStatsNode{
    52  		CreateStats: *n,
    53  		p:           p,
    54  	}, nil
    55  }
    56  
    57  // createStatsNode is a planNode implemented in terms of a function. The
    58  // startJob function starts a Job during Start, and the remainder of the
    59  // CREATE STATISTICS planning and execution is performed within the jobs
    60  // framework.
    61  type createStatsNode struct {
    62  	tree.CreateStats
    63  	p *planner
    64  
    65  	run createStatsRun
    66  }
    67  
    68  // createStatsRun contains the run-time state of createStatsNode during local
    69  // execution.
    70  type createStatsRun struct {
    71  	resultsCh chan tree.Datums
    72  	errCh     chan error
    73  }
    74  
    75  func (n *createStatsNode) startExec(params runParams) error {
    76  	telemetry.Inc(sqltelemetry.SchemaChangeCreateCounter("stats"))
    77  	n.run.resultsCh = make(chan tree.Datums)
    78  	n.run.errCh = make(chan error)
    79  	go func() {
    80  		err := n.startJob(params.ctx, n.run.resultsCh)
    81  		select {
    82  		case <-params.ctx.Done():
    83  		case n.run.errCh <- err:
    84  		}
    85  		close(n.run.errCh)
    86  		close(n.run.resultsCh)
    87  	}()
    88  	return nil
    89  }
    90  
    91  func (n *createStatsNode) Next(params runParams) (bool, error) {
    92  	select {
    93  	case <-params.ctx.Done():
    94  		return false, params.ctx.Err()
    95  	case err := <-n.run.errCh:
    96  		return false, err
    97  	case <-n.run.resultsCh:
    98  		return true, nil
    99  	}
   100  }
   101  
   102  func (*createStatsNode) Close(context.Context) {}
   103  func (*createStatsNode) Values() tree.Datums   { return nil }
   104  
   105  // startJob starts a CreateStats job to plan and execute statistics creation.
   106  func (n *createStatsNode) startJob(ctx context.Context, resultsCh chan<- tree.Datums) error {
   107  	record, err := n.makeJobRecord(ctx)
   108  	if err != nil {
   109  		return err
   110  	}
   111  
   112  	if n.Name == stats.AutoStatsName {
   113  		// Don't start the job if there is already a CREATE STATISTICS job running.
   114  		// (To handle race conditions we check this again after the job starts,
   115  		// but this check is used to prevent creating a large number of jobs that
   116  		// immediately fail).
   117  		if err := checkRunningJobs(ctx, nil /* job */, n.p); err != nil {
   118  			return err
   119  		}
   120  	} else {
   121  		telemetry.Inc(sqltelemetry.CreateStatisticsUseCounter)
   122  	}
   123  
   124  	job, errCh, err := n.p.ExecCfg().JobRegistry.CreateAndStartJob(ctx, resultsCh, *record)
   125  	if err != nil {
   126  		return err
   127  	}
   128  
   129  	if err = <-errCh; err != nil {
   130  		if errors.Is(err, stats.ConcurrentCreateStatsError) {
   131  			// Delete the job so users don't see it and get confused by the error.
   132  			const stmt = `DELETE FROM system.jobs WHERE id = $1`
   133  			if _ /* cols */, delErr := n.p.ExecCfg().InternalExecutor.Exec(
   134  				ctx, "delete-job", nil /* txn */, stmt, *job.ID(),
   135  			); delErr != nil {
   136  				log.Warningf(ctx, "failed to delete job: %v", delErr)
   137  			}
   138  		}
   139  	}
   140  	return err
   141  }
   142  
   143  // makeJobRecord creates a CreateStats job record which can be used to plan and
   144  // execute statistics creation.
   145  func (n *createStatsNode) makeJobRecord(ctx context.Context) (*jobs.Record, error) {
   146  	var tableDesc *ImmutableTableDescriptor
   147  	var fqTableName string
   148  	var err error
   149  	switch t := n.Table.(type) {
   150  	case *tree.UnresolvedObjectName:
   151  		tableDesc, err = n.p.ResolveExistingObjectEx(ctx, t, true /*required*/, resolver.ResolveRequireTableDesc)
   152  		if err != nil {
   153  			return nil, err
   154  		}
   155  		fqTableName = n.p.ResolvedName(t).FQString()
   156  
   157  	case *tree.TableRef:
   158  		flags := tree.ObjectLookupFlags{CommonLookupFlags: tree.CommonLookupFlags{
   159  			AvoidCached: n.p.avoidCachedDescriptors,
   160  		}}
   161  		tableDesc, err = n.p.Tables().GetTableVersionByID(ctx, n.p.txn, sqlbase.ID(t.TableID), flags)
   162  		if err != nil {
   163  			return nil, err
   164  		}
   165  		fqTableName, err = n.p.getQualifiedTableName(ctx, &tableDesc.TableDescriptor)
   166  		if err != nil {
   167  			return nil, err
   168  		}
   169  	}
   170  
   171  	if tableDesc.IsVirtualTable() {
   172  		return nil, pgerror.New(
   173  			pgcode.WrongObjectType, "cannot create statistics on virtual tables",
   174  		)
   175  	}
   176  
   177  	if tableDesc.IsView() {
   178  		return nil, pgerror.New(
   179  			pgcode.WrongObjectType, "cannot create statistics on views",
   180  		)
   181  	}
   182  
   183  	if err := n.p.CheckPrivilege(ctx, tableDesc, privilege.SELECT); err != nil {
   184  		return nil, err
   185  	}
   186  
   187  	// Identify which columns we should create statistics for.
   188  	var colStats []jobspb.CreateStatsDetails_ColStat
   189  	if len(n.ColumnNames) == 0 {
   190  		multiColEnabled := stats.MultiColumnStatisticsClusterMode.Get(&n.p.ExecCfg().Settings.SV)
   191  		if colStats, err = createStatsDefaultColumns(tableDesc, multiColEnabled); err != nil {
   192  			return nil, err
   193  		}
   194  	} else {
   195  		columns, err := tableDesc.FindActiveColumnsByNames(n.ColumnNames)
   196  		if err != nil {
   197  			return nil, err
   198  		}
   199  
   200  		columnIDs := make([]sqlbase.ColumnID, len(columns))
   201  		for i := range columns {
   202  			if columns[i].Type.Family() == types.JsonFamily {
   203  				return nil, unimplemented.NewWithIssuef(35844,
   204  					"CREATE STATISTICS is not supported for JSON columns")
   205  			}
   206  			columnIDs[i] = columns[i].ID
   207  		}
   208  		colStats = []jobspb.CreateStatsDetails_ColStat{{ColumnIDs: columnIDs, HasHistogram: false}}
   209  		if len(columnIDs) == 1 && columns[0].Type.Family() != types.ArrayFamily {
   210  			// By default, create histograms on all explicitly requested column stats
   211  			// with a single column. (We cannot create histograms on array columns
   212  			// because we do not support key encoding arrays.)
   213  			colStats[0].HasHistogram = true
   214  		}
   215  	}
   216  
   217  	// Evaluate the AS OF time, if any.
   218  	var asOf *hlc.Timestamp
   219  	if n.Options.AsOf.Expr != nil {
   220  		asOfTs, err := n.p.EvalAsOfTimestamp(ctx, n.Options.AsOf)
   221  		if err != nil {
   222  			return nil, err
   223  		}
   224  		asOf = &asOfTs
   225  	}
   226  
   227  	// Create a job to run statistics creation.
   228  	statement := tree.AsStringWithFQNames(n, n.p.EvalContext().Annotations)
   229  	var description string
   230  	if n.Name == stats.AutoStatsName {
   231  		// Use a user-friendly description for automatic statistics.
   232  		description = fmt.Sprintf("Table statistics refresh for %s", fqTableName)
   233  	} else {
   234  		// This must be a user query, so use the statement (for consistency with
   235  		// other jobs triggered by statements).
   236  		description = statement
   237  		statement = ""
   238  	}
   239  	return &jobs.Record{
   240  		Description: description,
   241  		Statement:   statement,
   242  		Username:    n.p.User(),
   243  		Details: jobspb.CreateStatsDetails{
   244  			Name:            string(n.Name),
   245  			FQTableName:     fqTableName,
   246  			Table:           tableDesc.TableDescriptor,
   247  			ColumnStats:     colStats,
   248  			Statement:       n.String(),
   249  			AsOf:            asOf,
   250  			MaxFractionIdle: n.Options.Throttling,
   251  		},
   252  		Progress: jobspb.CreateStatsProgress{},
   253  	}, nil
   254  }
   255  
   256  // maxNonIndexCols is the maximum number of non-index columns that we will use
   257  // when choosing a default set of column statistics.
   258  const maxNonIndexCols = 100
   259  
   260  // createStatsDefaultColumns creates column statistics on a default set of
   261  // column lists when no columns were specified by the caller.
   262  //
   263  // To determine a useful set of default column statistics, we rely on
   264  // information provided by the schema. In particular, the presence of an index
   265  // on a particular set of columns indicates that the workload likely contains
   266  // queries that involve those columns (e.g., for filters), and it would be
   267  // useful to have statistics on prefixes of those columns. For example, if a
   268  // table abc contains indexes on (a ASC, b ASC) and (b ASC, c ASC), we will
   269  // collect statistics on a, {a, b}, b, and {b, c}. (But if multiColEnabled is
   270  // false, we will only collect stats on a and b).
   271  //
   272  // In addition to the index columns, we collect stats on up to maxNonIndexCols
   273  // other columns from the table. We only collect histograms for index columns,
   274  // plus any other boolean columns (where the "histogram" is tiny).
   275  func createStatsDefaultColumns(
   276  	desc *ImmutableTableDescriptor, multiColEnabled bool,
   277  ) ([]jobspb.CreateStatsDetails_ColStat, error) {
   278  	colStats := make([]jobspb.CreateStatsDetails_ColStat, 0, len(desc.Indexes)+1)
   279  
   280  	requestedStats := make(map[string]struct{})
   281  
   282  	// Add column stats for the primary key.
   283  	for i := range desc.PrimaryIndex.ColumnIDs {
   284  		if i != 0 && !multiColEnabled {
   285  			break
   286  		}
   287  
   288  		// Remember the requested stats so we don't request duplicates.
   289  		key := makeColStatKey(desc.PrimaryIndex.ColumnIDs[: i+1 : i+1])
   290  		requestedStats[key] = struct{}{}
   291  
   292  		colStats = append(colStats, jobspb.CreateStatsDetails_ColStat{
   293  			ColumnIDs:    desc.PrimaryIndex.ColumnIDs[: i+1 : i+1],
   294  			HasHistogram: i == 0,
   295  		})
   296  	}
   297  
   298  	// Add column stats for each secondary index.
   299  	for i := range desc.Indexes {
   300  		if desc.Indexes[i].Type == sqlbase.IndexDescriptor_INVERTED {
   301  			// We don't yet support stats on inverted indexes.
   302  			continue
   303  		}
   304  		for j := range desc.Indexes[i].ColumnIDs {
   305  			if j != 0 && !multiColEnabled {
   306  				break
   307  			}
   308  
   309  			// Check for existing stats and remember the requested stats.
   310  			key := makeColStatKey(desc.Indexes[i].ColumnIDs[: j+1 : j+1])
   311  			if _, ok := requestedStats[key]; ok {
   312  				continue
   313  			}
   314  			requestedStats[key] = struct{}{}
   315  
   316  			colStats = append(colStats, jobspb.CreateStatsDetails_ColStat{
   317  				ColumnIDs:    desc.Indexes[i].ColumnIDs[: j+1 : j+1],
   318  				HasHistogram: j == 0,
   319  			})
   320  		}
   321  	}
   322  
   323  	// Add all remaining non-json columns in the table, up to maxNonIndexCols.
   324  	nonIdxCols := 0
   325  	for i := 0; i < len(desc.Columns) && nonIdxCols < maxNonIndexCols; i++ {
   326  		col := &desc.Columns[i]
   327  		colList := []sqlbase.ColumnID{col.ID}
   328  		key := makeColStatKey(colList)
   329  		if _, ok := requestedStats[key]; !ok && col.Type.Family() != types.JsonFamily {
   330  			colStats = append(colStats, jobspb.CreateStatsDetails_ColStat{
   331  				ColumnIDs:    colList,
   332  				HasHistogram: col.Type.Family() == types.BoolFamily || col.Type.Family() == types.EnumFamily,
   333  			})
   334  			nonIdxCols++
   335  		}
   336  	}
   337  
   338  	return colStats, nil
   339  }
   340  
   341  // makeColStatKey constructs a unique key representing cols that can be used
   342  // as the key in a map.
   343  func makeColStatKey(cols []sqlbase.ColumnID) string {
   344  	var colSet util.FastIntSet
   345  	for _, c := range cols {
   346  		colSet.Add(int(c))
   347  	}
   348  	return colSet.String()
   349  }
   350  
   351  // newPlanForExplainDistSQL is part of the distSQLExplainable interface.
   352  func (n *createStatsNode) newPlanForExplainDistSQL(
   353  	planCtx *PlanningCtx, distSQLPlanner *DistSQLPlanner,
   354  ) (*PhysicalPlan, error) {
   355  	// Create a job record but don't actually start the job.
   356  	record, err := n.makeJobRecord(planCtx.ctx)
   357  	if err != nil {
   358  		return nil, err
   359  	}
   360  	job := n.p.ExecCfg().JobRegistry.NewJob(*record)
   361  
   362  	return distSQLPlanner.createPlanForCreateStats(planCtx, job)
   363  }
   364  
   365  // createStatsResumer implements the jobs.Resumer interface for CreateStats
   366  // jobs. A new instance is created for each job.
   367  type createStatsResumer struct {
   368  	job     *jobs.Job
   369  	tableID sqlbase.ID
   370  }
   371  
   372  var _ jobs.Resumer = &createStatsResumer{}
   373  
   374  // Resume is part of the jobs.Resumer interface.
   375  func (r *createStatsResumer) Resume(
   376  	ctx context.Context, phs interface{}, resultsCh chan<- tree.Datums,
   377  ) error {
   378  	p := phs.(*planner)
   379  	details := r.job.Details().(jobspb.CreateStatsDetails)
   380  	if details.Name == stats.AutoStatsName {
   381  		// We want to make sure there is only one automatic CREATE STATISTICS job
   382  		// running at a time.
   383  		if err := checkRunningJobs(ctx, r.job, p); err != nil {
   384  			return err
   385  		}
   386  	}
   387  
   388  	r.tableID = details.Table.ID
   389  	evalCtx := p.ExtendedEvalContext()
   390  
   391  	ci := sqlbase.ColTypeInfoFromColTypes([]*types.T{})
   392  	rows := rowcontainer.NewRowContainer(evalCtx.Mon.MakeBoundAccount(), ci, 0)
   393  	defer func() {
   394  		if rows != nil {
   395  			rows.Close(ctx)
   396  		}
   397  	}()
   398  
   399  	dsp := p.DistSQLPlanner()
   400  	if err := p.ExecCfg().DB.Txn(ctx, func(ctx context.Context, txn *kv.Txn) error {
   401  		// Set the transaction on the EvalContext to this txn. This allows for
   402  		// use of the txn during processor setup during the execution of the flow.
   403  		evalCtx.Txn = txn
   404  
   405  		if details.AsOf != nil {
   406  			p.semaCtx.AsOfTimestamp = details.AsOf
   407  			p.extendedEvalCtx.SetTxnTimestamp(details.AsOf.GoTime())
   408  			txn.SetFixedTimestamp(ctx, *details.AsOf)
   409  		}
   410  
   411  		planCtx := dsp.NewPlanningCtx(ctx, evalCtx, txn, true /* distribute */)
   412  		planCtx.planner = p
   413  		if err := dsp.planAndRunCreateStats(
   414  			ctx, evalCtx, planCtx, txn, r.job, NewRowResultWriter(rows),
   415  		); err != nil {
   416  			// Check if this was a context canceled error and restart if it was.
   417  			if s, ok := status.FromError(errors.UnwrapAll(err)); ok {
   418  				if s.Code() == codes.Canceled && s.Message() == context.Canceled.Error() {
   419  					return jobs.NewRetryJobError("node failure")
   420  				}
   421  			}
   422  
   423  			// If the job was canceled, any of the distsql processors could have been
   424  			// the first to encounter the .Progress error. This error's string is sent
   425  			// through distsql back here, so we can't examine the err type in this case
   426  			// to see if it's a jobs.InvalidStatusError. Instead, attempt to update the
   427  			// job progress to coerce out the correct error type. If the update succeeds
   428  			// then return the original error, otherwise return this error instead so
   429  			// it can be cleaned up at a higher level.
   430  			if jobErr := r.job.FractionProgressed(
   431  				ctx,
   432  				func(ctx context.Context, _ jobspb.ProgressDetails) float32 {
   433  					// The job failed so the progress value here doesn't really matter.
   434  					return 0
   435  				},
   436  			); jobErr != nil {
   437  				return jobErr
   438  			}
   439  			return err
   440  		}
   441  
   442  		return nil
   443  	}); err != nil {
   444  		return err
   445  	}
   446  
   447  	// Invalidate the local cache synchronously; this guarantees that the next
   448  	// statement in the same session won't use a stale cache (whereas the gossip
   449  	// update is handled asynchronously).
   450  	evalCtx.ExecCfg.TableStatsCache.InvalidateTableStats(ctx, r.tableID)
   451  
   452  	// Record this statistics creation in the event log.
   453  	if !createStatsPostEvents.Get(&evalCtx.Settings.SV) {
   454  		return nil
   455  	}
   456  
   457  	// TODO(rytaft): This creates a new transaction for the CREATE STATISTICS
   458  	// event. It must be different from the CREATE STATISTICS transaction,
   459  	// because that transaction must be read-only. In the future we may want
   460  	// to use the transaction that inserted the new stats into the
   461  	// system.table_statistics table, but that would require calling
   462  	// MakeEventLogger from the distsqlrun package.
   463  	return evalCtx.ExecCfg.DB.Txn(ctx, func(ctx context.Context, txn *kv.Txn) error {
   464  		return MakeEventLogger(evalCtx.ExecCfg).InsertEventRecord(
   465  			ctx,
   466  			txn,
   467  			EventLogCreateStatistics,
   468  			int32(details.Table.ID),
   469  			int32(evalCtx.NodeID.SQLInstanceID()),
   470  			struct {
   471  				TableName string
   472  				Statement string
   473  			}{details.FQTableName, details.Statement},
   474  		)
   475  	})
   476  }
   477  
   478  // checkRunningJobs checks whether there are any other CreateStats jobs in the
   479  // pending, running, or paused status that started earlier than this one. If
   480  // there are, checkRunningJobs returns an error. If job is nil, checkRunningJobs
   481  // just checks if there are any pending, running, or paused CreateStats jobs.
   482  func checkRunningJobs(ctx context.Context, job *jobs.Job, p *planner) error {
   483  	var jobID int64
   484  	if job != nil {
   485  		jobID = *job.ID()
   486  	}
   487  	const stmt = `SELECT id, payload FROM system.jobs WHERE status IN ($1, $2, $3) ORDER BY created`
   488  
   489  	rows, err := p.ExecCfg().InternalExecutor.Query(
   490  		ctx,
   491  		"get-jobs",
   492  		nil, /* txn */
   493  		stmt,
   494  		jobs.StatusPending,
   495  		jobs.StatusRunning,
   496  		jobs.StatusPaused,
   497  	)
   498  	if err != nil {
   499  		return err
   500  	}
   501  
   502  	for _, row := range rows {
   503  		payload, err := jobs.UnmarshalPayload(row[1])
   504  		if err != nil {
   505  			return err
   506  		}
   507  
   508  		if payload.Type() == jobspb.TypeCreateStats || payload.Type() == jobspb.TypeAutoCreateStats {
   509  			id := (*int64)(row[0].(*tree.DInt))
   510  			if *id == jobID {
   511  				break
   512  			}
   513  
   514  			// This is not the first CreateStats job running. This job should fail
   515  			// so that the earlier job can succeed.
   516  			return stats.ConcurrentCreateStatsError
   517  		}
   518  	}
   519  	return nil
   520  }
   521  
   522  // OnFailOrCancel is part of the jobs.Resumer interface.
   523  func (r *createStatsResumer) OnFailOrCancel(context.Context, interface{}) error { return nil }
   524  
   525  func init() {
   526  	createResumerFn := func(job *jobs.Job, settings *cluster.Settings) jobs.Resumer {
   527  		return &createStatsResumer{job: job}
   528  	}
   529  	jobs.RegisterConstructor(jobspb.TypeCreateStats, createResumerFn)
   530  	jobs.RegisterConstructor(jobspb.TypeAutoCreateStats, createResumerFn)
   531  }