从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条件里有没有右表的列。这个习惯,算是那次经历留给我的教训。