package app import ( "encoding/xml" "fmt" "strings" "GoNavi-Wails/internal/connection" ) // SQL Server SHOWPLAN_XML 解析。 // // 典型 XML 结构(简化): // // // // // // // // // // // // // // // // // ... // // // // // // // // // // // // 解析要点: // - RelOp 是核心节点(递归嵌套),每个含 PhysicalOp + LogicalOp + EstimateRows + EstimatedTotalSubtreeCost // - 嵌套在 RelOp 内的同级 RelOp(在 IndexScan/NestedLoops/Hash 等子元素下)是子节点 // - PhysicalOp 直接对应执行算子(Clustered Index Scan / Index Seek / Hash Match / Sort / ...) // - Object 子元素含 Table/Index 信息 // - Predicate 的 ScalarOperator ScalarString 含过滤条件(供规则引擎提取列名) // - RunTimeCountersPerThread 含 ActualRows(对应 ANALYZE 信息) // sqlServerShowPlanXML 是 SHOWPLAN_XML 顶层文档的 Go 结构(部分字段,未识别的留 raw)。 type sqlServerShowPlanXML struct { XMLName xml.Name `xml:"ShowPlanXML"` Batches []sqlServerXMLBatch `xml:"BatchSequence>Batch"` } type sqlServerXMLBatch struct { Statements []sqlServerXMLStmtSimple `xml:"Statements>StmtSimple"` } type sqlServerXMLStmtSimple struct { StatementText string `xml:"StatementText,attr"` QueryPlan *sqlServerXMLPlan `xml:"QueryPlan"` } type sqlServerXMLPlan struct { RelOps []sqlServerXMLRelOp `xml:"RelOp"` } // sqlServerXMLRelOp 是 SHOWPLAN_XML 的核心节点;嵌套子 RelOp 通过多种容器元素持有。 // 为简化解析:先把所有层级的 RelOp 平铺出来(按 NodeId 排序),再按 NodeId 父子推断。 type sqlServerXMLRelOp struct { NodeID int `xml:"NodeId,attr"` PhysicalOp string `xml:"PhysicalOp,attr"` LogicalOp string `xml:"LogicalOp,attr"` EstimateRows float64 `xml:"EstimateRows,attr"` EstimateIO float64 `xml:"EstimateIO,attr"` EstimateCPU float64 `xml:"EstimateCPU,attr"` EstimatedTotalSubtreeCost float64 `xml:"EstimatedTotalSubtreeCost,attr"` EstimateRebinds float64 `xml:"EstimateRebinds,attr"` Parallel int `xml:"Parallel,attr"` // 子节点容器(不同 PhysicalOp 对应不同容器名,这里全收) Objects []sqlServerXMLObject `xml:"Object"` IndexScan *sqlServerXMLContainer `xml:"IndexScan"` NestedLoops *sqlServerXMLContainer `xml:"NestedLoops"` Hash *sqlServerXMLContainer `xml:"Hash"` Merge *sqlServerXMLContainer `xml:"Merge"` Concat *sqlServerXMLContainer `xml:"Concat"` Sort *sqlServerXMLContainer `xml:"Sort"` Filter *sqlServerXMLContainer `xml:"Filter"` ComputeScalar *sqlServerXMLContainer `xml:"ComputeScalar"` Top *sqlServerXMLContainer `xml:"Top"` GenericRelOps []sqlServerXMLContainer `xml:",any"` // 兜底:未识别容器 Predicate *sqlServerXMLPredicate `xml:"Predicate"` RunTimeInfo *sqlServerXMLRunTimeInfo `xml:"RunTimeInformation"` } type sqlServerXMLObject struct { Database string `xml:"Database,attr"` Schema string `xml:"Schema,attr"` Table string `xml:"Table,attr"` Index string `xml:"Index,attr"` } type sqlServerXMLContainer struct { Objects []sqlServerXMLObject `xml:"Object"` RelOps []sqlServerXMLRelOp `xml:"RelOp"` Predicate *sqlServerXMLPredicate `xml:"Predicate"` } type sqlServerXMLPredicate struct { ScalarString string `xml:"ScalarOperator>ScalarString"` } type sqlServerXMLRunTimeInfo struct { RunTimeCounters []sqlServerXMLRunTimeCounter `xml:"RunTimeCountersPerThread"` } type sqlServerXMLRunTimeCounter struct { ActualRows int64 `xml:"ActualRows,attr"` ActualElapsedMs float64 `xml:"ActualElapsedms,attr"` ActualScans int64 `xml:"ActualScans,attr"` } func parseSQLServerExplain(sourceSQL, raw string, format connection.ExplainFormat) (connection.ExplainResult, error) { result := connection.ExplainResult{ DBType: "sqlserver", SourceSQL: sourceSQL, } resetExplainNodeID() trimmed := strings.TrimSpace(raw) if trimmed == "" { return result, fmt.Errorf("SHOWPLAN_XML 输出为空") } // SQL Server 输出的 XML 含 `` 声明,Go encoding/xml 不支持 utf-16 // 直接报 "encoding declared but not supported"。从 标签开始截取即可规避。 showPlanStart := strings.Index(trimmed, " 0 { obj := rel.Objects[0] node.Table = stripSQLServerBrackets(obj.Table) node.Index = stripSQLServerBrackets(obj.Index) } // Predicate if rel.Predicate != nil && rel.Predicate.ScalarString != "" { if node.Extra == nil { node.Extra = map[string]any{} } node.Extra["filter"] = rel.Predicate.ScalarString } // ActualRows(来自 RunTimeCountersPerThread 累加) if rel.RunTimeInfo != nil { var actualRows int64 var elapsedMs float64 for _, c := range rel.RunTimeInfo.RunTimeCounters { actualRows += c.ActualRows elapsedMs += c.ActualElapsedMs } node.ActualRows = actualRows node.DurationMs = elapsedMs } // 物理算子归类 switch node.OpType { case connection.ExplainOpScan: node.Flags = append(node.Flags, connection.ExplainFlagFullScan, connection.ExplainFlagNoIndex) case connection.ExplainOpSort: node.Flags = append(node.Flags, connection.ExplainFlagFilesort) case connection.ExplainOpAggregate, connection.ExplainOpMaterialize: node.Flags = append(node.Flags, connection.ExplainFlagTempTable) } // 关联 LogicalOp 优化提示 if strings.Contains(strings.ToLower(rel.LogicalOp), "aggregate") { node.Flags = append(node.Flags, connection.ExplainFlagTempTable) } nodeID := appendExplainChild(result, parentID, node) nodeByOpID[rel.NodeID] = nodeID // 递归所有可能的容器 containers := []*sqlServerXMLContainer{ rel.IndexScan, rel.NestedLoops, rel.Hash, rel.Merge, rel.Concat, rel.Sort, rel.Filter, rel.ComputeScalar, rel.Top, } for _, c := range containers { if c == nil { continue } for i := range c.RelOps { parseSQLServerRelOp(&c.RelOps[i], nodeID, result, nodeByOpID) } } // GenericRelOps 是 ,any 兜底容器,按需递归 for i := range rel.GenericRelOps { for j := range rel.GenericRelOps[i].RelOps { parseSQLServerRelOp(&rel.GenericRelOps[i].RelOps[j], nodeID, result, nodeByOpID) } } } // classifySQLServerPhysicalOp 把 SQLServer PhysicalOp/LogicalOp 归一化到通用 OpType。 // 参考官方文档:Clustered Index Scan / Index Seek / RID Lookup / Key Lookup / Hash Match / Nested Loops / // Merge Join / Sort / Stream Aggregate / Filter / Compute Scalar / Top / Spool / Table-valued function。 func classifySQLServerPhysicalOp(physical, logical string) string { p := strings.ToLower(strings.TrimSpace(physical)) l := strings.ToLower(strings.TrimSpace(logical)) switch { case strings.Contains(p, "index scan"), strings.Contains(p, "clustered index scan"), strings.Contains(p, "table scan"): return connection.ExplainOpScan case strings.Contains(p, "index seek"), strings.Contains(p, "clustered index seek"): return connection.ExplainOpIndexScan case strings.Contains(p, "key lookup"), strings.Contains(p, "rid lookup"): return connection.ExplainOpIndexScan case strings.Contains(p, "hash match"), strings.Contains(p, "nested loops"), strings.Contains(p, "merge join"): return connection.ExplainOpJoin case strings.Contains(p, "sort"): return connection.ExplainOpSort case strings.Contains(l, "aggregate"), strings.Contains(p, "stream aggregate"), strings.Contains(p, "hash match") && strings.Contains(l, "aggregate"): return connection.ExplainOpAggregate case strings.Contains(p, "filter"): return connection.ExplainOpFilter case strings.Contains(p, "top"): return connection.ExplainOpLimit case strings.Contains(p, "spool"): return connection.ExplainOpMaterialize case strings.Contains(p, "compute scalar"): return connection.ExplainOpOther default: return connection.ExplainOpOther } } // stripSQLServerBrackets 去掉 SQLServer 标识符的方括号:[users] → users。 func stripSQLServerBrackets(s string) string { s = strings.TrimSpace(s) s = strings.TrimPrefix(s, "[") s = strings.TrimSuffix(s, "]") return s }