傳說阿里禁止使用存儲過程?
先說優(yōu)點
畢竟還是有不少人在用,總要有點好處。
SQL 過程化
很少人提及存儲過程的這個優(yōu)點,似乎是認(rèn)為理所當(dāng)然。SQL 的語法要求數(shù)據(jù)處理必須寫成一句,不管嵌套幾層、用多少子查詢,這對復(fù)雜數(shù)據(jù)處理簡直是災(zāi)難。而存儲過程讓 SQL 也能支持分步計算,雖然是多個獨立 SQL 語句拼接、雖然可能要頻繁寫臨時表、雖然… ,但至少過程化解決了多少人的數(shù)據(jù)處理困難。
但是,過程計算并不是存儲過程的專利,使用 Java 來指揮 SQL 也一樣可以,僅此一條優(yōu)點并不足以導(dǎo)致程序員偏愛存儲過程。
界面與邏輯分離
界面與邏輯分離是現(xiàn)代應(yīng)用開發(fā)的一個基本準(zhǔn)則。相對于后臺數(shù)據(jù)處理邏輯,界面會有更多樣性的環(huán)境,如 PC、手機(jī)等,而且業(yè)務(wù)穩(wěn)定性也不強(qiáng),經(jīng)常會改。如果能把兩者分離,開發(fā)和維護(hù)界面時不必綁著數(shù)據(jù)處理邏輯一起改,成本就低很多。
使用存儲過程能實現(xiàn)界面與邏輯分離。存儲過程在后臺數(shù)據(jù)庫中運算,只要向前端提供數(shù)據(jù),而不必關(guān)心界面的形式和異動。把所有的數(shù)據(jù)處理邏輯都寫成存儲過程,還有利于統(tǒng)一數(shù)據(jù)的出入口,易于實現(xiàn)數(shù)據(jù)權(quán)限管控。
再說一遍“但是”,實現(xiàn)界面與邏輯分離也不是存儲過程的專利。只要做一個數(shù)據(jù)訪問層,所有數(shù)據(jù)的進(jìn)出都通過這個訪問層,也會有同樣效果,事實上也確實有些應(yīng)用是這么做的,但在微服務(wù)流行以前并不普遍。這個原因在于開發(fā)復(fù)雜度上,處理結(jié)構(gòu)化數(shù)據(jù)寫 SQL 總比寫 Java 要簡單多得多。而微服務(wù)架構(gòu)強(qiáng)制在 Java 中實現(xiàn)數(shù)據(jù)處理邏輯的機(jī)制,其實是犧牲了開發(fā)效率。
性能好
通常,使用存儲過程會比在庫外用 Java 指揮 SQL 完成數(shù)據(jù)處理性能更好一點。這個原因主要在于數(shù)據(jù)不出庫。外部程序訪問庫內(nèi)數(shù)據(jù)時必須通過數(shù)據(jù)庫提供的接口,而這些接口的性能大都不好,特別是面向 Java 程序的 JDBC 接口。每次發(fā)出 SQL 讓數(shù)據(jù)庫執(zhí)行都會調(diào)用這個接口,速度就上不去。如果應(yīng)用程序和數(shù)據(jù)庫不在同一臺物理機(jī)器上時,還會有一些網(wǎng)絡(luò)延遲,不過和接口的低性能相比并不算嚴(yán)重。在外部計算時,從數(shù)據(jù)庫獲取數(shù)據(jù)的時間常常會超過計算本身的時間。
存儲過程的性能好主要得益于數(shù)據(jù)庫低效的訪問接口。
總結(jié)一下。存儲過程的優(yōu)點主要來源于兩方面:一是 SQL 的數(shù)據(jù)處理能力,至少目前來看結(jié)構(gòu)化數(shù)據(jù)處理尤其是復(fù)雜計算,SQL 仍然比 Java 強(qiáng)很多。二是庫內(nèi)計算的便利,數(shù)據(jù)不用出庫省下的 IO 成本對數(shù)據(jù)密集型任務(wù)有莫大優(yōu)勢。
再說缺點
傳說阿里有一條軍規(guī),“禁止使用存儲過程,存儲過程難以調(diào)試和擴(kuò)展,更沒有移植性”。我們這就來羅列一下存儲過程的缺點。
移植性差
存儲過程的移植確實很困難,一般業(yè)務(wù)邏輯復(fù)雜到需要寫存儲過程的地步,總會不可避免地用到數(shù)據(jù)庫獨有的特性和語法,更換數(shù)據(jù)庫時這部分代碼就需要重寫。如果只是簡單地替換函數(shù)名和參數(shù)規(guī)則(如日期轉(zhuǎn)換等),那成本還不高;如果用到了新數(shù)據(jù)庫不支持的某種特性,那還要重新設(shè)計算法來編寫計算邏輯;如果還要再兼顧性能因素,有時候就會是個不可能完成的任務(wù)了。
調(diào)試?yán)щy
編輯調(diào)試是個大問題,存儲過程的開發(fā)一直缺少有效的 IDE 環(huán)境。SQL 本身經(jīng)常很長,調(diào)試式要把句子拆開分別獨立執(zhí)行,非常麻煩。存儲過程中也常常有大量的長 SQL,當(dāng)然也有同樣的問題。即使是分步的運算,因為沒有好的 IDE 支持,要看哪一步出錯,也要把中間結(jié)果輸出才行,仍然是非常麻煩。
存儲過程的調(diào)試功能近年來略有起色,但離流暢實用始終差距甚遠(yuǎn),數(shù)據(jù)庫廠商似乎也無心解決。這和 Java 等擁有成熟的開發(fā)環(huán)境完全不可同日而語。困難的調(diào)試自然會導(dǎo)致低下的開發(fā)效率。
體系封閉
說到封閉性,其實是數(shù)據(jù)庫的問題。數(shù)據(jù)庫有“庫”的概念,外部數(shù)據(jù)只有入庫才能計算。而現(xiàn)代應(yīng)用數(shù)據(jù)源眾多,臨時轉(zhuǎn)入的效率很低(因為數(shù)據(jù)庫的 IO 成本高),很可能跟不上訪問需求,定時批量轉(zhuǎn)入又很難獲得最新的數(shù)據(jù),同樣影響計算結(jié)果的實時性。同時,ETL 往往有時間窗口(如當(dāng)天夜里到第二天凌晨),趕上業(yè)務(wù)繁忙的時候還可能因為時間窗口不足無法完成 ETL 工作而影響第二天的業(yè)務(wù)。
不僅如此,把外部數(shù)據(jù)存儲在數(shù)據(jù)庫中,又會形成眾多中間表,面臨中間表的各種問題。而且有些互聯(lián)網(wǎng)上取過來的數(shù)據(jù)常常是多層的 json 或 XML 格式,在關(guān)系數(shù)據(jù)庫中還要建立多個關(guān)聯(lián)的表來存儲,會進(jìn)一步加劇中間表的問題,占用過多寶貴的數(shù)據(jù)庫空間。
存儲過程在數(shù)據(jù)庫中運算自然繼承了封閉性的特點,想混合計算外部數(shù)據(jù)很不方便。
耦合性高
存儲過程通常是為前端應(yīng)用服務(wù)的,理論上兩者應(yīng)該在一起,從而組成完整的業(yè)務(wù)功能點。但存儲過程與數(shù)據(jù)庫緊密耦合,所以實踐中存儲過程與前端應(yīng)用是物理分離的,且無法使用統(tǒng)一的技術(shù)路線。對于同一個功能點的存儲過程和前端應(yīng)用,維護(hù)其中一處,通常就要維護(hù)另一處,但兩者物理上分離,維護(hù)因此變得很困難,而不統(tǒng)一的技術(shù)路線則加劇了這種困難。
存儲過程與數(shù)據(jù)庫緊密耦合,反而與前端應(yīng)用分離,這就容易使同一個存儲過程被多個前端應(yīng)用共享。時間一長,哪個存儲過程到底被哪些應(yīng)用調(diào)用就變成了謎團(tuán)。如果某個應(yīng)用的計算發(fā)生變化,面對謎團(tuán)一般的共享調(diào)用關(guān)系,管理員只能新建存儲過程而不敢修改原存儲過程。這樣惡性循環(huán)下去,存儲過程越來越多,謎團(tuán)越來越大,終將變得不可收拾。
管理困難
存儲過程的目錄是扁平的,而不是文件系統(tǒng)那樣的樹形結(jié)構(gòu),腳本少的時候還好辦,一旦多起來,目錄就會陷入混亂??梢韵胂?,多個項目的存儲過程、同一個項目不同模塊的存儲過程、同一模塊不同年份或不同版本的存儲過程,這些如果混雜在同一個目錄下,區(qū)分起來會非常困難,除非加大管理幅度,比如按項目、模塊、年代、版本命名,這顯然又會影響開發(fā)效率。有些項目管理較弱的團(tuán)隊,在開發(fā)周期緊張時,常常就顧不得這些規(guī)矩了,先趕著讓項目上線再說,而上線之后這些遺留問題又容易被忘掉,結(jié)果常常一直存在很久。
安全性差
很多存儲過程是為查詢分析服務(wù)的,而這類業(yè)務(wù)的需求經(jīng)常在變,由于存儲過程與數(shù)據(jù)庫緊密結(jié)合,所以程序員每次修改存儲過程代碼之后,都要提交給數(shù)據(jù)庫管理員,由管理員編譯并發(fā)布,無疑會大大增加管理員的工作負(fù)擔(dān)。所以通常的做法是:給程序員賦予高級權(quán)限,至少也是創(chuàng)建存儲過程的權(quán)限,這樣就不用頻繁打擾管理員了。這樣做雖然方便,但存在嚴(yán)重的安全隱患。本來做報表查詢只需要對數(shù)據(jù)庫有“讀”權(quán)限,而可以編譯存儲過程的權(quán)限就太大了,幾乎可以做一切了。程序員如果失誤,很可能刪除或修改了數(shù)據(jù),造成嚴(yán)重的安全事故。
開源SPL充當(dāng)庫外存儲過程
從羅列的優(yōu)缺點數(shù)量上看,缺點比優(yōu)點多不少,所以就不難理解阿里的軍規(guī)了:存儲過程能不用還是不要用了。當(dāng)然了,用不用存儲過程完全是自己綜合自己的情況考慮,如人飲水冷暖自知。
其實存儲過程也并非不可替代,開源 SPL 就可以搞定存儲過程的各類缺點,同時延續(xù)其優(yōu)點, 實現(xiàn)“庫外存儲過程”。
集算器 SPL 是一款專業(yè)的開源數(shù)據(jù)計算引擎,提供不依賴數(shù)據(jù)庫的計算能力,數(shù)據(jù)庫更換不需要更改 SPL 計算腳本,解決存儲過程的移植性問題;簡潔易用的 IDE 環(huán)境編輯調(diào)試功能齊全,算法實現(xiàn)更加簡單;SPL 體系更加開放,可以直接使用多樣數(shù)據(jù)源計算;“外置存儲過程”不依賴數(shù)據(jù)庫,可隨應(yīng)用存放解決耦合性問題;借助文件系統(tǒng)的樹狀結(jié)構(gòu)進(jìn)一步解決管理問題;SPL 獨立數(shù)據(jù)庫運行,更不會帶來安全問題。
SPL 在庫外實現(xiàn)存儲過程,不再依賴數(shù)據(jù)庫,這樣原來綁定數(shù)據(jù)庫帶來的各種問題也就解決了。
直觀易用的開發(fā)環(huán)境
相對存儲過程的編輯調(diào)試?yán)щy,SPL 提供了簡潔易用的開發(fā)環(huán)境,單步執(zhí)行、設(shè)置斷點,所見即所得的結(jié)果預(yù)覽窗口…,開發(fā)效率更高。
庫外計算降低耦合性,提升移植性與安全性
SPL 提供了不依賴數(shù)據(jù)庫的計算能力,在庫外實施計算。原來不得不依賴存儲過程的兩個能力(計算和分步)完全可以使用 SPL 替代,實現(xiàn)“庫外存儲過程”。這樣原本緊耦合在數(shù)據(jù)庫中的計算邏輯可以完全獨立到應(yīng)用中,從而降低與數(shù)據(jù)庫的耦合性。
與數(shù)據(jù)庫解耦以后,數(shù)據(jù)庫變化無需修改 SPL 的計算邏輯,可以做到輕松移植。同時,SPL 實現(xiàn)的“庫外存儲過程”創(chuàng)建和使用不需要對數(shù)據(jù)庫有寫權(quán)限,使用存儲過程帶來的安全性問題就可以徹底避免。
在運維方面,SPL 是解釋執(zhí)行的,天然支持熱切換??梢院芎眠m應(yīng)微服務(wù)架構(gòu)下多變的服務(wù)修改需求,應(yīng)用修改不需要重啟即時生效。
數(shù)據(jù)處理邏輯位于 SPL 文件(.splx)中,修改后實時生效,相對 Java 等編譯型語言需要重啟服務(wù)有很大優(yōu)勢。
在管理方面,SPL 文件使用文件系統(tǒng)管理機(jī)制,樹狀結(jié)構(gòu)可以很清晰地存放各個應(yīng)用、各個模塊的計算邏輯,不會不知道有哪些模塊在使用的情況,使用和管理都很方便。
多源支持與開放體系
不同于數(shù)據(jù)庫需要數(shù)據(jù)先入庫再計算,SPL 面對多樣性數(shù)據(jù)源時可以直接計算。數(shù)據(jù)入庫不僅時效性差,也無法保證數(shù)據(jù)的實時性。此外不同數(shù)據(jù)源有各自的優(yōu)點,文件的 IO 效率很高,NoSQL 可以存儲文檔數(shù)據(jù),RDB 計算能力較強(qiáng),數(shù)據(jù)入庫就無法享受這些優(yōu)點了。
SPL 提供了開放的數(shù)據(jù)源支持,你聽說過還是沒聽說過的數(shù)據(jù)源幾乎都能支持,不僅可以連接取數(shù),還可以進(jìn)行跨數(shù)據(jù)源混合計算。SPL 可以充分利用各類數(shù)據(jù)源的優(yōu)點后,再實施跨源計算也更加高效。
開放體系下,不再有“庫”的概念,充分利用各類數(shù)據(jù)源的特點,發(fā)揮其優(yōu)勢。
支持過程的簡潔代碼提高開發(fā)效率
存儲過程雖然支持過程計算,但 SQL 本身在實現(xiàn)復(fù)雜計算時就比較困難,這是由 SQL 的特性(缺乏離散性、集合化不徹底等)決定的,比如根據(jù)股票記錄查詢某只股票最長連續(xù)上漲天數(shù),SQL(oracle)的寫法如下:
SELECT code, MAX(ContinuousDays)-1
FROM (
SELECT code, NoRisingDays, COUNT(*) ContinuousDays
FROM (
SELECT code,
SUM(RisingFlag) OVER (PARTITION BY code ORDER BY day) NoRisingDays
FROM (
SELECT code, day,
CASE WHEN price>
LAG(price) OVER (PARTITION BY code ORDER BY day)
THEN 0 ELSE 1 END RisingFlag
FROM stock
)
) GROUP BY NoRisingDays
)
GROUP BY code
可以嘗試讀一下這個 SQL 在算什么。是不是很繞?其實按照分步解法,只需要 3 步就能搞定,根本不用這么繞來繞去。
而存儲過程是用 SQL 實現(xiàn)的自然繼承了這個缺點,我們經(jīng)常能在項目中看到上千行、上百 KB 存儲過程的情況,就是因為 SQL 對復(fù)雜計算支持不好的緣故。
SPL 不僅支持天然支持過程計算,數(shù)據(jù)處理可以分多步、按照自然思維一步一步實施,而且提供了豐富的計算類庫和敏捷語法,基于 SPL 可以更容易實現(xiàn)復(fù)雜計算。
上一段代碼看一下效果:
【計算目標(biāo)】要找出銷售額占到一半的前 n 個客戶(大客戶)的訂單情況。
A | |
1 | =file(“/opt/ods/orders.csv”).import@tc() |
2 | =A1.groups(customer;sum(amount):amount).sort(amount:-1) |
3 | =A2.sum(amount)/2 |
4 | =0 |
5 | =A2.pselect((A4=A4+amount,A4>=A3)) |
6 | =A2.(customer).to(,A5) |
7 | =A1.select(A6.pos(A1.customer)) |
通過分步的方式,先找到符合條件的大客戶,再查詢這些客戶的詳細(xì)訂單信息。這些計算都是在庫外完成的,甚至可以使用文件數(shù)據(jù)源。從實現(xiàn)的過程來看,SPL 的過程計算比存儲過程更加優(yōu)秀,語法也更為簡潔。
而前面提到的計算某只股票最長連續(xù)上漲天數(shù),SPL 的實現(xiàn)是這樣的:
A | |
1 | =db.query("select * from stock order by day") |
2 | =A1.group@i(price<price[-1]).max(~.len())-1 |
按交易日排好序,將連漲的記錄分到一組,然后求最大值 -1 就是最長連續(xù)上漲天數(shù)了,完全按照自然思維實現(xiàn),不用繞來繞去。SQL 雖然比 Java 在某些場合方便了點,但仍然差很多,SPL 不僅支持過程計算,實現(xiàn)復(fù)雜計算也更簡單,比 SQL 更有優(yōu)勢。
降低數(shù)據(jù)庫負(fù)擔(dān)并獲得更高性能
不過,SPL 是一種庫外計算引擎,需要將數(shù)據(jù)從數(shù)據(jù)庫中取出來再計算。這樣在涉及數(shù)據(jù)量較大時,可能會因為數(shù)據(jù)庫 IO 性能低下導(dǎo)致低性能。
這一點,SPL 也有應(yīng)對方法,可以實現(xiàn)數(shù)據(jù)外置。將數(shù)據(jù)庫中大量歷史冷數(shù)據(jù)外置到文件中,使用 SPL 讀取文件直接計算,這樣不僅可以降低數(shù)據(jù)庫負(fù)擔(dān)(數(shù)據(jù)庫不再需要承擔(dān)過多的數(shù)據(jù)存儲和數(shù)據(jù)計算工作自然壓力降低),基于文件還可以獲得更高的 IO 效率。
將大歷史數(shù)據(jù)外置后,借助 SPL 的多源混算能力,還很容易實現(xiàn) T+0 查詢。從數(shù)據(jù)庫中讀取當(dāng)期熱數(shù)據(jù),從文件中讀取歷史冷數(shù)據(jù),二者混合計算完成 T+0 全量實時數(shù)據(jù)查詢。
此外,使用 SPL 還能獲得更高的運算性能。
SPL 提供了眾多高性能數(shù)據(jù)存儲和高性能算法機(jī)制,SQL 中很難實現(xiàn)的高性能算法及存儲方案用 SPL 卻可以輕松實現(xiàn),而軟件提高性能關(guān)鍵就在于算法和存儲。
例如,SPL 可以把 TopN 理解為聚合運算,這樣可以將高復(fù)雜度的排序轉(zhuǎn)換成低復(fù)雜度的聚合運算,而且很還能擴(kuò)展應(yīng)用范圍。
A | ||
1 | =file(“data.ctx”).create().cursor() | |
2 | =A1.groups(;top(10,amount)) | 金額在前 10 名的訂單 |
3 | =A1.groups(area;top(10,amount)) | 每個地區(qū)金額在前 10 名的訂單 |
相比之下,SQL 描述 TopN 會涉及大排序,性能非常低下,只能寄希望于數(shù)據(jù)庫的優(yōu)化。但在稍復(fù)雜的情況(比如 A3 中伴隨分組運算)數(shù)據(jù)庫優(yōu)化器就會失效。
總體看來,SPL 可以作為存儲過程很好的替代和延伸。