diff --git a/statistics/index.go b/statistics/index.go index a8c39da22b637..257e6ff488963 100644 --- a/statistics/index.go +++ b/statistics/index.go @@ -240,8 +240,10 @@ func (idx *Index) GetRowCount(sctx sessionctx.Context, coll *HistColl, indexRang if fullLen { // At most 1 in this case. if idx.Info.Unique { - totalCount++ - continue + if !indexRange.IsOnlyNull() { + totalCount++ + continue + } } count = idx.equalRowCount(lb, realtimeRowCount) // If the current table row count has changed, we should scale the row count accordingly. diff --git a/statistics/integration_test.go b/statistics/integration_test.go index 0493d870aec89..6adb1008b490d 100644 --- a/statistics/integration_test.go +++ b/statistics/integration_test.go @@ -808,3 +808,18 @@ func TestIssue49986(t *testing.T) { " └─Selection 10.00 cop[tikv] eq(\"astp2019121731703151\", test.acc.m)", " └─TableFullScan 10000.00 cop[tikv] table:b keep order:false, stats:pseudo")) } + +func TestIssue56116(t *testing.T) { + store := testkit.CreateMockStore(t) + tk := testkit.NewTestKit(t, store) + tk.MustExec("use test") + + tk.MustExec("create table t2(id bigint(20) DEFAULT NULL, UNIQUE KEY index_on_id (id))") + tk.MustExec("insert into t2 values (), (), ()") + tk.MustExec("analyze table t2") + tk.MustQuery("explain select count(*) from t2 where id is null;").Check(testkit.Rows( + "StreamAgg_17 1.00 root funcs:count(Column#5)->Column#3", + "└─IndexReader_18 1.00 root index:StreamAgg_9", + " └─StreamAgg_9 1.00 cop[tikv] funcs:count(1)->Column#5", + " └─IndexRangeScan_16 3.00 cop[tikv] table:t2, index:index_on_id(id) range:[NULL,NULL], keep order:false")) +} diff --git a/util/ranger/types.go b/util/ranger/types.go index 6c6ab98c52157..6589d42e7e884 100644 --- a/util/ranger/types.go +++ b/util/ranger/types.go @@ -126,6 +126,18 @@ func (ran *Range) isPoint(stmtCtx *stmtctx.StatementContext, regardNullAsPoint b return !ran.LowExclude && !ran.HighExclude } +// IsOnlyNull checks if the range has [NULL, NULL] or [NULL NULL, NULL NULL] range. +func (ran *Range) IsOnlyNull() bool { + for i := range ran.LowVal { + a := ran.LowVal[i] + b := ran.HighVal[i] + if !(a.IsNull() && b.IsNull()) { + return false + } + } + return true +} + // IsPointNonNullable returns if the range is a point without NULL. func (ran *Range) IsPointNonNullable(sctx sessionctx.Context) bool { return ran.isPoint(sctx.GetSessionVars().StmtCtx, false)