Files
MyGoNavi/internal/app/explain_parse_sqlserver.go
Syngnat 8e24e40fdd feat(explain): 补齐 Oracle/SQLServer/ClickHouse 解析器与索引建议规则引擎
- 方言解析:新增 Oracle DBMS_XPLAN 表格、SQLServer SHOWPLAN_XML、ClickHouse EXPLAIN JSON 解析器
- 规则引擎:新增 10 条跨方言规则(全表扫描、缺索引 JOIN、filesort、估算偏差、缓冲命中、Nested Loop 高扇出等)
- 入口接入:DiagnoseQuery 返回的 Suggestions 自动填充规则匹配结果
- 容错增强:SQLServer strip 默认命名空间与 XML 声明;Oracle 表格列与独立 Predicate 段双源融合
- 测试覆盖:新增 27 个用例覆盖三方言解析与规则触发场景
2026-06-19 12:45:15 +08:00

307 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package app
import (
"encoding/xml"
"fmt"
"strings"
"GoNavi-Wails/internal/connection"
)
// SQL Server SHOWPLAN_XML 解析。
//
// 典型 XML 结构(简化):
//
// <ShowPlanXML Version="1.5" ...>
// <BatchSequence>
// <Batch>
// <Statements>
// <StmtSimple StatementText="SELECT * FROM users" ...>
// <QueryPlan ...>
// <RelOp NodeId="0" PhysicalOp="Clustered Index Scan" LogicalOp="Clustered Scan"
// EstimateRows="1000" EstimateIO="0.1" EstimateCPU="0.001" EstimatedTotalSubtreeCost="0.2"
// Parallel="0" EstimateRebinds="0">
// <Object Database="[db]" Schema="[dbo]" Table="[users]" Index="[PK_users]" />
// <RunTimeInformation>
// <RunTimeCountersPerThread ... ActualRows="1000" ActualElapsedms="5" ActualScans="1" />
// </RunTimeInformation>
// <IndexScan Ordered="0" ...>
// <Object .../>
// <Predicate>
// <ScalarOperator ScalarString="[age]>(18)">
// ...
// </ScalarOperator>
// </Predicate>
// </IndexScan>
// </RelOp>
// </QueryPlan>
// </StmtSimple>
// </Statements>
// </Batch>
// </BatchSequence>
// </ShowPlanXML>
//
// 解析要点:
// - 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 含 `<?xml encoding="utf-16"?>` 声明Go encoding/xml 不支持 utf-16
// 直接报 "encoding declared but not supported"。从 <ShowPlanXML> 标签开始截取即可规避。
showPlanStart := strings.Index(trimmed, "<ShowPlanXML")
if showPlanStart < 0 {
showPlanStart = 0
}
body := trimmed[showPlanStart:]
// SHOWPLAN_XML 携带默认命名空间 xmlns="http://schemas.microsoft.com/sqlserver/2004/07/showplan"
// Go encoding/xml 严格按 namespace 匹配,导致子元素无法识别。
// 该 namespace URL 是固定的(微软规范),直接 strip 掉即可让 struct 按 local name 匹配。
cleaned := stripSQLServerDefaultNamespace(body)
var doc sqlServerShowPlanXML
if err := xml.Unmarshal([]byte(cleaned), &doc); err != nil {
result.RawFormat = connection.ExplainFormatText
result.RawPayload = raw
result.Warnings = []string{fmt.Sprintf("SHOWPLAN_XML 解析失败:%v", err)}
return result, nil
}
// 平铺 RelOp + 记录 NodeId 到内部 ID 的映射
nodeByOpID := map[int]string{}
for _, batch := range doc.Batches {
for _, stmt := range batch.Statements {
if stmt.QueryPlan == nil {
continue
}
for _, relOp := range stmt.QueryPlan.RelOps {
parseSQLServerRelOp(&relOp, "", &result, nodeByOpID)
}
}
}
if len(result.Nodes) == 0 {
result.Warnings = append(result.Warnings, "SHOWPLANXML 解析未提取到任何 RelOp 节点")
}
result.RawFormat = connection.ExplainFormatXML
result.RawPayload = raw
finalizeExplainStats(&result)
return result, nil
}
// stripSQLServerDefaultNamespace 去掉 SHOWPLAN_XML 的默认命名空间属性。
// 该 namespace URL 是 SQL Server 固定规范值,从 SQL Server 2005 起未变化。
func stripSQLServerDefaultNamespace(xmlText string) string {
const ns = ` xmlns="http://schemas.microsoft.com/sqlserver/2004/07/showplan"`
return strings.ReplaceAll(xmlText, ns, "")
}
// parseSQLServerRelOp 递归解析 RelOp 节点及其所有子 RelOp在多种容器元素中
func parseSQLServerRelOp(rel *sqlServerXMLRelOp, parentID string, result *connection.ExplainResult, nodeByOpID map[int]string) {
if rel == nil {
return
}
node := connection.ExplainNode{
OpType: classifySQLServerPhysicalOp(rel.PhysicalOp, rel.LogicalOp),
OpDetail: rel.PhysicalOp,
EstRows: int64(rel.EstimateRows),
Cost: rel.EstimatedTotalSubtreeCost,
}
// 取第一个 Object 作为表/索引来源
if len(rel.Objects) > 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
}