github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/sql/stats/stats_cache.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 stats
    12  
    13  import (
    14  	"context"
    15  	"sync"
    16  
    17  	"github.com/cockroachdb/cockroach/pkg/gossip"
    18  	"github.com/cockroachdb/cockroach/pkg/keys"
    19  	"github.com/cockroachdb/cockroach/pkg/kv"
    20  	"github.com/cockroachdb/cockroach/pkg/roachpb"
    21  	"github.com/cockroachdb/cockroach/pkg/sql/catalog/resolver"
    22  	"github.com/cockroachdb/cockroach/pkg/sql/opt/cat"
    23  	"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
    24  	"github.com/cockroachdb/cockroach/pkg/sql/sqlbase"
    25  	"github.com/cockroachdb/cockroach/pkg/sql/sqlutil"
    26  	"github.com/cockroachdb/cockroach/pkg/sql/types"
    27  	"github.com/cockroachdb/cockroach/pkg/util/cache"
    28  	"github.com/cockroachdb/cockroach/pkg/util/encoding"
    29  	"github.com/cockroachdb/cockroach/pkg/util/log"
    30  	"github.com/cockroachdb/cockroach/pkg/util/protoutil"
    31  	"github.com/cockroachdb/cockroach/pkg/util/syncutil"
    32  	"github.com/cockroachdb/errors"
    33  )
    34  
    35  // A TableStatistic object holds a statistic for a particular column or group
    36  // of columns.
    37  type TableStatistic struct {
    38  	TableStatisticProto
    39  
    40  	// Histogram is the decoded histogram data.
    41  	Histogram []cat.HistogramBucket
    42  }
    43  
    44  // A TableStatisticsCache contains two underlying LRU caches:
    45  // (1) A cache of []*TableStatistic objects, keyed by table ID.
    46  //     Each entry consists of all the statistics for different columns and
    47  //     column groups for the given table.
    48  // (2) A cache of *HistogramData objects, keyed by
    49  //     HistogramCacheKey{table ID, statistic ID}.
    50  type TableStatisticsCache struct {
    51  	// NB: This can't be a RWMutex for lookup because UnorderedCache.Get
    52  	// manipulates an internal LRU list.
    53  	mu struct {
    54  		syncutil.Mutex
    55  		cache *cache.UnorderedCache
    56  		// Used for testing; keeps track of how many times we actually read stats
    57  		// from the system table.
    58  		numInternalQueries int64
    59  	}
    60  	ClientDB    *kv.DB
    61  	SQLExecutor sqlutil.InternalExecutor
    62  	Codec       keys.SQLCodec
    63  }
    64  
    65  // The cache stores *cacheEntry objects. The fields are protected by the
    66  // cache-wide mutex.
    67  type cacheEntry struct {
    68  	// If true, we are in the process of updating the statistics for this
    69  	// table. Other callers can wait on the waitCond until this is false.
    70  	mustWait bool
    71  	waitCond sync.Cond
    72  
    73  	stats []*TableStatistic
    74  
    75  	// err is populated if the internal query to retrieve stats hit an error.
    76  	err error
    77  }
    78  
    79  // NewTableStatisticsCache creates a new TableStatisticsCache that can hold
    80  // statistics for <cacheSize> tables.
    81  func NewTableStatisticsCache(
    82  	cacheSize int,
    83  	gw gossip.DeprecatedGossip,
    84  	db *kv.DB,
    85  	sqlExecutor sqlutil.InternalExecutor,
    86  	codec keys.SQLCodec,
    87  ) *TableStatisticsCache {
    88  	tableStatsCache := &TableStatisticsCache{
    89  		ClientDB:    db,
    90  		SQLExecutor: sqlExecutor,
    91  		Codec:       codec,
    92  	}
    93  	tableStatsCache.mu.cache = cache.NewUnorderedCache(cache.Config{
    94  		Policy:      cache.CacheLRU,
    95  		ShouldEvict: func(s int, key, value interface{}) bool { return s > cacheSize },
    96  	})
    97  	// The stat cache requires redundant callbacks as it is using gossip to
    98  	// signal the presence of new stats, not to actually propagate them.
    99  	if g, ok := gw.Optional(47925); ok {
   100  		g.RegisterCallback(
   101  			gossip.MakePrefixPattern(gossip.KeyTableStatAddedPrefix),
   102  			tableStatsCache.tableStatAddedGossipUpdate,
   103  			gossip.Redundant,
   104  		)
   105  	}
   106  	return tableStatsCache
   107  }
   108  
   109  // tableStatAddedGossipUpdate is the gossip callback that fires when a new
   110  // statistic is available for a table.
   111  func (sc *TableStatisticsCache) tableStatAddedGossipUpdate(key string, value roachpb.Value) {
   112  	tableID, err := gossip.TableIDFromTableStatAddedKey(key)
   113  	if err != nil {
   114  		log.Errorf(context.Background(), "tableStatAddedGossipUpdate(%s) error: %v", key, err)
   115  		return
   116  	}
   117  	sc.InvalidateTableStats(context.Background(), sqlbase.ID(tableID))
   118  }
   119  
   120  // GetTableStats looks up statistics for the requested table ID in the cache,
   121  // and if the stats are not present in the cache, it looks them up in
   122  // system.table_statistics.
   123  //
   124  // The statistics are ordered by their CreatedAt time (newest-to-oldest).
   125  func (sc *TableStatisticsCache) GetTableStats(
   126  	ctx context.Context, tableID sqlbase.ID,
   127  ) ([]*TableStatistic, error) {
   128  	if sqlbase.IsReservedID(tableID) {
   129  		// Don't try to get statistics for system tables (most importantly,
   130  		// for table_statistics itself).
   131  		return nil, nil
   132  	}
   133  	if sqlbase.IsVirtualTable(tableID) {
   134  		// Don't try to get statistics for virtual tables.
   135  		return nil, nil
   136  	}
   137  
   138  	sc.mu.Lock()
   139  	defer sc.mu.Unlock()
   140  
   141  	if found, stats, err := sc.lookupStatsLocked(ctx, tableID); found {
   142  		return stats, err
   143  	}
   144  
   145  	return sc.addCacheEntryLocked(ctx, tableID)
   146  }
   147  
   148  // lookupStatsLocked retrieves any existing stats for the given table.
   149  //
   150  // If another goroutine is in the process of retrieving the same stats, this
   151  // method waits until that completes.
   152  //
   153  // Assumes that the caller holds sc.mu. Note that the mutex can be unlocked and
   154  // locked again if we need to wait (this can only happen when found=true).
   155  func (sc *TableStatisticsCache) lookupStatsLocked(
   156  	ctx context.Context, tableID sqlbase.ID,
   157  ) (found bool, _ []*TableStatistic, _ error) {
   158  	eUntyped, ok := sc.mu.cache.Get(tableID)
   159  	if !ok {
   160  		return false, nil, nil
   161  	}
   162  	e := eUntyped.(*cacheEntry)
   163  
   164  	if e.mustWait {
   165  		// We are in the process of grabbing stats for this table. Wait until
   166  		// that is complete, at which point e.stats will be populated.
   167  		if log.V(1) {
   168  			log.Infof(ctx, "waiting for statistics for table %d", tableID)
   169  		}
   170  		e.waitCond.Wait()
   171  	} else {
   172  		if log.V(2) {
   173  			log.Infof(ctx, "statistics for table %d found in cache", tableID)
   174  		}
   175  	}
   176  	return true, e.stats, e.err
   177  }
   178  
   179  // addCacheEntryLocked creates a new cache entry and retrieves table statistics
   180  // from the database. It does this in a way so that the other goroutines that
   181  // need the same stats can wait on us:
   182  //  - an cache entry with wait=true is created;
   183  //  - mutex is unlocked;
   184  //  - stats are retrieved from database:
   185  //  - mutex is locked again and the entry is updated.
   186  //
   187  func (sc *TableStatisticsCache) addCacheEntryLocked(
   188  	ctx context.Context, tableID sqlbase.ID,
   189  ) (stats []*TableStatistic, err error) {
   190  	if log.V(1) {
   191  		log.Infof(ctx, "reading statistics for table %d", tableID)
   192  	}
   193  
   194  	// Add a cache entry that other queries can find and wait on until we have the
   195  	// stats.
   196  	e := &cacheEntry{
   197  		mustWait: true,
   198  		waitCond: sync.Cond{L: &sc.mu},
   199  	}
   200  	sc.mu.cache.Add(tableID, e)
   201  	sc.mu.numInternalQueries++
   202  
   203  	func() {
   204  		sc.mu.Unlock()
   205  		defer sc.mu.Lock()
   206  
   207  		stats, err = sc.getTableStatsFromDB(ctx, tableID)
   208  	}()
   209  
   210  	e.mustWait = false
   211  	e.stats, e.err = stats, err
   212  
   213  	// Wake up any other callers that are waiting on these stats.
   214  	e.waitCond.Broadcast()
   215  
   216  	if err != nil {
   217  		// Don't keep the cache entry around, so that we retry the query.
   218  		sc.mu.cache.Del(tableID)
   219  	}
   220  
   221  	return stats, err
   222  }
   223  
   224  // InvalidateTableStats invalidates the cached statistics for the given table ID.
   225  func (sc *TableStatisticsCache) InvalidateTableStats(ctx context.Context, tableID sqlbase.ID) {
   226  	if log.V(1) {
   227  		log.Infof(ctx, "evicting statistics for table %d", tableID)
   228  	}
   229  	sc.mu.Lock()
   230  	defer sc.mu.Unlock()
   231  	sc.mu.cache.Del(tableID)
   232  }
   233  
   234  const (
   235  	tableIDIndex = iota
   236  	statisticsIDIndex
   237  	nameIndex
   238  	columnIDsIndex
   239  	createdAtIndex
   240  	rowCountIndex
   241  	distinctCountIndex
   242  	nullCountIndex
   243  	histogramIndex
   244  	statsLen
   245  )
   246  
   247  // parseStats converts the given datums to a TableStatistic object. It might
   248  // need to run a query to get user defined type metadata.
   249  func parseStats(
   250  	ctx context.Context, db *kv.DB, codec keys.SQLCodec, datums tree.Datums,
   251  ) (*TableStatistic, error) {
   252  	if datums == nil || datums.Len() == 0 {
   253  		return nil, nil
   254  	}
   255  
   256  	// Validate the input length.
   257  	if datums.Len() != statsLen {
   258  		return nil, errors.Errorf("%d values returned from table statistics lookup. Expected %d", datums.Len(), statsLen)
   259  	}
   260  
   261  	// Validate the input types.
   262  	expectedTypes := []struct {
   263  		fieldName    string
   264  		fieldIndex   int
   265  		expectedType *types.T
   266  		nullable     bool
   267  	}{
   268  		{"tableID", tableIDIndex, types.Int, false},
   269  		{"statisticsID", statisticsIDIndex, types.Int, false},
   270  		{"name", nameIndex, types.String, true},
   271  		{"columnIDs", columnIDsIndex, types.IntArray, false},
   272  		{"createdAt", createdAtIndex, types.Timestamp, false},
   273  		{"rowCount", rowCountIndex, types.Int, false},
   274  		{"distinctCount", distinctCountIndex, types.Int, false},
   275  		{"nullCount", nullCountIndex, types.Int, false},
   276  		{"histogram", histogramIndex, types.Bytes, true},
   277  	}
   278  	for _, v := range expectedTypes {
   279  		if !datums[v.fieldIndex].ResolvedType().Equivalent(v.expectedType) &&
   280  			(!v.nullable || datums[v.fieldIndex].ResolvedType().Family() != types.UnknownFamily) {
   281  			return nil, errors.Errorf("%s returned from table statistics lookup has type %s. Expected %s",
   282  				v.fieldName, datums[v.fieldIndex].ResolvedType(), v.expectedType)
   283  		}
   284  	}
   285  
   286  	// Extract datum values.
   287  	res := &TableStatistic{
   288  		TableStatisticProto: TableStatisticProto{
   289  			TableID:       sqlbase.ID((int32)(*datums[tableIDIndex].(*tree.DInt))),
   290  			StatisticID:   (uint64)(*datums[statisticsIDIndex].(*tree.DInt)),
   291  			CreatedAt:     datums[createdAtIndex].(*tree.DTimestamp).Time,
   292  			RowCount:      (uint64)(*datums[rowCountIndex].(*tree.DInt)),
   293  			DistinctCount: (uint64)(*datums[distinctCountIndex].(*tree.DInt)),
   294  			NullCount:     (uint64)(*datums[nullCountIndex].(*tree.DInt)),
   295  		},
   296  	}
   297  	columnIDs := datums[columnIDsIndex].(*tree.DArray)
   298  	res.ColumnIDs = make([]sqlbase.ColumnID, len(columnIDs.Array))
   299  	for i, d := range columnIDs.Array {
   300  		res.ColumnIDs[i] = sqlbase.ColumnID((int32)(*d.(*tree.DInt)))
   301  	}
   302  	if datums[nameIndex] != tree.DNull {
   303  		res.Name = string(*datums[nameIndex].(*tree.DString))
   304  	}
   305  	if datums[histogramIndex] != tree.DNull {
   306  		res.HistogramData = &HistogramData{}
   307  		if err := protoutil.Unmarshal(
   308  			[]byte(*datums[histogramIndex].(*tree.DBytes)),
   309  			res.HistogramData,
   310  		); err != nil {
   311  			return nil, err
   312  		}
   313  
   314  		// Decode the histogram data so that it's usable by the opt catalog.
   315  		res.Histogram = make([]cat.HistogramBucket, len(res.HistogramData.Buckets))
   316  		typ := res.HistogramData.ColumnType
   317  		// Hydrate the type in case any user defined types are present.
   318  		// There are cases where typ is nil, so don't do anything if so.
   319  		if typ != nil && typ.UserDefined() {
   320  			// TODO (rohany): This should instead query a leased copy of the type.
   321  			// TODO (rohany): If we are caching data about types here, then this
   322  			//  cache needs to be invalidated as well when type metadata changes.
   323  			// TODO (rohany): It might be better to store the type metadata used when
   324  			//  collecting the stats in the HistogramData object itself, and avoid
   325  			//  this query and caching/leasing problem.
   326  			// The metadata accessed here is never older than the metadata used when
   327  			// collecting the stats. Changes to types are backwards compatible across
   328  			// versions, so using a newer version of the type metadata here is safe.
   329  			err := db.Txn(ctx, func(ctx context.Context, txn *kv.Txn) error {
   330  				typeLookup := func(id sqlbase.ID) (*tree.TypeName, *sqlbase.TypeDescriptor, error) {
   331  					return resolver.ResolveTypeDescByID(ctx, txn, codec, id)
   332  				}
   333  				name, typeDesc, err := typeLookup(sqlbase.ID(typ.StableTypeID()))
   334  				if err != nil {
   335  					return err
   336  				}
   337  				return typeDesc.HydrateTypeInfoWithName(typ, name, typeLookup)
   338  			})
   339  			if err != nil {
   340  				return nil, err
   341  			}
   342  		}
   343  		var a sqlbase.DatumAlloc
   344  		for i := range res.Histogram {
   345  			bucket := &res.HistogramData.Buckets[i]
   346  			datum, _, err := sqlbase.DecodeTableKey(&a, typ, bucket.UpperBound, encoding.Ascending)
   347  			if err != nil {
   348  				return nil, err
   349  			}
   350  			res.Histogram[i] = cat.HistogramBucket{
   351  				NumEq:         float64(bucket.NumEq),
   352  				NumRange:      float64(bucket.NumRange),
   353  				DistinctRange: bucket.DistinctRange,
   354  				UpperBound:    datum,
   355  			}
   356  		}
   357  	}
   358  
   359  	return res, nil
   360  }
   361  
   362  // getTableStatsFromDB retrieves the statistics in system.table_statistics
   363  // for the given table ID.
   364  func (sc *TableStatisticsCache) getTableStatsFromDB(
   365  	ctx context.Context, tableID sqlbase.ID,
   366  ) ([]*TableStatistic, error) {
   367  	const getTableStatisticsStmt = `
   368  SELECT
   369    "tableID",
   370  	"statisticID",
   371  	name,
   372  	"columnIDs",
   373  	"createdAt",
   374  	"rowCount",
   375  	"distinctCount",
   376  	"nullCount",
   377  	histogram
   378  FROM system.table_statistics
   379  WHERE "tableID" = $1
   380  ORDER BY "createdAt" DESC
   381  `
   382  	rows, err := sc.SQLExecutor.Query(
   383  		ctx, "get-table-statistics", nil /* txn */, getTableStatisticsStmt, tableID,
   384  	)
   385  	if err != nil {
   386  		return nil, err
   387  	}
   388  
   389  	var statsList []*TableStatistic
   390  	for _, row := range rows {
   391  		stats, err := parseStats(ctx, sc.ClientDB, sc.Codec, row)
   392  		if err != nil {
   393  			return nil, err
   394  		}
   395  		statsList = append(statsList, stats)
   396  	}
   397  
   398  	return statsList, nil
   399  }