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 }