從KingbaseES V9的自研優化器算子談起
9月30號發布的第二批數據庫國測結果中,電科金倉通過了兩款數據庫,算上第一批通過的KingbaseES V8(以下簡稱KES),電科金倉目前有3款數據庫在國測清單中。本次國測結果對于數據庫廠商來說是生死攸關的,因為大規模數據庫國產化替代工作馬上就要展開,這會讓通過國測的企業在市場上肯定會擁有一定的優勢。
KES V8/V9兩個版本都過了國測,這讓電科金倉的新老用戶在國產化替代工作中省了不少力氣。V8老用戶不必急著升級,新用戶可以大膽地選擇功能和性能更加優秀的V9版本。之前我聽一些同學吐槽過,說因為PG內核升級了,所以KES V9的性能就比V8好了。事實是這樣嗎?有些東西道聽途說總是不太靠譜,還是眼見為實才好。
圖片
上面的信息是D-SMART從KES V8R6中采集出來的,可以看出服務器版本是12.1。
圖片
上面是V9的信息,服務器版本并未升級。看樣子V9在某些SQL上的性能提升并不是如坊間傳聞的那樣,是因為使用了較新版本的內核。通過對KES V9的初步分析,我個人的推測是,電科金倉在KES數據庫內核可能上已經走上了自主分支的道路,不一定會緊跟PG社區內核升級了。在核心上脫離社區,構建自主的獨立分支,同時關注社區的技術發展,不斷把社區版本中的優秀的方案搬到自主內核上。既保證了對用戶需求的更好支撐,又可以不斷吸取社區的先進思想,從而確保技術演進高效的前提下成本最低,這對于目前研發資金不太足夠的國產數據庫來說至關重要。
目前國產化替代中,用戶遇到的最主要問題有兩方面,一方面是如何在最小改動的情況下將企業中原來在國外商用數據庫上跑得很好的應用遷移到國產數據庫上,這方面很多國產數據庫做得都不錯。比如達夢、電科金倉、神通這些老牌數據庫廠商,經過十多年的技術積累,在Oracle、MySQL、PG、DB2、SQL SERVER等數據庫的兼容性上做得都相當不錯了。另外一方面是遷移過來的應用性能不能太差,起碼能夠接近原有數據庫的水平或者相差不是太大。
第二方面的問題也是目前大多數國產數據庫在用戶現場遇到的最多的,就是一些SQL的執行計劃不如Oracle優秀,導致系統遷移后應用性能無法被用戶接受。其中很重要的原因是因為國產數據庫的優化器功能不足,某些Oracle支持的執行算子自身不支持。要解決這些問題,就需要數據庫廠商在內核上多下點功夫,提升優化器的能力。
還有一種情況是某些用戶的SQL的寫法并不常規,數據庫產品經理沒有想到會有這樣的SQL存在,所以在生成執行計劃時rewrite出來的等價SQL不夠合理,從而導致隨后生成的執行計劃性能不佳。這類問題往往是因為我們的國產數據庫實戰的應用場景還不夠豐富,因此沒有發現這類問題。如果這類問題能夠被發現的話,作為具有一定自主核心研發能力的數據庫廠商可以很快就解決掉這些問題的。
最近研究KES V9,發現雖然內核中優化器方面的功能提升還是挺明顯的,特別是自研算子和SQL REWRITE規則的豐富程度方面。舉個例子,在PG數據庫上遇到NOT IN子查詢的語句還是挺頭疼的,PG在大多數情況下會使用FILTER算子。我們來看下面的測試用例:
DROP TABLE JOIN1;
DROP TABLE JOIN2;
create table join1 (id integer,name varchar(300),k1 integer);
create table join2 (id integer,name varchar(300),score integer);
insert into join1 values ( generate_series(1,20000),'aaaaaaaaaaaaaaaaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaaAAAAAAAAAAAAAAAAAAAAaaaaaaaAAAAAAAAAAAAAAAAAAAAA',10);
insert into join1 values ( generate_series(1,20000),'aaaaaaaaaaaaaaaaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaaAAAAAAAAAAAAAAAAAAAAaaaaaaaAAAAAAAAAAAAAAAAAAAAA',10);
insert into join1 values ( generate_series(1,20000),'aaaaaaaaaaaaaaaaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaaAAAAAAAAAAAAAAAAAAAAaaaaaaaAAAAAAAAAAAAAAAAAAAAA',10);
insert into join1 values ( generate_series(50201,50300),'aaaaaaaaaaaaaaaaAAAAAAAAAAAAAAAAAAAAAASSSSSAAAAAAAAAAAAAAAAaaAAAAAAAAAAAAAAAAAAAAaaaaaaaAAAAAAAAAAAAAAAAAAAAA',10);
insert into join1 values ( generate_series(50201,50300),'aaaaaaaaaaaaaaaaAAAAAAAAAAAAAAAAAAAAAASSSSSAAAAAAAAAAAAAAAAaaAAAAAAAAAAAAAAAAAAAAaaaaaaaAAAAAAAAAAAAAAAAAAAAA',10);
insert into join1 values ( generate_series(150201,1350300),'aaaaaaaaaaaaaaaaAAAAAAAAAAAAAAAAAAAAAASSSSSAAAAAAAAAAAAAAAAaaAAAAAAAAAAAAAAAAAAAAaaaaaaaAAAAAAAAAAAAAAAAAAAAA',10);
insert into join2 values ( generate_series(1,40000),'aaaaaaaaaaaaaaaaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaaAAAAAAAAAAAAAAAAAAAAaaaaaaaAAAAAAAAAAAAAAAAAAAAA',1);
insert into join2 values ( generate_series(1,40000),'aaaaaaaaaaaaaaaaAAAAAAABBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaaAAAAAAAAAAAAAAAAAAAAaaaaaaaAAAAAAAAAAAAAAAAAAAAA',2);
insert into join2 values ( generate_series(20001,22000),'aaaaaaaaaaaaaaaaAACCCCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaaAAAAAAAAAAAAAAAAAAAAaaaaaaaAAAAAAAAAAAAAAAAAAAAA',3);
insert into join2 values ( generate_series(150201,950300),'aaaaaaaaaaaaaaaaAACCCCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaaAAAAAAAAAAAAAAAAAAAAaaaaaaaAAAAAAAAAAAAAAAAAAAAA',3);
create index idx_j1 on join1(id);
create index idx_j2 on join2(id);
VACUUM ANALYZE JOIN1;
VACUUM ANALYZE JOIN2;
首先我們在一套PG 14上測試一下下面的一個帶有NOT IN子查詢的SQL:
圖片
這是PG典型的過濾器算子。子查詢掃描出來的數據做HASH,然后對外表的每行計算HASH值,進行否定過濾。這種執行計劃與HASH ANTI JION相比存在一定的缺陷,無法更好選擇左表,而且當子計劃返回的數據超過WORK_MEM限制的時候,無法使用HASH表,會極大影響SQL的執行效率。以前在優化PG數據庫上的應用時,遇到此類情況,只能改寫SQL了。
圖片
我們再來看一下KES V9,它使用了Hash Anti LSNA Jion算子,效率也高了不少。Oracle、SQL SERVER等數據庫都支持Hash Anti Jion算子,這對于NOT IN等類型的SQL消除子查詢是十分有效的,特別對于數據量很大的情況。KES在算子方面從O記借鑒了很多,對于HASH ANTI JOIN,設計了NA ,LSNA,RSNA等多種算子,分別針對不同的場景。
上面的例子中,PG數據庫做Filter的subplan返回的數據集還不算很大,我們設置的32M的WORK_MEM還能夠放得下整個HASH表,PG可以采用Hash算法來做Filter,此時的性能與HASH ANTI JOIN差別還不算大。如果返回的數據集比較大,PG的執行計劃就會惡化。通過一個簡單的測試,把T2的數據加大,再做一次測試看看。
圖片
上面是KES V9的執行計劃,可以看出KES依然使用了Hash Anti Jion,因為我去掉了子查詢中的>條件,返回的結果集可能帶有空值,所以無法使用更加高效的LSNA算子,使用了NA算子。從響應時間上看是可以接受的,644毫秒相對數據量的增長還算線性。接下來再來看看PG 14的執行情況。
圖片
因為WORK_MEM不足,因此按照PG優化器的限制無法使用HASH,改為使用Materialize,所以這條SQL的執行時間惡化到75146毫秒。
圖片
當然我們也可以通過設置更大的WORK_MEM來優化這條SQL,上面是我們把WORK_MEM加大到64M后的執行效果。不過能夠在不需要調整WORK_MEM的情況下,通過優化器去解決這些問題,是不是對用戶更加友好呢?而實際生產環境中,很多情況下,子查詢的結果集可能會更大,我們也不能總是通過加大WORK_MEM來解決問題吧。
圖片
對于此類查詢,Hash Anti Jion算子并不一定是最優的選擇,如果子查詢能夠等價轉換為JOIN,那么在不同的情況下,可能需要使用其他的算子來解決問題。修改一下查詢條件,讓外表掃描返回的數據量更少,在這個案例里KES V9優化器認為走Nested Loop Anti Jion最佳,看上圖的結果,確實如此,執行時間降低到50毫秒。除此之外,適當調整數據量,我們還能看到這條SQL使用了MERGE ANTI JOIN算子,這些算子都是KES為了提升此類表連接的性能自研的。
圖片
PG 14則還是使用祖傳的Filter: (NOT (hashed SubPlan 1))算子,執行時間的差距拉得更大了。
實際上目前數據庫國產化替代工作中遇到的最麻煩的事情就是替換后很多執行計劃變差,而且無法優化,只能通過修改SQL來解決問題,這給數據庫國產化替代工作帶來了額外的成本。
KES V9版本里,多了很多面向用戶應用場景的優化器功能增強,比如參數kdb_rbo.enable_push_joininfo_to_union可以控制優化器的行為,讓一個帶有UNION操作的子查詢參與連接操作,該特性可以將連接的條件下推到UNION連接的各子查詢中,從而優化nested loop算子,從而提高SQL的性能。
另外一個例子是針對大表做count distinct這個算子的優化 ,在數據重復度比較高的情況下,KES通過等價變換邏輯變換,將select count(distinct name) from t1; 轉換成select count(name) from (select name from t1 group by name);的形式,可以大大提高SQL的效率。當然這種優化和數據的分布關系很大,因此并不是通用性的,通過調整kdb_rbo.attribute_distinct_value_threshold參數,用戶可以根據自己應用的數據分布特點,在普通情況下使用傳統的方式去處理,而達到參數規定的閾值后,自動啟用SQL改寫,從而能夠更加靈活地解決SQL的性能問題。
其實DB2、Oracle的優化器中就有大量的這樣的開關,這些開關,都是不斷地在解決用戶的實際問題的時候不斷積累出來的。聽電科金倉的同學說,目前他們正針對數百個客戶現場遇到的與執行計劃相關的性能問題,設計了大量的優化補丁 ,正在一個一個地投入研發解決。這些針對優化器的PATCH將會在未來的V9版本中陸續發布。
對于電科金倉的用戶來說,這是個福音,這比簡單地通過升級數據庫內核獲得某些方面的性能和功能的提升有價值得多。其實企業應用系統所需要的數據庫功能與并發處理能力,目前的絕大多數數據庫都已經夠用了。用戶最急迫需要的是無論自己的應用寫得多爛,數據庫廠商都能夠通過對優化器的改進讓用戶的應用能夠跑起來。在這方面,電科金倉的KES做得確實不錯。