從Hash Join的執(zhí)行計(jì)劃的細(xì)節(jié)中能看到點(diǎn)啥
?HASH JOIN是大數(shù)據(jù)量表連接中最為常用的方式,與最為常用的NESTED LOOP相比,其應(yīng)用場景不同。對于兩張表的連接,NESTED LOOP適合于類似查電話號碼的應(yīng)用場景,如果領(lǐng)導(dǎo)給你一張清單,讓你去查一下幾家企業(yè)的電話號碼,那么你要做的是找到一本電話號碼本,根據(jù)公司名稱的索引,挨個(gè)查一遍,很快就可以完成了。這種方式就是著名的NESTED LOOP,通過數(shù)個(gè)快速的循環(huán),完成兩個(gè)行源的關(guān)聯(lián)操作(待查清單,電話號碼簿)。
如果這個(gè)任務(wù)改一下,領(lǐng)導(dǎo)給你的清單上有幾萬家企業(yè),那么我們還這么一條條的去查,那不傻了。這時(shí)候,就不適合用NESTED LOOP循環(huán)了,HASH JOIN是比較快速的解決方法。很多SQL的執(zhí)行計(jì)劃出現(xiàn)錯(cuò)誤,有很大一部分就是選擇錯(cuò)誤使用了NESTED LOOP和HASH JOIN。因此現(xiàn)在一些CBO的優(yōu)化器中,都有針對NESTED LOOP和HASH JOIN的主動糾偏技術(shù)。Oracle 19C的可調(diào)節(jié)執(zhí)行計(jì)劃主要就是在執(zhí)行NESTED LOOP的過程中一旦發(fā)現(xiàn)循環(huán)數(shù)量超出評估預(yù)期,則可以動態(tài)改為HASH JOIN。
剛開始就有點(diǎn)扯遠(yuǎn)了,今天我們的重點(diǎn)不是討論NL和HASH JOIN的差異,而是帶大家看看PG數(shù)據(jù)庫的HASH JOIN執(zhí)行計(jì)劃中的一些容易被忽略的點(diǎn),在查看執(zhí)行計(jì)劃的時(shí)候,如果能夠比較好的抓住這些關(guān)注點(diǎn),對于SQL優(yōu)化來說很有幫助。
可能有朋友要說了,反正都是HASH JOIN,執(zhí)行計(jì)劃都差不多,有啥可看的。那么我們來看看上面的執(zhí)行計(jì)劃里的紅框里的內(nèi)容吧,Batches :32,這個(gè)是啥意思?如果你以前是Oracle DBA,那么優(yōu)化排序、one-pass 排序,multi-pass排序的概念應(yīng)該還有印象吧。當(dāng)需要做排序或者HASH TABLE的數(shù)據(jù)量太大,超出了SORT AREA SIZE的限制,那么這次排序/HASH join就無法一次完成,必須切分為多個(gè)分區(qū),一個(gè)個(gè)的完成。在PG的HASH JOIN里,就是把HASH JOIN切分為多個(gè)BATCHES。因?yàn)槟硞€(gè)BATCH完成后需要暫存在臨時(shí)文件中,因此遇到這種情況我們一般都可以看到temp written這個(gè)內(nèi)容,這部分內(nèi)容我也用紅框標(biāo)注出來了。
這種排序區(qū)不足導(dǎo)致的問題會帶來什么樣的性能問題呢?我們來看這個(gè)例子,BATCHES:1,也就是無需通過分區(qū)完成,此時(shí)使用了4540KB的WORK_MEM。實(shí)際上我給大家演示這個(gè)案例的時(shí)候,第一個(gè)例子用了256KB的work_mem設(shè)置,當(dāng)然無法滿足4M多的內(nèi)存需求了。而第二個(gè)例子我使用了一個(gè)極大的work_mem(256MB),當(dāng)然實(shí)際上的內(nèi)存使用以執(zhí)行計(jì)劃中的為準(zhǔn)。一次性在內(nèi)存中完成HASH JOIN的好處是什么呢?當(dāng)然是執(zhí)行效率,我們可以看出第二個(gè)執(zhí)行只用了90毫秒,而分裂為32個(gè)BATCH的執(zhí)行花了239毫秒。
看到這里可能有朋友要說了,既然效果那么好,那么我們把WORK_MEM參數(shù)設(shè)的足夠大不就行了。實(shí)際上設(shè)置過大的WORK_MEM也是存在隱患的。如果我們的物理內(nèi)存不是很大,那么設(shè)置過大的WORK_MEM可能導(dǎo)致極端情況下,物理內(nèi)存過度消耗而導(dǎo)致更嚴(yán)重的問題。
WORK_MEM參數(shù)是可會話級動態(tài)設(shè)置的,如果我們的某些要做大型排序或者HASH JOIN的SQL能夠在應(yīng)用層面做設(shè)置,執(zhí)行大型SQL的時(shí)候設(shè)置一個(gè)較大的值,SQL執(zhí)行完畢RESET一下參數(shù),這樣WORK_MEM的使用效率是最高的。否則我們?yōu)榱藵M足大型SQL的需求,就需要設(shè)置一個(gè)做大值。當(dāng)然雖然我們設(shè)置了WORK_MEM并不一定就會消耗那么多的內(nèi)存,不過活躍會話數(shù)*WORK_MEM這個(gè)數(shù)字還是需要關(guān)注的,確保我們的物理內(nèi)存有那么多的空閑可用(參考可用內(nèi)存,而不是FREE內(nèi)存)是十分必要的。如果我們不確定系統(tǒng)最大的內(nèi)存使用量,并且物理內(nèi)存比較緊張,那么設(shè)置大一點(diǎn)的SWAP是十分必要的,在極端情況下可以確保系統(tǒng)不會因?yàn)镺OM而出大問題。
上面的這個(gè)執(zhí)行計(jì)劃也是我們經(jīng)??吹降模琍G數(shù)據(jù)庫支持并行HASH JOIN,并且默認(rèn)是打開的。如果我們的系統(tǒng)中的CPU資源是充足的,那么enable_parallel_hash參數(shù)確保打開狀態(tài)就行了。并行HASH JOIN可以通過過parallel seq scan和parallel hash join兩種機(jī)制來進(jìn)一步提高HASH JOIN的性能。我們可以看到,通過并發(fā),這個(gè)SQL的執(zhí)行效率進(jìn)一步的提升了。
不過任何事情都是有利有弊,如果你的服務(wù)器的CPU資源十分緊張,那么過多的并行HASH JOIN可能會導(dǎo)致你的CPU資源經(jīng)常出現(xiàn)不足,引發(fā)其他問題。如果存在這種情況,那么關(guān)閉并行HASH JOIN,讓每個(gè)HASH JOIN變得略微慢一點(diǎn),但是確保CPU資源不過載,也是一種策略。?