sql-analyzer.js
3.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// 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'
}