接手一個 Oracle 遷移評估任務時,大家習慣先看語法相容性:這個儲存程序能不能跑、這個函式有沒有對應實作、這張檢視表的寫法要不要改。這些問題好查,工具能掃,跑一遍錯誤日誌就知道改哪。真正讓人頭痛的不是這些——是那些**語法完全合法、遷移後照樣能跑通、只是結果跟原來不一樣**的地方。這類問題不報錯,不會出現在任何遷移報告的「失敗項」裡,只會在業務端發現報表對不上、對帳差了幾行的時候才暴露出來,而且往往已經是上線之後了。
這次評估裡踩到兩個這樣的問題,一個是外連接消除,一個是空字串到底算不算 NULL。前者排第一,因為老系統裡到處都是 (+) 寫法的歷史 SQL,誰都說不清當初是不是故意要保留左表全量;後者第二,因為它牽扯到「KES 跟 Oracle 到底像不像」這個問題本身沒有固定答案,得看設定。環境是 KES V009R001C010,業務帳號連線:
bash 代碼解讀複製代碼ksql -h 127.0.0.1 -p 54321 -U app_user -d app_db
sql 代碼解讀複製代碼create table app_schema.t_mig_customer (
cust_id integer primary key,
cust_name varchar(50) not null,
grade_code varchar(10)
);
create table app_schema.t_mig_grade (
grade_code varchar(10) primary key,
grade_name varchar(30) not null,
grade_level integer not null
);
insert into app_schema.t_mig_customer(cust_id, cust_name, grade_code) values
(1, 'customer-a', 'G1'),
(2, 'customer-b', 'G2'),
(3, 'customer-c', null),
(4, 'customer-d', 'G3');
insert into app_schema.t_mig_grade(grade_code, grade_name, grade_level) values
('G1', 'silver', 1),
('G2', 'gold', 2);
customer-c 從來沒定過級,grade_code 是 NULL;customer-d 的 grade_code 是 G3,但等級表裡根本沒有 G3 這一級——這是很多老系統的常態,等級表維護跟不上業務擴張,歷史資料裡留著一堆對不上號的外鍵值。查「所有客戶」的場景下,這兩個客戶理論上都應該被保留,只是等級資訊顯示為空。
(+) 語法本身沒毛病,加個條件就變味了先確認 KES 對 Oracle 這個老寫法的相容程度。(+) 標在哪一邊,哪一邊就是外連接裡可以為空的一側,最基礎的寫法:
sql 代碼解讀複製代碼select c.cust_id, c.cust_name, g.grade_name
from app_schema.t_mig_customer c, app_schema.t_mig_grade g
where c.grade_code = g.grade_code(+);
四個客戶全部返回,customer-c、customer-d 的 grade_name 是空。這一步沒什麼好說的,(+) 語法接得很到位,老程式原樣搬過來完全能用,這也是 KES 主打 Oracle 相容的地方確實兌現了。
麻煩出在老系統裡更常見的寫法上:關聯條件之外,業務上往往還要再加一層過濾,比如「只看正式定級(等級值大於等於 1)的客戶」:
sql 代碼解讀複製代碼select c.cust_id, c.cust_name, g.grade_name
from app_schema.t_mig_customer c, app_schema.t_mig_grade g
where c.grade_code = g.grade_code(+)
and g.grade_level >= 1;
只返回 2 行,customer-a、customer-b。customer-c、customer-d 不見了——這條 SQL 裡明明寫著 (+),擺明了想要外連接的語義,結果卻只剩下內連接才會有的行數。
跑一下執行計畫確認這背後發生了什麼:
sql 代碼解讀複製代碼explain analyze
select c.cust_id, c.cust_name, g.grade_name
from app_schema.t_mig_customer c, app_schema.t_mig_grade g
where c.grade_code = g.grade_code(+)
and g.grade_level >= 1;
Hash Join,沒有 Left 或者 Right 字樣,純內連接。原因不難理解:g.grade_level >= 1 這個條件是加在右表上的,customer-c、customer-d 對應的 g.grade_level 是 NULL,NULL >= 1 的結果是「未知」,在 WHERE 裡等於被丟掉。既然這些因為外連接才該保留的行反正都會被這個條件濾掉,「外連接 + 這個過濾」和「內連接 + 這個過濾」結果完全一樣,最佳化器就選了成本更小的內連接去跑——這就是外連接消除,跟 (+) 寫沒寫關係不大,(+) 只是外連接的一種寫法,底層判斷邏輯跟 ANSI 的 LEFT JOIN 是同一套。
為了確認這一點,把同樣的邏輯換成 ANSI 寫法再跑一遍,作為遷移驗收階段常用的排查方式:
sql 代碼解讀複製代碼explain analyze
select c.cust_id, c.cust_name, g.grade_name
from app_schema.t_mig_customer c
left join app_schema.t_mig_grade g
on c.grade_code = g.grade_code
where g.grade_level >= 1;
同樣是 Hash Join,同樣沒有 Left 字樣,跟 (+) 寫法的表現完全一致。這也提醒做遷移驗收的人:不管老系統用的是 (+) 還是轉譯後的 LEFT JOIN,只要 WHERE 裡對右表欄位做了這種會把 NULL 判定為假的過濾,這個坑就跑不掉,光看語法轉譯對不對沒用,得拿執行計畫和行數說話。
修復辦法是把這個過濾條件挪進關聯條件裡:
sql 代碼解讀複製代碼select c.cust_id, c.cust_name, g.grade_name
from app_schema.t_mig_customer c
left join app_schema.t_mig_grade g
on c.grade_code = g.grade_code
and g.grade_level >= 1;
四行全部回來,customer-c、customer-d 的等級欄位照常顯示為空。這不是換個位置隨便試出來的,ON 決定的是「怎麼連」,先按條件篩一遍右表再去外連接;WHERE 決定的是「連完之後再篩掉什麼」,條件放這兒就已經是連接做完之後的事了,NULL 行躲不掉。
遷移驗收階段發現這類問題沒有捷徑:對每一條涉及外連接的存量 SQL,遷移前後的行數必須逐條比對,光看「能不能跑通」是不夠的。
這一節動筆之前,先把兩件事確認清楚,不能憑「KES 核心衍生自 PostgreSQL」這種直覺去猜:
sql 代碼解讀複製代碼show database_mode;
show ora_input_emptystr_isnull;
database_mode 是 oracle,ora_input_emptystr_isnull 是 on。這套環境預設走的是 Oracle 相容模式——很多人下意識覺得「核心是 PG 系的,行為應該跟 PG 一致」,這個假設在這裡是不成立的,還是要先確認相容模式,除非建庫時特別指定成 PG 模式。而 ora_input_emptystr_isnull 這個參數,顧名思義就是用來復刻 Oracle 那個經典行為:把長度為零的字串當成 NULL 處理。
實測驗證一下:
sql 代碼解讀複製代碼insert into app_schema.t_mig_customer(cust_id, cust_name, grade_code)
values (5, 'customer-e', '');
sql 代碼解讀複製代碼select
cust_id, cust_name, grade_code,
grade_code is null as is_null_flag,
length(grade_code) as code_len
from app_schema.t_mig_customer
where cust_id = 5;
插入的空字串 '',查出來 is_null_flag 是 t——被當成 NULL 存了,code_len 也是空白,不是 0。這跟很多人預想的「PostgreSQL 體系不會這樣」正好相反,KES 在 Oracle 相容模式下把 Oracle 這個特有行為原樣復刻了下來。
對遷移讀者來說,這其實是個好消息:如果老系統的 PL/SQL 或者業務程式依賴了「空字串等於 NULL」這個假設(比如用 col = '' 判斷「未填寫」),只要遷移後的 KES 還在 Oracle 相容模式下,這批程式不用改,行為跟原來一致。真正要小心的是反過來的情況——如果維運後續把資料庫切換成 PG 相容模式,或者手動關掉了這個參數,這批老程式會在沒有任何報錯的情況下悄悄失效。這也是這個坑最值得記住的地方:它不是一個寫死的結論,是隨相容模式和參數走的,遷移評估階段必須在目標環境裡把這兩項親自確認一遍,不能憑經驗預判。
LEFT/RIGHT JOIN 或 (+))的存量 SQL 做台帳登記,遷移前先跑一遍拿到基準行數。WHERE 子句裡是否有對右表(Nullable 一側)欄位的過濾,且沒有對稱寫 (+) 或沒有下推到 ON。database_mode 和 ora_input_emptystr_isnull,把「空字串算不算 NULL」這件事釘死,再決定這部分程式要不要改。