mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-25 07:53:44 +08:00
🐛 fix(sql-diagnose): 兼容新版 MySQL JSON EXPLAIN 解析
- 兼容 query_plan 包装和新版 JSON V2 执行计划结构 - 补齐 covering index、table scan 等节点类型与统计归一化 - 增加新版 MySQL EXPLAIN 解析测试并修正 TotalCost 汇总逻辑
This commit is contained in:
@@ -81,7 +81,9 @@ func finalizeExplainStats(result *connection.ExplainResult) {
|
||||
}
|
||||
}
|
||||
}
|
||||
result.Stats.TotalCost = totalCost
|
||||
if result.Stats.TotalCost == 0 {
|
||||
result.Stats.TotalCost = totalCost
|
||||
}
|
||||
if result.Stats.TotalDurationMs == 0 && accumulatedDuration > 0 {
|
||||
result.Stats.TotalDurationMs = accumulatedDuration
|
||||
}
|
||||
|
||||
@@ -82,36 +82,36 @@ func parseMySQLExplain(dbType, sourceSQL, raw string, format connection.ExplainF
|
||||
|
||||
// mysqlQueryBlock 对应 MySQL FORMAT=JSON 顶层 query_block。
|
||||
type mysqlQueryBlock struct {
|
||||
SelectID json.Number `json:"select_id"`
|
||||
CostInfo map[string]string `json:"cost_info"`
|
||||
Table *mysqlTableNode `json:"table"`
|
||||
NestedLoop []map[string]json.RawMessage `json:"nested_loop"`
|
||||
OrderingOperation *map[string]any `json:"ordering_operation"`
|
||||
GroupingOperation *map[string]any `json:"grouping_operation"`
|
||||
DuplicatesRemoval *map[string]any `json:"duplicates_removal"`
|
||||
Windowing *map[string]any `json:"windowing"`
|
||||
Distinct *map[string]any `json:"distinct"`
|
||||
Message string `json:"message"`
|
||||
SelectID json.Number `json:"select_id"`
|
||||
CostInfo map[string]string `json:"cost_info"`
|
||||
Table *mysqlTableNode `json:"table"`
|
||||
NestedLoop []map[string]json.RawMessage `json:"nested_loop"`
|
||||
OrderingOperation *map[string]any `json:"ordering_operation"`
|
||||
GroupingOperation *map[string]any `json:"grouping_operation"`
|
||||
DuplicatesRemoval *map[string]any `json:"duplicates_removal"`
|
||||
Windowing *map[string]any `json:"windowing"`
|
||||
Distinct *map[string]any `json:"distinct"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type mysqlTableNode struct {
|
||||
TableName string `json:"table_name"`
|
||||
Alias string `json:"alias"`
|
||||
AccessType string `json:"access_type"`
|
||||
RowsExaminedPerScan json.Number `json:"rows_examined_per_scan"`
|
||||
RowsProducedPerJoin json.Number `json:"rows_produced_per_join"`
|
||||
Filtered string `json:"filtered"`
|
||||
PossibleKeys []string `json:"possible_keys"`
|
||||
Key string `json:"key"`
|
||||
UsedKeyParts []string `json:"used_key_parts"`
|
||||
KeyLength json.Number `json:"key_length"`
|
||||
Ref []string `json:"ref"`
|
||||
RowsExaminedPerJoin json.Number `json:"rows_examined_per_join"`
|
||||
CostInfo map[string]string `json:"cost_info"`
|
||||
AttachedCondition string `json:"attached_condition"`
|
||||
AttachedSubqueries []map[string]any `json:"attached_subqueries"`
|
||||
UsingIntersection []map[string]any `json:"using_intersect"`
|
||||
Message string `json:"message"`
|
||||
TableName string `json:"table_name"`
|
||||
Alias string `json:"alias"`
|
||||
AccessType string `json:"access_type"`
|
||||
RowsExaminedPerScan json.Number `json:"rows_examined_per_scan"`
|
||||
RowsProducedPerJoin json.Number `json:"rows_produced_per_join"`
|
||||
Filtered string `json:"filtered"`
|
||||
PossibleKeys []string `json:"possible_keys"`
|
||||
Key string `json:"key"`
|
||||
UsedKeyParts []string `json:"used_key_parts"`
|
||||
KeyLength json.Number `json:"key_length"`
|
||||
Ref []string `json:"ref"`
|
||||
RowsExaminedPerJoin json.Number `json:"rows_examined_per_join"`
|
||||
CostInfo map[string]string `json:"cost_info"`
|
||||
AttachedCondition string `json:"attached_condition"`
|
||||
AttachedSubqueries []map[string]any `json:"attached_subqueries"`
|
||||
UsingIntersection []map[string]any `json:"using_intersect"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// parseMySQLJSONExplain 递归解析 MySQL FORMAT=JSON 输出。
|
||||
@@ -122,29 +122,103 @@ func parseMySQLJSONExplain(raw string) (*connection.ExplainResult, []string, err
|
||||
return nil, nil, fmt.Errorf("顶层 JSON 解析失败:%w", err)
|
||||
}
|
||||
|
||||
result := &connection.ExplainResult{}
|
||||
var warnings []string
|
||||
|
||||
qbRaw, ok := top["query_block"]
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("缺少 query_block 字段")
|
||||
if ok {
|
||||
result := &connection.ExplainResult{}
|
||||
var warnings []string
|
||||
|
||||
// query_block 总成本
|
||||
var qb map[string]json.RawMessage
|
||||
if err := json.Unmarshal(qbRaw, &qb); err != nil {
|
||||
return nil, nil, fmt.Errorf("query_block 解析失败:%w", err)
|
||||
}
|
||||
if costRaw, ok := qb["cost_info"]; ok {
|
||||
var ci map[string]string
|
||||
if err := json.Unmarshal(costRaw, &ci); err == nil {
|
||||
result.Stats.TotalCost = parseExplainFloat64(ci["query_cost"])
|
||||
}
|
||||
}
|
||||
|
||||
// 递归 query_block(可能套 ordering/grouping/distinct 等操作层)
|
||||
parseMySQLQueryBlock(qbRaw, "", result, &warnings)
|
||||
|
||||
return result, warnings, nil
|
||||
}
|
||||
|
||||
// query_block 总成本
|
||||
var qb map[string]json.RawMessage
|
||||
if err := json.Unmarshal(qbRaw, &qb); err != nil {
|
||||
return nil, nil, fmt.Errorf("query_block 解析失败:%w", err)
|
||||
if rootRaw, ok := resolveMySQLJSONV2Root(top, raw); ok {
|
||||
return parseMySQLJSONExplainV2(rootRaw)
|
||||
}
|
||||
if costRaw, ok := qb["cost_info"]; ok {
|
||||
var ci map[string]string
|
||||
if err := json.Unmarshal(costRaw, &ci); err == nil {
|
||||
result.Stats.TotalCost = parseExplainFloat64(ci["query_cost"])
|
||||
|
||||
return nil, nil, fmt.Errorf("缺少 query_block 字段")
|
||||
}
|
||||
|
||||
type mysqlJSONV2Node struct {
|
||||
Query string `json:"query"`
|
||||
Operation string `json:"operation"`
|
||||
AccessType string `json:"access_type"`
|
||||
TableName string `json:"table_name"`
|
||||
Alias string `json:"alias"`
|
||||
SchemaName string `json:"schema_name"`
|
||||
IndexName string `json:"index_name"`
|
||||
IndexAccessType string `json:"index_access_type"`
|
||||
Condition string `json:"condition"`
|
||||
LookupCondition string `json:"lookup_condition"`
|
||||
JoinType string `json:"join_type"`
|
||||
JoinAlgorithm string `json:"join_algorithm"`
|
||||
EstimatedRows json.Number `json:"estimated_rows"`
|
||||
EstimatedTotalCost json.Number `json:"estimated_total_cost"`
|
||||
ActualRows json.Number `json:"actual_rows"`
|
||||
ActualLoops json.Number `json:"actual_loops"`
|
||||
ActualFirstRowMs json.Number `json:"actual_first_row_ms"`
|
||||
ActualLastRowMs json.Number `json:"actual_last_row_ms"`
|
||||
Covering *bool `json:"covering"`
|
||||
UsedColumns []string `json:"used_columns"`
|
||||
KeyColumns []string `json:"key_columns"`
|
||||
Ranges []string `json:"ranges"`
|
||||
Inputs []json.RawMessage `json:"inputs"`
|
||||
}
|
||||
|
||||
func resolveMySQLJSONV2Root(top map[string]json.RawMessage, raw string) (json.RawMessage, bool) {
|
||||
if queryPlanRaw, ok := top["query_plan"]; ok && len(strings.TrimSpace(string(queryPlanRaw))) > 0 {
|
||||
return queryPlanRaw, true
|
||||
}
|
||||
if isMySQLJSONV2Root(top) {
|
||||
return json.RawMessage(raw), true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func isMySQLJSONV2Root(top map[string]json.RawMessage) bool {
|
||||
if len(top) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, key := range []string{"operation", "inputs", "access_type", "table_name", "estimated_total_cost"} {
|
||||
if _, ok := top[key]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 递归 query_block(可能套 ordering/grouping/distinct 等操作层)
|
||||
parseMySQLQueryBlock(qbRaw, "", result, &warnings)
|
||||
func parseMySQLJSONExplainV2(rootRaw json.RawMessage) (*connection.ExplainResult, []string, error) {
|
||||
var rawMap map[string]json.RawMessage
|
||||
if err := json.Unmarshal(rootRaw, &rawMap); err != nil {
|
||||
return nil, nil, fmt.Errorf("新版根节点解析失败:%w", err)
|
||||
}
|
||||
var root mysqlJSONV2Node
|
||||
if err := json.Unmarshal(rootRaw, &root); err != nil {
|
||||
return nil, nil, fmt.Errorf("新版根节点反序列化失败:%w", err)
|
||||
}
|
||||
|
||||
result := &connection.ExplainResult{}
|
||||
var warnings []string
|
||||
if root.EstimatedTotalCost != "" {
|
||||
result.Stats.TotalCost = parseExplainFloat64(root.EstimatedTotalCost.String())
|
||||
}
|
||||
parseMySQLJSONV2Node(rootRaw, "", result, &warnings, true)
|
||||
if len(result.Nodes) == 0 {
|
||||
return nil, nil, fmt.Errorf("新版 JSON 结构未解析出有效节点")
|
||||
}
|
||||
return result, warnings, nil
|
||||
}
|
||||
|
||||
@@ -212,6 +286,221 @@ func parseMySQLQueryBlock(qbRaw json.RawMessage, parentID string, result *connec
|
||||
}
|
||||
}
|
||||
|
||||
func parseMySQLJSONV2Node(raw json.RawMessage, parentID string, result *connection.ExplainResult, warnings *[]string, isRoot bool) {
|
||||
var node mysqlJSONV2Node
|
||||
if err := json.Unmarshal(raw, &node); err != nil {
|
||||
*warnings = append(*warnings, fmt.Sprintf("新版 JSON 节点反序列化失败:%v", err))
|
||||
return
|
||||
}
|
||||
|
||||
nextParentID := parentID
|
||||
if shouldAppendMySQLJSONV2Node(&node, isRoot) {
|
||||
nextParentID = appendExplainChild(result, parentID, buildMySQLJSONV2ExplainNode(&node, isRoot))
|
||||
}
|
||||
|
||||
for _, child := range node.Inputs {
|
||||
parseMySQLJSONV2Node(child, nextParentID, result, warnings, false)
|
||||
}
|
||||
}
|
||||
|
||||
func shouldAppendMySQLJSONV2Node(node *mysqlJSONV2Node, isRoot bool) bool {
|
||||
if node == nil {
|
||||
return false
|
||||
}
|
||||
if strings.TrimSpace(node.Operation) != "" {
|
||||
return true
|
||||
}
|
||||
if strings.TrimSpace(node.AccessType) != "" {
|
||||
return true
|
||||
}
|
||||
if strings.TrimSpace(node.TableName) != "" || strings.TrimSpace(node.IndexName) != "" {
|
||||
return true
|
||||
}
|
||||
if len(node.Inputs) == 0 && isRoot {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func buildMySQLJSONV2ExplainNode(node *mysqlJSONV2Node, isRoot bool) connection.ExplainNode {
|
||||
if node == nil {
|
||||
return connection.ExplainNode{OpType: connection.ExplainOpOther}
|
||||
}
|
||||
|
||||
lowerOperation := strings.ToLower(strings.TrimSpace(node.Operation))
|
||||
explainNode := connection.ExplainNode{
|
||||
OpType: classifyMySQLJSONV2OpType(node.AccessType, node.Operation, node.Covering),
|
||||
OpDetail: strings.TrimSpace(node.Operation),
|
||||
Table: strings.TrimSpace(node.TableName),
|
||||
Index: strings.TrimSpace(node.IndexName),
|
||||
EstRows: parseExplainInt64(node.EstimatedRows.String()),
|
||||
ActualRows: parseExplainInt64(node.ActualRows.String()),
|
||||
Loops: parseExplainInt64(node.ActualLoops.String()),
|
||||
DurationMs: parseExplainFloat64(node.ActualLastRowMs.String()),
|
||||
}
|
||||
if explainNode.DurationMs == 0 {
|
||||
explainNode.DurationMs = parseExplainFloat64(node.ActualFirstRowMs.String())
|
||||
}
|
||||
if cost := parseExplainFloat64(node.EstimatedTotalCost.String()); cost > 0 {
|
||||
if isRoot {
|
||||
explainNode.Cost = cost
|
||||
} else {
|
||||
if explainNode.Extra == nil {
|
||||
explainNode.Extra = map[string]any{}
|
||||
}
|
||||
explainNode.Extra["estimatedTotalCost"] = cost
|
||||
}
|
||||
}
|
||||
|
||||
if strings.TrimSpace(node.Alias) != "" && node.Alias != node.TableName {
|
||||
if explainNode.Extra == nil {
|
||||
explainNode.Extra = map[string]any{}
|
||||
}
|
||||
explainNode.Extra["alias"] = node.Alias
|
||||
}
|
||||
if strings.TrimSpace(node.SchemaName) != "" {
|
||||
if explainNode.Extra == nil {
|
||||
explainNode.Extra = map[string]any{}
|
||||
}
|
||||
explainNode.Extra["schemaName"] = node.SchemaName
|
||||
}
|
||||
if strings.TrimSpace(node.Query) != "" {
|
||||
if explainNode.Extra == nil {
|
||||
explainNode.Extra = map[string]any{}
|
||||
}
|
||||
explainNode.Extra["query"] = node.Query
|
||||
}
|
||||
if strings.TrimSpace(node.Condition) != "" {
|
||||
if explainNode.Extra == nil {
|
||||
explainNode.Extra = map[string]any{}
|
||||
}
|
||||
explainNode.Extra["condition"] = node.Condition
|
||||
}
|
||||
if strings.TrimSpace(node.LookupCondition) != "" {
|
||||
if explainNode.Extra == nil {
|
||||
explainNode.Extra = map[string]any{}
|
||||
}
|
||||
explainNode.Extra["lookupCondition"] = node.LookupCondition
|
||||
}
|
||||
if strings.TrimSpace(node.JoinType) != "" {
|
||||
if explainNode.Extra == nil {
|
||||
explainNode.Extra = map[string]any{}
|
||||
}
|
||||
explainNode.Extra["joinType"] = node.JoinType
|
||||
}
|
||||
if strings.TrimSpace(node.JoinAlgorithm) != "" {
|
||||
if explainNode.Extra == nil {
|
||||
explainNode.Extra = map[string]any{}
|
||||
}
|
||||
explainNode.Extra["joinAlgorithm"] = node.JoinAlgorithm
|
||||
}
|
||||
if strings.TrimSpace(node.IndexAccessType) != "" {
|
||||
if explainNode.Extra == nil {
|
||||
explainNode.Extra = map[string]any{}
|
||||
}
|
||||
explainNode.Extra["indexAccessType"] = node.IndexAccessType
|
||||
}
|
||||
if node.Covering != nil {
|
||||
if explainNode.Extra == nil {
|
||||
explainNode.Extra = map[string]any{}
|
||||
}
|
||||
explainNode.Extra["covering"] = *node.Covering
|
||||
}
|
||||
if len(node.UsedColumns) > 0 {
|
||||
if explainNode.Extra == nil {
|
||||
explainNode.Extra = map[string]any{}
|
||||
}
|
||||
explainNode.Extra["usedColumns"] = node.UsedColumns
|
||||
}
|
||||
if len(node.KeyColumns) > 0 {
|
||||
if explainNode.Extra == nil {
|
||||
explainNode.Extra = map[string]any{}
|
||||
}
|
||||
explainNode.Extra["keyColumns"] = node.KeyColumns
|
||||
}
|
||||
if len(node.Ranges) > 0 {
|
||||
if explainNode.Extra == nil {
|
||||
explainNode.Extra = map[string]any{}
|
||||
}
|
||||
explainNode.Extra["ranges"] = node.Ranges
|
||||
}
|
||||
|
||||
if explainNode.OpType == connection.ExplainOpScan || strings.Contains(lowerOperation, "table scan") {
|
||||
explainNode.Flags = append(explainNode.Flags, connection.ExplainFlagFullScan, connection.ExplainFlagNoIndex)
|
||||
}
|
||||
if explainNode.OpType == connection.ExplainOpSort || strings.Contains(lowerOperation, "filesort") {
|
||||
explainNode.Flags = append(explainNode.Flags, connection.ExplainFlagFilesort)
|
||||
}
|
||||
if strings.Contains(lowerOperation, "temporary") || strings.Contains(lowerOperation, "temp table") || strings.Contains(lowerOperation, "materialize") {
|
||||
explainNode.Flags = append(explainNode.Flags, connection.ExplainFlagTempTable)
|
||||
}
|
||||
|
||||
return explainNode
|
||||
}
|
||||
|
||||
func classifyMySQLJSONV2OpType(accessType, operation string, covering *bool) string {
|
||||
switch strings.ToLower(strings.TrimSpace(accessType)) {
|
||||
case "table":
|
||||
return connection.ExplainOpScan
|
||||
case "index":
|
||||
if covering != nil && *covering {
|
||||
return connection.ExplainOpIndexOnly
|
||||
}
|
||||
return connection.ExplainOpIndexScan
|
||||
case "filter":
|
||||
return connection.ExplainOpFilter
|
||||
case "join":
|
||||
return connection.ExplainOpJoin
|
||||
case "sort":
|
||||
return connection.ExplainOpSort
|
||||
case "aggregate":
|
||||
return connection.ExplainOpAggregate
|
||||
case "limit":
|
||||
return connection.ExplainOpLimit
|
||||
case "materialize":
|
||||
return connection.ExplainOpMaterialize
|
||||
case "window":
|
||||
return connection.ExplainOpWindow
|
||||
case "union":
|
||||
return connection.ExplainOpUnion
|
||||
case "subquery":
|
||||
return connection.ExplainOpSubquery
|
||||
}
|
||||
|
||||
lowerOperation := strings.ToLower(strings.TrimSpace(operation))
|
||||
switch {
|
||||
case strings.Contains(lowerOperation, "table scan"):
|
||||
return connection.ExplainOpScan
|
||||
case strings.Contains(lowerOperation, "covering index"):
|
||||
return connection.ExplainOpIndexOnly
|
||||
case strings.Contains(lowerOperation, "index lookup"),
|
||||
strings.Contains(lowerOperation, "index range"),
|
||||
strings.Contains(lowerOperation, "index scan"):
|
||||
return connection.ExplainOpIndexScan
|
||||
case strings.Contains(lowerOperation, "filter"):
|
||||
return connection.ExplainOpFilter
|
||||
case strings.Contains(lowerOperation, "join"):
|
||||
return connection.ExplainOpJoin
|
||||
case strings.Contains(lowerOperation, "sort"):
|
||||
return connection.ExplainOpSort
|
||||
case strings.Contains(lowerOperation, "aggregate"),
|
||||
strings.Contains(lowerOperation, "group"):
|
||||
return connection.ExplainOpAggregate
|
||||
case strings.Contains(lowerOperation, "limit"):
|
||||
return connection.ExplainOpLimit
|
||||
case strings.Contains(lowerOperation, "materialize"):
|
||||
return connection.ExplainOpMaterialize
|
||||
case strings.Contains(lowerOperation, "window"):
|
||||
return connection.ExplainOpWindow
|
||||
case strings.Contains(lowerOperation, "union"):
|
||||
return connection.ExplainOpUnion
|
||||
case strings.Contains(lowerOperation, "subquery"):
|
||||
return connection.ExplainOpSubquery
|
||||
default:
|
||||
return connection.ExplainOpOther
|
||||
}
|
||||
}
|
||||
|
||||
// buildMySQLTableNode 把 mysqlTableNode 转成归一化的 ExplainNode,并探测 Flags。
|
||||
func buildMySQLTableNode(t *mysqlTableNode) connection.ExplainNode {
|
||||
node := connection.ExplainNode{
|
||||
@@ -298,7 +587,7 @@ func parseMySQLTableExplain(raw string) (*connection.ExplainResult, error) {
|
||||
accessType = strings.TrimSpace(row[colType])
|
||||
}
|
||||
node := connection.ExplainNode{
|
||||
OpType: classifyMySQLAccessType(accessType),
|
||||
OpType: classifyMySQLAccessType(accessType),
|
||||
OpDetail: fmt.Sprintf("id=%s type=%s", idStr, strings.ToLower(accessType)),
|
||||
}
|
||||
if colTable >= 0 && colTable < len(row) {
|
||||
|
||||
@@ -155,6 +155,88 @@ func TestParseMySQLExplain_WithOrderingOperation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
const mySQLFormatJSONV2FilterWithCoveringIndex = `{
|
||||
"query": "/* select#1 */ select ` + "`f1`" + ` from ` + "`t1`" + ` where (` + "`f2`" + ` > 3)",
|
||||
"operation": "Filter: (` + "`t1`" + `. ` + "`f2`" + ` > 3)",
|
||||
"access_type": "filter",
|
||||
"estimated_rows": 2,
|
||||
"estimated_total_cost": 2.65,
|
||||
"condition": "(` + "`t1`" + `. ` + "`f2`" + ` > 3)",
|
||||
"inputs": [
|
||||
{
|
||||
"operation": "Covering index scan on t1 using f1",
|
||||
"access_type": "index",
|
||||
"index_access_type": "index_scan",
|
||||
"table_name": "t1",
|
||||
"index_name": "f1",
|
||||
"covering": true,
|
||||
"estimated_rows": 3,
|
||||
"estimated_total_cost": 1.56
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
func TestParseMySQLExplain_JSONV2FilterWithCoveringIndex(t *testing.T) {
|
||||
result, err := parseMySQLExplain("mysql", "SELECT f1 FROM t1 WHERE f2 > 3", mySQLFormatJSONV2FilterWithCoveringIndex, connection.ExplainFormatJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("解析新版 JSON 失败:%v", err)
|
||||
}
|
||||
if len(result.Nodes) != 2 {
|
||||
t.Fatalf("应有 2 个节点,got=%d", len(result.Nodes))
|
||||
}
|
||||
if result.Nodes[0].OpType != connection.ExplainOpFilter {
|
||||
t.Fatalf("根节点应为 FILTER,got=%s", result.Nodes[0].OpType)
|
||||
}
|
||||
if result.Nodes[1].OpType != connection.ExplainOpIndexOnly {
|
||||
t.Fatalf("covering index scan 应为 INDEX_ONLY,got=%s", result.Nodes[1].OpType)
|
||||
}
|
||||
if result.Nodes[1].Table != "t1" {
|
||||
t.Fatalf("子节点表名应为 t1,got=%s", result.Nodes[1].Table)
|
||||
}
|
||||
if result.Nodes[1].Index != "f1" {
|
||||
t.Fatalf("子节点索引应为 f1,got=%s", result.Nodes[1].Index)
|
||||
}
|
||||
if len(result.Edges) != 1 || result.Edges[0].From != result.Nodes[0].ID || result.Edges[0].To != result.Nodes[1].ID {
|
||||
t.Fatalf("应建立 FILTER -> INDEX_ONLY 边,got=%v", result.Edges)
|
||||
}
|
||||
if result.Stats.TotalCost != 2.65 {
|
||||
t.Fatalf("TotalCost got=%v want=2.65", result.Stats.TotalCost)
|
||||
}
|
||||
}
|
||||
|
||||
const mySQLFormatJSONV2WrappedTableScan = `{
|
||||
"query_plan": {
|
||||
"operation": "Table scan on messages",
|
||||
"access_type": "table",
|
||||
"table_name": "messages",
|
||||
"estimated_rows": 958400,
|
||||
"estimated_total_cost": 49827.15
|
||||
}
|
||||
}`
|
||||
|
||||
func TestParseMySQLExplain_JSONV2WrappedTableScan(t *testing.T) {
|
||||
result, err := parseMySQLExplain("mysql", "SELECT * FROM messages", mySQLFormatJSONV2WrappedTableScan, connection.ExplainFormatJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("解析 query_plan 包装的新版 JSON 失败:%v", err)
|
||||
}
|
||||
if len(result.Nodes) != 1 {
|
||||
t.Fatalf("应有 1 个节点,got=%d", len(result.Nodes))
|
||||
}
|
||||
node := result.Nodes[0]
|
||||
if node.OpType != connection.ExplainOpScan {
|
||||
t.Fatalf("table access 应为 SCAN,got=%s", node.OpType)
|
||||
}
|
||||
if node.Table != "messages" {
|
||||
t.Fatalf("table got=%q want=messages", node.Table)
|
||||
}
|
||||
if !containsFlag(node.Flags, connection.ExplainFlagFullScan) || !containsFlag(node.Flags, connection.ExplainFlagNoIndex) {
|
||||
t.Fatalf("table scan 应标记 FULL_SCAN/NO_INDEX,got=%v", node.Flags)
|
||||
}
|
||||
if result.Stats.TotalCost != 49827.15 {
|
||||
t.Fatalf("TotalCost got=%v want=49827.15", result.Stats.TotalCost)
|
||||
}
|
||||
}
|
||||
|
||||
// MySQL 5.7 表格模式 fallback。
|
||||
const mySQLTableExplainOutput = `id select_type table type possible_keys key key_len ref rows Extra
|
||||
1 SIMPLE users ALL NULL NULL NULL NULL 10000 Using where
|
||||
|
||||
Reference in New Issue
Block a user