From 2abcda068414b717293d5b7d477a32dd959e77c9 Mon Sep 17 00:00:00 2001 From: Haibin Xie Date: Wed, 27 Mar 2019 18:52:35 +0800 Subject: [PATCH 1/3] planner/core: raise warning for unmatched join hint --- planner/core/exhaust_physical_plans.go | 2 +- planner/core/logical_plan_builder.go | 18 +++++++-- planner/core/physical_plan_test.go | 55 ++++++++++++++++++++++++++ planner/core/planbuilder.go | 33 ++++++++++------ 4 files changed, 92 insertions(+), 16 deletions(-) diff --git a/planner/core/exhaust_physical_plans.go b/planner/core/exhaust_physical_plans.go index cc796c16e8b89..e33d775d8dc5a 100644 --- a/planner/core/exhaust_physical_plans.go +++ b/planner/core/exhaust_physical_plans.go @@ -637,7 +637,7 @@ func (p *LogicalJoin) tryToGetIndexJoin(prop *property.PhysicalProperty) (indexJ // Construct warning message prefix. errMsg := "Optimizer Hint TIDB_INLJ is inapplicable" if p.hintInfo != nil { - errMsg = fmt.Sprintf("Optimizer Hint %s is inapplicable", p.hintInfo.restore2IndexJoinHint()) + errMsg = fmt.Sprintf("Optimizer Hint %s is inapplicable", restore2JoinHint(TiDBIndexNestedLoopJoin, p.hintInfo.indexNestedLoopJoinHint)) } // Append inapplicable reason. diff --git a/planner/core/logical_plan_builder.go b/planner/core/logical_plan_builder.go index 8bcea079c74aa..c6742b48ca79a 100644 --- a/planner/core/logical_plan_builder.go +++ b/planner/core/logical_plan_builder.go @@ -1853,9 +1853,9 @@ func (b *PlanBuilder) pushTableHints(hints []*ast.TableOptimizerHint) bool { } if len(sortMergeTables)+len(INLJTables)+len(hashJoinTables) > 0 { b.tableHintInfo = append(b.tableHintInfo, tableHintInfo{ - sortMergeJoinTables: sortMergeTables, - indexNestedLoopJoinTables: INLJTables, - hashJoinTables: hashJoinTables, + sortMergeJoinHint: joinHintInfo{tables: sortMergeTables}, + indexNestedLoopJoinHint: joinHintInfo{tables: INLJTables}, + hashJoinHint: joinHintInfo{tables: hashJoinTables}, }) return true } @@ -1863,9 +1863,21 @@ func (b *PlanBuilder) pushTableHints(hints []*ast.TableOptimizerHint) bool { } func (b *PlanBuilder) popTableHints() { + hintInfo := b.tableHintInfo[len(b.tableHintInfo)-1] + b.appendUnmatchedJoinHintWarning(TiDBIndexNestedLoopJoin, hintInfo.indexNestedLoopJoinHint) + b.appendUnmatchedJoinHintWarning(TiDBMergeJoin, hintInfo.sortMergeJoinHint) + b.appendUnmatchedJoinHintWarning(TiDBHashJoin, hintInfo.hashJoinHint) b.tableHintInfo = b.tableHintInfo[:len(b.tableHintInfo)-1] } +func (b *PlanBuilder) appendUnmatchedJoinHintWarning(joinType string, hintInfo joinHintInfo) { + if len(hintInfo.tables) == 0 || hintInfo.matched { + return + } + errMsg := fmt.Sprintf("Optimizer Hint %s is inapplicable because there are no matching table names. Maybe you can use the table alias name", restore2JoinHint(joinType, hintInfo)) + b.ctx.GetSessionVars().StmtCtx.AppendWarning(ErrInternal.GenWithStack(errMsg)) +} + // TableHints returns the *tableHintInfo of PlanBuilder. func (b *PlanBuilder) TableHints() *tableHintInfo { if len(b.tableHintInfo) == 0 { diff --git a/planner/core/physical_plan_test.go b/planner/core/physical_plan_test.go index 2b8fc1230f63a..121a2df2b23bc 100644 --- a/planner/core/physical_plan_test.go +++ b/planner/core/physical_plan_test.go @@ -26,6 +26,7 @@ import ( "github.com/pingcap/tidb/planner/core" "github.com/pingcap/tidb/session" "github.com/pingcap/tidb/sessionctx" + "github.com/pingcap/tidb/sessionctx/stmtctx" "github.com/pingcap/tidb/util/testleak" ) @@ -1434,3 +1435,57 @@ func (s *testPlanSuite) TestSemiJoinToInner(c *C) { c.Assert(err, IsNil) c.Assert(core.ToString(p), Equals, "Apply{TableReader(Table(t))->IndexJoin{IndexReader(Index(t.c_d_e)[[NULL,+inf]]->HashAgg)->HashAgg->IndexReader(Index(t.g)[[NULL,+inf]])}(t3.d,t2.g)}->StreamAgg") } + +func (s *testPlanSuite) TestUnmatchedJoinHint(c *C) { + defer testleak.AfterTest(c)() + store, dom, err := newStoreWithBootstrap() + c.Assert(err, IsNil) + defer func() { + dom.Close() + store.Close() + }() + se, err := session.CreateSession4Test(store) + c.Assert(err, IsNil) + _, err = se.Execute(context.Background(), "use test") + c.Assert(err, IsNil) + tests := []struct { + sql string + warning string + }{ + { + sql: "SELECT /*+ TIDB_SMJ(t3, t4) */ * from t t1, t t2 where t1.a = t2.a", + warning: "[planner:1815]Optimizer Hint /*+ TIDB_SMJ(t3, t4) */ is inapplicable because there are no matching table names. Maybe you can use the table alias name", + }, + { + sql: "SELECT /*+ TIDB_HJ(t3, t4) */ * from t t1, t t2 where t1.a = t2.a", + warning: "[planner:1815]Optimizer Hint /*+ TIDB_HJ(t3, t4) */ is inapplicable because there are no matching table names. Maybe you can use the table alias name", + }, + { + sql: "SELECT /*+ TIDB_INLJ(t3, t4) */ * from t t1, t t2 where t1.a = t2.a", + warning: "[planner:1815]Optimizer Hint /*+ TIDB_INLJ(t3, t4) */ is inapplicable because there are no matching table names. Maybe you can use the table alias name", + }, + { + sql: "SELECT /*+ TIDB_SMJ(t1, t2) */ * from t t1, t t2 where t1.a = t2.a", + warning: "", + }, + { + sql: "SELECT /*+ TIDB_SMJ(t3, t4) */ * from t t1, t t2, t t3 where t1.a = t2.a and t2.a = t3.a", + warning: "", + }, + } + for _, test := range tests { + se.GetSessionVars().StmtCtx.SetWarnings(nil) + stmt, err := s.ParseOneStmt(test.sql, "", "") + c.Assert(err, IsNil) + _, err = planner.Optimize(se, stmt, s.is) + c.Assert(err, IsNil) + warnings := se.GetSessionVars().StmtCtx.GetWarnings() + if test.warning == "" { + c.Assert(len(warnings), Equals, 0) + } else { + c.Assert(len(warnings), Equals, 1) + c.Assert(warnings[0].Level, Equals, stmtctx.WarnLevelWarning) + c.Assert(warnings[0].Err.Error(), Equals, test.warning) + } + } +} diff --git a/planner/core/planbuilder.go b/planner/core/planbuilder.go index 1a327bd3bd880..ab1969874bbe3 100644 --- a/planner/core/planbuilder.go +++ b/planner/core/planbuilder.go @@ -47,21 +47,26 @@ type visitInfo struct { } type tableHintInfo struct { - indexNestedLoopJoinTables []model.CIStr - sortMergeJoinTables []model.CIStr - hashJoinTables []model.CIStr + indexNestedLoopJoinHint joinHintInfo + sortMergeJoinHint joinHintInfo + hashJoinHint joinHintInfo +} + +type joinHintInfo struct { + tables []model.CIStr + matched bool } func (info *tableHintInfo) ifPreferMergeJoin(tableNames ...*model.CIStr) bool { - return info.matchTableName(tableNames, info.sortMergeJoinTables) + return info.matchTableName(tableNames, &info.sortMergeJoinHint) } func (info *tableHintInfo) ifPreferHashJoin(tableNames ...*model.CIStr) bool { - return info.matchTableName(tableNames, info.hashJoinTables) + return info.matchTableName(tableNames, &info.hashJoinHint) } func (info *tableHintInfo) ifPreferINLJ(tableNames ...*model.CIStr) bool { - return info.matchTableName(tableNames, info.indexNestedLoopJoinTables) + return info.matchTableName(tableNames, &info.indexNestedLoopJoinHint) } // matchTableName checks whether the hint hit the need. @@ -72,13 +77,14 @@ func (info *tableHintInfo) ifPreferINLJ(tableNames ...*model.CIStr) bool { // Which it joins on with depend on sequence of traverse // and without reorder, user might adjust themselves. // This is similar to MySQL hints. -func (info *tableHintInfo) matchTableName(tables []*model.CIStr, tablesInHints []model.CIStr) bool { +func (info *tableHintInfo) matchTableName(tables []*model.CIStr, joinHint *joinHintInfo) bool { for _, tableName := range tables { if tableName == nil { continue } - for _, curEntry := range tablesInHints { + for _, curEntry := range joinHint.tables { if curEntry.L == tableName.L { + joinHint.matched = true return true } } @@ -86,11 +92,14 @@ func (info *tableHintInfo) matchTableName(tables []*model.CIStr, tablesInHints [ return false } -func (info *tableHintInfo) restore2IndexJoinHint() string { - buffer := bytes.NewBufferString("/*+ TIDB_INLJ(") - for i, tableName := range info.indexNestedLoopJoinTables { +func restore2JoinHint(joinType string, joinHint joinHintInfo) string { + buffer := bytes.NewBufferString("/*+ ") + buffer.WriteString(strings.ToUpper(joinType)) + buffer.WriteString("(") + tables := joinHint.tables + for i, tableName := range tables { buffer.WriteString(tableName.O) - if i < len(info.indexNestedLoopJoinTables)-1 { + if i < len(tables)-1 { buffer.WriteString(", ") } } From 9b8c4d9840653687df58d6290a02050195d48d7b Mon Sep 17 00:00:00 2001 From: Haibin Xie Date: Thu, 28 Mar 2019 13:02:36 +0800 Subject: [PATCH 2/3] address comments --- planner/core/exhaust_physical_plans.go | 2 +- planner/core/logical_plan_builder.go | 2 +- planner/core/planbuilder.go | 9 ++++----- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/planner/core/exhaust_physical_plans.go b/planner/core/exhaust_physical_plans.go index e33d775d8dc5a..fe4d133730bdd 100644 --- a/planner/core/exhaust_physical_plans.go +++ b/planner/core/exhaust_physical_plans.go @@ -637,7 +637,7 @@ func (p *LogicalJoin) tryToGetIndexJoin(prop *property.PhysicalProperty) (indexJ // Construct warning message prefix. errMsg := "Optimizer Hint TIDB_INLJ is inapplicable" if p.hintInfo != nil { - errMsg = fmt.Sprintf("Optimizer Hint %s is inapplicable", restore2JoinHint(TiDBIndexNestedLoopJoin, p.hintInfo.indexNestedLoopJoinHint)) + errMsg = fmt.Sprintf("Optimizer Hint %s is inapplicable", p.hintInfo.indexNestedLoopJoinHint.restore2JoinHint(TiDBIndexNestedLoopJoin)) } // Append inapplicable reason. diff --git a/planner/core/logical_plan_builder.go b/planner/core/logical_plan_builder.go index c6742b48ca79a..f8ac4b91cfc06 100644 --- a/planner/core/logical_plan_builder.go +++ b/planner/core/logical_plan_builder.go @@ -1874,7 +1874,7 @@ func (b *PlanBuilder) appendUnmatchedJoinHintWarning(joinType string, hintInfo j if len(hintInfo.tables) == 0 || hintInfo.matched { return } - errMsg := fmt.Sprintf("Optimizer Hint %s is inapplicable because there are no matching table names. Maybe you can use the table alias name", restore2JoinHint(joinType, hintInfo)) + errMsg := fmt.Sprintf("Optimizer Hint %s is inapplicable because there are no matching table names. Maybe you can use the table alias name", hintInfo.restore2JoinHint(joinType)) b.ctx.GetSessionVars().StmtCtx.AppendWarning(ErrInternal.GenWithStack(errMsg)) } diff --git a/planner/core/planbuilder.go b/planner/core/planbuilder.go index ab1969874bbe3..cbf93db2ddcf2 100644 --- a/planner/core/planbuilder.go +++ b/planner/core/planbuilder.go @@ -92,14 +92,13 @@ func (info *tableHintInfo) matchTableName(tables []*model.CIStr, joinHint *joinH return false } -func restore2JoinHint(joinType string, joinHint joinHintInfo) string { +func (info *joinHintInfo) restore2JoinHint(hintType string) string { buffer := bytes.NewBufferString("/*+ ") - buffer.WriteString(strings.ToUpper(joinType)) + buffer.WriteString(strings.ToUpper(hintType)) buffer.WriteString("(") - tables := joinHint.tables - for i, tableName := range tables { + for i, tableName := range info.tables { buffer.WriteString(tableName.O) - if i < len(tables)-1 { + if i < len(info.tables)-1 { buffer.WriteString(", ") } } From 35c72a92504ef35e007f38db5feb1c6f9f75021b Mon Sep 17 00:00:00 2001 From: Haibin Xie Date: Fri, 29 Mar 2019 15:21:01 +0800 Subject: [PATCH 3/3] address comments --- planner/core/exhaust_physical_plans.go | 2 +- planner/core/logical_plan_builder.go | 28 ++++++------ planner/core/physical_plan_test.go | 10 ++--- planner/core/planbuilder.go | 59 ++++++++++++++++++-------- 4 files changed, 62 insertions(+), 37 deletions(-) diff --git a/planner/core/exhaust_physical_plans.go b/planner/core/exhaust_physical_plans.go index fe4d133730bdd..6ab2ba83a48a3 100644 --- a/planner/core/exhaust_physical_plans.go +++ b/planner/core/exhaust_physical_plans.go @@ -637,7 +637,7 @@ func (p *LogicalJoin) tryToGetIndexJoin(prop *property.PhysicalProperty) (indexJ // Construct warning message prefix. errMsg := "Optimizer Hint TIDB_INLJ is inapplicable" if p.hintInfo != nil { - errMsg = fmt.Sprintf("Optimizer Hint %s is inapplicable", p.hintInfo.indexNestedLoopJoinHint.restore2JoinHint(TiDBIndexNestedLoopJoin)) + errMsg = fmt.Sprintf("Optimizer Hint %s is inapplicable", restore2JoinHint(TiDBIndexNestedLoopJoin, p.hintInfo.indexNestedLoopJoinTables)) } // Append inapplicable reason. diff --git a/planner/core/logical_plan_builder.go b/planner/core/logical_plan_builder.go index f8ac4b91cfc06..693eee59c4cb2 100644 --- a/planner/core/logical_plan_builder.go +++ b/planner/core/logical_plan_builder.go @@ -1838,24 +1838,24 @@ func (b *PlanBuilder) unfoldWildStar(p LogicalPlan, selectFields []*ast.SelectFi } func (b *PlanBuilder) pushTableHints(hints []*ast.TableOptimizerHint) bool { - var sortMergeTables, INLJTables, hashJoinTables []model.CIStr + var sortMergeTables, INLJTables, hashJoinTables []hintTableInfo for _, hint := range hints { switch hint.HintName.L { case TiDBMergeJoin: - sortMergeTables = append(sortMergeTables, hint.Tables...) + sortMergeTables = tableNames2HintTableInfo(hint.Tables) case TiDBIndexNestedLoopJoin: - INLJTables = append(INLJTables, hint.Tables...) + INLJTables = tableNames2HintTableInfo(hint.Tables) case TiDBHashJoin: - hashJoinTables = append(hashJoinTables, hint.Tables...) + hashJoinTables = tableNames2HintTableInfo(hint.Tables) default: // ignore hints that not implemented } } if len(sortMergeTables)+len(INLJTables)+len(hashJoinTables) > 0 { b.tableHintInfo = append(b.tableHintInfo, tableHintInfo{ - sortMergeJoinHint: joinHintInfo{tables: sortMergeTables}, - indexNestedLoopJoinHint: joinHintInfo{tables: INLJTables}, - hashJoinHint: joinHintInfo{tables: hashJoinTables}, + sortMergeJoinTables: sortMergeTables, + indexNestedLoopJoinTables: INLJTables, + hashJoinTables: hashJoinTables, }) return true } @@ -1864,17 +1864,19 @@ func (b *PlanBuilder) pushTableHints(hints []*ast.TableOptimizerHint) bool { func (b *PlanBuilder) popTableHints() { hintInfo := b.tableHintInfo[len(b.tableHintInfo)-1] - b.appendUnmatchedJoinHintWarning(TiDBIndexNestedLoopJoin, hintInfo.indexNestedLoopJoinHint) - b.appendUnmatchedJoinHintWarning(TiDBMergeJoin, hintInfo.sortMergeJoinHint) - b.appendUnmatchedJoinHintWarning(TiDBHashJoin, hintInfo.hashJoinHint) + b.appendUnmatchedJoinHintWarning(TiDBIndexNestedLoopJoin, hintInfo.indexNestedLoopJoinTables) + b.appendUnmatchedJoinHintWarning(TiDBMergeJoin, hintInfo.sortMergeJoinTables) + b.appendUnmatchedJoinHintWarning(TiDBHashJoin, hintInfo.hashJoinTables) b.tableHintInfo = b.tableHintInfo[:len(b.tableHintInfo)-1] } -func (b *PlanBuilder) appendUnmatchedJoinHintWarning(joinType string, hintInfo joinHintInfo) { - if len(hintInfo.tables) == 0 || hintInfo.matched { +func (b *PlanBuilder) appendUnmatchedJoinHintWarning(joinType string, hintTables []hintTableInfo) { + unMatchedTables := extractUnmatchedTables(hintTables) + if len(unMatchedTables) == 0 { return } - errMsg := fmt.Sprintf("Optimizer Hint %s is inapplicable because there are no matching table names. Maybe you can use the table alias name", hintInfo.restore2JoinHint(joinType)) + errMsg := fmt.Sprintf("There are no matching table names for (%s) in optimizer hint %s. Maybe you can use the table alias name", + strings.Join(unMatchedTables, ", "), restore2JoinHint(joinType, hintTables)) b.ctx.GetSessionVars().StmtCtx.AppendWarning(ErrInternal.GenWithStack(errMsg)) } diff --git a/planner/core/physical_plan_test.go b/planner/core/physical_plan_test.go index 121a2df2b23bc..8a5b34fd20882 100644 --- a/planner/core/physical_plan_test.go +++ b/planner/core/physical_plan_test.go @@ -1436,7 +1436,7 @@ func (s *testPlanSuite) TestSemiJoinToInner(c *C) { c.Assert(core.ToString(p), Equals, "Apply{TableReader(Table(t))->IndexJoin{IndexReader(Index(t.c_d_e)[[NULL,+inf]]->HashAgg)->HashAgg->IndexReader(Index(t.g)[[NULL,+inf]])}(t3.d,t2.g)}->StreamAgg") } -func (s *testPlanSuite) TestUnmatchedJoinHint(c *C) { +func (s *testPlanSuite) TestUnmatchedTableInHint(c *C) { defer testleak.AfterTest(c)() store, dom, err := newStoreWithBootstrap() c.Assert(err, IsNil) @@ -1454,15 +1454,15 @@ func (s *testPlanSuite) TestUnmatchedJoinHint(c *C) { }{ { sql: "SELECT /*+ TIDB_SMJ(t3, t4) */ * from t t1, t t2 where t1.a = t2.a", - warning: "[planner:1815]Optimizer Hint /*+ TIDB_SMJ(t3, t4) */ is inapplicable because there are no matching table names. Maybe you can use the table alias name", + warning: "[planner:1815]There are no matching table names for (t3, t4) in optimizer hint /*+ TIDB_SMJ(t3, t4) */. Maybe you can use the table alias name", }, { sql: "SELECT /*+ TIDB_HJ(t3, t4) */ * from t t1, t t2 where t1.a = t2.a", - warning: "[planner:1815]Optimizer Hint /*+ TIDB_HJ(t3, t4) */ is inapplicable because there are no matching table names. Maybe you can use the table alias name", + warning: "[planner:1815]There are no matching table names for (t3, t4) in optimizer hint /*+ TIDB_HJ(t3, t4) */. Maybe you can use the table alias name", }, { sql: "SELECT /*+ TIDB_INLJ(t3, t4) */ * from t t1, t t2 where t1.a = t2.a", - warning: "[planner:1815]Optimizer Hint /*+ TIDB_INLJ(t3, t4) */ is inapplicable because there are no matching table names. Maybe you can use the table alias name", + warning: "[planner:1815]There are no matching table names for (t3, t4) in optimizer hint /*+ TIDB_INLJ(t3, t4) */. Maybe you can use the table alias name", }, { sql: "SELECT /*+ TIDB_SMJ(t1, t2) */ * from t t1, t t2 where t1.a = t2.a", @@ -1470,7 +1470,7 @@ func (s *testPlanSuite) TestUnmatchedJoinHint(c *C) { }, { sql: "SELECT /*+ TIDB_SMJ(t3, t4) */ * from t t1, t t2, t t3 where t1.a = t2.a and t2.a = t3.a", - warning: "", + warning: "[planner:1815]There are no matching table names for (t4) in optimizer hint /*+ TIDB_SMJ(t3, t4) */. Maybe you can use the table alias name", }, } for _, test := range tests { diff --git a/planner/core/planbuilder.go b/planner/core/planbuilder.go index cbf93db2ddcf2..ff87a206faf9a 100644 --- a/planner/core/planbuilder.go +++ b/planner/core/planbuilder.go @@ -47,26 +47,37 @@ type visitInfo struct { } type tableHintInfo struct { - indexNestedLoopJoinHint joinHintInfo - sortMergeJoinHint joinHintInfo - hashJoinHint joinHintInfo + indexNestedLoopJoinTables []hintTableInfo + sortMergeJoinTables []hintTableInfo + hashJoinTables []hintTableInfo } -type joinHintInfo struct { - tables []model.CIStr +type hintTableInfo struct { + name model.CIStr matched bool } +func tableNames2HintTableInfo(tableNames []model.CIStr) []hintTableInfo { + if len(tableNames) == 0 { + return nil + } + hintTables := make([]hintTableInfo, 0, len(tableNames)) + for _, tableName := range tableNames { + hintTables = append(hintTables, hintTableInfo{name: tableName}) + } + return hintTables +} + func (info *tableHintInfo) ifPreferMergeJoin(tableNames ...*model.CIStr) bool { - return info.matchTableName(tableNames, &info.sortMergeJoinHint) + return info.matchTableName(tableNames, info.sortMergeJoinTables) } func (info *tableHintInfo) ifPreferHashJoin(tableNames ...*model.CIStr) bool { - return info.matchTableName(tableNames, &info.hashJoinHint) + return info.matchTableName(tableNames, info.hashJoinTables) } func (info *tableHintInfo) ifPreferINLJ(tableNames ...*model.CIStr) bool { - return info.matchTableName(tableNames, &info.indexNestedLoopJoinHint) + return info.matchTableName(tableNames, info.indexNestedLoopJoinTables) } // matchTableName checks whether the hint hit the need. @@ -77,28 +88,30 @@ func (info *tableHintInfo) ifPreferINLJ(tableNames ...*model.CIStr) bool { // Which it joins on with depend on sequence of traverse // and without reorder, user might adjust themselves. // This is similar to MySQL hints. -func (info *tableHintInfo) matchTableName(tables []*model.CIStr, joinHint *joinHintInfo) bool { +func (info *tableHintInfo) matchTableName(tables []*model.CIStr, hintTables []hintTableInfo) bool { + hintMatched := false for _, tableName := range tables { if tableName == nil { continue } - for _, curEntry := range joinHint.tables { - if curEntry.L == tableName.L { - joinHint.matched = true - return true + for i, curEntry := range hintTables { + if curEntry.name.L == tableName.L { + hintTables[i].matched = true + hintMatched = true + break } } } - return false + return hintMatched } -func (info *joinHintInfo) restore2JoinHint(hintType string) string { +func restore2JoinHint(hintType string, hintTables []hintTableInfo) string { buffer := bytes.NewBufferString("/*+ ") buffer.WriteString(strings.ToUpper(hintType)) buffer.WriteString("(") - for i, tableName := range info.tables { - buffer.WriteString(tableName.O) - if i < len(info.tables)-1 { + for i, table := range hintTables { + buffer.WriteString(table.name.L) + if i < len(hintTables)-1 { buffer.WriteString(", ") } } @@ -106,6 +119,16 @@ func (info *joinHintInfo) restore2JoinHint(hintType string) string { return buffer.String() } +func extractUnmatchedTables(hintTables []hintTableInfo) []string { + var tableNames []string + for _, table := range hintTables { + if !table.matched { + tableNames = append(tableNames, table.name.O) + } + } + return tableNames +} + // clauseCode indicates in which clause the column is currently. type clauseCode int