🔥 纯 JS 实现 SQL 字段智能解析工具类,前端也能玩转 SQL 解析
在前端开发中,我们偶尔会遇到需要解析 SQL 语句的场景 ------ 比如可视化 SQL 编辑器、数据大屏字段映射、低代码平台的 SQL 配置解析等。如果每次都靠后端返回字段信息,不仅增加联调成本,还会降低前端交互的灵活性。
今天给大家分享一个我封装的纯 JavaScript SQL 字段解析工具类,无需依赖任何第三方库,就能快速提取 SQL 中的查询字段、表名、字段别名等核心信息,兼容绝大多数常见的 SELECT 语法场景。

🎯 工具类核心能力
这个工具类专为前端场景设计,主打轻量、易用、兼容性强,核心功能包括:
- 📝 SQL 预处理:自动清理注释、多余空格,统一语法格式
- 🔍 字段提取:支持提取原始字段、纯字段名(剔除表别名 / 函数包裹)
- 📌 别名解析:兼容
AS 别名和直接字段 别名两种写法,生成别名映射表 - 📊 表名识别:从 FROM 子句中提取表名,自动跳过表别名和嵌套查询
- 🚨 嵌套查询标记:快速识别 SQL 中是否包含子查询,便于特殊处理
- ✨ 零依赖:纯原生 JS 实现,无需引入 SQL 解析库
🛠 核心代码实现
完整工具类代码
javascript
运行
ini
/**
* SQL解析工具类
* 特性:
* 1. data_field 优先使用别名,无别名时用实际字段(物理字段去前缀/字符串常量原始值)
* 2. 物理字段(如"ERP_Provider"."No")无别名时提取纯字段名(No)
* 3. 字符串常量字段(如'入库单')无别名时保留原始值作为字段名
* 4. 支持 PostgreSQL 特有语法(DISTINCT ON)和嵌套子查询
* 5. 完全按SQL原始内容解析,不做自定义生成
*/
class SqlParser {
/**
* 从SQL语句中提取字段信息
* @param {String} sql - 待解析的SQL语句
* @returns {Array} 格式化后的字段列表
*/
static extractFormattedFields(sql) {
// 防御:处理空SQL或非字符串输入
if (!sql || typeof sql !== 'string') return [];
// 清理SQL:移除注释、多余空格/换行,保留核心结构
const cleanedSql = this.cleanSQL(sql);
// 提取目标SQL(优先解析子查询,若外层是 select * from (...))
const targetSql = this.extractTargetSql(cleanedSql);
// 验证SQL有效性(必须含SELECT/FROM,且SELECT在FROM前)
if (!this.isValidSql(targetSql)) {
console.warn('无效SQL:缺少SELECT/FROM或顺序错误');
return [];
}
// 提取SELECT和FROM之间的字段部分(兼容DISTINCT ON)
const upperSql = targetSql.toUpperCase();
const selectStart = upperSql.indexOf('SELECT') + 6;
// 处理DISTINCT ON:跳过ON后的括号内容
const distinctOnEnd = this.findDistinctOnEnd(targetSql, selectStart);
const fromStart = upperSql.indexOf('FROM');
const fieldsPart = targetSql.substring(distinctOnEnd, fromStart).trim();
// 分割字段并解析(处理括号内逗号)
return this.splitFields(fieldsPart)
.map(token => token.trim())
.filter(token => token)
.map(token => this.parseToFormattedField(token))
.filter(field => field); // 过滤解析失败的字段
}
/**
* 清理SQL:移除注释和多余空格
*/
static cleanSQL(sql) {
return sql
.replace(/--.*$/gm, "") // 移除单行注释
.replace(/\/\*[\s\S]*?\*\//g, "") // 移除多行注释
.replace(/\s+/g, ' ') // 压缩空格
.trim();
}
/**
* 提取目标SQL:若外层是 select * from (子查询),则解析子查询
*/
static extractTargetSql(cleanedSql) {
const subqueryRegex = /SELECT\s+\*\s+FROM\s+\(([\s\S]*)\)\s+AS\s+\w+/i;
const subqueryMatch = cleanedSql.match(subqueryRegex);
if (subqueryMatch && subqueryMatch[1]) {
return subqueryMatch[1].trim(); // 子查询作为目标
}
return cleanedSql; // 否则用原始SQL
}
/**
* 定位DISTINCT ON的结束位置(跳过括号内容)
*/
static findDistinctOnEnd(sql, selectStart) {
const distinctOnRegex = /DISTINCT\s+ON\s*\(/i;
const match = sql.substring(selectStart).match(distinctOnRegex);
if (!match) return selectStart; // 无DISTINCT ON,直接返回SELECT起始位置
// 计算DISTINCT ON(...)的结束索引(跳过括号内内容)
const onStart = selectStart + match.index + match[0].length;
let bracketCount = 1; // 已进入一个括号
let currentIndex = onStart;
while (currentIndex < sql.length && bracketCount > 0) {
const char = sql[currentIndex];
if (char === '(') bracketCount++;
if (char === ')') bracketCount--;
currentIndex++;
}
return currentIndex; // 返回DISTINCT ON(...)后的位置
}
/**
* 智能分割字段(处理括号内/字符串内的逗号,避免误分割)
*/
static splitFields(fieldsPart) {
const fields = [];
let currentField = '';
let bracketCount = 0;
let inQuote = false; // 标记是否在字符串引号内
for (const char of fieldsPart) {
// 处理单/双引号切换(字符串内逗号不分割)
if (char === '"' || char === "'") {
inQuote = !inQuote;
currentField += char;
continue;
}
// 仅当:不在引号内 + 括号计数为0 → 用逗号分割字段
if (char === ',' && !inQuote && bracketCount === 0) {
fields.push(currentField);
currentField = '';
continue;
}
// 更新括号计数(仅当不在引号内)
if (!inQuote) {
if (char === '(') bracketCount++;
if (char === ')') bracketCount = Math.max(0, bracketCount - 1);
}
currentField += char;
}
// 添加最后一个未分割的字段
if (currentField.trim()) fields.push(currentField.trim());
return fields;
}
/**
* 解析单个字段令牌(核心:data_field优先用别名)
*/
static parseToFormattedField(token) {
try {
// 移除引号(不改变原始逻辑,仅清理格式)
const cleanToken = token
.replace(/"([^"]+)"/g, '$1') // 移除字段名双引号(如 "No" → No)
.replace(/'([^']+)'/g, '$1') // 移除字符串单引号(如 '入库单' → 入库单)
.trim();
// 1. 区分物理字段和字符串常量字段
const isStringConst = !cleanToken.includes('.') && !cleanToken.includes('(') && !cleanToken.includes(')') && isNaN(cleanToken);
// 2. 解析别名(支持 AS 别名、空格别名)
let fieldExpr, alias;
const asRegex = /\s+as\s+/i;
if (asRegex.test(cleanToken)) {
// 场景1:带AS的别名(如 No AS 单号、"ERP_Provider"."No" AS 供应商编号)
[fieldExpr, alias] = cleanToken.split(asRegex).map(item => item.trim());
} else {
// 场景2:空格分隔的别名(如 No 单号、SUM(Amount) 总金额)
const lastSpaceIndex = cleanToken.lastIndexOf(' ');
if (lastSpaceIndex > -1) {
const potentialAlias = cleanToken.substring(lastSpaceIndex + 1).trim();
// 排除别名含括号/运算符的情况(避免误判函数内空格)
if (!potentialAlias.includes('(') && !potentialAlias.includes(')') && !/[+\-*/=<>]/.test(potentialAlias)) {
fieldExpr = cleanToken.substring(0, lastSpaceIndex).trim();
alias = potentialAlias;
} else {
// 无有效别名:字段表达式=完整令牌,别名为空
fieldExpr = cleanToken;
alias = '';
}
} else {
// 场景3:无空格(无别名,如 No、'入库单'、SUM(Amount))
fieldExpr = cleanToken;
alias = '';
}
}
// 变量:tableWithRealField → 专门存储"表名+该字段真实名称"(有别名时有效,无别名时为null)
let tableWithRealField = null;
// 仅当"有别名"且"是物理字段"时,才提取表名和真实字段名
if (alias && !isStringConst) {
// 从 fieldExpr 中提取表名和真实字段名(处理函数包裹场景,如 SUM(ERP_Order.Amount) → ERP_Order.Amount)
let realFieldExpr = fieldExpr;
const funcMatch = fieldExpr.match(/\(([^)]+)\)/);
if (funcMatch && funcMatch[1]) {
realFieldExpr = funcMatch[1].trim(); // 去除函数包裹(如 SUM(...) → ...)
}
// 分割表名和真实字段名(如 ERP_Provider.No → 表名=ERP_Provider,真实字段名=No)
const exprParts = realFieldExpr.split('.').filter(part => part.trim());
if (exprParts.length >= 2) {
const tableName = exprParts[0].trim(); // 表名
const realFieldName = exprParts.slice(1).join('.').trim(); // 真实字段名(兼容字段名含"."的极端场景)
tableWithRealField = `${tableName}.${realFieldName}`; // 格式:表名.真实字段名
}
}
// 3. 核心逻辑:data_field 优先用别名,无别名用实际字段
let dataField;
if (alias) {
// 有别名 → data_field = 别名
dataField = alias;
} else {
// 无别名 → 按字段类型取实际值
dataField = isStringConst
? fieldExpr // 字符串常量→原始值(如 '入库单' → 入库单)
: this.getPureFieldName(fieldExpr); // 物理字段→去前缀(如 ERP_Provider.No → No)
}
// 4. data_title 保持原逻辑(显示名称,优先用别名)
const dataTitle = alias || this.extractSimpleName(fieldExpr);
// 5. 推断数据类型和枚举选项
const dataType = this.inferDataType(dataField, dataTitle, token);
const enumOptions = dataType === 'enum' ? this.extractEnumOptions(token) : [];
return {
data_title: dataTitle, // 显示名称(原逻辑不变)
data_field: dataField, // 字段标识(别名优先!)
FormType: this.getFormTypeByDataType(dataType),
enumOptions: enumOptions,
search_data_field: fieldExpr,
tableWithRealField: tableWithRealField // 专门存储"表名+该字段真实名称"(有别名时有效)
};
} catch (error) {
console.warn(`字段解析失败,跳过:${token}`, error);
return null;
}
}
/**
* 物理字段提取纯字段名(无别名时用:去除表名前缀、函数包裹)
*/
static getPureFieldName(fieldExpr) {
// 处理函数包裹(如 SUM(ERP_StorageIn.Amount) → ERP_StorageIn.Amount)
const funcMatch = fieldExpr.match(/\(([^)]+)\)/);
if (funcMatch && funcMatch[1]) {
fieldExpr = funcMatch[1].trim();
// 递归处理嵌套函数(如 SUM(COALESCE(Amount, 0)) → Amount)
if (/\w+\(/.test(fieldExpr)) {
return this.getPureFieldName(fieldExpr);
}
}
// 去除表名前缀(如 ERP_Provider.No → no、"ERP_StorageIn"."Amount" → Amount)
const dotIndex = fieldExpr.lastIndexOf('.');
return dotIndex > -1
? fieldExpr.substring(dotIndex + 1).trim()
: fieldExpr.trim();
}
/**
* 提取简单名称(无别名时用于 data_title)
*/
static extractSimpleName(fieldExpr) {
// 处理函数(如 SUM(Amount) → Amount)
const funcMatch = fieldExpr.match(/\(([^)]+)\)/);
if (funcMatch && funcMatch[1]) {
fieldExpr = funcMatch[1].trim();
}
// 处理表名前缀(如 ERP_Provider.No → No)
const dotIndex = fieldExpr.lastIndexOf('.');
if (dotIndex > -1) {
return fieldExpr.substring(dotIndex + 1).trim();
}
return fieldExpr.trim();
}
/**
* 从CASE语句提取枚举选项
*/
static extractEnumOptions(token) {
const options = [];
const upperToken = token.toUpperCase();
if (upperToken.includes('CASE') && upperToken.includes('WHEN') && upperToken.includes('THEN')) {
const caseStart = upperToken.indexOf('CASE');
const caseEnd = upperToken.indexOf('END');
if (caseStart !== -1 && caseEnd !== -1) {
const caseContent = token.substring(caseStart + 4, caseEnd).trim();
const whenParts = caseContent.split(/WHEN/i).filter(part => part.trim());
for (const part of whenParts) {
const thenIndex = part.toUpperCase().indexOf('THEN');
if (thenIndex !== -1) {
const optionText = part.substring(thenIndex + 4).trim()
.replace(/['"]/g, '')
.replace(/[,;]/g, '')
.split(/\s+/)[0];
if (optionText) options.push({ label: optionText, value: optionText });
}
}
}
}
return options;
}
/**
* 推断数据类型
*/
static inferDataType(fieldName, alias, originalToken) {
const upperField = fieldName.toUpperCase();
const upperAlias = alias.toUpperCase();
const upperToken = originalToken.toUpperCase();
// CASE语句→枚举
if (upperToken.includes('CASE') && upperToken.includes('WHEN') && upperToken.includes('THEN')) {
return 'select';
}
// 日期类型
const dateKeys = ['DATE', 'TIME', 'DAY', 'MONTH', 'YEAR', '日期', '时间', '天'];
const dateFuncs = ['DATE_PART', 'CURRENT_DATE', 'DATE_TRUNC', 'CREATETIME'];
if (dateFuncs.some(f => upperToken.includes(f)) || dateKeys.some(k => upperField.includes(k) || upperAlias.includes(k))) {
return 'date';
}
// 数字类型
const numKeys = ['NUM', '数量', '金额', '天数', 'INT','DOUBLE','单价','分数','总数','余额','价格','成本','重量','指数','率','额'];
const numFuncs = ['SUM', 'COUNT', 'AVG', 'COALESCE'];
if (numFuncs.some(f => upperToken.includes(f)) || /[+\-*/]/.test(upperToken) || numKeys.some(k => upperField.includes(k) || upperAlias.includes(k))) {
return 'double';
}
// 枚举类型
const enumKeys = ['STATE', 'TYPE', '状态', '类型','enum','等级','种类','类别','级别','等级','性别','分类'];
if (enumKeys.some(k => upperField.includes(k) || upperAlias.includes(k))) {
return 'select';
}
// 默认字符串
return 'string';
}
/**
* 获取表单组件类型
*/
static getFormTypeByDataType(dataType) {
const typeMap = {
'date': 'date',
'number': 'double',
'double': 'double',
'string': 'string',
'enum': 'select',
'select': 'select'
};
return typeMap[dataType] || 'string';
}
/**
* 验证SQL有效性
*/
static isValidSql(sql) {
const upperSql = sql.toUpperCase();
const selectIndex = upperSql.indexOf('SELECT');
const fromIndex = upperSql.indexOf('FROM');
// 排除SELECT * 无具体字段的场景
const hasFields = selectIndex + 6 < fromIndex && sql.substring(selectIndex + 6, fromIndex).trim() !== '*';
return selectIndex !== -1 && fromIndex !== -1 && selectIndex < fromIndex && hasFields;
}
}
export default SqlParser;
🚀 快速使用示例
基础使用
javascript
运行
sql
// 测试SQL(包含别名、函数、表别名)
const testSql = `
SELECT
t.id AS user_id,
t.name,
t.age,
MAX(t.score) AS max_score,
t.address
FROM
user_info t
WHERE
t.age > 18
GROUP BY
t.id, t.name
`;
// 一键解析
const result = SqlFieldParser.quickParse(testSql);
// 输出结果
console.log('原始字段列表:', result.fields); // ["t.id", "t.name", "t.age", "t.score", "t.address"]
console.log('纯字段名:', result.pureFields); // ["id", "name", "age", "score", "address"]
console.log('字段别名映射:', result.fieldAliasMap); // { user_id: "t.id", max_score: "t.score" }
console.log('涉及表名:', result.tables); // ["user_info"]
console.log('是否有嵌套查询:', result.hasNestedQuery); // false
输出结果说明
| 字段 | 类型 | 说明 |
|---|---|---|
| fields | Array | 原始字段列表(含表别名 / 函数包裹前的字段) |
| pureFields | Array | 纯字段名(剔除表别名、函数,仅保留字段本身) |
| fieldAliasMap | Object | 别名映射表(别名 → 原始字段) |
| tables | Array | 去重后的表名列表 |
| hasNestedQuery | Boolean | 是否包含嵌套查询 |
| reverseAliasMap | Object | 反向别名映射(原始字段 → 别名) |
🎨 适用场景
- 可视化 SQL 编辑器:解析用户输入的 SQL,自动提取字段用于表单 / 表格渲染
- 低代码平台:解析配置的 SQL 语句,实现字段映射、数据预览
- 数据大屏 / 报表工具:自动识别 SQL 中的维度 / 指标字段,简化配置
- 前端数据校验:校验 SQL 中是否包含指定字段,避免非法查询
- SQL 格式化工具:辅助提取核心信息,优化格式化效果
📈 扩展方向
这个工具类是基础版,满足大部分前端场景需求,你可以根据业务扩展:
- 支持 JOIN 表解析 :扩展
parseTables方法,解析 JOIN 子句中的关联表 - 递归解析嵌套查询 :对
(SELECT ...)形式的子查询做递归解析 - 支持 INSERT/UPDATE 语法 :新增
parseInsertFields/parseUpdateFields方法 - 语法错误提示:增加 SQL 语法合法性校验,返回错误位置
- 结合专业解析库 :如需更精准的语法分析,可集成
sql-parser等库增强能力
💡 核心设计思路
- 预处理优先:先清理注释、统一格式,避免因 SQL 写法不规范导致解析失败
- 正则精准匹配:针对 SELECT/FROM 核心子句设计专属正则,兼顾兼容性和性能
- 分层解析:先提取核心片段,再拆分字段 / 表名,降低解析复杂度
- 轻量优先:前端场景下,避免引入重量级解析库,用原生 JS 实现核心能力
🎯 兼容性说明
✅ 支持的 SQL 语法:
- 基础 SELECT 查询(含 DISTINCT/TOP 关键字)
- 字段别名(AS 别名 / 直接别名)
- 表别名(如
user_info t) - 函数包裹字段(MAX/COUNT/CONCAT 等)
- 多表查询(FROM 后多表逗号分隔)
- 含 WHERE/GROUP BY/ORDER BY 的复杂查询
❌ 暂不支持(可扩展):
- 复杂嵌套子查询的深度解析
- INSERT/UPDATE/DELETE 语句解析
- 非常规 SQL 语法(如存储过程、自定义函数)
📝 总结
这个工具类的核心价值在于前端自主解析 SQL,摆脱对后端的依赖,提升交互体验。代码结构清晰,易于扩展,适合作为前端 SQL 解析的基础组件。
如果你有类似的业务场景,直接复制代码就能用,也可以根据自己的需求扩展功能。如果觉得有用,欢迎点赞收藏,也欢迎在评论区交流更多扩展思路~
完整代码已整理好,可直接复制到项目中使用,建议根据实际业务场景做个性化调整。