PostgreSQL 在 MyBatis 中执行正常 SQL 失效:一次 DELETE USING 踩坑记录

最近在处理计资名单账期中间表数据时,遇到一个比较隐蔽的问题:SQL 在 PostgreSQL 客户端中可以正常执行,但放到 MyBatis XML 里执行却失效或报错。

问题 SQL 大致如下:

ini 复制代码
<delete id="deleteLockedLeaveJobs" parameterType="java.util.Map">
    DELETE FROM xxx.salary_list_middle_monthly t1
    USING xxx.salary_list t2
    WHERE t1.task_unique_id = #{taskUniqueId}
      AND t2.ytenant_id = #{ytenantId}
      AND t1.staff_id = t2.staff_id
      AND t1.period = t2.period
      AND t2.dr = 0
      AND t1.depart_date IS NOT NULL
      AND t1.depart_date <![CDATA[ < ]]> #{today}
</delete>

这段 SQL 的业务含义是:

删除账期中间表中,已经离职且离职时间小于当前日期,并且计资名单中已经存在对应离职账期数据的人员记录。


一、现象

这段 SQL 直接放到 PostgreSQL 数据库工具里执行是正常的。

但是放到项目 MyBatis XML 中,通过程序执行时,可能出现:

sql 复制代码
SQL 执行失败
SQL 被框架改写
SQL 结构被重新排序
DELETE USING 语法失效

一开始容易误以为是 PostgreSQL SQL 写法有问题。

但实际排查后发现,问题不在数据库本身,而更可能出在项目里的 SQL 解析、租户翻译、数据权限、SQL 改写等框架层。


二、原因分析

PostgreSQL 支持这种写法:

sql 复制代码
DELETE FROM table1 t1
USING table2 t2
WHERE t1.id = t2.id;

这是 PostgreSQL 中常用的关联删除语法。

但是在某些项目框架里,MyBatis 执行 SQL 前,SQL 可能会经过一层或多层处理,例如:

sql 复制代码
租户字段注入
SQL 翻译
数据权限拦截
SQL 解析器重写
多数据源适配

这些组件有时对 PostgreSQL 的 DELETE ... USING ... 语法支持不完整。

尤其是遇到下面这种结构时:

sql 复制代码
DELETE FROM schema.table t1
USING schema.table2 t2
WHERE ...

框架可能会错误识别:

sql 复制代码
表别名
USING 语法
DELETE 目标表
关联表
WHERE 条件归属

最后导致原本正确的 PostgreSQL SQL,在进入数据库前已经被框架改坏了。

所以这个问题的关键不是:

sql 复制代码
PostgreSQL 不支持 DELETE USING

而是:

sql 复制代码
项目框架中的 SQL 翻译/拦截逻辑没有正确处理 PostgreSQL 的 DELETE USING 语法

三、解决方式

最终处理方式是在 SQL 前加上跳过翻译的标记,并把别名写得更明确:

ini 复制代码
<delete id="deleteLockedLeaveJobs" parameterType="java.util.Map">
    /*!sql_no_translate*/
    DELETE FROM xxx.salary_list_middle_monthly t1
    USING xxx.salary_list AS t2
    WHERE t1.task_unique_id = #{taskUniqueId}
      AND t2.ytenant_id = #{ytenantId}
      AND t1.staff_id = t2.staff_id
      AND t1.period = t2.period
      AND t2.dr = 0
      AND t1.depart_date IS NOT NULL
      AND t1.depart_date <![CDATA[ < ]]> #{today}
</delete>

关键改动有两个:

markdown 复制代码
1. 增加 /*!sql_no_translate*/
2. USING 表别名改成显式 AS t2

其中最核心的是:

arduino 复制代码
/*!sql_no_translate*/

它的作用是告诉项目框架:

这段 SQL 不要再进行自动翻译或改写,直接按原 SQL 执行。

AS t2 不是必须解决点,但可以让 SQL 语义更清晰,减少部分 SQL 解析器对别名的误判。


四、为什么数据库里能执行,项目里不行?

这个问题很典型。

数据库客户端执行时,链路是:

sql 复制代码
SQL → PostgreSQL

而项目中执行时,链路可能是:

sql 复制代码
MyBatis XML
  → MyBatis 参数替换
  → 框架 SQL 拦截器
  → 租户/权限/翻译处理
  → JDBC
  → PostgreSQL

所以你在数据库里验证通过,只能说明:

sql 复制代码
SQL 本身符合 PostgreSQL 语法

但不能说明:

sql 复制代码
这段 SQL 在项目框架链路中不会被改写

如果项目里有 SQL 拦截器、租户插件、数据权限插件、SQL 翻译组件,就要特别注意这种差异。


五、排查建议

遇到这类问题时,不要只盯着 SQL 本身,可以按下面顺序排查:

1. 先确认原 SQL 是否能在数据库中执行

如果数据库中都不能执行,那就是 SQL 写法问题。

如果数据库中可以执行,但项目中失败,继续往下查。

2. 打印最终执行 SQL

重点看程序最终发给数据库的 SQL,是否和 XML 里写的一致。

如果发现 SQL 被改写、排序、加条件、换别名,就说明中间有框架处理。

3. 检查是否存在 SQL 翻译/租户/权限拦截器

常见影响点包括:

sql 复制代码
租户字段自动注入
数据权限 SQL 改写
分页插件
多数据库方言转换
SQL 翻译器
自定义 MyBatis Interceptor

4. 对 PostgreSQL 特有语法加跳过翻译标记

例如:

arduino 复制代码
/*!sql_no_translate*/

尤其是这类 PostgreSQL 语法:

sql 复制代码
DELETE ... USING ...
INSERT ... RETURNING ...
UPDATE ... FROM ...
DISTINCT ON (...)
LATERAL JOIN
IS DISTINCT FROM

这些语法在 PostgreSQL 中很常见,但某些通用 SQL 解析器未必支持完整。


六、踩坑总结

这次问题的核心结论是:

SQL 在数据库里能正常执行,不代表它在 MyBatis 项目中一定能正常执行。中间如果存在 SQL 翻译、租户注入、权限拦截等框架逻辑,SQL 可能在真正到达数据库前已经被改写。

对于 PostgreSQL 的特殊语法,尤其是:

sql 复制代码
DELETE ... USING ...

如果项目框架存在 SQL 改写能力,建议:

markdown 复制代码
1. 优先打印最终 SQL;
2. 确认 SQL 是否被框架改写;
3. 必要时使用 sql_no_translate 跳过翻译;
4. 表别名尽量写完整,例如 USING table AS alias;
5. 不要只用数据库客户端执行结果判断项目一定没问题。

这类问题表面看是 SQL 报错,实际往往是框架链路问题。排查时要区分清楚:

复制代码
数据库语法问题

sql 复制代码
项目框架 SQL 改写问题

否则很容易在 SQL 本身上反复修改,浪费大量时间。

相关推荐
ServBay1 小时前
不会写代码也能建站?AI 时代,非技术创始人如何从零搭建自己的 Web 项目
后端·mcp
Moladev2 小时前
如何在 Electron 中接入 OpenAI 兼容的大模型 API:Snaptium 的主进程代理实践
后端
Oneslide2 小时前
根分区爆满却找不到大文件?深度解析 Linux df 与 du 不一致的经典故障
后端
魏祖潇2 小时前
framework 整合实战——DDD/TDD/SDD 三件套在 framework 仓的真实落地
人工智能·后端
神奇小汤圆2 小时前
责任链模式 + 策略模式:优雅处理多级请求的方式
后端
神奇小汤圆2 小时前
没啃透无锁队列,高并发底层你只懂了皮毛!
后端
大鸡腿同学3 小时前
大模型是怎么训练出来的?
后端
lizhongxuan3 小时前
判断一个人懂不懂 agent harness
后端