Skip to content

Commit 4721bc3

Browse files
planner: Choose index with statistics vs one without (#58593)
close #46375
1 parent e18ca24 commit 4721bc3

File tree

3 files changed

+49
-3
lines changed

3 files changed

+49
-3
lines changed

pkg/planner/cardinality/BUILD.bazel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ go_test(
5959
data = glob(["testdata/**"]),
6060
embed = [":cardinality"],
6161
flaky = True,
62-
shard_count = 29,
62+
shard_count = 30,
6363
deps = [
6464
"//pkg/config",
6565
"//pkg/domain",

pkg/planner/cardinality/selectivity_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,28 @@ func TestEstimationForUnknownValuesAfterModify(t *testing.T) {
341341
require.Truef(t, count > 20, "expected: between 20 to 40, got: %v", count)
342342
}
343343

344+
func TestNewIndexWithoutStats(t *testing.T) {
345+
store, _ := testkit.CreateMockStoreAndDomain(t)
346+
testKit := testkit.NewTestKit(t, store)
347+
testKit.MustExec("use test")
348+
testKit.MustExec("drop table if exists t")
349+
testKit.MustExec("create table t(a int, b int, c int, index idxa(a))")
350+
testKit.MustExec("set @@tidb_analyze_version=2")
351+
testKit.MustExec("set @@global.tidb_enable_auto_analyze='OFF'")
352+
testKit.MustExec("insert into t values (1, 1, 1)")
353+
testKit.MustExec("insert into t select mod(a,250), mod(a,10), mod(a,100) from (with recursive x as (select 1 as a union all select a + 1 AS a from x where a < 500) select a from x) as subquery")
354+
testKit.MustExec("analyze table t")
355+
testKit.MustExec("create index idxb on t(b)")
356+
// Create index after ANALYZE. SkyLine pruning should ensure that idxa is chosen because it has statistics
357+
testKit.MustQuery("explain format='brief' select * from t where a = 5 and b = 5").CheckContain("idxa(a)")
358+
testKit.MustExec("analyze table t")
359+
// idxa should still win after statistics
360+
testKit.MustQuery("explain format='brief' select * from t where a = 5 and b = 5").CheckContain("idxa(a)")
361+
testKit.MustExec("create index idxab on t(a, b)")
362+
// New index idxab should win due to having the most matching equal predicates - regardless of no statistics
363+
testKit.MustQuery("explain format='brief' select * from t where a = 5 and b = 5").CheckContain("idxab(a, b)")
364+
}
365+
344366
func TestEstimationUniqueKeyEqualConds(t *testing.T) {
345367
store, dom := testkit.CreateMockStoreAndDomain(t)
346368
testKit := testkit.NewTestKit(t, store)

pkg/planner/core/find_best_task.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -711,13 +711,37 @@ func compareGlobalIndex(lhs, rhs *candidatePath) int {
711711

712712
// compareCandidates is the core of skyline pruning, which is used to decide which candidate path is better.
713713
// The return value is 1 if lhs is better, -1 if rhs is better, 0 if they are equivalent or not comparable.
714-
func compareCandidates(sctx base.PlanContext, prop *property.PhysicalProperty, lhs, rhs *candidatePath) int {
714+
func compareCandidates(sctx base.PlanContext, statsTbl *statistics.Table, prop *property.PhysicalProperty, lhs, rhs *candidatePath) int {
715715
// Due to #50125, full scan on MVIndex has been disabled, so MVIndex path might lead to 'can't find a proper plan' error at the end.
716716
// Avoid MVIndex path to exclude all other paths and leading to 'can't find a proper plan' error, see #49438 for an example.
717717
if isMVIndexPath(lhs.path) || isMVIndexPath(rhs.path) {
718718
return 0
719719
}
720720

721+
// If one index has statistics and the other does not, choose the index with statistics if it
722+
// has the same or higher number of equal/IN predicates.
723+
lhsHasStatistics := statsTbl.Pseudo
724+
if statsTbl != nil && lhs.path.Index != nil {
725+
lhsHasStatistics = statsTbl.ColAndIdxExistenceMap.HasAnalyzed(lhs.path.Index.ID, true)
726+
}
727+
rhsHasStatistics := statsTbl.Pseudo
728+
if statsTbl != nil && rhs.path.Index != nil {
729+
rhsHasStatistics = statsTbl.ColAndIdxExistenceMap.HasAnalyzed(rhs.path.Index.ID, true)
730+
}
731+
if !lhs.path.IsTablePath() && !rhs.path.IsTablePath() && // Not a table scan
732+
(lhsHasStatistics || rhsHasStatistics) && // At least one index has statistics
733+
(!lhsHasStatistics || !rhsHasStatistics) && // At least one index doesn't have statistics
734+
len(lhs.path.PartialIndexPaths) == 0 && len(rhs.path.PartialIndexPaths) == 0 { // not IndexMerge due to unreliability
735+
lhsTotalEqual := lhs.path.EqCondCount + lhs.path.EqOrInCondCount
736+
rhsTotalEqual := rhs.path.EqCondCount + rhs.path.EqOrInCondCount
737+
if lhsHasStatistics && lhsTotalEqual > 0 && lhsTotalEqual >= rhsTotalEqual {
738+
return 1
739+
}
740+
if rhsHasStatistics && rhsTotalEqual > 0 && rhsTotalEqual >= lhsTotalEqual {
741+
return -1
742+
}
743+
}
744+
721745
// This rule is empirical but not always correct.
722746
// If x's range row count is significantly lower than y's, for example, 1000 times, we think x is better.
723747
if lhs.path.CountAfterAccess > 100 && rhs.path.CountAfterAccess > 100 && // to prevent some extreme cases, e.g. 0.01 : 10
@@ -1140,7 +1164,7 @@ func skylinePruning(ds *logicalop.DataSource, prop *property.PhysicalProperty) [
11401164
if candidates[i].path.StoreType == kv.TiFlash {
11411165
continue
11421166
}
1143-
result := compareCandidates(ds.SCtx(), prop, candidates[i], currentCandidate)
1167+
result := compareCandidates(ds.SCtx(), ds.StatisticTable, prop, candidates[i], currentCandidate)
11441168
if result == 1 {
11451169
pruned = true
11461170
// We can break here because the current candidate cannot prune others anymore.

0 commit comments

Comments
 (0)