在涉及大規模數據的復雜分析或即時查詢時,列式存儲是支撐業務負載的關鍵技術之一。相較于傳統的行式存儲,列式存儲采用了不同的數據文件組織方式,它將表中的數據以列為單位進行物理排列。這種存儲模式允許在分析過程中,查詢計算僅需針對所需的列數據進行掃描,從而避免了不必要的整行掃描,顯著降低了IO和內存等資源的消耗,進而提升了計算效率。此外,列式存儲天然具備更佳的數據壓縮優勢,能夠實現較高的壓縮比,有效節約了存儲空間,并降低了網絡傳輸帶寬的占用。
常見的列存存儲引擎在實現上往往假設不會有大量隨機更新, 盡量保證列存組織數據是靜態的。當真正伴隨大量數據隨機更新時,也會不可避免的存在系統性能問題。OceanBase LSM-Tree 架構可以將基線數據和增量數據分別處理,正好可以解決這一場景問題。因此 OceanBase 4.3 版本基于當前架構基礎進行擴展,正式推出列存引擎,在一個架構、一個數據庫上,實現了列存和行存數據存儲一體化,兼顧 TP 和 AP 查詢性能。
為了讓有分析訴求的用戶順暢使用新版本,圍繞列存引擎,從優化器到執行器、從 DDL 到事務處理等多模塊都進行了適配優化。包括基于列存的新的代價模型和向量化引擎,查詢下壓功能的擴展和增強,Skip Index,新的列式編碼算法,自適應 Compaction 等。本文將深入探討 OceanBase 4.3 版本帶來的列存能力、應用場景,以及用戶關心的未來發展規劃。
一、列存整體架構
OceanBase 作為原生分布式數據庫,默認情況下會為用戶數據創建多個副本。為了充分利用多副本的優勢,為用戶提供數據強校驗和數據遷移重用等增強體驗,OceanBase 自研的 LSM-Tree 存儲引擎做了深度優化:
○ ?基線數據:相較于業內常見的 LSM-Tree 實現邏輯,OceanBase 提出了"每日合并"的概念。用戶可定期或根據操作選擇一個全局版本號,所有副本的租戶數據將在這個版本上進行一輪 Major Compaction,生成這個版本的基線數據。所有副本在同一版本下的基線數據完全一致,物理上保持一致。
○ ?增量數據:相對于基線數據,增量數據是指在最新版本的基線數據之后寫入的數據。增量數據可以是剛寫入Memtable的內存數據,也可以是已經轉儲為SSTable 的磁盤數據。增量數據在每個副本中獨立維護,不保證一致性,并且包含了所有多版本的數據。
基于列存應用場景隨機更新量可控的背景,OceanBase 4.3 結合自身基線數據和增量數據的特質,提出了一套對上層透明的列存實現方式:基線數據存儲為列存模式,增量數據保持行存,確保用戶所有 DML 操作不受影響,上下游同步無縫接入,列存表數據仍然可以像行存表一樣進行所有事務操作。列存模式下每列數據存儲為一個獨立 SSTable,所有列的 SSTable 組合成為一個虛擬 SSTable 作為用戶的列存基線數據。同時,用戶可根據實際業務訴求在建表環節指定設置,基線數據可以支持行存、列存、行存列存冗余三種模式,提供更好的靈活性。
OceanBase 4.3 版本中不僅在存儲引擎中實現了列存模式,更從優化器、執行器以等多維度進行列存的適配優化。用戶在遷移到列存模式后基本上不會感受到業務變化,能夠像使用行存一樣享受到列存帶來的性能優勢。列存引擎的全面優化,也使得 OceanBase 真正實現了 TP & AP 一體化,實現了一套引擎、一套代碼支持不同類型業務的目標,打造更加完善的 HTAP 混合負載實時分析能力。
二、OceanBase 實現列存,有哪些天然優勢
(一)成熟的 LSM-Tree 引擎
與傳統數據庫相比,OceanBase 擁有天然的 Delta Store,非常適合實現列存。基于 LSM-Tree 存儲引擎的支持,OceanBase 列存不僅支持完整的事務,而且基礎算子的性能不弱于傳統的 TP 數據庫。在列存上,完整的事務支持使得 OceanBase 在更新方面具有天然優勢,所有事物語義和多樣事物的管理對用戶來說完全透明的,用戶可以輕松切換到列存模式,將列存數據庫當成行存數據庫使用,對業務完全透明,不需要做任何改動。
(二)完善的執行引擎
OceanBase 不僅擁有完整的執行引擎,還具備通用的優化器是通用的。在行存模式下,OceanBase 已經實現向量化存儲引擎的無縫對接,無需任何修改即可支持向量化執行。此外,OceanBase 實現一套優化器的代碼在上層對行存和列存進行不同代價的估算,使得用戶的 SQL 可以自動選擇行存或列存。
(三)靈活的原生分布式
OceanBase 天然支持分布式并行查詢引擎,未來還可以輕松擴展到列存異構副本。列存異構副本的優勢體現在用戶需要完全硬隔離的應用場景中,未來的OceanBase 版本將新增這一功能。
綜上所述,OceanBase 憑借其天然優勢推動了 4.3 版本中列存功能的實現。引入列存儲引擎后,OceanBase 整體架構在外部表現上完全不變,并且從架構層面支持了列存相關的三種模式:
○ ?基線列存 +增量行存:基線數據采用列存方式存儲,增量數據采用行存方式存儲。
○ ?靈活的行存/列存索引:可以對行存表建立列存索引,也可以對列存表建立行存索引,還可以對兩者進行任意組合。由于所有列存表和索引的底層存儲結構是統一的,因此 OceanBase 可以自動支持列存和行存的索引。
○ ?列存副本:OceanBase 正在研發的列存副本功能。得益于原生分布式能力,只需對模式或表做部分修改,即可以通過 Compaction 將新增的只讀副本轉換為列存存儲模式。
三、列存使用方法
(一)默認創建列存表
對于 OLAP 業務需求,我們推薦默認創建列存表。如何確保租戶創建出來的表默認為列存表?只通過下面的配置項即可實現:
alter system set default_table_store_format = "column";
隨后我們創建的表格沒有指定 column group 時,默認創建為列存表。
OceanBase(root@test)>create table t1 (c1 int primary key, c2 int ,c3 int);
Query OK,0 rows affected (0.301 sec)OceanBase(root@test)>show create table t1;CREATE TABLE `t1` (`c1` int(11) NOT NULL,`c2` int(11) DEFAULT NULL,`c3` int(11) DEFAULT NULL,PRIMARY KEY (`c1`)
) DEFAULT CHARSET = utf8mb4 ROW_FORMAT = DYNAMIC COMPRESSION = 'zstd_1.3.8' REPLICA_NUM = 1 BLOCK_SIZE = 16384 USE_BLOOM_FILTER = FALSE TABLET_SIZE = 134217728 PCTFREE = 0
WITH COLUMN GROUP(each column)1 row in set (0.101 sec)
(二)指定創建列存表
為了方便用戶創建列存表,列存引入新的語法 with column group,當用戶建表時最后指定 `with column group(each column)` ,即表示創建列存表。
OceanBase(root@test)>create table tt_column_store (c1 int primary key, c2 int ,c3 int) with column group (each column);
Query OK,0 rows affected (0.308 sec)OceanBase(root@test)>show create table tt_column_store;CREATE TABLE `tt_column_store` (`c1` int(11) NOT NULL,`c2` int(11) DEFAULT NULL,`c3` int(11) DEFAULT NULL,PRIMARY KEY (`c1`)
) DEFAULT CHARSET = utf8mb4 ROW_FORMAT = DYNAMIC COMPRESSION = 'zstd_1.3.8' REPLICA_NUM = 1 BLOCK_SIZE = 16384 USE_BLOOM_FILTER = FALSE TABLET_SIZE = 134217728 PCTFREE = 0 WITH COLUMN GROUP(each column)1 row in set (0.108 sec)
(三)指定創建列存行存冗余表
在某些場景下,用戶可以容忍一定程度的數據冗余,以滿足 AP/TP 業務場景的雙重需求。此時,可以增加行存數據的冗余,通過 `with column group` 語法增加指定 `all columns` 即可實現。
create table tt_column_row (c1 int primary key, c2 int , c3 int) with column group (all columns, each column);
Query OK, 0 rows affected (0.252 sec)OceanBase(root@test)>show create table tt_column_row;
CREATE TABLE `tt_column_row` (`c1` int(11) NOT NULL, `c2` int(11) DEFAULT NULL, `c3` int(11) DEFAULT NULL, PRIMARY KEY (`c1`)
) DEFAULT CHARSET = utf8mb4 ROW_FORMAT = DYNAMIC COMPRESSION = 'zstd_1.3.8' REPLICA_NUM = 1 BLOCK_SIZE = 16384 USE_BLOOM_FILTER = FALSE TABLET_SIZE = 134217728 PCTFREE = 0 WITH COLUMN GROUP(all columns, each column)1 row in set (0.075 sec)
(四)列存掃描
如何查看是否列存掃描計劃?計劃展示上新增 COLUMN TABLE FULL SCAN,描述列存表的范圍掃描。
OceanBase(root@test)>explain select * from tt_column_store;
+--------------------------------------------------------------------------------------------------------+
| Query Plan |
+--------------------------------------------------------------------------------------------------------+
| ================================================================= |
| |ID|OPERATOR |NAME |EST.ROWS|EST.TIME(us)| |
| ----------------------------------------------------------------- |
| |0 |COLUMN TABLE FULL SCAN|tt_column_store|1 |7 | |
| ================================================================= |
| Outputs & filters: |
| ------------------------------------- |
| 0 - output([tt_column_store.c1], [tt_column_store.c2], [tt_column_store.c3]), filter(nil), rowset=16 |
| access([tt_column_store.c1], [tt_column_store.c2], [tt_column_store.c3]), partitions(p0) |
| is_index_back=false, is_glOceanBaseal_index=false, |
| range_key([tt_column_store.c1]), range(MIN ; MAX)always true |
+--------------------------------------------------------------------------------------------------------+
計劃展示上新增 COLUMN TABLE GET,描述列存表上的指定主鍵的 get 操作。
OceanBase(root@test)>explain select * from tt_column_store where c1 = 1;
+--------------------------------------------------------------------------------------------------------+
| Query Plan |
+--------------------------------------------------------------------------------------------------------+
| =========================================================== |
| |ID|OPERATOR |NAME |EST.ROWS|EST.TIME(us)| |
| ----------------------------------------------------------- |
| |0 |COLUMN TABLE GET|tt_column_store|1 |14 | |
| =========================================================== |
| Outputs & filters: |
| ------------------------------------- |
| 0 - output([tt_column_store.c1], [tt_column_store.c2], [tt_column_store.c3]), filter(nil), rowset=16 |
| access([tt_column_store.c1], [tt_column_store.c2], [tt_column_store.c3]), partitions(p0) |
| is_index_back=false, is_global_index=false, |
| range_key([tt_column_store.c1]), range[1 ; 1], |
| range_cond([tt_column_store.c1 = 1]) |
+--------------------------------------------------------------------------------------------------------+
12 rows in set (0.051 sec)
如何通過 Hint 指定列存行存冗余表走列存掃描?對于列存行存冗余表,優化器會根據代價選擇走行存或者列存掃描,如簡單場景做全表掃描,會默認使用行存生成計劃。
OceanBase(root@test)>explain select * from tt_column_row;
+--------------------------------------------------------------------------------------------------+
| Query Plan |
+--------------------------------------------------------------------------------------------------+
| ======================================================== |
| |ID|OPERATOR |NAME |EST.ROWS|EST.TIME(us)| |
| -------------------------------------------------------- |
| |0 |TABLE FULL SCAN|tt_column_row|1 |3 | |
| ======================================================== |
| Outputs & filters: |
| ------------------------------------- |
| 0 - output([tt_column_row.c1], [tt_column_row.c2], [tt_column_row.c3]), filter(nil), rowset=16 |
| access([tt_column_row.c1], [tt_column_row.c2], [tt_column_row.c3]), partitions(p0) |
| is_index_back=false, is_global_index=false, |
| range_key([tt_column_row.c1]), range(MIN ; MAX)always true |
+--------------------------------------------------------------------------------------------------+
如果用戶希望通過手動調優走列存掃描,可以通過 hint USE_COLUMN_TABLE 來強制 tt_column_row 表走列存掃描。
OceanBase(root@test)>explain select /*+ USE_COLUMN_TABLE(tt_column_row) */ * from tt_column_row;
+--------------------------------------------------------------------------------------------------+
| Query Plan |
+--------------------------------------------------------------------------------------------------+
| =============================================================== |
| |ID|OPERATOR |NAME |EST.ROWS|EST.TIME(us)| |
| --------------------------------------------------------------- |
| |0 |COLUMN TABLE FULL SCAN|tt_column_row|1 |7 | |
| =============================================================== |
| Outputs & filters: |
| ------------------------------------- |
| 0 - output([tt_column_row.c1], [tt_column_row.c2], [tt_column_row.c3]), filter(nil), rowset=16 |
| access([tt_column_row.c1], [tt_column_row.c2], [tt_column_row.c3]), partitions(p0) |
| is_index_back=false, is_global_index=false, |
| range_key([tt_column_row.c1]), range(MIN ; MAX)always true |
+--------------------------------------------------------------------------------------------------+
類似的方式,通過 Hint NO_USE_COLUMN_TABLE 可以強制表不進行列存掃描。
OceanBase(root@test)>explain select /*+ NO_USE_COLUMN_TABLE(tt_column_row) */ c2 from tt_column_row;
+------------------------------------------------------------------+
| Query Plan |
+------------------------------------------------------------------+
| ======================================================== |
| |ID|OPERATOR |NAME |EST.ROWS|EST.TIME(us)| |
| -------------------------------------------------------- |
| |0 |TABLE FULL SCAN|tt_column_row|1 |3 | |
| ======================================================== |
| Outputs & filters: |
| ------------------------------------- |
| 0 - output([tt_column_row.c2]), filter(nil), rowset=16 |
| access([tt_column_row.c2]), partitions(p0) |
| is_index_back=false, is_global_index=false, |
| range_key([tt_column_row.c1]), range(MIN ; MAX)always true |
+------------------------------------------------------------------+
11 rows in set (0.053 sec)
四、未來展望
OceanBase 4.3 列存的引入,為用戶的數據分析以及實時分析場景提供了新的選擇。未來,OceanBase 列存將持續演進,為用戶帶來更加豐富的 feature、更強勁的性能以及更靈活的部署模式。
第一,更豐富的功能。目前,我們支持純列存儲引擎,未來將實現可自定義的靈活列組組織支持,滿足不同場景的分析需求。此外,我們計劃將增量旁路導入功能進一步增強,幫助用戶實現高效的數據導入,縮短數據分析準備時間。
第二,更好的性能。增強 Skip Index 的支持,使其能夠更好地滿足用戶的查詢需求。此外,我們計劃實現格式一體化,目前存儲的格式多樣化,未來將實現存儲格式與 SQL 向量化引擎的緊密結合,使得在執行 SQL 計算時,系統能夠識別不同的存儲格式,從而幫助用戶節省更多的數據轉換開銷。
第三,更靈活的部署模式。在未來的版本中,我們將支持 OLAP 所需的異構副本,以滿足用戶對強依賴異構副本的需求。此外,未來還將支持存算分離模式,使得所有用戶的 AP 數據庫都能夠以更低的成本享受存儲與計算的分離。