我們討論過(guò)代碼編寫(xiě)的難和繁的原理問(wèn)題,現(xiàn)在關(guān)注性能問(wèn)題,運(yùn)行速度當(dāng)然是非常重要的事情。
我們知道,軟件不能改變硬件的性能,CPU 和硬盤(pán)該多快就多快。不過(guò),我們可以設(shè)計(jì)出低復(fù)雜度的算法,也就是計(jì)算量更小的算法,計(jì)算機(jī)執(zhí)行的動(dòng)作變少,自然也就會(huì)快了。本來(lái)要做 1 億次運(yùn)算,如果有個(gè)好算法能把計(jì)算量降低到 100 萬(wàn)次,那快出 100 倍就不奇怪了。但是,光想出算法還不夠,還要把這個(gè)算法實(shí)實(shí)在在地用某種程序語(yǔ)言寫(xiě)出來(lái),否則計(jì)算機(jī)不會(huì)執(zhí)行。
然而,如果采用的程序語(yǔ)言不給力,就有可能真地寫(xiě)不出來(lái),這時(shí)候就干瞪眼忍受低速度。
SQL 就會(huì)經(jīng)常發(fā)生這種事。我們用這個(gè)簡(jiǎn)單例子來(lái)說(shuō)明:從 1 億條數(shù)據(jù)中取出前 10 名,用 SQL 寫(xiě)出來(lái)并不復(fù)雜:
SELECT TOP 10 * FROM Orders ORDER BY Amount DESC
這個(gè)語(yǔ)句里有個(gè) ORDER BY 的字樣,對(duì)應(yīng)的執(zhí)行邏輯就是要對(duì) 1 億條數(shù)據(jù)做大排序。我們知道,排序是個(gè)非常慢的動(dòng)作,要把數(shù)據(jù)遍歷多次(具體復(fù)雜度是 N*logN 次 ),如果數(shù)據(jù)量大到內(nèi)存裝不下,那還需要外存做緩存,性能還會(huì)急劇下降。如果嚴(yán)格按這句 SQL 描述的邏輯去執(zhí)行,這個(gè)運(yùn)算無(wú)論如何是跑不快的。
其實(shí),我們知道,只是取前 10 名并不需要將所有數(shù)據(jù)做大排序,只要在遍歷時(shí)始終保持一個(gè)前 10 名的小集合,遍歷過(guò)程中不斷地修正這個(gè)小集合,一次遍歷可以了,復(fù)雜度是 N*log10,和 log1億相比差了 8 倍左右,也就是計(jì)算量能小 8 倍,而且這種算法完全不需要外存做緩存。
遺憾的是,用 SQL 無(wú)法描述這樣的計(jì)算過(guò)程,只能寫(xiě)成上面那個(gè)樣子,然后指望數(shù)據(jù)庫(kù)去優(yōu)化。所幸,幾乎所有數(shù)據(jù)庫(kù)都會(huì)優(yōu)化這個(gè)句子,沒(méi)有傻到去做大排序了,所以也能跑得比較快。
但是,情況再?gòu)?fù)雜一些會(huì)怎樣呢?比如我們把需求改成計(jì)算分組內(nèi)的前 10 名,SQL 寫(xiě)出來(lái)是這樣的:
SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY Area ORDER BY Amount DESC) rn FROM Orders ) WHERE rn<=10
這和剛才針對(duì)全集取前 10 名的寫(xiě)法相差就很大了,這是我們之前說(shuō)的缺失離散性導(dǎo)致的。要先借助窗口函數(shù)造一個(gè)組內(nèi)序號(hào)出來(lái),再用子查詢(xún)過(guò)濾出符合條件的記錄,也就是有點(diǎn)“繞”了。
無(wú)論如何,這里還是有 ORDER BY 字樣,其中的運(yùn)算邏輯還是要排序。我們實(shí)際測(cè)試發(fā)現(xiàn),在 Oracle 中,同樣的數(shù)據(jù)量,計(jì)算這種分組前 10 名要比上面那個(gè)全集前 10 名慢出幾十倍,按說(shuō)多個(gè)分組應(yīng)該只慢一點(diǎn)點(diǎn)才對(duì)。Oracle 有很大可能性真地去做了排序甚至是外存排序(當(dāng)然我們沒(méi)讀過(guò) Oracle 的源代碼并不能確定),數(shù)據(jù)庫(kù)優(yōu)化引擎在這種稍復(fù)雜的情況下就暈掉了,只能老老實(shí)實(shí)按 SQL 寫(xiě)的邏輯去執(zhí)行,性能就會(huì)陡降。
當(dāng)然,也許哪天數(shù)據(jù)庫(kù)又進(jìn)化了,碰到這種情況也會(huì)優(yōu)化了(事實(shí)上確實(shí)有了)。但總有更復(fù)雜的情況,而數(shù)據(jù)庫(kù)優(yōu)化引擎的進(jìn)化速度是非常慢的。
比如這么一個(gè)更復(fù)雜的需求,做電商系統(tǒng)中的漏斗分析,計(jì)算每一步動(dòng)作后的用戶(hù)流失率。下面是一個(gè)實(shí)際業(yè)務(wù)中發(fā)生的三步漏斗計(jì)算,SQL 會(huì)寫(xiě)成這樣:
WITH e1 AS ( SELECT userid, visittime AS step1_time, MIN(sessionid) AS sessionid, 1 AS step1 FROM defined_events e1 JOIN eventgroup ON eventgroup.id = e1.eventgroup WHERE visittime >= DATE_ADD(arg_date,INTERVAL -14 day) AND visittime < arg_date AND eventgroup.name='SiteVisit' GROUP BY userid,visittime ), e2 AS ( SELECT e2.userid, MIN(e2.sessionid) AS sessionid, 1 AS step2, MIN(visittime) AS step2_time, MIN(e1.step1_time) AS step1_time FROM defined_events e2 JOIN e1 ON e1.sessionid = e2.sessionid AND visittime > step1_time JOIN eventgroup ON eventgroup.id = e2.eventgroup WHERE visittime < DATE_ADD(step1_time ,INTERVAL +1 day) AND eventgroup.name = 'ProductDetailPage' GROUP BY e2.userid ), e3 AS ( SELECT e3.userid, MIN(e3.sessionid) AS sessionid, 1 AS step3, MIN(visittime) AS step3_time, MIN(e2.step1_time) AS step1_time FROM defined_events e3 JOIN e2 ON e2.sessionid = e3.sessionid AND visittime > step2_time JOIN eventgroup ON eventgroup.id = e3.eventgroup WHERE visittime < DATE_ADD(step1_time ,INTERVAL +1 day) AND (eventgroup.name = 'OrderConfirmationType1') GROUP BY e3.userid ) SELECT s.devicetype AS devicetype, COUNT(DISTINCT CASE WHEN funnel_conversions.step1 IS NOT NULL THEN funnel_conversions.step1_userid ELSE NULL END) AS step1_count, COUNT(DISTINCT CASE WHEN funnel_conversions.step2 IS NOT NULL THEN funnel_conversions.step2_userid ELSE NULL END) AS step2_count, COUNT(DISTINCT CASE WHEN funnel_conversions.step3 IS NOT NULL THEN funnel_conversions.step3_userid ELSE NULL END) AS step3_count, COUNT(DISTINCT CASE WHEN funnel_conversions.step3 IS NOT NULL THEN funnel_conversions.step3_userid ELSE NULL END) / COUNT(DISTINCT CASE WHEN funnel_conversions.step1 IS NOT NULL THEN funnel_conversions.step1_userid ELSE NULL END) AS step3_rate FROM ( SELECT e1.step1_time AS step1_time, e1.userid AS userid, e1.userid AS step1_userid, e2.userid AS step2_userid,e3.userid AS step3_userid, e1.sessionid AS step1_sessionid, step1, step2, tep3 FROM e1 LEFT JOIN e2 ON e1.userid=e2.userid LEFT JOIN e3 ON e2.userid=e3.userid ) funnel_conversions LEFT JOIN sessions s ON funnel_conversions.step1_sessionid = s.id GROUP BY s.devicetype
這個(gè) SQL“繞”得很?chē)?yán)重了,看懂非常費(fèi)勁,擺在這里就是感受一下。這還只是三步,想再多算幾步還得寫(xiě)更多子查詢(xún),那就擺不出來(lái)了。這種復(fù)雜的 SQL,真想不出這能怎么做優(yōu)化。結(jié)果,這句 SQL 在 Snwoflake 的 Medium 集群上(4 節(jié)點(diǎn))跑了 3 分鐘沒(méi)出來(lái),用戶(hù)只能放棄。
所以,不能也不要指望數(shù)據(jù)庫(kù)優(yōu)化,還是要自己寫(xiě)出高性能算法出來(lái)。
那么,什么樣的程序語(yǔ)言才能寫(xiě)出剛才說(shuō)的這些算法呢?
很多,C++,Java 這些都可以。理論上用 SQL 寫(xiě)的存儲(chǔ)過(guò)程也可以,但工程實(shí)現(xiàn)的結(jié)果還是會(huì)太慢,這還是因?yàn)槿笔щx散性。比如 TopN 問(wèn)題,在 SQL 中要保存那 10 個(gè)成員的小集合也得用臨時(shí)表,很難寫(xiě)也很慢。
那就不用 SQL,用 C++,Java 這些去寫(xiě)好了。
這又回到我們之前說(shuō)的集合化特性了,這些語(yǔ)言缺乏集合化,寫(xiě)出來(lái)很繁瑣。像分組 TopN,如果要考慮各種數(shù)據(jù)類(lèi)型、多個(gè)分組鍵、還可能帶著計(jì)算式,幾千行也未必寫(xiě)得出來(lái)。而漏斗分析這種復(fù)雜運(yùn)算,還要考慮大數(shù)據(jù)存儲(chǔ)問(wèn)題,寫(xiě)個(gè)幾萬(wàn)行也很正常。開(kāi)發(fā)成本極高,以后維護(hù)時(shí)也累死人。
這樣,在現(xiàn)實(shí)中就沒(méi)有可操作性了。
所以,代碼能寫(xiě)著簡(jiǎn)單就變得非常有意義了。一方面是短小,這意味著工作量少,另一方面還要容易,這意味著更多的程序員可以寫(xiě)。從這個(gè)角度上看,寫(xiě)著簡(jiǎn)單和跑得快是一回事。想跑得快,就是要有一種程序語(yǔ)言能讓高性能算法寫(xiě)著簡(jiǎn)單,這才有可操作性。
這樣的標(biāo)準(zhǔn),可能只有 esProc SPL 適合了,它同時(shí)擁有集合化和離散性?xún)煞N特性,又提供相當(dāng)多固有的高性能算法庫(kù)函數(shù),這才能寫(xiě)著簡(jiǎn)單,從而跑得快。
前面的前 10 名問(wèn)題用 SPL 寫(xiě)出來(lái)是這樣的:
Orders.groups(;top(10;-Amount) Orders.groups(Area;top(10;-Amount))
SPL 把 TopN 理解為聚合計(jì)算,這個(gè)語(yǔ)句中沒(méi)有排序的字樣,也就不會(huì)做大排序,而采用剛才說(shuō)的快速算法了。而且,這里分組前 10 名和全集前 10 名的寫(xiě)法基本一樣,只是多了分組鍵。這也是在集合化的基礎(chǔ)上支持了離散性的結(jié)果。
漏斗分析用 SPL 寫(xiě)出來(lái)是這樣:
A | |
1 | =now() |
2 | =eventgroup=file("eventgroup.btx").import@b() |
3 | =devicetype=file("devicetype.btx").import@b() |
4 | =long(elapse(arg_date,-14)) |
5 | =long(arg_date) |
6 | =long(arg_date+1) |
7 | =A2.(case(NAME,"SiteVisit":1,"ProductDetailPage":2,"OrderConfirmationType1":3;null)) |
8 | =file("defined_events.ctx").open() |
9 |
=A8.cursor@m(USERID,SESSIONID,VISITTIME,EVENTGROUPNO;VISITTIME>=A4 && VISITTIME |
10 | =sessions=file("sessions.ctx").open().cursor@m(USERID,ID,DEVICETYPENO;;A9) |
11 | =A9.joinx@m(USERID:SESSIONID,A10ID,DEVICETYPENO) |
12 | =A11.group(USERID) |
13 |
=A12.new(~.align@a(3,EVENTGROUPNO):e,e(1).select(VISITTIME |
14 |
=A13.run(e=join@m(e1:e1,SESSIONID;e2:e2,SESSIONID).select(e2=e2.select(VISITTIME>e1.VISITTIME && VISITTIME |
15 |
=A14.run(e0=e1.id(DEVICETYPENO),e1=e.min(e1.VISITTIME),e2=e.min(e2),e=e.min(e1.SESSIONID),e3=e3.select(SESSIONID==e && VISITTIME>e2 && VISITTIME |
16 | =A15.news(e;~:DEVICETYPE,e2,e3) |
17 | =A16.groups(DEVICETYPE;count(1):STEP1_COUNT,count(e2):STEP2_COUNT,count(e3):STEP3_COUNT,null:STEP3_RATE) |
18 | =A17.run(DEVICETYPE=devicetype.m(DEVICETYPE).DEVICETYPE,STEP3_RATE=STEP3_COUNT/STEP1_COUNT) |
19 | =interval@s(A1,now()) |
這個(gè)代碼要比前面的 SQL 更短,而且更靈活,再增加幾步也還是這段代碼。實(shí)測(cè)的結(jié)果,這段代碼用單臺(tái)服務(wù)器 10 秒就跑完了。 有了集合化和離散性的 SPL,才能做到寫(xiě)著簡(jiǎn)單又跑得快。
本文來(lái)源:github.com
審核編輯:湯梓紅
-
cpu
+關(guān)注
關(guān)注
68文章
10855瀏覽量
211605 -
計(jì)算機(jī)
+關(guān)注
關(guān)注
19文章
7489瀏覽量
87872 -
SQL
+關(guān)注
關(guān)注
1文章
762瀏覽量
44117 -
程序
+關(guān)注
關(guān)注
117文章
3785瀏覽量
81008
原文標(biāo)題:寫(xiě)著簡(jiǎn)單和跑得快是一回事,SQL 為什么不可能跑得快?
文章出處:【微信號(hào):芋道源碼,微信公眾號(hào):芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論