Repository: pingcap/tidb (Go)

Issue:
Add column `End_time` in `show analyze status` and add related infromation in logs.
## Feature Request

**Is your feature request related to a problem? Please describe:**
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->

I suggest a customer analyze a table, it cost a lot of time, but we all don't know when it is ended.

**Describe the feature you'd like:**
<!-- A clear and concise description of what you want to happen. -->

1. Add column `End_time` in `show analyze status`:
```sql
mysql> show analyze status;
+--------------+------------+----------------+-----------------+----------------+---------------------+----------+
| Table_schema | Table_name | Partition_name | Job_info        | Processed_rows | Start_time          | State    |
+--------------+------------+----------------+-----------------+----------------+---------------------+----------+
| test         | t          |                | analyze columns |              0 | 2021-03-26 08:27:27 | finished |
+--------------+------------+----------------+-----------------+----------------+---------------------+----------+
1 row in set (0.00 sec)
```

2. And when the analysis ended, print information to log.


**Describe alternatives you've considered:**
<!-- A clear and concise description of any alternative solutions or features you've considered. -->

Can't find an alternative.

**Teachability, Documentation, Adoption, Migration Strategy:**
<!-- If you can, explain some scenarios how users might use this, situations it would be helpful in. Any API designs, mockups, or diagrams are also helpful. -->



Discussion:
/pick-up
@LittleFall Hello! I want to pick up this issue.
/assign @hidehalo
> @LittleFall Hello! I want to pick up this issue.

welcome!



Patch:
diff --git a/executor/analyze.go b/executor/analyze.go
index 8169b1f2015aa..5b8c954d1ce5e 100644
--- a/executor/analyze.go
+++ b/executor/analyze.go
@@ -16,6 +16,7 @@ package executor
 import (
 	"bytes"
 	"context"
+	"fmt"
 	"math"
 	"math/rand"
 	"runtime"
@@ -118,7 +119,17 @@ func (e *AnalyzeExec) Next(ctx context.Context, req *chunk.Chunk) error {
 	// The meaning of key in map is the structure that used to store the tableID and indexID.
 	// The meaning of value in map is some additional information needed to build global-level stats.
 	globalStatsMap := make(map[globalStatsKey]globalStatsInfo)
-
+	finishJobWithLogFn := func(ctx context.Context, job *statistics.AnalyzeJob, meetError bool) {
+		job.Finish(meetError)
+		if job != nil {
+			logutil.Logger(ctx).Info(fmt.Sprintf("analyze table `%s`.`%s` has %s", job.DBName, job.TableName, job.State),
+				zap.String("partition", job.PartitionName),
+				zap.String("job info", job.JobInfo),
+				zap.Time("start time", job.StartTime),
+				zap.Time("end time", job.EndTime),
+				zap.String("cost", job.EndTime.Sub(job.StartTime).String()))
+		}
+	}
 	for panicCnt < concurrency {
 		result, ok := <-resultCh
 		if !ok {
@@ -131,7 +142,7 @@ func (e *AnalyzeExec) Next(ctx context.Context, req *chunk.Chunk) error {
 			} else {
 				logutil.Logger(ctx).Error("analyze failed", zap.Error(err))
 			}
-			result.job.Finish(true)
+			finishJobWithLogFn(ctx, result.job, true)
 			continue
 		}
 		statisticsID := result.TableID.GetStatisticsID()
@@ -151,16 +162,16 @@ func (e *AnalyzeExec) Next(ctx context.Context, req *chunk.Chunk) error {
 			if err1 != nil {
 				err = err1
 				logutil.Logger(ctx).Error("save stats to storage failed", zap.Error(err))
-				result.job.Finish(true)
+				finishJobWithLogFn(ctx, result.job, true)
 				continue
 			}
 		}
 		if err1 := statsHandle.SaveExtendedStatsToStorage(statisticsID, result.ExtStats, false); err1 != nil {
 			err = err1
 			logutil.Logger(ctx).Error("save extended stats to storage failed", zap.Error(err))
-			result.job.Finish(true)
+			finishJobWithLogFn(ctx, result.job, true)
 		} else {
-			result.job.Finish(false)
+			finishJobWithLogFn(ctx, result.job, false)
 		}
 	}
 	for _, task := range e.tasks {
diff --git a/executor/infoschema_reader.go b/executor/infoschema_reader.go
index 4d9a5b19ec1ad..c9f887d4f7d36 100644
--- a/executor/infoschema_reader.go
+++ b/executor/infoschema_reader.go
@@ -1669,12 +1669,17 @@ func dataForAnalyzeStatusHelper(sctx sessionctx.Context) (rows [][]types.Datum)
 	checker := privilege.GetPrivilegeManager(sctx)
 	for _, job := range statistics.GetAllAnalyzeJobs() {
 		job.Lock()
-		var startTime interface{}
+		var startTime, endTime interface{}
 		if job.StartTime.IsZero() {
 			startTime = nil
 		} else {
 			startTime = types.NewTime(types.FromGoTime(job.StartTime), mysql.TypeDatetime, 0)
 		}
+		if job.EndTime.IsZero() {
+			endTime = nil
+		} else {
+			endTime = types.NewTime(types.FromGoTime(job.EndTime), mysql.TypeDatetime, 0)
+		}
 		if checker == nil || checker.RequestVerification(sctx.GetSessionVars().ActiveRoles, job.DBName, job.TableName, "", mysql.AllPrivMask) {
 			rows = append(rows, types.MakeDatums(
 				job.DBName,        // TABLE_SCHEMA
@@ -1683,6 +1688,7 @@ func dataForAnalyzeStatusHelper(sctx sessionctx.Context) (rows [][]types.Datum)
 				job.JobInfo,       // JOB_INFO
 				job.RowCount,      // ROW_COUNT
 				startTime,         // START_TIME
+				endTime,           // END_TIME
 				job.State,         // STATE
 			))
 		}
diff --git a/infoschema/tables.go b/infoschema/tables.go
index 9d48b67cf0189..4021c2844ad9c 100644
--- a/infoschema/tables.go
+++ b/infoschema/tables.go
@@ -868,6 +868,7 @@ var tableAnalyzeStatusCols = []columnInfo{
 	{name: "JOB_INFO", tp: mysql.TypeVarchar, size: 64},
 	{name: "PROCESSED_ROWS", tp: mysql.TypeLonglong, size: 20, flag: mysql.UnsignedFlag},
 	{name: "START_TIME", tp: mysql.TypeDatetime},
+	{name: "END_TIME", tp: mysql.TypeDatetime},
 	{name: "STATE", tp: mysql.TypeVarchar, size: 64},
 }
 
diff --git a/planner/core/planbuilder.go b/planner/core/planbuilder.go
index d035835e86989..f4cfea53af924 100644
--- a/planner/core/planbuilder.go
+++ b/planner/core/planbuilder.go
@@ -3736,8 +3736,8 @@ func buildShowSchema(s *ast.ShowStmt, isView bool, isSequence bool) (schema *exp
 		names = []string{"Original_sql", "Bind_sql", "Default_db", "Status", "Create_time", "Update_time", "Charset", "Collation", "Source"}
 		ftypes = []byte{mysql.TypeVarchar, mysql.TypeVarchar, mysql.TypeVarchar, mysql.TypeVarchar, mysql.TypeDatetime, mysql.TypeDatetime, mysql.TypeVarchar, mysql.TypeVarchar, mysql.TypeVarchar}
 	case ast.ShowAnalyzeStatus:
-		names = []string{"Table_schema", "Table_name", "Partition_name", "Job_info", "Processed_rows", "Start_time", "State"}
-		ftypes = []byte{mysql.TypeVarchar, mysql.TypeVarchar, mysql.TypeVarchar, mysql.TypeVarchar, mysql.TypeLonglong, mysql.TypeDatetime, mysql.TypeVarchar}
+		names = []string{"Table_schema", "Table_name", "Partition_name", "Job_info", "Processed_rows", "Start_time", "End_time", "State"}
+		ftypes = []byte{mysql.TypeVarchar, mysql.TypeVarchar, mysql.TypeVarchar, mysql.TypeVarchar, mysql.TypeLonglong, mysql.TypeDatetime, mysql.TypeDatetime, mysql.TypeVarchar}
 	case ast.ShowBuiltins:
 		names = []string{"Supported_builtin_functions"}
 		ftypes = []byte{mysql.TypeVarchar}
diff --git a/statistics/analyze_jobs.go b/statistics/analyze_jobs.go
index 18fe32c4e3d3a..d74a2f9dffca7 100644
--- a/statistics/analyze_jobs.go
+++ b/statistics/analyze_jobs.go
@@ -36,6 +36,7 @@ type AnalyzeJob struct {
 	JobInfo       string
 	RowCount      int64
 	StartTime     time.Time
+	EndTime       time.Time
 	State         string
 	updateTime    time.Time
 }
@@ -126,6 +127,7 @@ func (job *AnalyzeJob) Finish(meetError bool) {
 		job.State = finished
 	}
 	job.updateTime = time.Now()
+	job.EndTime = job.updateTime
 	job.Mutex.Unlock()
 }
 


Test patch:
diff --git a/executor/infoschema_reader_test.go b/executor/infoschema_reader_test.go
index f677ccba54588..c3e125824873d 100644
--- a/executor/infoschema_reader_test.go
+++ b/executor/infoschema_reader_test.go
@@ -550,6 +550,10 @@ func (s *testInfoschemaTableSuite) TestForAnalyzeStatus(c *C) {
 	analyzeTester.MustExec("set role r_t1")
 	resultT1 := tk.MustQuery("select * from information_schema.analyze_status where TABLE_NAME='t1'").Sort()
 	c.Assert(len(resultT1.Rows()), Greater, 0)
+	for _, row := range resultT1.Rows() {
+		c.Assert(len(row), Equals, 8) // test length of row
+		c.Assert(row[6], NotNil)      // test `End_time` field
+	}
 }
 
 func (s *testInfoschemaTableSerialSuite) TestForServersInfo(c *C) {
diff --git a/executor/show_stats_test.go b/executor/show_stats_test.go
index d673499193651..e9e0ed0ebe82a 100644
--- a/executor/show_stats_test.go
+++ b/executor/show_stats_test.go
@@ -225,7 +225,8 @@ func (s *testShowStatsSuite) TestShowAnalyzeStatus(c *C) {
 	c.Assert(result.Rows()[0][3], Equals, "analyze columns")
 	c.Assert(result.Rows()[0][4], Equals, "2")
 	c.Assert(result.Rows()[0][5], NotNil)
-	c.Assert(result.Rows()[0][6], Equals, "finished")
+	c.Assert(result.Rows()[0][6], NotNil)
+	c.Assert(result.Rows()[0][7], Equals, "finished")
 
 	c.Assert(len(result.Rows()), Equals, 2)
 	c.Assert(result.Rows()[1][0], Equals, "test")
@@ -234,7 +235,8 @@ func (s *testShowStatsSuite) TestShowAnalyzeStatus(c *C) {
 	c.Assert(result.Rows()[1][3], Equals, "analyze index idx")
 	c.Assert(result.Rows()[1][4], Equals, "2")
 	c.Assert(result.Rows()[1][5], NotNil)
-	c.Assert(result.Rows()[1][6], Equals, "finished")
+	c.Assert(result.Rows()[1][6], NotNil)
+	c.Assert(result.Rows()[1][7], Equals, "finished")
 }
 
 func (s *testShowStatsSuite) TestShowStatusSnapshot(c *C) {