当前位置:首页 > 技术 > 正文内容

Druid SQL 解析引擎:全面剖析 SQLTableSource 数据源抽象体系

访客 技术 2026年7月17日 3

SQLTableSource 架构概览

在 Alibaba Druid 的 SQL 解析引擎中,SQLTableSource 是抽象表示 FROM 子句中数据源的核心接口。无论是简单的物理表、多表关联、派生表(子查询)还是公共表表达式(CTE),在抽象语法树(AST)中均通过该接口的具体实现类来表达。其核心继承关系如下:

SQLTableSource (核心接口)
└── SQLTableSourceImpl (基础抽象实现)
    ├── SQLExprTableSource       // 物理表或视图
    ├── SQLJoinTableSource       // 表连接操作 (JOIN)
    ├── SQLSubqueryTableSource   // 派生表 (子查询)
    └── SQLWithSubqueryClause.Entry  // CTE (WITH 子句定义的数据源)

核心子类深度解析与代码实战

1. SQLExprTableSource:基础物理表源

此类用于映射 FROM 关键字后直接跟随的物理表、视图及其别名,是最基础的数据源节点。

关键属性

  • expr:表名标识(通常为 SQLIdentifierExprSQLPropertyExpr)。
  • alias:表别名(可选)。

SQL 场景

SELECT order_id FROM sales_orders;
SELECT o.amount FROM sales_orders o;
SELECT * FROM inventory_db.stock_items;

Java 解析实现

import com.alibaba.druid.sql.SQLUtils;
import com.alibaba.druid.sql.ast.SQLStatement;
import com.alibaba.druid.sql.ast.statement.SQLExprTableSource;
import com.alibaba.druid.sql.ast.statement.SQLSelectQueryBlock;
import com.alibaba.druid.sql.ast.statement.SQLSelectStatement;
import com.alibaba.druid.util.JdbcConstants;

public class PhysicalTableDemo {
    public static void main(String[] args) {
        String rawSql = "SELECT o.amount FROM sales_orders o";
        SQLStatement parsedStmt = SQLUtils.parseSingleStatement(rawSql, JdbcConstants.MYSQL);
        SQLSelectStatement selectStmt = (SQLSelectStatement) parsedStmt;
        SQLSelectQueryBlock queryBlock = (SQLSelectQueryBlock) selectStmt.getSelect().getQuery();
        
        SQLExprTableSource tableSrc = (SQLExprTableSource) queryBlock.getFrom();
        
        System.out.println("Target Table: " + tableSrc.getExpr().toString()); 
        System.out.println("Table Alias: " + tableSrc.getAlias()); 
    }
}

2. SQLJoinTableSource:多表关联数据源

FROM 子句中包含 JOIN 操作时,Druid 会将其解析为 SQLJoinTableSource。它封装了左右数据源、连接类型以及关联条件。

关键属性

  • left / right:左右两侧的数据源(均为 SQLTableSource 类型)。
  • joinType:连接枚举(如 INNER_JOIN, LEFT_OUTER_JOIN)。
  • condition:ON 后面的条件表达式。

2.1 标准双表关联

SELECT u.username, d.dept_name 
FROM users u 
INNER JOIN departments d ON u.dept_id = d.id;
public class DualTableJoinDemo {
    public static void main(String[] args) {
        String rawSql = "SELECT u.username, d.dept_name FROM users u INNER JOIN departments d ON u.dept_id = d.id";
        SQLStatement parsedStmt = SQLUtils.parseSingleStatement(rawSql, JdbcConstants.MYSQL);
        SQLSelectQueryBlock queryBlock = (SQLSelectQueryBlock) ((SQLSelectStatement) parsedStmt).getSelect().getQuery();
        
        SQLJoinTableSource joinSrc = (SQLJoinTableSource) queryBlock.getFrom();
        
        SQLExprTableSource leftSrc = (SQLExprTableSource) joinSrc.getLeft();
        System.out.println("Left: " + leftSrc.getExpr() + " AS " + leftSrc.getAlias()); 
        
        SQLExprTableSource rightSrc = (SQLExprTableSource) joinSrc.getRight();
        System.out.println("Right: " + rightSrc.getExpr() + " AS " + rightSrc.getAlias()); 
        
        System.out.println("Join Type: " + joinSrc.getJoinType()); 
        System.out.println("Condition: " + joinSrc.getCondition()); 
    }
}

2.2 多表关联与左嵌套树结构

在 Druid 的 AST 设计中,当存在三个或更多表的连续 JOIN 时,解析器会自动构建左深树(Left-deep Tree)结构。例如 A JOIN B JOIN C 会被解析为 (A JOIN B) JOIN C

SELECT * 
FROM products p 
JOIN categories c ON p.cat_id = c.id 
JOIN brands b ON c.brand_id = b.id;

AST 嵌套模型

SQLJoinTableSource (外层: (p JOIN c) JOIN b)
   ├── left = SQLJoinTableSource (内层: p JOIN c)
   │      ├── left  = products (SQLExprTableSource)
   │      └── right = categories (SQLExprTableSource)
   └── right = brands (SQLExprTableSource)
public class MultiTableJoinDemo {
    public static void main(String[] args) {
        String rawSql = "SELECT * FROM products p "
                + "JOIN categories c ON p.cat_id = c.id "
                + "JOIN brands b ON c.brand_id = b.id";

        SQLStatement parsedStmt = SQLUtils.parseSingleStatement(rawSql, JdbcConstants.MYSQL);
        SQLSelectQueryBlock queryBlock = (SQLSelectQueryBlock)
                ((SQLSelectStatement) parsedStmt).getSelect().getQuery();

        SQLJoinTableSource outerJoin = (SQLJoinTableSource) queryBlock.getFrom();

        System.out.println("--- Outer Join ---");
        System.out.println("Condition: " + outerJoin.getCondition()); 

        SQLJoinTableSource innerJoin = (SQLJoinTableSource) outerJoin.getLeft();
        System.out.println("\n--- Inner Join ---");
        System.out.println("Left Table: " + ((SQLExprTableSource) innerJoin.getLeft()).getExpr()); 
        System.out.println("Right Table: " + ((SQLExprTableSource) innerJoin.getRight()).getExpr()); 
        System.out.println("Condition: " + innerJoin.getCondition()); 

        SQLExprTableSource outerRight = (SQLExprTableSource) outerJoin.getRight();
        System.out.println("\n--- Outer Right Table ---");
        System.out.println("Table: " + outerRight.getExpr()); 
    }
}

3. SQLSubqueryTableSource:派生表(子查询)

用于表示 FROM 子句中由括号包裹的 SELECT 语句,即派生表。此类数据源必须指定别名。

关键属性

  • select:内部的 SQLSelect 对象。
  • alias:派生表的强制别名。
SELECT derived.total_amount 
FROM (SELECT SUM(amount) AS total_amount FROM transactions) derived;
public class DerivedTableDemo {
    public static void main(String[] args) {
        String rawSql = "SELECT derived.total_amount FROM (SELECT SUM(amount) AS total_amount FROM transactions) derived";
        SQLStatement parsedStmt = SQLUtils.parseSingleStatement(rawSql, JdbcConstants.MYSQL);
        SQLSelectQueryBlock queryBlock = (SQLSelectQueryBlock) ((SQLSelectStatement) parsedStmt).getSelect().getQuery();
        
        SQLSubqueryTableSource subSrc = (SQLSubqueryTableSource) queryBlock.getFrom();
        
        System.out.println("Derived Table Alias: " + subSrc.getAlias()); 
        
        System.out.println("Inner SQL: " + subSrc.getSelect()); 
    }
}

4. SQLWithSubqueryClause.Entry:公共表表达式 (CTE)

映射 WITH ... AS (...) 语法定义的 CTE 节点。支持普通 CTE 和递归 CTE,是处理层次化数据或简化复杂查询的重要结构。

关键属性

  • name:CTE 的标识名称。
  • query:CTE 内部的查询逻辑。
  • recursive:标识是否为递归 CTE。
-- 递归 CTE 示例:查询菜单树
WITH RECURSIVE menu_tree AS (
    SELECT id, title, parent_id FROM menus WHERE parent_id IS NULL
    UNION ALL
    SELECT m.id, m.title, m.parent_id 
    FROM menus m 
    JOIN menu_tree mt ON m.parent_id = mt.id
)
SELECT * FROM menu_tree;
public class CteNodeDemo {
    public static void main(String[] args) {
        String rawSql = "WITH active_users AS (SELECT user_id FROM users WHERE status = 1) SELECT * FROM active_users";
        SQLStatement parsedStmt = SQLUtils.parseSingleStatement(rawSql, JdbcConstants.MYSQL);
        SQLSelectStatement selectStmt = (SQLSelectStatement) parsedStmt;
        SQLSelect select = selectStmt.getSelect();
        
        SQLWithSubqueryClause withClause = select.getWithSubqueryClause();
        System.out.println("Is Recursive: " + withClause.isRecursive()); 
        
        for (SQLWithSubqueryClause.Entry cteEntry : withClause.getEntries()) {
            System.out.println("CTE Name: " + cteEntry.getName()); 
            System.out.println("CTE Query: " + cteEntry.getQuery()); 
        }
        
        SQLSelectQueryBlock queryBlock = (SQLSelectQueryBlock) select.getQuery();
        SQLExprTableSource fromSrc = (SQLExprTableSource) queryBlock.getFrom();
        System.out.println("Main Query FROM: " + fromSrc.getExpr()); 
    }
}

复杂嵌套场景的递归解析

在真实的业务系统中,SQL 语句往往混合了上述多种数据源类型。为了全面提取表元数据,我们需要编写递归算法来遍历 AST 树。

混合嵌套 SQL 示例

WITH recent_orders AS (
    SELECT id, user_id, amount FROM orders WHERE created_at > '2023-01-01'
)
SELECT r.amount, u.username
FROM recent_orders r
JOIN (SELECT id, username FROM users) u ON r.user_id = u.id;

AST 解析逻辑

  1. WITH 块解析为 SQLWithSubqueryClause.Entry
  2. 主查询的 FROM recent_orders r 解析为 SQLExprTableSource
  3. JOIN 操作解析为 SQLJoinTableSource,其右表为 SQLSubqueryTableSource

通用递归解析器实现

public class AstTableSourceVisitor {
    
    public static void traverse(SQLTableSource source) {
        if (source instanceof SQLExprTableSource) {
            SQLExprTableSource exprSrc = (SQLExprTableSource) source;
            System.out.printf("[Physical Table] Name: %s, Alias: %s%n", exprSrc.getExpr(), exprSrc.getAlias());
        } 
        else if (source instanceof SQLJoinTableSource) {
            SQLJoinTableSource joinSrc = (SQLJoinTableSource) source;
            System.out.printf("[Join Operation] Type: %s%n", joinSrc.getJoinType());
            traverse(joinSrc.getLeft());
            traverse(joinSrc.getRight());
        } 
        else if (source instanceof SQLSubqueryTableSource) {
            SQLSubqueryTableSource subSrc = (SQLSubqueryTableSource) source;
            System.out.printf("[Derived Table] Alias: %s%n", subSrc.getAlias());
            // 可在此处继续递归解析 subSrc.getSelect().getQuery()
        }
    }

    public static void main(String[] args) {
        String complexSql = "WITH recent_orders AS (SELECT id, user_id FROM orders) " +
                            "SELECT r.id, u.username FROM recent_orders r " +
                            "JOIN (SELECT id, username FROM users) u ON r.user_id = u.id";
                            
        SQLStatement parsedStmt = SQLUtils.parseSingleStatement(complexSql, JdbcConstants.MYSQL);
        SQLSelectQueryBlock queryBlock = (SQLSelectQueryBlock) ((SQLSelectStatement) parsedStmt).getSelect().getQuery();
        
        traverse(queryBlock.getFrom());
    }
}

核心工程应用场景

  • 数据血缘追踪:通过遍历 SQLTableSource 提取所有物理表名及关联关系,构建企业级数据血缘图谱。
  • SQL 动态改写与优化:在 AST 层面替换表名、注入租户 ID 条件,或调整多表 JOIN 的执行顺序以提升查询性能。
  • 安全合规与防注入:拦截并校验 FROM 子句中的数据源,防止恶意子查询或越权访问敏感物理表。
  • 分库分表路由计算:精准提取目标表名,结合分片键(Sharding Key)计算路由规则,将请求分发至正确的底层数据库节点。

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。