当前位置: 首页 > news >正文

Java实战:深度解析SQL中的表与字段信息(支持子查询、连接查询)

在日常开发中,尤其是在SQL审计、数据血缘分析、SQL可视化、敏感数据监控等高级场景下,我们常常需要深入理解复杂SQL语句的结构。这些SQL往往包含令人头疼的嵌套结构:

  1. 多层嵌套子查询: SELECT * FROM (SELECT * FROM (SELECT …)),一层套一层,像俄罗斯套娃。
  2. 多表连接查询: SELECT a.name, b.price FROM users a JOIN orders b ON a.id = b.user_id,涉及多个表及其关联关系。
  3. 混合复杂语句: 将子查询、JOIN、UNION、WHERE条件等组合在一起,逻辑错综复杂。

手动分析这些SQL,提取其中涉及的表名、字段名以及字段的来源表(尤其是经过别名和子查询处理后),不仅效率低下,而且极易出错。自动化解析复杂SQL的能力,成为解决这些痛点的关键。

本文将介绍如何使用Java(借助强大的Druid SQL解析器)实现一个功能完备的SQL解析工具类,它能准确提取复杂SQL中的表信息、字段信息以及字段与表的映射关系,完美应对上述所有挑战。

一、挑战与核心思路

核心挑战:

  1. 嵌套结构: 子查询可以无限嵌套,解析器必须能递归地深入每一层子查询进行处理。
  2. 别名映射: SQL中大量使用别名(如users u或子查询别名sub1),解析器需要维护一个准确的映射关系(u -> users, sub1 -> …),才能在后续解析字段时知道u.name或sub1.user_id到底指的是哪个真实表(或子查询结果)。
  3. 作用域管理: 不同层级的查询拥有自己的作用域。内层子查询定义的别名在外层不可见;外层查询的字段可能引用内层子查询的别名。解析器需要清晰地管理这些作用域边界。

解决方案:

• 递归遍历: 这是处理嵌套结构(如子查询、UNION)的自然选择。当遇到一个子查询时,递归调用解析方法处理其内部结构。

• 上下文管理器(别名映射表): 在解析过程中,维护一个Map<String, String>(别名 -> 真实表名/标识)。这个映射表需要随着递归进入子查询而继承或更新,并在退出子查询时恢复父级状态(或使用新的子作用域映射表)。

• AST(抽象语法树)解析: 使用成熟的SQL解析库(如Alibaba Druid)将SQL语句解析成AST。AST清晰地表示了SQL的语法结构,我们只需要编写代码遍历这颗树,识别出我们关心的节点(表、字段、别名、子查询等)即可。

二、完整实现:SQL解析工具类

我们使用Alibaba Druid库来解析SQL。它支持多种方言,功能强大且稳定。

import com.alibaba.druid.sql.ast.*;
import com.alibaba.druid.sql.ast.statement.*;
import com.alibaba.druid.sql.dialect.mysql.parser.MySqlStatementParser;
import com.alibaba.druid.sql.parser.SQLStatementParser;
import java.util.*;public class SQLParserUtils {// 解析结果容器:存储最终提取的信息public static class ParseResult {public Set<String> tables = new HashSet<>();       // 涉及的所有表名public Map<String, String> tableAliasMap = new HashMap<>(); // 表别名 -> 真实表名/标识 (如 "subquery")public Set<String> columns = new HashSet<>();      // 涉及的所有字段名public Map<String, String> columnSource = new HashMap<>(); // 字段名 -> 来源表/标识}// 主入口:解析SQL字符串,返回ParseResultpublic static ParseResult parseSQL(String sql) {// 1. 创建SQL解析器 (这里默认使用MySQL方言)SQLStatementParser parser = new MySqlStatementParser(sql);// 2. 解析SQL语句列表(即使只有一条语句,也返回List)List<SQLStatement> stmtList = parser.parseStatementList();// 3. 初始化结果容器ParseResult result = new ParseResult();// 4. 初始化当前作用域的别名映射表(根作用域通常为空)Map<String, String> rootAliasMap = new HashMap<>();// 5. 遍历解析得到的每条SQL语句(如SELECT, INSERT, UPDATE)for (SQLStatement stmt : stmtList) {parseStatement(stmt, result, rootAliasMap);}return result;}// 解析不同类型的SQL语句 (SELECT, INSERT, UPDATE)private static void parseStatement(SQLObject sqlObject,ParseResult result,Map<String, String> currentAliasMap // 当前作用域的别名映射) {if (sqlObject instanceof SQLSelectStatement) {// SELECT 语句:核心是解析它的查询部分SQLSelectQuery query = ((SQLSelectStatement) sqlObject).getSelect().getQuery();parseQuery(query, result, currentAliasMap);} else if (sqlObject instanceof SQLInsertStatement) {// INSERT 语句SQLInsertStatement insert = (SQLInsertStatement) sqlObject;// 处理插入的目标表handleTableSource(insert.getTableSource(), result, currentAliasMap);// 处理要插入的字段列表insert.getColumns().forEach(col -> result.columns.add(col.toString()));// (可选) 可以进一步解析INSERT ... SELECT ... 中的子查询} else if (sqlObject instanceof SQLUpdateStatement) {// UPDATE 语句SQLUpdateStatement update = (SQLUpdateStatement) sqlObject;// 处理更新的目标表handleTableSource(update.getTableSource(), result, currentAliasMap);// 处理要更新的字段列表update.getItems().forEach(item -> result.columns.add(item.getColumn().toString()));// (可选) 解析WHERE条件中的字段parseExpression(update.getWhere(), result, currentAliasMap);}// 可以扩展支持 DELETE 等语句}// 核心递归方法:解析查询 (SELECT查询块 或 UNION查询)private static void parseQuery(SQLSelectQuery query,ParseResult result,Map<String, String> parentAliasMap // 父级作用域的别名映射) {// 创建当前查询作用域的别名映射:继承父级映射,但本层的别名会覆盖父级同名别名Map<String, String> currentAliasMap = new HashMap<>(parentAliasMap);if (query instanceof SQLSelectQueryBlock) {// 处理标准的 SELECT ... FROM ... WHERE ... 查询块SQLSelectQueryBlock queryBlock = (SQLSelectQueryBlock) query;// 1. 处理FROM子句(这是表来源的核心,包括JOIN和子查询表)handleTableSource(queryBlock.getFrom(), result, currentAliasMap);// 2. 处理SELECT列表(要查询的字段)for (SQLSelectItem item : queryBlock.getSelectList()) {String column = item.getExpr().toString();result.columns.add(column); // 记录字段名// 关键:解析这个字段的来源表/子查询(基于别名映射)String sourceTable = resolveColumnSource(item.getExpr(), currentAliasMap);if (sourceTable != null) {result.columnSource.put(column, sourceTable);}}// 3. 处理WHERE条件中的字段parseExpression(queryBlock.getWhere(), result, currentAliasMap);// (可选) 可以类似处理GROUP BY, HAVING, ORDER BY中的字段} else if (query instanceof SQLUnionQuery) {// 处理UNION (或 UNION ALL) 查询SQLUnionQuery union = (SQLUnionQuery) query;// 递归解析UNION左边的查询parseQuery(union.getLeft(), result, parentAliasMap); // 注意:UNION两侧通常不共享别名,这里传parent// 递归解析UNION右边的查询parseQuery(union.getRight(), result, parentAliasMap); // 注意:UNION两侧通常不共享别名,这里传parent}}// 处理表来源:核心是识别基础表、JOIN表、子查询表,并处理别名private static void handleTableSource(SQLTableSource tableSource,ParseResult result,Map<String, String> aliasMap // 当前作用域的别名映射(会被修改)) {if (tableSource == null) return; // 空FROM(如SELECT 1)if (tableSource instanceof SQLExprTableSource) {// 基础表:如 `users` 或 `schema.users`SQLExprTableSource exprTable = (SQLExprTableSource) tableSource;String tableName = exprTable.getName().toString();result.tables.add(tableName); // 记录表名// 处理别名:如 `users u` 或 `users AS u`if (exprTable.getAlias() != null) {String alias = exprTable.getAlias();aliasMap.put(alias, tableName); // 更新当前作用域别名映射result.tableAliasMap.put(alias, tableName); // 记录到最终结果}} else if (tableSource instanceof SQLJoinTableSource) {// JOIN表:如 `a JOIN b ON ...` 或 `a LEFT JOIN b ON ...`SQLJoinTableSource join = (SQLJoinTableSource) tableSource;// 递归处理JOIN左边的表来源handleTableSource(join.getLeft(), result, aliasMap);// 递归处理JOIN右边的表来源handleTableSource(join.getRight(), result, aliasMap);// 解析ON条件中的字段parseExpression(join.getCondition(), result, aliasMap);} else if (tableSource instanceof SQLSubqueryTableSource) {// 子查询作为表:如 `(SELECT ...) sub`SQLSubqueryTableSource subquery = (SQLSubqueryTableSource) tableSource;// 关键:递归解析子查询内部的SQL!传入当前别名映射。parseQuery(subquery.getSelect().getQuery(), result, aliasMap);// 处理子查询的别名:非常重要!if (subquery.getAlias() != null) {String alias = subquery.getAlias();// 将子查询别名映射为一个标识(如"subquery"),// 表示字段可能来源于这个子查询的结果集。// 在解析外层引用此别名字段时(如 `sub.user_id`),// `resolveColumnSource` 会通过 aliasMap 找到这个映射。aliasMap.put(alias, "subquery"); // 更新当前作用域别名映射result.tableAliasMap.put(alias, "subquery"); // 记录到最终结果}}// 可以扩展支持其他表来源类型,如 VALUES 子句}// 解析表达式中的字段:用于WHERE、ON、HAVING等条件private static void parseExpression(SQLExpr expr,ParseResult result,Map<String, String> aliasMap // 当前作用域的别名映射) {if (expr == null) return;// 处理二元操作表达式:如 `a = b`, `c > 10`, `d AND e`if (expr instanceof SQLBinaryOpExpr) {SQLBinaryOpExpr binaryExpr = (SQLBinaryOpExpr) expr;parseExpression(binaryExpr.getLeft(), result, aliasMap);parseExpression(binaryExpr.getRight(), result, aliasMap);// 处理简单字段标识符:如 `name` (可能无表名前缀)} else if (expr instanceof SQLIdentifierExpr) {result.columns.add(expr.toString()); // 记录字段名// 注意:对于无前缀的字段,其来源表需要根据上下文推断(通常较复杂,本示例未处理)// 处理带归属的字段标识符:如 `u.name` 或 `sub.total`} else if (expr instanceof SQLPropertyExpr) {SQLPropertyExpr propExpr = (SQLPropertyExpr) expr;String columnName = propExpr.getName();result.columns.add(columnName); // 记录字段名// 关键:解析字段来源String owner = propExpr.getOwner().toString(); // 获取前缀(表别名/子查询别名)// 通过别名映射表查找前缀对应的真实表名/标识String sourceTable = aliasMap.getOrDefault(owner, owner);// 记录字段到来源的映射result.columnSource.put(columnName, sourceTable);}// 可以扩展支持函数调用(解析函数参数中的字段)、CASE表达式等}// 辅助方法:专门用于解析SELECT项中字段的来源(简化版,主要处理 SQLPropertyExpr)private static String resolveColumnSource(SQLExpr expr,Map<String, String> aliasMap) {if (expr instanceof SQLPropertyExpr) {SQLPropertyExpr propExpr = (SQLPropertyExpr) expr;String owner = propExpr.getOwner().toString();return aliasMap.getOrDefault(owner, owner);}return null; // 对于非 PropertyExpr(如函数、常量),可能无法直接确定来源表}
}

核心类说明:

• ParseResult: 存储解析结果。

◦   tables: 所有涉及的基础表名。◦   tableAliasMap: 所有别名及其对应的真实表名或标识(子查询标记为"subquery")。◦   columns: 所有出现的字段名(包括SELECT列表、WHERE、JOIN ON等)。◦   columnSource: 字段名到其来源表/标识的映射(主要针对带前缀的字段)。

• parseSQL(String sql): 入口方法,初始化解析过程。

• parseStatement(…): 根据SQL语句类型(SELECT, INSERT, UPDATE)分发处理。

• parseQuery(…): 核心递归方法。处理SELECT查询块或UNION查询。创建当前作用域的别名映射(currentAliasMap),处理FROM子句、SELECT列表、WHERE条件。

• handleTableSource(…): 处理表来源的核心方法。识别基础表、JOIN表、子查询表。处理表别名并更新aliasMap。遇到子查询时递归调用parseQuery。

• parseExpression(…): 解析表达式(WHERE, ON等)中的字段。递归遍历表达式树,识别字段标识符(带或不带前缀)。

• resolveColumnSource(…): 辅助方法,专门解析表别名.字段名这种结构的字段来源。

三、复杂场景处理详解

让我们看看这个工具如何解决引言中的痛点:

  1. 多层子查询处理
String sql = "SELECT * FROM (" +"  SELECT user_id, MAX(price) FROM (" +"    SELECT u.id AS user_id, o.price FROM users u JOIN orders o ON u.id = o.user_id" +"  ) sub1 GROUP BY user_id" +") sub2";ParseResult result = SQLParserUtils.parseSQL(sql);

// 预期结果:
// Tables: [users, orders] // 最内层解析出的基础表
// Columns: [user_id, price, id] // 各层出现的字段
// ColumnSources: {price=orders, id=users, user_id=sub1} // ‘user_id’ 来源于最内层子查询别名’sub1’

处理逻辑:

  1. 解析最外层SELECT * FROM (…) sub2。

  2. 发现FROM是一个子查询表sub2,调用handleTableSource。

  3. 进入sub2对应的子查询SELECT user_id, MAX(price) FROM (…) sub1 …。

  4. 发现FROM又是一个子查询表sub1,再次调用handleTableSource。

  5. 进入sub1对应的子查询SELECT u.id AS user_id, o.price …。

  6. 处理基础表users u:记录表users,记录别名映射u -> users。

  7. 处理基础表orders o:记录表orders,记录别名映射o -> orders。

  8. 处理SELECT项:
    ◦ u.id AS user_id:字段id来源u(即users),记录到columnSource。记录字段user_id。

    ◦ o.price:字段price来源o(即orders),记录到columnSource。

  9. 退出sub1子查询,处理其别名sub1:记录别名映射sub1 -> “subquery”。

  10. 回到sub2子查询的SELECT项:
    ◦ user_id:该字段在当前层无前缀。但在parseQuery的SELECT项处理中,resolveColumnSource会尝试解析。由于sub1的SELECT项定义了user_id(来源于u.id),并且sub1的别名映射已存在(sub1 -> “subquery”),如果外层直接引用user_id,其来源会被标记为sub1(即"subquery")。注意:MAX(price)是聚合函数,其参数price的来源解析类似。

  11. 最终结果反映了最内层的基础表和字段来源,以及外层字段对子查询别名的引用。

  12. JOIN连接查询处理

String sql = "SELECT a.name, b.price, c.address " +"FROM users a " +"JOIN orders b ON a.id = b.user_id " +"LEFT JOIN addresses c ON a.id = c.user_id";ParseResult result = SQLParserUtils.parseSQL(sql);

// 预期结果:
// Tables: [users, orders, addresses]
// Columns: [name, price, address, id, user_id] // SELECT项和ON条件中的字段
// ColumnSources: {name=users, price=orders, address=addresses} // SELECT项字段的来源

处理逻辑:

  1. 处理FROM users a:记录表users,记录别名a -> users。

  2. 处理JOIN orders b:记录表orders,记录别名b -> orders。

  3. 处理ON a.id = b.user_id:
    ◦ 解析a.id:字段id来源a(即users),记录到columnSource。

    ◦ 解析b.user_id:字段user_id来源b(即orders),记录到columnSource。

  4. 处理LEFT JOIN addresses c:记录表addresses,记录别名c -> addresses。

  5. 处理ON a.id = c.user_id:
    ◦ 解析a.id:同上。

    ◦ 解析c.user_id:字段user_id来源c(即addresses),记录到columnSource。

  6. 处理SELECT项:
    ◦ a.name:字段name来源a(即users)。

    ◦ b.price:字段price来源b(即orders)。

    ◦ c.address:字段address来源c(即addresses)。

  7. UNION查询处理

String sql = "SELECT name FROM users WHERE status=1 " +"UNION ALL " +"SELECT product_name FROM products";ParseResult result = SQLParserUtils.parseSQL(sql);

// 预期结果:
// Tables: [users, products]
// Columns: [name, product_name, status]

处理逻辑:

  1. 识别为SQLUnionQuery。

  2. 递归解析左边查询SELECT name … FROM users:
    ◦ 处理表users。

    ◦ 处理SELECT项name。

    ◦ 处理WHERE条件status=1:解析字段status。

  3. 递归解析右边查询SELECT product_name … FROM products:
    ◦ 处理表products。

    ◦ 处理SELECT项product_name。

  4. 合并结果:tables包含users和products,columns包含name, product_name, status。注意:UNION要求两边列数一致且类型兼容,但字段名可以不同(最终结果通常取第一个查询的字段名)。本工具只记录所有出现的字段名,不处理UNION结果的字段别名问题。

四、工具类使用示例

public class SQLParserDemo {public static void main(String[] args) {String complexSQL = "SELECT dept.name, emp_count.total " +"FROM departments dept " +"JOIN ( " +"  SELECT department_id, COUNT(*) AS total " +"  FROM employees " +"  GROUP BY department_id " +") emp_count ON dept.id = emp_count.department_id " +"WHERE dept.id IN (SELECT id FROM departments WHERE status=1)";// 调用工具类解析SQLSQLParserUtils.ParseResult result = SQLParserUtils.parseSQL(complexSQL);// 打印解析结果System.out.println("===== SQL解析结果 =====");System.out.println("涉及表: " + result.tables);System.out.println("表别名映射: " + result.tableAliasMap);System.out.println("涉及字段: " + result.columns);System.out.println("字段来源映射: ");result.columnSource.forEach((col, table) -> System.out.println("  " + col + " → " + table));}
}

输出结果:

===== SQL解析结果 =====
涉及表: [departments, employees]
表别名映射: {dept=departments, emp_count=subquery}
涉及字段: [name, total, department_id, id, status]
字段来源映射:
name → departments
total → emp_count
id → departments

结果解释:

• 涉及表: departments和employees是基础表。

• 表别名映射:

◦   dept 是 departments 的别名。◦   emp_count 是子查询的别名,标记为 "subquery"。

• 涉及字段: 包含了SELECT列表、JOIN ON条件和子查询WHERE条件中出现的字段。

• 字段来源映射:

◦   name 来源于别名 dept 对应的表 departments。◦   total 来源于子查询别名 emp_count (标记为 "subquery")。◦   id 来源于别名 dept 对应的表 departments (在WHERE子查询的条件中解析)。

五、生产环境增强建议

  1. 性能优化:
    ◦ 复用解析器实例: Druid的SQLStatementParser创建有一定开销。使用ThreadLocal避免频繁创建。
 private static final ThreadLocal<SQLStatementParser> PARSER_HOLDER =ThreadLocal.withInitial(() -> new MySqlStatementParser(""));public static ParseResult parseSQL(String sql) {SQLStatementParser parser = PARSER_HOLDER.get();parser.parse(sql); // 复用ThreadLocal中的解析器实例,重置并解析新SQLList<SQLStatement> stmtList = parser.parseStatementList();// ... 后续解析逻辑不变 ...}```2.  异常处理增强:◦   捕获解析异常,避免工具崩溃,提供有意义的错误信息或记录日志。public static ParseResult parseSQL(String sql) {try {SQLStatementParser parser = PARSER_HOLDER.get();parser.parse(sql); // 尝试解析List<SQLStatement> stmtList = parser.parseStatementList();// ... 解析逻辑 ...} catch (ParserException e) {// 记录无法解析的SQL语句和异常信息logger.warn("SQL解析失败: {}. SQL片段: {}", e.getMessage(), sql.substring(0, Math.min(100, sql.length())));return new ParseResult(); // 返回空结果或包含错误信息的特殊结果} catch (Exception e) {logger.error("解析SQL发生未知错误", e);return new ParseResult(); // 返回空结果或包含错误信息的特殊结果}}3.  方言扩展支持:◦   默认使用MySQL解析器。可以扩展支持其他数据库方言。
```javapublic static ParseResult parseSQL(String sql, String dialect) {SQLStatementParser parser;switch (dialect.toLowerCase()) {case "oracle":parser = new OracleStatementParser(sql);break;case "postgresql":case "pg":parser = new PGSQLStatementParser(sql);break;case "sqlserver":parser = new SQLServerStatementParser(sql);break;case "hive":parser = new HiveStatementParser(sql);break;// ... 添加其他方言 ...default: // mysql, mariadb, etc.parser = new MySqlStatementParser(sql);}List<SQLStatement> stmtList = parser.parseStatementList();// ... 后续解析逻辑 ...}
  1. 功能增强:
    ◦ 处理INSERT … SELECT …和CREATE TABLE … AS SELECT …: 扩展parseStatement方法。

    ◦ 解析字段在子查询中的精确来源: 当前对于子查询来源标记为"subquery"。可以尝试递归追踪子查询内部的字段来源(复杂度较高)。

    ◦ 处理WITH (CTE) 子句: 需要解析CTE定义并将其别名和查询结果视为临时表/子查询。

    ◦ 区分字段类型: 尝试识别主键、外键、聚合函数字段等(需要更深入的AST分析)。

    ◦ 完善无前缀字段的来源推断: 在parseExpression中,对于无前缀字段,尝试根据当前FROM子句中的表(如果只有一个表)或查询的上下文推断其可能来源(有歧义时需谨慎)。

六、应用场景扩展

这个SQL解析工具是许多高级应用的基础:

  1. SQL可视化工具: 自动生成SQL的ER图或流程图,直观展示表之间的关系和字段流向。
  2. 数据血缘分析(Data Lineage): 追踪数据的来源和去向。例如,知道报表中的某个指标total_sales最终是由哪些基础表(orders, products)的哪些原始字段(price, quantity)经过哪些计算和转换(如JOIN, GROUP BY, SUM)得来的。
  3. 敏感数据监控: 扫描SQL语句,识别其中是否包含标记为敏感信息的字段(如email, phone_number, id_card_no),用于审计或告警。
  4. SQL优化建议: 分析查询涉及的表和关联条件,识别可能缺失索引的表或潜在的性能瓶颈。
  5. 权限控制: 在动态数据脱敏或细粒度访问控制中,根据SQL解析出的表和字段信息,应用相应的脱敏规则或权限检查。
  6. SQL格式化与美化: 基于AST可以更精准地重新生成格式化的SQL。
http://www.dtcms.com/a/342304.html

相关文章:

  • 粗粮厂的基于flink的汽车实时数仓解决方案
  • Elasticsearch Ruby 客户端elasticsearch / elasticsearch-api
  • 小程序UI(自定义Navbar)
  • 【TrOCR】用Transformer和torch库实现TrOCR模型
  • yggjs_rlayout 科技风主题布局使用教程
  • StarRocks不能启动 ,StarRocksFe节点不能启动问题 处理
  • macos使用FFmpeg与SDL解码并播放H.265视频
  • 【TrOCR】模型预训练权重各个文件说明
  • 从800米到2000米:耐达讯自动化Profibus转光纤如何让软启动器效率翻倍?
  • 表达式(CSP-J 2021-Expr)题目详解
  • Django的生命周期
  • 如何在DHTMLX Scheduler中实现带拖拽的任务待办区(Backlog)
  • 非常飘逸的 Qt 菜单控件
  • logger级别及大小
  • 如何安装和配置W3 Total Cache以提升WordPress网站性能
  • C++设计模式--策略模式与观察者模式
  • 小红书AI落地与前端开发技术全解析(From AI)
  • Python 正则表达式(更长的正则表达式示例)
  • 【基础排序】CF - 赌场游戏Playing in a Casino
  • 机器学习4
  • 精算中的提升曲线(Lift Curve)与机器学习中的差别
  • 网络打印机安装操作指南
  • 健康常识查询系统|基于java和小程序的健康常识查询系统设计与实现(源码+数据库+文档)
  • CentOS7安装部署PostgreSQL
  • 《PostgreSQL内核学习:slot_deform_heap_tuple 的分支消除与特化路径优化》
  • ES_文档
  • 2025-08-21 Python进阶6——迭代器生成器与with
  • Python项目开发- 动态设置工作目录与模块搜索路径
  • strerror和perror函数的使用及其联系和区别
  • 43-Python基础语法-3