Files
MyGoNavi/internal/app/methods_query_history.go
Syngnat 034e968888 feat(sql-analysis): 完善慢 SQL 监控与执行计划诊断
- 完善慢查询采集、聚合、迁移与跨进程持久化

- 加固只读诊断边界并补齐多数据库执行计划解析

- 优化慢 SQL 面板、计划图、响应式布局与交互体验

- 补充后端、前端及 SQL Server 会话回归测试
2026-07-11 16:25:07 +08:00

209 lines
7.8 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/json"
"os"
"path/filepath"
"strings"
"GoNavi-Wails/internal/connection"
"GoNavi-Wails/internal/logger"
)
// 慢 SQL 历史的 Wails 绑定入口。
//
// 前端调用:
// - GetSlowQueries(connectionId, dbName, sortBy, limit) → []QueryExecutionRecord
// - ClearSlowQueries(connectionId, dbName) → 错误(清空当前连接的历史)
//
// sortBy: "duration" | "frequency" | "rowsReturned" | "recent"
// GetSlowQueries 返回当前连接的慢 SQL 历史,按 SQL 指纹聚合执行统计、按指定字段排序后取前 N。
// limit <= 0 时返回前 100 条。
func (a *App) GetSlowQueries(config connection.ConnectionConfig, dbName, sortBy string, limit int) connection.QueryResult {
fingerprints, ok := queryHistoryConnectionFingerprints(config, dbName)
if !ok {
return connection.QueryResult{Success: false, Message: a.appText("query_history.backend.error.connection_fingerprint_invalid", nil)}
}
limit = normalizeQueryHistoryLimit(limit)
primaryFingerprint := fingerprints[0]
primaryStore := newQueryHistoryStore(a.configDir, primaryFingerprint)
failedLegacyStores := make([]*queryHistoryStore, 0)
for _, fingerprint := range fingerprints[1:] {
legacyStore := newQueryHistoryStore(a.configDir, fingerprint)
if err := migrateQueryHistoryStore(legacyStore, primaryStore, primaryFingerprint); err != nil {
logger.Warnf("GetSlowQueries 迁移旧历史失败legacyFp=%s targetFp=%s err=%v", fingerprint, primaryFingerprint, err)
failedLegacyStores = append(failedLegacyStores, legacyStore)
}
}
records, err := primaryStore.LoadAll()
if err != nil {
logger.Warnf("GetSlowQueries 加载失败connFp=%s err=%v", primaryFingerprint, err)
return connection.QueryResult{Success: false, Message: err.Error()}
}
for _, legacyStore := range failedLegacyStores {
storeRecords, err := legacyStore.LoadAll()
if err != nil {
logger.Warnf("GetSlowQueries 加载旧历史失败path=%s err=%v", legacyStore.filePath, err)
return connection.QueryResult{Success: false, Message: err.Error()}
}
records = append(records, storeRecords...)
}
records = dedupeRawQueryHistoryRecords(records)
records = aggregateQueryRecords(records)
sortQueryRecords(records, strings.TrimSpace(sortBy))
if len(records) > limit {
records = records[:limit]
}
return connection.QueryResult{Success: true, Message: a.appText("query_history.backend.message.loaded", nil), Data: records}
}
// ClearSlowQueries 清空当前连接的慢 SQL 历史。
// 删除主文件 + rotate 文件(.1)。
func (a *App) ClearSlowQueries(config connection.ConnectionConfig, dbName string) connection.QueryResult {
fingerprints, ok := queryHistoryConnectionFingerprints(config, dbName)
if !ok {
return connection.QueryResult{Success: false, Message: a.appText("query_history.backend.error.connection_fingerprint_invalid", nil)}
}
for _, fingerprint := range fingerprints {
if err := newQueryHistoryStore(a.configDir, fingerprint).Clear(); err != nil {
logger.Warnf("ClearSlowQueries 失败connFp=%s err=%v", fingerprint, err)
return connection.QueryResult{Success: false, Message: err.Error()}
}
}
return connection.QueryResult{Success: true, Message: a.appText("query_history.backend.message.cleared", nil)}
}
// queryHistoryConnectionFingerprints returns the stable ID-based fingerprint and
// the previous host/config-based fingerprint for backward-compatible reads/clear.
func queryHistoryConnectionFingerprints(config connection.ConnectionConfig, dbName string) ([]string, bool) {
primary, ok := buildQueryHistoryConnectionFingerprint(config, dbName)
if !ok || strings.TrimSpace(primary) == "" {
return nil, false
}
fingerprints := []string{primary}
seen := map[string]struct{}{primary: {}}
for _, legacyConfig := range []connection.ConnectionConfig{normalizeRunConfig(config, dbName), config} {
legacy, legacyOK := buildConnectionFingerprint(legacyConfig)
if queryHistoryUsesOpaqueEndpoint(legacyConfig) || !legacyOK || strings.TrimSpace(legacy) == "" {
continue
}
if _, exists := seen[legacy]; exists {
continue
}
seen[legacy] = struct{}{}
fingerprints = append(fingerprints, legacy)
}
return fingerprints, true
}
func migrateQueryHistoryStore(source, target *queryHistoryStore, targetFingerprint string) error {
if source == nil || target == nil || source.filePath == target.filePath {
return nil
}
if err := os.MkdirAll(filepath.Dir(target.filePath), 0o700); err != nil {
return err
}
migrationLock, err := acquireQueryHistoryFileLock(target.filePath + ".migration.lock")
if err != nil {
return err
}
defer migrationLock.Close()
// Source and target data locks stay held for the complete copy+clear window.
// A legacy process that appends concurrently waits until clear finishes and
// then writes a new source record, which the next read migrates; no record is
// deleted between a stale snapshot and source.Clear().
return withQueryHistoryStoresLocked([]*queryHistoryStore{source, target}, func() error {
sourceRecords := decodeQueryHistorySnapshots(source.readFileSnapshots())
if len(sourceRecords) == 0 {
return nil
}
targetRecords := decodeQueryHistorySnapshots(target.readFileSnapshots())
seen := make(map[string]struct{}, len(targetRecords)+len(sourceRecords))
for _, record := range targetRecords {
seen[queryHistoryRawRecordKey(record)] = struct{}{}
}
for _, record := range sourceRecords {
key := queryHistoryRawRecordKey(record)
if _, exists := seen[key]; exists {
continue
}
record.ConnectionFP = targetFingerprint
if err := target.appendLocked(record); err != nil {
return err
}
seen[key] = struct{}{}
}
return source.clearLocked()
})
}
func dedupeRawQueryHistoryRecords(records []connection.QueryExecutionRecord) []connection.QueryExecutionRecord {
if len(records) < 2 {
return records
}
seen := make(map[string]struct{}, len(records))
result := make([]connection.QueryExecutionRecord, 0, len(records))
for _, record := range records {
key := queryHistoryRawRecordKey(record)
if _, exists := seen[key]; exists {
continue
}
seen[key] = struct{}{}
result = append(result, record)
}
return result
}
func queryHistoryRawRecordKey(record connection.QueryExecutionRecord) string {
if id := strings.TrimSpace(record.ID); id != "" {
return "id:" + id
}
payload, _ := json.Marshal(record)
return "record:" + string(payload)
}
// recordQueryExecution 追加一条慢查询记录。仅慢查询会触发一次小型追加写;同步写入确保
// 执行完成后立刻可见,并避免异步追加与清空操作发生乱序。
func (a *App) recordQueryExecution(config connection.ConnectionConfig, dbName, dbType, sql string, durationMs, rowsRead, rowsReturned int64) {
if durationMs < queryHistorySlowThresholdMs {
return
}
runConfig := normalizeRunConfig(config, dbName)
record := buildQueryExecutionRecord(runConfig, dbName, dbType, sql, durationMs, rowsRead, rowsReturned)
if strings.TrimSpace(record.ConnectionFP) == "" {
logger.Warnf("跳过慢查询记录:连接指纹无效 dbType=%s", dbType)
return
}
store := newQueryHistoryStore(a.configDir, record.ConnectionFP)
store.Append(record)
}
// queryResultRowsReturned 只统计真实查询结果集affectedRows 写操作摘要不计入返回行数。
func queryResultRowsReturned(result connection.QueryResult) int64 {
if result.Data == nil {
return 0
}
switch data := result.Data.(type) {
case []map[string]interface{}:
return int64(len(data))
case []connection.ResultSetData:
var total int64
for _, resultSet := range data {
if isAffectedRowsResultSet(resultSet) {
continue
}
total += int64(len(resultSet.Rows))
}
return total
default:
return 0
}
}
func isAffectedRowsResultSet(resultSet connection.ResultSetData) bool {
return len(resultSet.Columns) == 1 && strings.EqualFold(strings.TrimSpace(resultSet.Columns[0]), "affectedRows")
}