Skip to content

Commit a98763c

Browse files
Merge pull request #3034 from actiontech/optimize_00055_rule
Optimize 00055 rule
2 parents b501d29 + ed52cb5 commit a98763c

File tree

5 files changed

+180
-97
lines changed

5 files changed

+180
-97
lines changed

sqle/driver/mysql/plocale/active.en.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -837,7 +837,7 @@ Rule00054Desc = "Use unsigned BIGINT for primary key fields."
837837
Rule00054Message = "Use unsigned BIGINT for primary key fields."
838838
Rule00055Annotation = "MySQL maintains redundant indexes separately, increasing maintenance costs and impacting update performance."
839839
Rule00055Desc = "Avoid creating redundant indexes."
840-
Rule00055Message = "An index %v already exists. Index %v is a redundant index."
840+
Rule00055Message = "%v indexes already exist in the %v fields, and the indexes %v are redundant indexes."
841841
Rule00056Annotation = "Using non-standard character sets in the database may lead to encoding or decoding issues, resulting in data write failures or garbled query results, affecting database availability."
842842
Rule00056Desc = "Use specified character sets for tables."
843843
Rule00056Message = "Use specified character sets for tables: %v."

sqle/driver/mysql/plocale/active.zh.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -837,7 +837,7 @@ Rule00054Desc = "建议主键字段使用BIGINT时采用无符号的BIGINT"
837837
Rule00054Message = "建议主键字段使用BIGINT时采用无符号的BIGINT"
838838
Rule00055Annotation = "MySQL需要单独维护重复的索引,冗余索引增加维护成本,影响更新性能"
839839
Rule00055Desc = "不建议创建冗余索引"
840-
Rule00055Message = "已存在索引 %v , 索引 %v 为冗余索引"
840+
Rule00055Message = " %v 字段已存在 %v 索引,索引 %v 为冗余索引"
841841
Rule00056Annotation = "数据库内使用非标准的字符集,可能导致字符无法编码或者编码不全引起的乱码,最终出现应用写入数据失败或者查询结果显示乱码,影响数据库服务可用性。"
842842
Rule00056Desc = "表建议使用指定的字符集"
843843
Rule00056Message = "表建议使用指定的字符集: %v"

sqle/driver/mysql/plocale/message_zh.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -944,7 +944,7 @@ var (
944944
Rule00054Message = &i18n.Message{ID: "Rule00054Message", Other: "建议主键字段使用BIGINT时采用无符号的BIGINT"}
945945
Rule00055Desc = &i18n.Message{ID: "Rule00055Desc", Other: "不建议创建冗余索引"}
946946
Rule00055Annotation = &i18n.Message{ID: "Rule00055Annotation", Other: "MySQL需要单独维护重复的索引,冗余索引增加维护成本,影响更新性能"}
947-
Rule00055Message = &i18n.Message{ID: "Rule00055Message", Other: "已存在索引 %v , 索引 %v 为冗余索引"}
947+
Rule00055Message = &i18n.Message{ID: "Rule00055Message", Other: " %v 字段已存在 %v 索引,索引 %v 为冗余索引"}
948948
Rule00056Desc = &i18n.Message{ID: "Rule00056Desc", Other: "表建议使用指定的字符集"}
949949
Rule00056Annotation = &i18n.Message{ID: "Rule00056Annotation", Other: "数据库内使用非标准的字符集,可能导致字符无法编码或者编码不全引起的乱码,最终出现应用写入数据失败或者查询结果显示乱码,影响数据库服务可用性。"}
950950
Rule00056Message = &i18n.Message{ID: "Rule00056Message", Other: "表建议使用指定的字符集: %v"}
Lines changed: 171 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package ai
22

33
import (
4+
"sort"
5+
"strings"
6+
47
rulepkg "github.com/actiontech/sqle/sqle/driver/mysql/rule"
58
util "github.com/actiontech/sqle/sqle/driver/mysql/rule/ai/util"
69
driverV2 "github.com/actiontech/sqle/sqle/driver/v2"
@@ -54,22 +57,24 @@ func RuleSQLE00055(input *rulepkg.RuleHandlerInput) error {
5457
switch stmt := input.Node.(type) {
5558
case *ast.CreateTableStmt:
5659
// "create table..."
57-
indexes := [][]string{}
58-
59-
// get index column in column definition
60+
colIndexes := map[string][]string{}
61+
// 获取列定义中的索引列
6062
for _, col := range stmt.Cols {
61-
if util.IsColumnHasOption(col, ast.ColumnOptionUniqKey) || util.IsColumnPrimaryKey(col) {
62-
indexes = append(indexes, []string{util.GetColumnName(col)})
63+
if util.IsColumnHasOption(col, ast.ColumnOptionUniqKey) {
64+
colIndexes["UNIQUE["+col.Name.Name.O+"]"] = []string{util.GetColumnName(col)}
65+
}
66+
if util.IsColumnPrimaryKey(col) {
67+
colIndexes["Primary Key"] = []string{util.GetColumnName(col)}
6368
}
6469
}
6570

66-
// get index column in table constraint
67-
indexes = append(indexes, extractIndexesFromConstraints(util.GetTableConstraints(stmt.Constraints, util.GetIndexConstraintTypes()...))...)
68-
69-
// check the index is not duplicated
70-
for redundant, source := range calculateIndexRedundant(indexes) {
71-
rulepkg.AddResult(input.Res, input.Rule, SQLE00055, indexes[source], indexes[redundant])
72-
}
71+
// 获取表约束中的索引列
72+
constraintsIndexes := extractIndexesFromConstraints(util.GetTableConstraints(stmt.Constraints, util.GetIndexConstraintTypes()...))
73+
// 此处要聚合到一个map中,否则无法获取存在于表约束中的冗余索引
74+
mergeMap := mergeIndexsMaps(colIndexes, constraintsIndexes)
75+
// 获取冗余索引
76+
existsIndexs, redundantIndexs := GroupDuplicatesByValue(mergeMap)
77+
buildAuditResult(input, existsIndexs, redundantIndexs)
7378

7479
case *ast.CreateIndexStmt:
7580
// "create index..."
@@ -80,12 +85,11 @@ func RuleSQLE00055(input *rulepkg.RuleHandlerInput) error {
8085
}
8186

8287
indexes := extractIndexesFromConstraints(util.GetTableConstraints(createTableStmt.Constraints, util.GetIndexConstraintTypes()...))
83-
newIndex := extractIndexesFromIndexStmt(stmt.IndexPartSpecifications)
84-
85-
// check the index is not duplicated
86-
for redundant, source := range calculateNewIndexRedundant(indexes, newIndex) {
87-
rulepkg.AddResult(input.Res, input.Rule, SQLE00055, indexes[source], newIndex[redundant])
88-
}
88+
newIndexMap := make(map[string][]string)
89+
newIndexMap[stmt.IndexName] = extractIndexesFromIndexStmt(stmt.IndexPartSpecifications)
90+
// 获取冗余索引
91+
existsIndexs, redundantIndexs := GetIntersectionByValue(indexes, newIndexMap)
92+
buildAuditResult(input, existsIndexs, redundantIndexs)
8993

9094
case *ast.AlterTableStmt:
9195
// "alter table"
@@ -96,25 +100,39 @@ func RuleSQLE00055(input *rulepkg.RuleHandlerInput) error {
96100
}
97101

98102
indexes := extractIndexesFromConstraints(util.GetTableConstraints(createTableStmt.Constraints, util.GetIndexConstraintTypes()...))
99-
100-
newIndex := [][]string{}
103+
constraints := make([]*ast.Constraint, 0)
101104
for _, spec := range util.GetAlterTableCommandsByTypes(stmt, ast.AlterTableAddConstraint) {
102105
// "alter table... add index..."
103-
indexCols := extractIndexesFromConstraints(util.GetTableConstraints([]*ast.Constraint{spec.Constraint}, util.GetIndexConstraintTypes()...))
104-
newIndex = append(newIndex, indexCols...)
105-
106-
}
107-
// check the index is not duplicated
108-
for redundant, source := range calculateNewIndexRedundant(indexes, newIndex) {
109-
rulepkg.AddResult(input.Res, input.Rule, SQLE00055, indexes[source], newIndex[redundant])
106+
constraints = append(constraints, spec.Constraint)
110107
}
108+
109+
// // 获取冗余索引
110+
newIndexs := extractIndexesFromConstraints(util.GetTableConstraints(constraints, util.GetIndexConstraintTypes()...))
111+
existsIndexs, redundantIndexs := GetIntersectionByValue(indexes, newIndexs)
112+
buildAuditResult(input, existsIndexs, redundantIndexs)
111113
}
112114

113115
return nil
114116
}
115117

116-
func extractIndexesFromConstraints(constraints []*ast.Constraint) [][]string {
117-
indexes := [][]string{}
118+
func buildAuditResult(input *rulepkg.RuleHandlerInput, existsIndexs, redundantIndexs map[string][]string) {
119+
idxNames := make([]string, 0, len(existsIndexs))
120+
idxColNames := make([][]string, 0, len(existsIndexs))
121+
redundantIdxNames := make([]string, 0, len(redundantIndexs))
122+
for key, idx := range existsIndexs {
123+
idxNames = append(idxNames, key)
124+
idxColNames = append(idxColNames, idx)
125+
}
126+
for key := range redundantIndexs {
127+
redundantIdxNames = append(redundantIdxNames, key)
128+
}
129+
if len(idxNames) > 0 && len(idxColNames) > 0 && len(redundantIdxNames) > 0 {
130+
rulepkg.AddResult(input.Res, input.Rule, SQLE00055, idxColNames, strings.Join(idxNames, "、"), redundantIdxNames)
131+
}
132+
}
133+
134+
func extractIndexesFromConstraints(constraints []*ast.Constraint) map[string][]string {
135+
indexes := make(map[string][]string)
118136

119137
// Iterate over all constraints to extract index columns.
120138
for _, constraint := range constraints {
@@ -130,14 +148,30 @@ func extractIndexesFromConstraints(constraints []*ast.Constraint) [][]string {
130148

131149
// Only append to indexes if indexCols is not empty, avoiding adding empty index definitions.
132150
if len(indexCols) > 0 {
133-
indexes = append(indexes, indexCols)
151+
key := constraint.Name
152+
if key == "" && constraint.Tp == ast.ConstraintPrimaryKey {
153+
key = "Primary Key"
154+
}
155+
indexes[key] = indexCols
134156
}
135157
}
136158

137159
return indexes
138160
}
139161

140-
func extractIndexesFromIndexStmt(index []*ast.IndexPartSpecification) [][]string {
162+
func mergeIndexsMaps(m1, m2 map[string][]string,
163+
) map[string][]string {
164+
result := make(map[string][]string, len(m1)+len(m2))
165+
for k, v := range m1 {
166+
result[k] = v
167+
}
168+
for k, v := range m2 {
169+
result[k] = v
170+
}
171+
return result
172+
}
173+
174+
func extractIndexesFromIndexStmt(index []*ast.IndexPartSpecification) []string {
141175
// Initialize the slice that will hold the column names for the index.
142176
var indexCols []string
143177

@@ -150,73 +184,129 @@ func extractIndexesFromIndexStmt(index []*ast.IndexPartSpecification) [][]string
150184

151185
// Return the indexes as a slice of a single index if indexCols is not empty.
152186
if len(indexCols) > 0 {
153-
return [][]string{indexCols}
187+
return indexCols
154188
}
155-
return [][]string{} // Return an empty slice of indexes if no index columns were found.
189+
return []string{} // Return an empty slice of indexes if no index columns were found.
156190
}
157191

158-
// calculateIndexRedundant takes a slice of string slices representing indexes and their columns
159-
// and returns a map where the key is the redundant index and the value is the corresponding
160-
// original index that makes it redundant.
161-
func calculateIndexRedundant(indexes [][]string) map[int]int {
162-
redundantIndexes := make(map[int]int)
163-
164-
// Compare each index with every other index to check for redundancy.
165-
for i, indexColumns := range indexes {
166-
for j, otherIndexColumns := range indexes {
167-
// Skip comparing the same index.
168-
if i == j {
169-
continue
170-
}
192+
// GetIntersectionByValue 比较两个映射的值集合,返回两个结果映射:
193+
//
194+
// commonInLeft: 左映射中,其值集合(或其左前缀)出现在右映射中的条目
195+
// commonInRight: 右映射中,其值集合(或其左前缀)出现在左映射中的条目
196+
//
197+
// 参数:
198+
//
199+
// leftMap - 键到字符串切片的映射,作为左侧数据源
200+
// rightMap - 键到字符串切片的映射,作为右侧数据源
201+
//
202+
// 返回:
203+
//
204+
// commonInLeft - 所有 leftMap 中,其值(或左前缀)能在 rightMap 中找到的项
205+
// commonInRight - 所有 rightMap 中,其值(或左前缀)能在 leftMap 中找到的项
206+
func GetIntersectionByValue(leftMap, rightMap map[string][]string) (commonInLeft map[string][]string, commonInRight map[string][]string) {
207+
208+
// 初始化返回容器
209+
commonInLeft = make(map[string][]string)
210+
commonInRight = make(map[string][]string)
211+
212+
// --- 构建 rightFullIndex:右侧完整切片的快速查找索引 ---
213+
// 示例:rightMap 中有 entry ["a","b","c"],则索引中添加 "a,b,c"
214+
rightFullIndex := make(map[string]bool)
215+
for _, values := range rightMap {
216+
fullKey := strings.Join(values, ",")
217+
rightFullIndex[fullKey] = true
218+
}
171219

172-
// Check if indexColumns are redundant with respect to otherIndexColumns.
173-
if isRedundant(indexColumns, otherIndexColumns) {
174-
// If index i is redundant and either it's not in the map yet, or it's in the map
175-
// but the current source index j has fewer columns (and thus is a 'stronger' source of redundancy),
176-
// then add/update the map with the index pair (i, j).
177-
if _, exists := redundantIndexes[i]; !exists || len(indexes[redundantIndexes[i]]) > len(otherIndexColumns) {
178-
redundantIndexes[i] = j
179-
}
220+
// --- 扫描 leftMap,检测左侧切片或其左前缀是否命中 rightFullIndex ---
221+
for key, values := range leftMap {
222+
// 从最长前缀到最短前缀依次尝试
223+
for length := len(values); length > 0; length-- {
224+
prefixKey := strings.Join(values[:length], ",")
225+
if rightFullIndex[prefixKey] {
226+
// 一旦命中,记录该 leftMap 条目为 commonInLeft,跳出前缀循环
227+
commonInLeft[key] = copySlice(values)
228+
break
180229
}
181230
}
182231
}
183232

184-
return redundantIndexes
233+
// --- 构造 leftPrefixIndex:左侧所有可能前缀的索引 ---
234+
// 为了对称地支持右侧完整切片的匹配,需要将左侧每个 values 的所有左前缀都注册
235+
// 例如 values=["a","b","c"],则注册 "a","a,b","a,b,c"
236+
leftPrefixIndex := make(map[string]bool)
237+
for _, values := range leftMap {
238+
for i := 1; i <= len(values); i++ {
239+
sig := strings.Join(values[:i], ",")
240+
leftPrefixIndex[sig] = true
241+
}
242+
}
243+
244+
// --- 扫描 rightMap,检测右侧完整切片是否命中 leftPrefixIndex ---
245+
for key, values := range rightMap {
246+
fullKey := strings.Join(values, ",")
247+
if leftPrefixIndex[fullKey] {
248+
// 如果右侧完整切片恰好是左侧某个切片的左前缀,则记录为 commonInRight
249+
commonInRight[key] = copySlice(values)
250+
}
251+
}
252+
253+
return
185254
}
186255

187-
// calculateNewIndexRedundant takes a slice of string slices representing existing indexes and their columns
188-
// and a slice of string slices representing new indexes' columns. It returns a map where the key is the index
189-
// of the new index in `newIndexes` and the value is the index of the existing index in `indexes` that makes it redundant.
190-
func calculateNewIndexRedundant(indexes [][]string, newIndexes [][]string) map[int]int {
191-
redundantIndexes := make(map[int]int)
192-
193-
// Iterate over each new index
194-
for newIndexI, newIndexCols := range newIndexes {
195-
// Compare against each existing index
196-
for existingIndexI, existingIndexCols := range indexes {
197-
if isRedundant(newIndexCols, existingIndexCols) {
198-
// If the new index is redundant with respect to an existing index, add to the map.
199-
redundantIndexes[newIndexI] = existingIndexI
200-
// Since we only care about the first occurrence of redundancy, we can break here.
256+
// GroupDuplicatesByValue 检测值集合重复的键,返回分组结果:
257+
// - originals: 每个唯一值集合(或其最长左前缀)首次出现的键及其值
258+
// - duplicates: 后续出现且与某已登记左前缀匹配的键及其值
259+
func GroupDuplicatesByValue(inputMap map[string][]string) (originals map[string][]string, duplicates map[string][]string) {
260+
originals = make(map[string][]string)
261+
duplicates = make(map[string][]string)
262+
263+
// prefixIndex:将“值切片左前缀签名”映射到首次出现该前缀的 key
264+
prefixIndex := make(map[string]string)
265+
266+
// 为保证稳定性,先对所有 key 排序
267+
keys := make([]string, 0, len(inputMap))
268+
for k := range inputMap {
269+
keys = append(keys, k)
270+
}
271+
sort.Strings(keys)
272+
273+
for _, key := range keys {
274+
values := inputMap[key]
275+
matched := false
276+
277+
// 从最长前缀到最短前缀尝试匹配
278+
for length := len(values); length > 0; length-- {
279+
sig := strings.Join(values[:length], ",")
280+
if origKey, exists := prefixIndex[sig]; exists {
281+
// 找到已有前缀,归为 duplicates
282+
if !matched {
283+
// 确保 originals 中保留最早出现的完整值
284+
if _, ok := originals[origKey]; !ok {
285+
originals[origKey] = copySlice(inputMap[origKey])
286+
}
287+
matched = true
288+
}
289+
duplicates[key] = copySlice(values)
201290
break
202291
}
203292
}
293+
if !matched {
294+
// 全新起点,将自身所有左前缀注册
295+
for length := len(values); length > 0; length-- {
296+
sig := strings.Join(values[:length], ",")
297+
prefixIndex[sig] = key
298+
}
299+
}
204300
}
205301

206-
return redundantIndexes
302+
return
207303
}
208304

209-
// isRedundant checks if the first index is redundant with respect to the second index,
210-
// meaning all columns in the first index are a leftmost prefix of the second.
211-
func isRedundant(index1, index2 []string) bool {
212-
for i, col := range index1 {
213-
// If we reach the end of index2 or the columns differ,
214-
// then index1 cannot be a leftmost prefix of index2.
215-
if i >= len(index2) || col != index2[i] {
216-
return false
217-
}
218-
}
219-
return true
305+
// copySlice 深拷贝字符串slice
306+
func copySlice(src []string) []string {
307+
dst := make([]string, len(src))
308+
copy(dst, src)
309+
return dst
220310
}
221311

222312
// ==== Rule code end ====

0 commit comments

Comments
 (0)