前段時間,優化一張報表,在優化一條SQL查詢時候,出現這樣一個問題:
結果列中,有某一列因爲條件關聯原因,查詢的結果會報:
ORA-01427: single-row subquery returns more than one row,顧名思義就是查詢的結果出現多行了,與其他列保持不一致。
對於這種狀況,之前開發的邏輯是這樣的:先查詢除了此列外的所有結果,然後再把此列的結果一一Reset到報表裏。
這樣無疑增加了很大的time cost,剛開始因爲數據量不多,還沒出現速度上的隱患,隨時間推移,便暴露出來了。對此做法,以如今pro 數據增長速度堅決不可取。
後來在網上找了例子,可以把多行數據合併成一行顯示,查詢速度快得驚人。當時沒有好好研究其原理,現在有空摸索下其實現邏輯。
Template SQL:
SELECT
PICKING_KEY,TRANSLATE (LTRIM (text, '/'), '*/', '*,') RESULT
FROM (SELECT ROW_NUMBER () OVER (PARTITION BY PICKING_KEY ORDER BY PICKING_KEY,lvl DESC) rn,
PICKING_KEY,
text
FROM (SELECT PICKING_KEY,
LEVEL lvl,
SYS_CONNECT_BY_PATH (ship_mark, '/') text
FROM (SELECT
ship_mark AS ship_mark,
PICKING_KEY,
ROW_NUMBER () OVER (PARTITION BY PICKING_KEY ORDER BY PICKING_KEY, ship_mark) x
FROM
( SELECT
DISTINCT ship_mark AS ship_mark,
PICKING_KEY
FROM con_pack_carton, con_pack_order
WHERE CON_PACK_CARTON.CON_PACK_KEY = CON_PACK_ORDER.CON_PACK_KEY
AND CON_PACK_ORDER.PICKING_KEY in ('0002927779')
AND CON_PACK_ORDER.BATCH_NO = 1
ORDER BY ship_mark,PICKING_KEY)
ORDER BY ship_mark, PICKING_KEY)
CONNECT BY PRIOR PICKING_KEY = PICKING_KEY AND PRIOR x - 1 = x)
)
WHERE rn = 1;
結果:
圖1
上圖的效果,就是我想得到的結果。
雖然結果很簡單,但是實現的SQL嵌套5層邏輯,涉及的知識可不少,如層次化查詢、窗口函數、替換函數和刪除函數。
作者認爲,把這條SQL摸索透徹,差不多把SQL函數好好複習了一遍,OMG。
開始一層一層開始解析。
第一層
SELECT DISTINCT ship_mark AS ship_mark, picking_key
FROM con_pack_carton, con_pack_order
WHERE con_pack_carton.con_pack_key = con_pack_order.con_pack_key
AND con_pack_order.picking_key IN ('0002927779')
AND con_pack_order.batch_no = 1
ORDER BY ship_mark, picking_key
結果:
圖2
這層是源數據,以PICKING_KEY爲準,合併所有SHIP_MARK值,這是我想要的結果,如圖1
第二層
SELECT ship_mark AS ship_mark, picking_key,
ROW_NUMBER () OVER (PARTITION BY picking_key ORDER BY picking_key,
ship_mark) x
FROM (SELECT DISTINCT ship_mark AS ship_mark, picking_key
FROM con_pack_carton, con_pack_order
WHERE con_pack_carton.con_pack_key =
con_pack_order.con_pack_key
AND con_pack_order.picking_key IN ('0002927779')
AND con_pack_order.batch_no = 1
ORDER BY ship_mark, picking_key)
ORDER BY ship_mark, picking_key
結果:
圖3
這層很簡單,也就是賦予一個順序值。關鍵一點使用到窗口函數ROW_NUMBER () OVER (PARTITION BY PICKING_KEY ORDER BY PICKING_KEY, ship_mark),
平常幾乎不用,現在認識它,以後就是我的朋友了。
第三層
SELECT picking_key, LEVEL lvl, SYS_CONNECT_BY_PATH (ship_mark, '/') text
FROM (SELECT ship_mark AS ship_mark, picking_key,
ROW_NUMBER () OVER (PARTITION BY picking_key ORDER BY picking_key,
ship_mark) x
FROM (SELECT DISTINCT ship_mark AS ship_mark, picking_key
FROM con_pack_carton, con_pack_order
WHERE con_pack_carton.con_pack_key =
con_pack_order.con_pack_key
AND con_pack_order.picking_key IN
('0002927779')
AND con_pack_order.batch_no = 1
ORDER BY ship_mark, picking_key)
ORDER BY ship_mark, picking_key)
CONNECT BY PRIOR picking_key = picking_key AND PRIOR x - 1 = x
結果:
圖4
這層最複雜,關鍵點再層次化查詢及金字塔式合併字符串,最終的目的就是把所有數據合併起來(雖然會把其他亂七八糟數據帶出來)。
CONNECT BY PRIOR PICKING_KEY = PICKING_KEY AND PRIOR x - 1 = x這句SQL的意思大概這麼講:凡是符合繼承關係的,均羅列出來。
如員工屬於哪個小組,小組屬於哪個部門,部門屬於哪事業羣。具體的用法大家看書吧,我在這裏也不好講清楚。
至於爲什麼PICKING_KEY = PICKING_KEY AND x - 1 = x,我也不是很清楚作者爲什麼這麼寫,我想最終的目的實現繼承的關係吧。
第四層
SELECT ROW_NUMBER () OVER (PARTITION BY picking_key ORDER BY picking_key,
lvl DESC) rn,
picking_key, text
FROM (SELECT picking_key, LEVEL lvl,
SYS_CONNECT_BY_PATH (ship_mark, '/') text
FROM (SELECT ship_mark AS ship_mark, picking_key,
ROW_NUMBER () OVER (PARTITION BY picking_key ORDER BY picking_key,
ship_mark) x
FROM (SELECT DISTINCT ship_mark AS ship_mark,
picking_key
FROM con_pack_carton, con_pack_order
WHERE con_pack_carton.con_pack_key =
con_pack_order.con_pack_key
AND con_pack_order.picking_key IN
('0002927779')
AND con_pack_order.batch_no = 1
ORDER BY ship_mark, picking_key)
ORDER BY ship_mark, picking_key)
CONNECT BY PRIOR picking_key = picking_key AND PRIOR x - 1 = x)
結果:
圖5
經過上層精心處理後,我們想要的結果已經快出來了,這層我們只是讓它排序,再次用到
函數ROW_NUMBER () OVER (PARTITION BY PICKING_KEY ORDER BY PICKING_KEY,lvl DESC)
第五層
SELECT
PICKING_KEY,TRANSLATE (LTRIM (text, '/'), '*/', '*,') RESULT
FROM (SELECT ROW_NUMBER () OVER (PARTITION BY PICKING_KEY ORDER BY PICKING_KEY,lvl DESC) rn,
PICKING_KEY,
text
FROM (SELECT PICKING_KEY,
LEVEL lvl,
SYS_CONNECT_BY_PATH (ship_mark, '/') text
FROM (SELECT
ship_mark AS ship_mark,
PICKING_KEY,
ROW_NUMBER () OVER (PARTITION BY PICKING_KEY ORDER BY PICKING_KEY, ship_mark) x
FROM
( SELECT
DISTINCT ship_mark AS ship_mark,
PICKING_KEY
FROM con_pack_carton, con_pack_order
WHERE CON_PACK_CARTON.CON_PACK_KEY = CON_PACK_ORDER.CON_PACK_KEY
AND CON_PACK_ORDER.PICKING_KEY in ('0002927779')
AND CON_PACK_ORDER.BATCH_NO = 1
ORDER BY ship_mark,PICKING_KEY)
ORDER BY ship_mark, PICKING_KEY)
CONNECT BY PRIOR PICKING_KEY = PICKING_KEY AND PRIOR x - 1 = x)
)
WHERE rn = 1;
最終結果:
圖6
最後一層了,現在需要把'/'字符去掉,還有取RN=1的數據,一切的一切浮出水面。
TRANSLATE這個函數我還是第一回撞見,用Oracle都兩年了,OMG。剛開始看到時,TRANSLATE這不是“翻譯”的意思嗎,在這裏幹嘛用?後來再次翻譯,也可以是轉換的意思 -_-。
今天是馬丁·路德·金紀念日,還記得他那句:我有個夢想嗎?
I have a dream, make a nice extraordinary life