从Oracle迁到金仓,第一个月踩的最大的坑就是它

从Oracle迁到金仓,第一个月踩的最大的坑就是它

迁移上线当天,报表数据对不上

去年帮一个客户做Oracle到金仓的迁移,折腾了两个月,终于到了上线那天。

下午三点,业务方打电话过来,语气不对:"报表数据好像少了,你们查一下。"

我赶紧登录数据库,把那条报表SQL拉出来看:

sql 复制代码
SELECT o.order_id, o.amount, r.refund_amount 
FROM orders o 
LEFT JOIN refunds r ON o.order_id = r.order_id 
WHERE r.status = 1;

Oracle那边跑出来的结果有1280行,金仓这边只有1123行。差了157条。

我盯着这条SQL看了半天,没发现语法问题。LEFT JOIN对吧?条件也对吧?后来把执行计划拉出来一看,懵了------金仓的执行计划里就是个普通的Hash Join,压根没有外连接。

优化器把LEFT JOIN转成INNER JOIN了。

157条退款状态不是1的订单,在Oracle里被保留下来了,在金仓里直接被过滤掉了。因为r.status = 1这个条件在WHERE里,右表没匹配上的行,r.status是NULL,NULL不等于1,就被干掉了。

这个逻辑在Oracle里不一样。Oracle在某些场景下对外连接的处理更保守,不会轻易把LEFT JOIN转成INNER JOIN。但金仓的优化器更激进,只要它觉得结果等价,就会换更快的执行方式。

问题是它觉得等价,业务上不等价。

这种写法在Oracle里太常见了

我后来翻了翻代码,发现这种写法在原来的Oracle系统里到处都是。甚至Oracle官方的文档示例里都有类似的写法。习惯了这个写法的人,迁移到金仓基本上都会踩坑。

还有更隐蔽的。

有些系统用了Oracle的(+)语法:

sql 复制代码
SELECT * 
FROM t1, t2 
WHERE t1.id = t2.id(+) 
AND t2.status = 1;

这个(+)放在t2那边,表示t1左连接t2。然后在WHERE里又加了t2.status = 1。语法上没毛病,Oracle能跑。但到了金仓,同样的逻辑,(+)语法被解析成LEFT JOIN之后,优化器一看WHERE里有右表条件,照样给你转成INNER JOIN。

还有一种是视图里套外连接,外面再过滤:

sql 复制代码
CREATE VIEW v_orders AS 
SELECT * FROM orders LEFT JOIN refunds ON orders.id = refunds.order_id;

-- 查询的时候
SELECT * FROM v_orders WHERE status = 1;

视图里定义的LEFT JOIN,外面一加右表的条件,优化器照样给你消掉。你在视图里写LEFT JOIN也没用,外层条件一来,优化器照消不误。

后来我怎么排查的

那个报表数据对不上之后,我整理了一套排查流程,后来每次迁移都这么干。

第一步,看执行计划。

EXPLAIN跑一下,如果SQL里写的是LEFT JOIN,计划里显示的是Hash Join或者Nested Loop,而且没有Left字样,大概率是被消掉了。

第二步,查右表条件。

凡是WHERE条件里引用了右表的非空列,不管是等值、范围、LIKE还是IN,都有可能触发消除。逐个检查,把确实需要外连接的挑出来。

第三步,数据量验证。

找几条左表有、右表无的记录,单独跑一遍完整查询,看结果还在不在。如果在,说明没事。如果不在了,那就说明外连接被消掉了。

我当时写了个脚本,把几百条涉及外连接的SQL全部过了一遍,找出大概30多条存在隐患的SQL,逐一改掉才稳住上线。

怎么改才稳妥

后来我定了两条规矩。

第一条,外连接的右表条件,一律放ON里。

sql 复制代码
-- 之前这么写
LEFT JOIN refunds r ON o.id = r.order_id WHERE r.status = 1

-- 现在这么写
LEFT JOIN refunds r ON o.id = r.order_id AND r.status = 1

这样优化器就没理由消除了,因为连接的时候右表已经过滤过了,外连接的特性保持住了。

第二条,如果不需要保留左表所有行,直接改成INNER JOIN。

有时候开发写LEFT JOIN只是习惯,其实业务上不需要左表全保留。比如查"有退款且状态为1的订单",用INNER JOIN更准确,执行计划也清晰。

sql 复制代码
-- 语义明确,优化器不会乱动
INNER JOIN refunds r ON o.id = r.order_id AND r.status = 1

还有(+)语法,能改就改成标准的LEFT JOIN,然后把条件放ON里。

为什么金仓要做这个优化

说实话,外连接消除本身是个好东西。LEFT JOIN比INNER JOIN重,这是常识。如果能安全地转成INNER JOIN,性能提升是实打实的。

金仓的优化器会判断WHERE条件能不能把外连接生成的NULL行全部过滤掉。如果能,就转。不能,就不转。

逻辑上没毛病,问题出在"安全"的判断标准上。优化器判断的是数学等价,开发者关心的是业务语义。数学上等价,不代表业务上等价。

金仓在这块做了不少努力,支持Oracle的(+)语法、支持多种外连接形式,就是为了让迁移更平滑。但这个优化器行为差异,确实是迁移中容易忽略的点。

后来我总结了几条经验

现在每次做Oracle到金仓的迁移,我都会提前做几件事:

迁移前先扫一遍所有的外连接,把右表的WHERE条件找出来,该改的提前改掉。别等上线了才发现。

上线前数据量核对不能省。找几条边界数据验证一下,左表有右表无的、右表有左表无的、两边都有的,至少各取几条跑一下,确认结果集数量对得上。

执行计划要养成看的习惯。外连接有没有被消掉,计划里一眼就能看出来。看不懂执行计划,被坑了都不知道。

那次报表数据对不上的事,虽然最后改过来了,但上线当天搞得手忙脚乱。后来每次写LEFT JOIN,我都会多看一眼WHERE条件里有没有右表的列。这个习惯,算是那次经历留给我的教训。

相关推荐
aramae1 小时前
C++11:现代C++的里程碑
c语言·开发语言·c++·windows·git·后端
AZaLEan__1 小时前
CSS文档流相关:BFC
前端·css
小林ixn2 小时前
Vue3接入DeepSeek流式输出,把“二进制碎片”和“局部热更新”扒个精光
前端·vue.js·人工智能
无相求码2 小时前
为什么你的第二个 scanf 还没输入就直接跳过了?C语言输入缓冲区陷阱
c语言·后端
叼馒女友郭芙蓉2 小时前
Function Calling理解
后端
玉宇夕落2 小时前
LangChain RAG Retriever 的学习
后端
2501_912784082 小时前
Taocarts 前端国际化实战:多语言与多币种怎么落地不翻车
前端·多语言·taocarts
用户298698530142 小时前
Python 一键导出 Excel 中的图表和形状为图片
后端·python·excel
用户8130077816372 小时前
PageHelper分页查询步骤
后端