-
Notifications
You must be signed in to change notification settings - Fork 3.8k
/
index_recommendation_set.go
311 lines (276 loc) · 10.9 KB
/
index_recommendation_set.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
// Copyright 2021 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.
package indexrec
import (
"fmt"
"sort"
"strings"
"github.com/cockroachdb/cockroach/pkg/sql/opt"
"github.com/cockroachdb/cockroach/pkg/sql/opt/cat"
"github.com/cockroachdb/cockroach/pkg/sql/opt/memo"
"github.com/cockroachdb/cockroach/pkg/util"
)
// FindIndexRecommendationSet finds index candidates that are scanned in an
// expression to determine a statement's index recommendation set.
//
// Note that because the index recommendation engine stores all table columns in
// its hypothetical indexes when optimizing, and then prunes the stored columns
// after, this may result in the best plan not being chosen. Meaning, a plan
// might not be recommended because the optimizer includes the overhead of
// scanning stored columns of an index, which results in plans with hypothetical
// indexes being more expensive and reduces their likelihood of being
// recommended. This is a tradeoff we accept in order to recommend STORING
// columns, as the difference in plan costs is not very significant.
func FindIndexRecommendationSet(expr opt.Expr, md *opt.Metadata) IndexRecommendationSet {
var recommendationSet IndexRecommendationSet
recommendationSet.init(md)
recommendationSet.createIndexRecommendations(expr)
recommendationSet.pruneIndexRecommendations()
return recommendationSet
}
// IndexRecommendationSet stores the hypothetical indexes that are scanned in a
// statement's optimal plan (in indexRecs), as well as the statement's metadata.
type IndexRecommendationSet struct {
md *opt.Metadata
indexRecs map[cat.Table][]indexRecommendation
}
// init initializes an IndexRecommendationSet by allocating memory for it.
func (irs *IndexRecommendationSet) init(md *opt.Metadata) {
numTables := len(md.AllTables())
irs.md = md
irs.indexRecs = make(map[cat.Table][]indexRecommendation, numTables)
}
// createIndexRecommendations recursively walks an expression tree to find
// hypothetical indexes that are scanned in it.
func (irs *IndexRecommendationSet) createIndexRecommendations(expr opt.Expr) {
switch expr := expr.(type) {
case *memo.ScanExpr:
irs.addIndexToRecommendationSet(expr.Index, expr.Cols, expr.Table)
case *memo.LookupJoinExpr:
irs.addIndexToRecommendationSet(expr.Index, expr.Cols, expr.Table)
case *memo.InvertedJoinExpr:
irs.addIndexToRecommendationSet(expr.Index, expr.Cols, expr.Table)
case *memo.ZigzagJoinExpr:
irs.addIndexToRecommendationSet(expr.LeftIndex, expr.Cols, expr.LeftTable)
irs.addIndexToRecommendationSet(expr.RightIndex, expr.Cols, expr.RightTable)
}
for i, n := 0, expr.ChildCount(); i < n; i++ {
irs.createIndexRecommendations(expr.Child(i))
}
}
// pruneIndexRecommendations removes redundant index recommendations from the
// recommendation set. These are recommendations where the existingIndex field
// is not nil and no new columns are being stored.
func (irs *IndexRecommendationSet) pruneIndexRecommendations() {
for t, indexRecs := range irs.indexRecs {
updatedIndexRecs := make([]indexRecommendation, 0, len(indexRecs))
for _, indexRec := range indexRecs {
if indexRec.existingIndex == nil || !indexRec.redundantRecommendation() {
updatedIndexRecs = append(updatedIndexRecs, indexRec)
}
}
irs.indexRecs[t] = updatedIndexRecs
}
}
// getColOrdSet returns the set of column ordinals within the given table that
// are contained in cols.
func (irs *IndexRecommendationSet) getColOrdSet(
cols opt.ColSet, tabID opt.TableID,
) util.FastIntSet {
var colsOrdSet util.FastIntSet
cols.ForEach(func(col opt.ColumnID) {
table := irs.md.ColumnMeta(col).Table
// Do not add columns from other tables.
if table != tabID {
return
}
colOrd := table.ColumnOrdinal(col)
colsOrdSet.Add(colOrd)
})
return colsOrdSet
}
// addIndexToRecommendationSet adds an index to the indexes map if it does not
// exist already in the map and in the table. The scannedCols argument contains
// the columns of the index that are actually scanned, used to determine which
// columns should be stored columns in the index recommendation.
func (irs *IndexRecommendationSet) addIndexToRecommendationSet(
indexOrd cat.IndexOrdinal, scannedCols opt.ColSet, tabID opt.TableID,
) {
// Do not recommend the primary index.
if indexOrd == cat.PrimaryIndex {
return
}
// Do not add real table indexes (non-hypothetical table indexes).
switch hypTable := irs.md.TableMeta(tabID).Table.(type) {
case *hypotheticalTable:
scannedColOrds := irs.getColOrdSet(scannedCols, tabID)
// Try to find an identical existing index recommendation.
for _, indexRec := range irs.indexRecs[hypTable] {
index := indexRec.index
if index.indexOrdinal == indexOrd {
// Update newStoredColOrds to include all stored column ordinals that
// are in scannedColOrds.
indexRec.addStoredColOrds(scannedColOrds)
return
}
}
// Create a new index recommendation with pruned stored columns. Only store
// columns that are in scannedColOrds.
var newIndexRec indexRecommendation
newIndexRec.init(indexOrd, hypTable, scannedColOrds)
irs.indexRecs[hypTable] = append(irs.indexRecs[hypTable], newIndexRec)
}
}
// Output returns a string slice of index recommendation output that will be
// displayed below the statement plan in EXPLAIN.
func (irs *IndexRecommendationSet) Output() []string {
indexRecCount := 0
for t := range irs.indexRecs {
indexRecCount += len(irs.indexRecs[t])
}
if indexRecCount == 0 {
return nil
}
var output []string
output = append(output, fmt.Sprintf("index recommendations: %d", indexRecCount))
sortedTables := make([]cat.Table, 0, len(irs.indexRecs))
for t := range irs.indexRecs {
sortedTables = append(sortedTables, t)
}
sort.Slice(sortedTables, func(i, j int) bool {
return sortedTables[i].Name() < sortedTables[j].Name()
})
indexRecOrd := 1
for _, t := range sortedTables {
indexes := irs.indexRecs[t]
for _, indexRec := range indexes {
var sb strings.Builder
recTypeStr := "index creation"
if indexRec.existingIndex != nil {
recTypeStr = "index replacement"
}
sb.WriteString(fmt.Sprintf("%d. type: %s", indexRecOrd, recTypeStr))
indexRecOrd++
indexCols := indexRec.indexColsString()
storingClause := indexRec.storingClauseString()
sb.WriteString(indexRec.indexRecommendationString(indexCols, storingClause))
output = append(output, sb.String())
}
}
return output
}
// indexRecommendation stores the information pertaining to a single index
// recommendation.
type indexRecommendation struct {
// index stores the hypotheticalIndex that is being recommended.
index *hypotheticalIndex
// existingIndex stores an existing index with the same explicit columns, if
// one exists.
existingIndex cat.Index
// newStoredColOrds stores the stored column ordinals that are scanned by the
// optimizer in the expression tree passed to FindIndexRecommendationSet.
newStoredColOrds util.FastIntSet
// existingStoredColOrds stores the column ordinals of the existingIndex's
// stored columns. It is empty if there is no existingIndex.
existingStoredColOrds util.FastIntSet
}
// init initializes an index recommendation. If there is an existingIndex with
// the same explicit key columns, it is stored here.
func (ir *indexRecommendation) init(
indexOrd int, hypTable *hypotheticalTable, scannedColOrds util.FastIntSet,
) {
index := hypTable.Index(indexOrd).(*hypotheticalIndex)
ir.index = index
ir.existingIndex = hypTable.existingRedundantIndex(index)
// Only store columns useful to the statement plan, found in scannedColOrds.
ir.newStoredColOrds = index.storedColsOrdSet.Intersection(scannedColOrds)
// If there is no existing index, return.
if ir.existingIndex == nil {
return
}
// Iterate through the stored columns of the existing index to set
// existingStoredColOrds.
var existingStoredOrds util.FastIntSet
for i, n := ir.existingIndex.KeyColumnCount(), ir.existingIndex.ColumnCount(); i < n; i++ {
existingStoredOrds.Add(ir.existingIndex.Column(i).Ordinal())
}
ir.existingStoredColOrds = existingStoredOrds
// Update newStoredColOrds to contain the existingStoredColOrds. We want to
// include all existing stored columns in the potential replacement
// recommendation.
ir.newStoredColOrds.UnionWith(ir.existingStoredColOrds)
}
// redundantRecommendation compares newStoredColOrds with the existing index's
// stored columns. It returns true if there are no new columns being stored in
// newStoredColOrds.
func (ir *indexRecommendation) redundantRecommendation() bool {
return ir.newStoredColOrds.Difference(ir.existingStoredColOrds).Empty()
}
// addStoredColOrds updates an index recommendation's newStoredColOrds field to
// also contain the scannedColOrds columns.
func (ir *indexRecommendation) addStoredColOrds(scannedColOrds util.FastIntSet) {
scannedStoredColOrds := ir.index.storedColsOrdSet.Intersection(scannedColOrds)
ir.newStoredColOrds.UnionWith(scannedStoredColOrds)
}
// indexColsString returns a string containing the explicit key columns of the
// index, used in indexRecommendationString.
func (ir *indexRecommendation) indexColsString() string {
indexCols := make([]string, len(ir.index.cols))
for i, n := 0, len(ir.index.cols); i < n; i++ {
var indexColSb strings.Builder
indexCol := ir.index.Column(i)
colName := indexCol.Column.ColName()
indexColSb.WriteString(colName.String())
if indexCol.Descending {
indexColSb.WriteString(" DESC")
}
indexCols[i] = indexColSb.String()
}
return strings.Join(indexCols, ", ")
}
// storingClauseString returns the STORING clause string output of an index
// recommendation.
func (ir *indexRecommendation) storingClauseString() string {
if ir.newStoredColOrds.Len() == 0 {
return ""
}
var storingSb strings.Builder
for i, col := range ir.newStoredColOrds.Ordered() {
colName := ir.index.tab.Column(col).ColName()
if i > 0 {
storingSb.WriteString(", ")
}
storingSb.WriteString(colName.String())
}
return " STORING (" + storingSb.String() + ")"
}
// indexRecommendationString returns the string output for an index
// recommendation, containing the SQL command(s) needed to follow this
// recommendation.
func (ir *indexRecommendation) indexRecommendationString(indexCols, storingClause string) string {
var sb strings.Builder
tableName := ir.index.tab.Name()
if ir.existingIndex != nil {
sb.WriteString("\n SQL commands: ")
indexName := ir.existingIndex.Name()
dropCmd := fmt.Sprintf("DROP INDEX %s@%s; ", tableName.String(), indexName.String())
sb.WriteString(dropCmd)
} else {
sb.WriteString("\n SQL command: ")
}
createCmd := fmt.Sprintf(
"CREATE INDEX ON %s (%s)%s;",
tableName.String(),
indexCols,
storingClause,
)
sb.WriteString(createCmd)
return sb.String()
}