sql-analyzer.js 3.9 KB
// SQL预执行分析器
import { executeExplain } from './db.js'

/**
 * 对SQL进行预执行检查
 * 1. 语法检查(通过EXPLAIN)
 * 2. 执行计划分析(全表扫描、索引使用等)
 * 3. 锁风险分析
 * 4. 预估影响行数
 */
export async function analyzeSql(connId, sql) {
  const result = {
    syntaxOk: false,
    syntaxError: null,
    warnings: [],
    risks: [],
    explainRows: [],
    estimatedRows: 0,
    sqlType: detectSqlType(sql),
  }

  // 检测SQL类型,非SELECT给出风险提示
  if (result.sqlType !== 'SELECT') {
    result.risks.push({
      level: 'high',
      type: 'DML_OPERATION',
      message: `当前SQL为 ${result.sqlType} 语句,可能造成数据变更和锁表风险`,
    })
  }

  // 语法检查:通过EXPLAIN验证
  try {
    const explainRows = await executeExplain(connId, sql)
    if (explainRows) {
      result.syntaxOk = true
      result.explainRows = explainRows

      // 分析执行计划
      for (const row of explainRows) {
        const type = row.type || row.access_type
        const key = row.key || row.key_name
        const extra = row.Extra || row.extra || ''
        const rows = row.rows || row.rows_examined || 0
        const table = row.table_name || row.table || ''

        // 累计预估行数
        result.estimatedRows += Number(rows) || 0

        // 检测全表扫描
        if (type === 'ALL') {
          result.warnings.push({
            level: 'warn',
            type: 'FULL_TABLE_SCAN',
            message: `表 ${table} 将进行全表扫描,预估扫描 ${rows} 行`,
            table,
          })
        }

        // 检测未使用索引
        if (!key || key === 'NULL' || key === null) {
          result.warnings.push({
            level: 'warn',
            type: 'NO_INDEX',
            message: `表 ${table} 未使用索引`,
            table,
          })
        }

        // 检测filesort
        if (extra.includes('Using filesort')) {
          result.warnings.push({
            level: 'warn',
            type: 'FILESORT',
            message: `表 ${table} 使用了文件排序(filesort),大数据量下可能较慢`,
            table,
          })
        }

        // 检测临时表
        if (extra.includes('Using temporary')) {
          result.risks.push({
            level: 'medium',
            type: 'TEMPORARY_TABLE',
            message: `表 ${table} 使用了临时表,可能影响性能`,
            table,
          })
        }
      }

      // 大数据量风险
      if (result.estimatedRows > 100000) {
        result.risks.push({
          level: 'high',
          type: 'LARGE_RESULT',
          message: `预估扫描行数 ${result.estimatedRows.toLocaleString()} 行,可能消耗大量资源`,
        })
      } else if (result.estimatedRows > 10000) {
        result.warnings.push({
          level: 'warn',
          type: 'MEDIUM_RESULT',
          message: `预估扫描行数 ${result.estimatedRows.toLocaleString()} 行,请关注执行时间`,
        })
      }
    } else {
      // EXPLAIN无法执行,但SQL可能是正确的(如某些DDL)
      result.syntaxOk = true
      result.warnings.push({
        level: 'info',
        type: 'NO_EXPLAIN',
        message: '该SQL类型无法通过EXPLAIN分析,请自行评估风险',
      })
    }
  } catch (e) {
    result.syntaxOk = false
    result.syntaxError = e.message
  }

  return result
}

/**
 * 检测SQL类型
 */
function detectSqlType(sql) {
  const trimmed = sql.trim().toUpperCase()
  if (trimmed.startsWith('SELECT')) return 'SELECT'
  if (trimmed.startsWith('INSERT')) return 'INSERT'
  if (trimmed.startsWith('UPDATE')) return 'UPDATE'
  if (trimmed.startsWith('DELETE')) return 'DELETE'
  if (trimmed.startsWith('CREATE')) return 'CREATE'
  if (trimmed.startsWith('ALTER')) return 'ALTER'
  if (trimmed.startsWith('DROP')) return 'DROP'
  if (trimmed.startsWith('TRUNCATE')) return 'TRUNCATE'
  return 'OTHER'
}