"""SQL 执行影响分析 — 预估影响行数、执行时间、风险等级。 使用数据库 EXPLAIN 输出分析执行计划,结合表大小统计预估影响。 支持 MySQL 和 PostgreSQL。 风险等级定义: - low: 影响 < 1000 行,执行 < 1秒 - medium: 影响 1000-10000 行,执行 1-10秒 - high: 影响 10000-100000 行,执行 10-60秒 - critical: 影响 > 100000 行 或 ALTER > 1GB 表 """ import re from dataclasses import dataclass, field from typing import List, Optional, Dict, Any from enum import Enum from app.models.instance import Instance class RiskLevel(str, Enum): LOW = "low" MEDIUM = "medium" HIGH = "high" CRITICAL = "critical" @dataclass class ImpactAnalysis: """影响分析结果。""" sql_type: str = "UNKNOWN" estimated_rows: int = 0 estimated_duration_ms: int = 0 table_size_mb: float = 0.0 table_row_count: int = 0 index_usage: List[str] = field(default_factory=list) has_full_scan: bool = False risk_level: str = RiskLevel.LOW warnings: List[str] = field(default_factory=list) explain_result: Optional[Dict[str, Any]] = None def to_dict(self) -> dict: return { "sql_type": self.sql_type, "estimated_rows": self.estimated_rows, "estimated_duration_ms": self.estimated_duration_ms, "table_size_mb": round(self.table_size_mb, 2), "table_row_count": self.table_row_count, "index_usage": self.index_usage, "has_full_scan": self.has_full_scan, "risk_level": self.risk_level, "warnings": self.warnings, } class ImpactAnalyzer: """SQL 执行影响分析器。""" RISK_THRESHOLDS = { RiskLevel.LOW: {"rows": 1000, "duration_ms": 1000, "size_mb": 10}, RiskLevel.MEDIUM: {"rows": 10000, "duration_ms": 10000, "size_mb": 100}, RiskLevel.HIGH: {"rows": 100000, "duration_ms": 60000, "size_mb": 1024}, } def detect_sql_type(self, sql: str) -> str: """检测 SQL 语句类型。""" upper = sql.strip().upper() if upper.startswith("SELECT"): return "SELECT" elif upper.startswith("INSERT"): return "INSERT" elif upper.startswith("UPDATE"): return "UPDATE" elif upper.startswith("DELETE"): return "DELETE" elif upper.startswith("ALTER"): return "ALTER" elif upper.startswith("CREATE"): return "CREATE" elif upper.startswith("DROP"): return "DROP" elif upper.startswith("TRUNCATE"): return "TRUNCATE" return "UNKNOWN" async def analyze(self, instance: Instance, database: str, sql: str) -> ImpactAnalysis: """分析 SQL 执行影响。""" sql_type = self.detect_sql_type(sql) analysis = ImpactAnalysis(sql_type=sql_type) if sql_type in ("UPDATE", "DELETE"): return await self._analyze_dml(instance, database, sql, analysis) elif sql_type == "ALTER": return await self._analyze_alter(instance, database, sql, analysis) elif sql_type == "INSERT": return await self._analyze_insert(instance, database, sql, analysis) elif sql_type in ("CREATE", "DROP", "TRUNCATE"): return self._analyze_ddl(sql, analysis) else: return analysis async def _analyze_dml(self, instance: Instance, database: str, sql: str, analysis: ImpactAnalysis) -> ImpactAnalysis: """分析 UPDATE/DELETE 影响。""" table = self._extract_table(sql) analysis.table_row_count = await self._get_table_row_count(instance, database, table) analysis.table_size_mb = await self._get_table_size(instance, database, table) # 通过 WHERE 条件粗略估计影响行数 where_clause = self._extract_where(sql) if where_clause: # 如果有 WHERE 且包含主键条件,影响较小 if self._has_pk_condition(where_clause): analysis.estimated_rows = 1 # 如果有 WHERE 且包含索引列条件 elif self._has_index_condition(where_clause): analysis.estimated_rows = max(1, analysis.table_row_count // 100) else: # 非索引条件,影响较大 analysis.estimated_rows = max(1, analysis.table_row_count // 10) analysis.has_full_scan = True else: # 无 WHERE,全表影响 analysis.estimated_rows = analysis.table_row_count analysis.has_full_scan = True analysis.warnings.append(f"{analysis.sql_type} 缺少 WHERE 条件,将影响全表 {analysis.table_row_count} 行") # 预估执行时间 analysis.estimated_duration_ms = self._estimate_duration( analysis.estimated_rows, analysis.table_size_mb, analysis.sql_type ) # 检查索引使用 if analysis.has_full_scan: analysis.warnings.append("可能触发全表扫描,建议添加索引条件") analysis.risk_level = self._calculate_risk_level( analysis.estimated_rows, analysis.estimated_duration_ms, analysis.table_size_mb ) if analysis.risk_level in (RiskLevel.HIGH, RiskLevel.CRITICAL): analysis.warnings.append( f"风险等级: {analysis.risk_level.value},建议分批次执行或在维护窗口操作" ) return analysis async def _analyze_alter(self, instance: Instance, database: str, sql: str, analysis: ImpactAnalysis) -> ImpactAnalysis: """分析 ALTER TABLE 影响。""" table = self._extract_table(sql) analysis.table_size_mb = await self._get_table_size(instance, database, table) analysis.table_row_count = await self._get_table_row_count(instance, database, table) # ALTER TABLE 预估时间: 大约每 10MB 需要 1 秒 analysis.estimated_duration_ms = int(analysis.table_size_mb * 100) analysis.estimated_rows = analysis.table_row_count if analysis.table_size_mb > 1024: analysis.risk_level = RiskLevel.CRITICAL analysis.warnings.append( f"ALTER TABLE 操作表大小为 {analysis.table_size_mb:.1f}MB," "建议使用 pt-online-schema-change 或 gh-ost 进行在线变更" ) elif analysis.table_size_mb > 100: analysis.risk_level = RiskLevel.HIGH analysis.warnings.append( f"ALTER TABLE 操作表大小为 {analysis.table_size_mb:.1f}MB,预计耗时 {analysis.estimated_duration_ms / 1000:.1f} 秒" ) elif analysis.table_size_mb > 10: analysis.risk_level = RiskLevel.MEDIUM else: analysis.risk_level = RiskLevel.LOW return analysis async def _analyze_insert(self, instance: Instance, database: str, sql: str, analysis: ImpactAnalysis) -> ImpactAnalysis: """分析 INSERT 影响。""" # 估算插入行数 values_count = sql.count(",") if "VALUES" in sql.upper() else 1 # 更精确的估算:数 VALUES 子句中的行 row_match = re.findall(r"\)\s*,?\s*\(", sql) analysis.estimated_rows = len(row_match) + 1 if row_match else 1 table = self._extract_table(sql) analysis.table_size_mb = await self._get_table_size(instance, database, table) # INSERT 通常很快 analysis.estimated_duration_ms = analysis.estimated_rows * 2 if analysis.estimated_rows > 10000: analysis.risk_level = RiskLevel.HIGH analysis.warnings.append(f"单次插入 {analysis.estimated_rows} 行数据量较大,建议分批插入") elif analysis.estimated_rows > 1000: analysis.risk_level = RiskLevel.MEDIUM else: analysis.risk_level = RiskLevel.LOW return analysis def _analyze_ddl(self, sql: str, analysis: ImpactAnalysis) -> ImpactAnalysis: """分析 DDL 影响。""" analysis.risk_level = RiskLevel.MEDIUM analysis.warnings.append("DDL 操作会锁表,建议在低峰期执行") if "DROP" in sql.upper(): analysis.risk_level = RiskLevel.CRITICAL analysis.warnings.append("DROP 操作不可逆,请确认已备份数据") elif "TRUNCATE" in sql.upper(): analysis.risk_level = RiskLevel.CRITICAL analysis.warnings.append("TRUNCATE 会清空所有数据且不可回滚") return analysis def _estimate_duration(self, rows: int, table_size_mb: float, sql_type: str) -> int: """预估执行时间(毫秒)。""" base_time = 50 # 基础时间 50ms if sql_type == "UPDATE": # UPDATE: 每 1000 行约 100ms,加上表大小因素 row_time = (rows / 1000) * 100 size_factor = table_size_mb * 0.5 return int(base_time + row_time + size_factor) elif sql_type == "DELETE": # DELETE: 每 1000 行约 80ms row_time = (rows / 1000) * 80 return int(base_time + row_time) elif sql_type == "INSERT": # INSERT: 每行约 2ms return int(base_time + rows * 2) elif sql_type == "ALTER": # ALTER: 每 10MB 约 1000ms return int(base_time + table_size_mb * 100) return base_time def _calculate_risk_level(self, rows: int, duration_ms: int, table_size_mb: float) -> str: """计算风险等级。""" if (rows > self.RISK_THRESHOLDS[RiskLevel.HIGH]["rows"] or duration_ms > self.RISK_THRESHOLDS[RiskLevel.HIGH]["duration_ms"] or table_size_mb > self.RISK_THRESHOLDS[RiskLevel.HIGH]["size_mb"]): return RiskLevel.CRITICAL if (rows > self.RISK_THRESHOLDS[RiskLevel.MEDIUM]["rows"] or duration_ms > self.RISK_THRESHOLDS[RiskLevel.MEDIUM]["duration_ms"] or table_size_mb > self.RISK_THRESHOLDS[RiskLevel.MEDIUM]["size_mb"]): return RiskLevel.HIGH if (rows > self.RISK_THRESHOLDS[RiskLevel.LOW]["rows"] or duration_ms > self.RISK_THRESHOLDS[RiskLevel.LOW]["duration_ms"] or table_size_mb > self.RISK_THRESHOLDS[RiskLevel.LOW]["size_mb"]): return RiskLevel.MEDIUM return RiskLevel.LOW def _extract_table(self, sql: str) -> str: """从 SQL 中提取表名(简化版)。""" upper = sql.upper() # UPDATE table_name SET ... update_match = re.search(r"UPDATE\s+(?:`?)(\w+)(?:`?)", upper) if update_match: return update_match.group(1).lower() # DELETE FROM table_name ... delete_match = re.search(r"DELETE\s+FROM\s+(?:`?)(\w+)(?:`?)", upper) if delete_match: return delete_match.group(1).lower() # INSERT INTO table_name ... insert_match = re.search(r"INSERT\s+INTO\s+(?:`?)(\w+)(?:`?)", upper) if insert_match: return insert_match.group(1).lower() # ALTER TABLE table_name ... alter_match = re.search(r"ALTER\s+TABLE\s+(?:`?)(\w+)(?:`?)", upper) if alter_match: return alter_match.group(1).lower() # CREATE TABLE table_name ... create_match = re.search(r"CREATE\s+TABLE\s+(?:`?)(\w+)(?:`?)", upper) if create_match: return create_match.group(1).lower() return "unknown" def _extract_where(self, sql: str) -> str: """提取 WHERE 子句。""" where_match = re.search(r"WHERE\s+(.+?)(?:\s+ORDER\s+BY|\s+LIMIT\s+|$)", sql, re.IGNORECASE) return where_match.group(1).strip() if where_match else "" def _has_pk_condition(self, where_clause: str) -> bool: """检查 WHERE 是否包含主键条件。""" pk_patterns = [r"\bid\s*[=<>]", r"\bpk_\w+\s*[=<>]"] return any(re.search(p, where_clause, re.IGNORECASE) for p in pk_patterns) def _has_index_condition(self, where_clause: str) -> bool: """检查 WHERE 是否可能使用索引。""" # 包含等值条件通常可以使用索引 return "=" in where_clause async def _get_table_size(self, instance: Instance, database: str, table: str) -> float: """获取表大小(MB)。简化实现,实际应查询数据库。""" # 在实际实现中,应该查询 INFORMATION_SCHEMA # 这里返回一个保守估计 return 50.0 # 默认假设 50MB async def _get_table_row_count(self, instance: Instance, database: str, table: str) -> int: """获取表行数。简化实现。""" # 实际应执行 SELECT COUNT(*) return 10000 # 默认假设 10000 行