quicklite
Version:
A lightweight ORM toolkit for SQLite in Node.js applications
189 lines • 8.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.QueryAnalyzer = void 0;
/**
* SQL查询分析器类
* 提供分析查询性能、生成优化建议和索引推荐的功能
*
* 兼容 better-sqlite3 v11.8.1 及 SQLite 3.48.0
*/
class QueryAnalyzer {
/**
* 分析SQL查询
* @param db 数据库实例
* @param sql SQL查询语句
* @param params 查询参数
* @returns 分析结果
*/
static analyze(db, sql, params = []) {
// 开启分析选项
db.pragma('analysis_limit = 1000');
// 获取查询计划
const explainQuery = `EXPLAIN QUERY PLAN ${sql}`;
const planStmt = db.prepare(explainQuery);
const queryPlan = planStmt.all(...params);
// 计时执行查询
const startTime = Date.now();
// 实际执行查询
db.prepare(sql).all(...params);
const executionTime = Date.now() - startTime;
// 分析查询计划,提供优化建议
const suggestions = this.analyzePlan(queryPlan, sql);
return {
sql,
queryPlan,
executionTime,
suggestions
};
}
/**
* 分析查询计划,提供优化建议
* @param queryPlan 查询计划
* @param sql 原始SQL
* @returns 优化建议
*/
static analyzePlan(queryPlan, sql) {
const suggestions = [];
// 检查是否使用了索引
const usesIndex = queryPlan.some(p => p.detail.includes('USING INDEX') ||
p.detail.includes('USING COVERING INDEX'));
if (!usesIndex) {
suggestions.push('查询未使用索引,请考虑为相关列添加索引以提高查询性能。');
}
// 检查表扫描
const hasFullScan = queryPlan.some(p => p.detail.includes('SCAN TABLE') &&
!p.detail.includes('USING INDEX'));
if (hasFullScan) {
suggestions.push('检测到全表扫描,这在大表上可能导致性能问题。');
}
// 检查临时表使用
const usesTempTable = queryPlan.some(p => p.detail.includes('USE TEMP') ||
p.detail.includes('TEMP TABLE'));
if (usesTempTable) {
suggestions.push('查询使用了临时表,可能影响性能,尤其是处理大量数据时。');
}
// 检查ORDER BY或GROUP BY
if (sql.toUpperCase().includes('ORDER BY')) {
const hasIndexForSort = queryPlan.some(p => p.detail.includes('USING INDEX') &&
p.detail.includes('SCAN') &&
p.detail.includes('ORDER BY'));
if (!hasIndexForSort && sql.toUpperCase().includes('ORDER BY')) {
suggestions.push('ORDER BY操作没有使用索引,请考虑为排序列添加索引。');
}
}
// 检查JOIN操作
const hasJoin = queryPlan.some(p => p.detail.includes('JOIN'));
if (hasJoin) {
const hasIndexForJoin = queryPlan.some(p => p.detail.includes('JOIN') &&
p.detail.includes('USING INDEX'));
if (!hasIndexForJoin) {
suggestions.push('JOIN操作没有使用索引,请考虑为连接条件的列添加索引。');
}
}
// 如果没有发现明显问题
if (suggestions.length === 0) {
suggestions.push('查询计划良好,未发现明显的性能问题。');
}
return suggestions;
}
/**
* 查找查询中使用的表名
* @param sql SQL查询语句
* @returns 表名数组
*/
static extractTableNames(sql) {
// 简单实现,可能不适用于复杂查询
const fromRegex = /\bFROM\s+([^\s,()]+)/gi;
const joinRegex = /\bJOIN\s+([^\s,()]+)/gi;
const tables = [];
// 找出FROM子句中的表
let match;
while ((match = fromRegex.exec(sql)) !== null) {
if (match[1] && !match[1].toUpperCase().startsWith('SELECT')) {
tables.push(match[1].replace(/[\[\]"`']/g, ''));
}
}
// 找出JOIN子句中的表
while ((match = joinRegex.exec(sql)) !== null) {
if (match[1]) {
tables.push(match[1].replace(/[\[\]"`']/g, ''));
}
}
// 移除重复表名
return [...new Set(tables)];
}
/**
* 生成可能有用的索引建议
* @param db 数据库实例
* @param sql SQL查询语句
* @returns 索引建议
*/
static suggestIndices(db, sql) {
// 提取表名
const tableNames = this.extractTableNames(sql);
// 将SQL转为大写便于分析
const uppercaseSql = sql.toUpperCase();
const suggestions = [];
// 对每个表分析潜在的索引列
for (const tableName of tableNames) {
// 获取表的列信息
const columns = db.prepare(`PRAGMA table_info(${tableName})`).all();
// 获取已有索引
const existingIndices = db.prepare(`PRAGMA index_list(${tableName})`).all();
const existingIndexColumns = [];
// 获取每个索引覆盖的列
for (const idx of existingIndices) {
const indexColumns = db.prepare(`PRAGMA index_info(${idx.name})`).all();
existingIndexColumns.push(...indexColumns.map(c => c.name));
}
// 检查WHERE子句中的列
const wherePattern = new RegExp(`\\bWHERE\\b[^()]*\\b${tableName}\\.([\\w_]+)\\b`, 'gi');
let whereMatch;
while ((whereMatch = wherePattern.exec(sql)) !== null) {
const columnName = whereMatch[1];
const column = columns.find(c => c.name.toLowerCase() === columnName.toLowerCase());
if (column && !existingIndexColumns.includes(column.name) && column.pk === 0) {
suggestions.push(`考虑在表 ${tableName} 的列 ${column.name} 上创建索引,它在WHERE子句中被使用。`);
}
}
// 检查JOIN条件中的列
const joinPattern = new RegExp(`\\bJOIN\\b[^()]*\\bON\\b[^()]*\\b${tableName}\\.([\\w_]+)\\b`, 'gi');
let joinMatch;
while ((joinMatch = joinPattern.exec(sql)) !== null) {
const columnName = joinMatch[1];
const column = columns.find(c => c.name.toLowerCase() === columnName.toLowerCase());
if (column && !existingIndexColumns.includes(column.name) && column.pk === 0) {
suggestions.push(`考虑在表 ${tableName} 的列 ${column.name} 上创建索引,它在JOIN条件中被使用。`);
}
}
// 检查ORDER BY子句中的列
if (uppercaseSql.includes('ORDER BY')) {
const orderByPattern = new RegExp(`\\bORDER\\s+BY\\b[^()]*\\b${tableName}\\.([\\w_]+)\\b`, 'gi');
let orderByMatch;
while ((orderByMatch = orderByPattern.exec(sql)) !== null) {
const columnName = orderByMatch[1];
const column = columns.find(c => c.name.toLowerCase() === columnName.toLowerCase());
if (column && !existingIndexColumns.includes(column.name) && column.pk === 0) {
suggestions.push(`考虑在表 ${tableName} 的列 ${column.name} 上创建索引,它在ORDER BY中被使用。`);
}
}
}
// 检查GROUP BY子句中的列
if (uppercaseSql.includes('GROUP BY')) {
const groupByPattern = new RegExp(`\\bGROUP\\s+BY\\b[^()]*\\b${tableName}\\.([\\w_]+)\\b`, 'gi');
let groupByMatch;
while ((groupByMatch = groupByPattern.exec(sql)) !== null) {
const columnName = groupByMatch[1];
const column = columns.find(c => c.name.toLowerCase() === columnName.toLowerCase());
if (column && !existingIndexColumns.includes(column.name) && column.pk === 0) {
suggestions.push(`考虑在表 ${tableName} 的列 ${column.name} 上创建索引,它在GROUP BY中被使用。`);
}
}
}
}
// 移除重复建议
return [...new Set(suggestions)];
}
}
exports.QueryAnalyzer = QueryAnalyzer;
//# sourceMappingURL=QueryAnalyzer.js.map