From 25be698c5553c5db20d98dae3a75a1a87d3556fb Mon Sep 17 00:00:00 2001 From: 0xPoe Date: Mon, 23 Mar 2026 10:21:38 +0100 Subject: [PATCH 1/5] executor: add regression test for analyze status remaining time --- pkg/executor/show_stats_test.go | 37 +++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/pkg/executor/show_stats_test.go b/pkg/executor/show_stats_test.go index 23534b721fc98..860a5e4297a37 100644 --- a/pkg/executor/show_stats_test.go +++ b/pkg/executor/show_stats_test.go @@ -399,4 +399,41 @@ func TestShowAnalyzeStatus(t *testing.T) { "merge global stats for test.t2 columns", "analyze table all indexes, all columns with 256 buckets, 100 topn, 1 samplerate", }, jobInfos) + + tk.MustExec("delete from mysql.analyze_jobs") + tk.MustExec("drop table if exists t3") + tk.MustExec("create table t3 (a int, b int, primary key(a))") + tk.MustExec(`insert into t3 values (1, 1), (2, 2)`) + tk.MustExec("analyze table t3") + tk.MustExec("delete from mysql.analyze_jobs") + + originalTZ := tk.MustQuery("select @@time_zone").Rows()[0][0] + defer func() { + tk.MustExec("set @@time_zone = ?", originalTZ) + }() + tk.MustExec("set @@time_zone = '+08:00'") + tk.MustExec(`insert into mysql.analyze_jobs ( + table_schema, + table_name, + partition_name, + job_info, + processed_rows, + start_time, + state, + instance + ) values ( + 'test', + 't3', + '', + 'analyze table all indexes, all columns with 256 buckets, 100 topn, 1 samplerate', + 1, + CURRENT_TIMESTAMP - INTERVAL 1 MINUTE, + 'running', + '127.0.0.1:4000' + )`) + rows = tk.MustQuery("show analyze status where table_name = 't3' and state = 'running'").Rows() + require.Len(t, rows, 1) + remainingDuration, err := time.ParseDuration(rows[0][11].(string)) + require.NoError(t, err) + require.GreaterOrEqual(t, remainingDuration, time.Duration(0)) } From c35c7a2615e088f73b4d707a62355e3bd187678b Mon Sep 17 00:00:00 2001 From: 0xPoe Date: Mon, 23 Mar 2026 10:24:41 +0100 Subject: [PATCH 2/5] executor: keep analyze status remaining time in UTC --- pkg/executor/infoschema_reader.go | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/pkg/executor/infoschema_reader.go b/pkg/executor/infoschema_reader.go index 3c4b2f6d0a41d..15466425f8b22 100644 --- a/pkg/executor/infoschema_reader.go +++ b/pkg/executor/infoschema_reader.go @@ -2548,11 +2548,13 @@ func dataForAnalyzeStatusHelper(ctx context.Context, e *memtableRetriever, sctx jobInfo := chunkRow.GetString(3) processedRows := chunkRow.GetInt64(4) var startTime, endTime any + var startTimeUTC *time.Time if !chunkRow.IsNull(5) { t, err := chunkRow.GetTime(5).GoTime(time.UTC) if err != nil { return nil, err } + startTimeUTC = &t startTime = types.NewTime(types.FromGoTime(t.In(sctx.GetSessionVars().TimeZone)), mysql.TypeDatetime, 0) } if !chunkRow.IsNull(6) { @@ -2576,11 +2578,10 @@ func dataForAnalyzeStatusHelper(ctx context.Context, e *memtableRetriever, sctx var remainDurationStr, progressDouble, estimatedRowCntStr any if state == statistics.AnalyzeRunning && !strings.HasPrefix(jobInfo, "merge global stats") { - startTime, ok := startTime.(types.Time) - if !ok { + if startTimeUTC == nil { return nil, errors.New("invalid start time") } - remainingDuration, progress, estimatedRowCnt, remainDurationErr := getRemainDurationForAnalyzeStatusHelper(ctx, sctx, &startTime, + remainingDuration, progress, estimatedRowCnt, remainDurationErr := getRemainDurationForAnalyzeStatusHelper(ctx, sctx, startTimeUTC, dbName, tableName, partitionName, processedRows) if remainDurationErr != nil { logutil.BgLogger().Warn("get remaining duration failed", zap.Error(remainDurationErr)) @@ -2617,16 +2618,12 @@ func dataForAnalyzeStatusHelper(ctx context.Context, e *memtableRetriever, sctx func getRemainDurationForAnalyzeStatusHelper( ctx context.Context, - sctx sessionctx.Context, startTime *types.Time, + sctx sessionctx.Context, startTimeUTC *time.Time, dbName, tableName, partitionName string, processedRows int64, ) (_ *time.Duration, percentage, totalCnt float64, err error) { remainingDuration := time.Duration(0) - if startTime != nil { - start, err := startTime.GoTime(time.UTC) - if err != nil { - return nil, percentage, totalCnt, err - } - duration := time.Now().UTC().Sub(start) + if startTimeUTC != nil { + duration := time.Now().UTC().Sub(startTimeUTC.UTC()) if intest.InTest { if val := ctx.Value(AnalyzeProgressTest); val != nil { remainingDuration, percentage = calRemainInfoForAnalyzeStatus(ctx, int64(totalCnt), processedRows, duration) From cc47e0c9e734cd3277f93e5078a33dfa4f5190c3 Mon Sep 17 00:00:00 2001 From: 0xPoe Date: Wed, 1 Apr 2026 12:11:22 +0200 Subject: [PATCH 3/5] docs: add comments Signed-off-by: 0xPoe fix Signed-off-by: 0xPoe --- pkg/executor/infoschema_reader.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/executor/infoschema_reader.go b/pkg/executor/infoschema_reader.go index 15466425f8b22..728698a56021e 100644 --- a/pkg/executor/infoschema_reader.go +++ b/pkg/executor/infoschema_reader.go @@ -2548,6 +2548,8 @@ func dataForAnalyzeStatusHelper(ctx context.Context, e *memtableRetriever, sctx jobInfo := chunkRow.GetString(3) processedRows := chunkRow.GetInt64(4) var startTime, endTime any + // startTime and endTime use the local timezone for displaying. + // startTimeUTC is used to calculate the remaining duration of the job. var startTimeUTC *time.Time if !chunkRow.IsNull(5) { t, err := chunkRow.GetTime(5).GoTime(time.UTC) From 3239183a75e7aa8f4d4303a4ae5958d5292ed126 Mon Sep 17 00:00:00 2001 From: 0xPoe Date: Wed, 1 Apr 2026 12:46:23 +0200 Subject: [PATCH 4/5] fix: simplify code Signed-off-by: 0xPoe --- pkg/executor/infoschema_reader.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/executor/infoschema_reader.go b/pkg/executor/infoschema_reader.go index 728698a56021e..70629e96610db 100644 --- a/pkg/executor/infoschema_reader.go +++ b/pkg/executor/infoschema_reader.go @@ -2625,7 +2625,8 @@ func getRemainDurationForAnalyzeStatusHelper( ) (_ *time.Duration, percentage, totalCnt float64, err error) { remainingDuration := time.Duration(0) if startTimeUTC != nil { - duration := time.Now().UTC().Sub(startTimeUTC.UTC()) + // time.Time.Sub uses the actual instant. + duration := time.Now().Sub(*startTimeUTC) if intest.InTest { if val := ctx.Value(AnalyzeProgressTest); val != nil { remainingDuration, percentage = calRemainInfoForAnalyzeStatus(ctx, int64(totalCnt), processedRows, duration) From 6d1ca895639e001d49500be87de76876ea7c2be3 Mon Sep 17 00:00:00 2001 From: 0xPoe Date: Wed, 1 Apr 2026 13:01:27 +0200 Subject: [PATCH 5/5] fix: make lint happy Signed-off-by: 0xPoe --- pkg/executor/infoschema_reader.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/executor/infoschema_reader.go b/pkg/executor/infoschema_reader.go index 70629e96610db..fbc06b7feb16e 100644 --- a/pkg/executor/infoschema_reader.go +++ b/pkg/executor/infoschema_reader.go @@ -2626,7 +2626,7 @@ func getRemainDurationForAnalyzeStatusHelper( remainingDuration := time.Duration(0) if startTimeUTC != nil { // time.Time.Sub uses the actual instant. - duration := time.Now().Sub(*startTimeUTC) + duration := time.Since(*startTimeUTC) if intest.InTest { if val := ctx.Value(AnalyzeProgressTest); val != nil { remainingDuration, percentage = calRemainInfoForAnalyzeStatus(ctx, int64(totalCnt), processedRows, duration)