UNPKG

quicklite

Version:

A lightweight ORM toolkit for SQLite in Node.js applications

189 lines 8.5 kB
"use strict"; 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