一、前言
在上一篇文章中,我們有講到pg_duckdb對于SELECT語句的一個較為詳細的處理流程,能大概了解到pg_duckdb是如何加速查詢效率的(甚至在某些場景之下,獲得上千倍的提升不在話下),同樣也知道了它的加速是有限制的,其限制的瓶頸點在于它需要將PostgreSQL中的行存儲的數據一行一行的轉換填充之duckdb的列存之中(實話實話,這成本過于高昂),所以當且僅當這個加載數據轉換的時間和原本查詢的時間越顯得不值一提的時候,實際加速的效果就越好。
依舊是在上篇文章中,有聊到pg_duckdb的技術路線是Custom Scan,但其實并不僅僅是Custom Scan,它也實現了TAM(Table Access Method),只不過限制僅支持臨時表,所以在當時我也沒有多少想講的意愿,而是留給今天的主角pg_mooncake。
pg_mooncake是在pg_duckdb上構建的,相對而言是青出于藍而勝于藍,因為它涵蓋了pg_duckdb,所以它的技術路線也包括了Custom Scan,不過它在TAM(Table Access Method)方面做的更好,它不再局限于臨時表的同時給PostgreSQL帶來了真正的列式存儲。
這里我稱呼它為PostgreSQL的列存新貴的原因在于:在它之前,其實就存在列式存儲插件,因為要想AP跑的好,列式存儲少不了,其實有了列存也還不夠,計算方面也得跟上。
所以在pg_mooncake的前面還有些"老前輩",比如說 hydradatabase(現在在整pg_duckdb的那位)的開源項目columnar,加速效果也有,但是效果不是那么理想。但是它也解決了不少問題,比方說是數據壓縮,因為列式存儲在壓縮上具有天然的優勢,可以做到較高的壓縮率,節省空間。
伴隨著時代的進步,duckdb的出現,從而出現了更加優秀的列式存儲插件pg_mooncake,還是非常值得高興的。
關于TAM(Table Access Method)相關資料請參考PostgreSQL的官方文檔https://www.postgresql.org/docs/16/sql-create-access-method.html。
二、Mooncake Labs團隊
在正式玩起來之前呢,我想先簡單介紹一下pg_mooncake的技術團隊,畢竟我和他們團隊的cc大佬還算是挺聊的來、也蠻有緣的(雖然我總是因為有事情要忙,以至于經常咕咕咕人家)。
pg_mooncake是由Mooncake Labs團隊打造的一個開源項目,而Mooncake Labs是坐落在美國舊金山的一支技術非常NB的團隊。
他們中大多數的成員都來自海外的一家名為SingleStore的公司,如果大家對這家公司有過了解的話,應該能大概知道這家公司在海外HTAP還是算挺有名氣的。因此Mooncake Labs團隊中的成員也積攢了大量關于這方面的經驗,這可能也是他們能把pg_mooncake優化到ClickBench前10的一個原因吧(雖然現在可能降了一點,但是無傷大雅)。
(這是之前的記錄圖片)
他們團隊也是正在招人(有挺多的方向,這里我只提一嘴數據庫內核方向),感興趣的同學可以去試試。
官網主頁:https://pgmooncake.com/
招聘信息:https://mooncakelabs.notion.site/build-the-mooncake-11cb7b68b5c1802a84a9e21649f49477
三、pg_mooncake
接下來進入正篇,讓我們走進pg_mooncake。
github項目地址:https://github.com/Mooncake-Labs/pg_mooncake
還是以源碼安裝為例,可參考https://github.com/Mooncake-Labs/pg_mooncake/blob/main/CONTRIBUTING.md
# contrib目錄
cd $PostgreSQL源碼目錄/contrib
# 拉取項目源碼
git clone https://github.com/Mooncake-Labs/pg_mooncake.git
# 進入代碼目錄
cd pg_mooncake/
# 拉取duckdb子項目
git submodule update --init --recursive
# 編譯debug版本或者release版本
make debug # make release
# 等待編譯完成即可 此處還有一個前提是安裝好Rust的cargo
make install
和pg_duckdb不同的是在使用上pg_mooncake不需要設置shared_preload_libraries,直接連接數據庫,創建拓展即可。
CREATE EXTENSION pg_mooncake;
3.1、簡單羅列一下相關參數
和pg_duckdb相比,pg_mooncake的參數則是要少得多,很清爽(畢竟pg_duckdb要帶貨)。如果只是簡單體驗體驗的話,就默認設置什么都不動就可以了。這里簡單羅列一下相關參數信息:
name
short_desc
mooncake.allow_local_tables
Allow columnstore tables on local disk
mooncake.default_bucket
Default bucket for columnstore tables
mooncake.enable_local_cache
Enable local cache for columnstore tables
mooncake.enable_memory_metadata_cache
Enable memory cache for Parquet metadata
mooncake.maximum_memory
The maximum memory DuckDB can use (e.g., 1GB)
mooncake.maximum_threads
Maximum number of DuckDB threads per Postgres backend
上面我們有說過pg_mooncake是將pg_duckdb糅合在一起,那可能有的朋友就很好奇,可能會提出這樣子的疑問就是說那么pg_duckdb的參數在pg_mooncake中是否有效呢?
答案是無效的,讓我們簡單看一下pgmooncake.cpp中的_PG_init就一目了然了
void _PG_init() {
MooncakeInitGUC();
DuckdbInitHooks();
DuckdbInitNode();
pgduckdb::RegisterDuckdbXactCallback();
// 略過部分代碼
}
可以看到pg_mooncake在_PG_init時不初始化pg_duckdb的相關參數,所以pg_duckdb的相關參數自然就無效了。
同時我們注意到它調用了pg_duckdb的DuckdbInitHooks、DuckdbInitNode這兩個接口(內部實際會有些許改動),也就是說從代碼層面論證了我們在上一篇文章中講到的內容在pg_mooncake依舊有效。
所以實際上來說,什么都不需要設置,創建完拓展之后就可以愉快的玩耍了。
3.2、簡單使用
在最開始的時候,我們指出了pg_duckdb的性能瓶頸可能在于將PostgreSQL中存儲的行數據轉換成duckdb的列數據。并且pg_duckdb僅對SELECT語句進行了額外的處理,對于其他SQL語句而言,如INSERT、UPDATE、DETELE都是交由PostgreSQL處理。
可能是pg_mooncake看到了這些痛點,所以它將INSERT、UPDATE、DETELE等等都給支持了,并在執行這些相關語句的時候,創建對應的Parquet文件,并存儲相關數據。當訪問列存表時,對于數據加載那塊便僅需要訪問對應的Parquet文件即可,就這樣避免了pg_duckdb在加載數據時的需要付出的昂貴成本。
而且物理文件使用Parquet文件作為外部存儲,對于構建數據湖也很方便。
以官方的測試案例為例:
[postgres@halo-centos8 mooncake_local_tables]$ psql mooncake
psql (16.8)
Type "help" for help.
mooncake=# \dx
List of installed extensions
Name | Version | Schema | Description
-------------+---------+------------+-------------------------------
pg_mooncake | 0.1.2 | public | Columnstore Table in Postgres
plpgsql | 1.0 | pg_catalog | PL/pgSQL procedural language
(2 rows)
mooncake=# -- 創建列存表
mooncake=# CREATE TABLE user_activity(
mooncake(# user_id BIGINT,
mooncake(# activity_type TEXT,
mooncake(# activity_timestamp TIMESTAMP,
mooncake(# duration INT
mooncake(# ) USING columnstore;
CREATE TABLE
mooncake=# -- 通過explain來簡單判斷INSERT是否被支持
mooncake=# EXPLAIN VERBOSE INSERT INTO user_activity VALUES
mooncake-# (1, 'login', '2024-01-01 08:00:00', 120),
mooncake-# (2, 'page_view', '2024-01-01 08:05:00', 30),
mooncake-# (3, 'logout', '2024-01-01 08:30:00', 60),
mooncake-# (4, 'error', '2024-01-01 08:13:00', 60);
QUERY PLAN
--------------------------------------------------------------------
Custom Scan (MooncakeDuckDBScan) (cost=0.00..0.00 rows=0 width=0)
Output: duckdb_scan.explain_key, duckdb_scan.explain_value
DuckDB Execution Plan:
┌───────────────────────────┐
│ COLUMNSTORE_INSERT │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ COLUMN_DATA_SCAN │
│ ──────────────────── │
│ ~4 Rows │
└───────────────────────────┘
(14 rows)
mooncake=# -- 插入數據
mooncake=# INSERT INTO user_activity VALUES
mooncake-# (1, 'login', '2024-01-01 08:00:00', 120),
mooncake-# (2, 'page_view', '2024-01-01 08:05:00', 30),
mooncake-# (3, 'logout', '2024-01-01 08:30:00', 60),
mooncake-# (4, 'error', '2024-01-01 08:13:00', 60);
INSERT 0 4
mooncake=# -- 查詢數據
mooncake=# SELECT * from user_activity;
user_id | activity_type | activity_timestamp | duration
---------+---------------+---------------------+----------
1 | login | 2024-01-01 08:00:00 | 120
2 | page_view | 2024-01-01 08:05:00 | 30
3 | logout | 2024-01-01 08:30:00 | 60
4 | error | 2024-01-01 08:13:00 | 60
(4 rows)
mooncake=# -- 查看列存表對應物理文件位置
mooncake=# SELECT * FROM mooncake.columnstore_tables;
table_name | path
---------------+------------------------------------------------------------------------
user_activity | /data/16/mooncake_local_tables/mooncake_mooncake_user_activity_279647/
(1 row)
如果你按照我的操作來進行的話,這個數據目錄不一定和我這里一樣,這個目錄的命名規則為$PGDATA/mooncake_local_tables/mooncake_數據庫名_表名_表oid
string ColumnstoreMetadata::GetTablePath(Oid oid) {
::Relation table = table_open(oid, AccessShareLock);
string path =
StringUtil::Format("mooncake_%s_%s_%d/", get_database_name(MyDatabaseId), RelationGetRelationName(table), oid);
table_close(table, AccessShareLock);
if (mooncake_default_bucket != nullptr && mooncake_default_bucket[0] != '\0') {
path = StringUtil::Format("%s/%s", mooncake_default_bucket, path);
} else if (mooncake_allow_local_tables) {
path = StringUtil::Format("%s/mooncake_local_tables/%s", DataDir, path);
} else {
elog(ERROR, "Columnstore tables on local disk are not allowed. Set mooncake.default_bucket to default "
"S3 bucket");
}
return path;
}
而對應的parquet文件的命名則是非常明顯的uuid風格
file_name = UUID::ToString(UUID::GenerateRandomUUID()) + ".parquet";
讓我們看一下
/data/16/mooncake_local_tables/mooncake_mooncake_user_activity_279647/
這個目錄存在哪些東西
[postgres@halo-centos8 mooncake_local_tables]$ tree /data/16/mooncake_local_tables/mooncake_mooncake_user_activity_279647/
/data/16/mooncake_local_tables/mooncake_mooncake_user_activity_279647/
├── 721e8499-b4c3-4a4a-a822-5af1fc45e237.parquet
└── _delta_log
├── 00000000000000000000.json
└── 00000000000000000001.json
1 directory, 3 files
[postgres@halo-centos8 mooncake_local_tables]$ cat /data/16/mooncake_local_tables/mooncake_mooncake_user_activity_279647/_delta_log/00000000000000000001.json
{"add":{"path":"721e8499-b4c3-4a4a-a822-5af1fc45e237.parquet","partitionValues":{},"size":699,"modificationTime":0,"dataChange":true,"stats":null,"tags":null,"deletionVector":null,"baseRowId":null,"defaultRowCommitVersion":null,"clusteringProvider":null}}
{"commitInfo":{"timestamp":1740987491586,"operation":"WRITE","operationParameters":{"mode":"Append"},"clientVersion":"delta-rs.0.21.0"}}
這里我們想嘗試去讀一下721e8499-b4c3-4a4a-a822-5af1fc45e237.parquet,可以用mooncake.read_parquet也可以用duckdb,這里我還是用duckdb來讀取
[postgres@halo-centos8 ~]$ ./duckdb
v1.1.3 19864453f7
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
D select * from read_parquet('/data/16/mooncake_local_tables/mooncake_mooncake_user_activity_279647/721e8499-b4c3-4a4a-a822-5af1fc45e237.parquet');
┌─────────┬───────────────┬─────────────────────┬──────────┐
│ user_id │ activity_type │ activity_timestamp │ duration │
│ int64 │ varchar │ timestamp │ int32 │
├─────────┼───────────────┼─────────────────────┼──────────┤
│ 1 │ login │ 2024-01-01 08:00:00 │ 120 │
│ 2 │ page_view │ 2024-01-01 08:05:00 │ 30 │
│ 3 │ logout │ 2024-01-01 08:30:00 │ 60 │
│ 4 │ error │ 2024-01-01 08:13:00 │ 60 │
└─────────┴───────────────┴─────────────────────┴──────────┘
可以看到和在PostgreSQL中讀取到的數據是一致的。
_delta_log中的相關json文件,就是實際對應的事務日志。json文件和parquet文件并不總是線性增長的,如下所示:
[postgres@halo-centos8 ~]$ psql mooncake
psql (16.8)
Type "help" for help.
mooncake=# BEGIN; -- 開啟事務塊
BEGIN
mooncake=*# INSERT INTO user_activity VALUES (5, 'test', '2024-01-01 08:13:00', 60);
INSERT 0 1
mooncake=*# INSERT INTO user_activity VALUES (6, 'test', '2024-01-01 08:13:00', 60);
INSERT 0 1
mooncake=*# ROLLBACK; -- 回滾事務
ROLLBACK
讓我們再次查看一下
/data/16/mooncake_local_tables/mooncake_mooncake_user_activity_279647/
的目錄結構和json數據
[postgres@halo-centos8 ~]$ tree /data/16/mooncake_local_tables/mooncake_mooncake_user_activity_279647/
/data/16/mooncake_local_tables/mooncake_mooncake_user_activity_279647/
├── 721e8499-b4c3-4a4a-a822-5af1fc45e237.parquet
├── 81d8cb44-0136-4cf6-8188-7a673ad1da92.parquet
├── _delta_log
│ ├── 00000000000000000000.json
│ └── 00000000000000000001.json
└── f7afb9b9-a876-4905-9990-74177f65ee22.parquet
1 directory, 5 files
[postgres@halo-centos8 ~]$ cat /data/16/mooncake_local_tables/mooncake_mooncake_user_activity_279647/_delta_log/00000000000000000001.json
{"add":{"path":"721e8499-b4c3-4a4a-a822-5af1fc45e237.parquet","partitionValues":{},"size":699,"modificationTime":0,"dataChange":true,"stats":null,"tags":null,"deletionVector":null,"baseRowId":null,"defaultRowCommitVersion":null,"clusteringProvider":null}}
{"commitInfo":{"timestamp":1740987491586,"operation":"WRITE","operationParameters":{"mode":"Append"},"clientVersion":"delta-rs.0.21.0"}}
可以發現json文件沒有發生任何變化,但是新增了兩個parquet文件,我們再次使用duckdb讀取一下此目錄中的所有parquet
[postgres@halo-centos8 ~]$ ./duckdb
v1.1.3 19864453f7
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
D select * from read_parquet('/data/16/mooncake_local_tables/mooncake_mooncake_user_activity_279647/*'); -- * 代表所有的parquet
┌─────────┬───────────────┬─────────────────────┬──────────┐
│ user_id │ activity_type │ activity_timestamp │ duration │
│ int64 │ varchar │ timestamp │ int32 │
├─────────┼───────────────┼─────────────────────┼──────────┤
│ 1 │ login │ 2024-01-01 08:00:00 │ 120 │
│ 2 │ page_view │ 2024-01-01 08:05:00 │ 30 │
│ 3 │ logout │ 2024-01-01 08:30:00 │ 60 │
│ 4 │ error │ 2024-01-01 08:13:00 │ 60 │
│ 5 │ test │ 2024-01-01 08:13:00 │ 60 │
│ 6 │ test │ 2024-01-01 08:13:00 │ 60 │
└─────────┴───────────────┴─────────────────────┴──────────┘
可以看到我們回滾的那兩個記錄能被查詢到,所以實際上那兩個parquet分別對應一條INSERT語句。
3.3、行列混存
也正是因為pg_mooncake實現了列式存儲,所以對于PostgreSQL而言,便出現了行列混存的情形。最簡單的場景便是構建一張堆表和一張列存表,那么這兩張表可以join嗎?
答案是可以的。如下所示:
[postgres@halo-centos8 ~]$ psql mooncake
psql (16.8)
Type "help" for help.
mooncake=# CREATE TABLE ta(user_id BIGINT); -- 簡單堆表
CREATE TABLE
mooncake=# INSERT INTO ta SELECT * FROM generate_series(1, 6); -- 生成六行數據
INSERT 0 6
mooncake=# SELECT * FROM ta Inner Join user_activity on ta.user_id = user_activity.user_id; -- 簡單測試查看結果
user_id | user_id | activity_type | activity_timestamp | duration
---------+---------+---------------+---------------------+----------
1 | 1 | login | 2024-01-01 08:00:00 | 120
2 | 2 | page_view | 2024-01-01 08:05:00 | 30
3 | 3 | logout | 2024-01-01 08:30:00 | 60
4 | 4 | error | 2024-01-01 08:13:00 | 60
(4 rows)
mooncake=# EXPLAIN VERBOSE SELECT * FROM ta Inner Join user_activity on ta.user_id = user_activity.user_id; -- 簡單測試查看執行計劃
QUERY PLAN
--------------------------------------------------------------------
Custom Scan (MooncakeDuckDBScan) (cost=0.00..0.00 rows=0 width=0)
Output: duckdb_scan.explain_key, duckdb_scan.explain_value
DuckDB Execution Plan:
┌───────────────────────────┐
│ PROJECTION │
│ ──────────────────── │
│ user_id │
│ user_id │
│ activity_type │
│ activity_timestamp │
│ duration │
│ │
│ ~2260 Rows │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│ HASH_JOIN │
│ ──────────────────── │
│ Join Type: INNER │
│ │
│ Conditions: ├──────────────┐
│ user_id = user_id │ │
│ │ │
│ ~2260 Rows │ │
└─────────────┬─────────────┘ │
┌─────────────┴─────────────┐┌─────────────┴─────────────┐
│ POSTGRES_SEQ_SCAN ││ COLUMNSTORE_SCAN │
│ ──────────────────── ││ ──────────────────── │
│ Function: ││ Function: │
│ POSTGRES_SEQ_SCAN ││ COLUMNSTORE_SCAN │
│ ││ │
│ Projections: user_id ││ Projections: │
│ ││ user_id │
│ ││ activity_type │
│ ││ activity_timestamp │
│ ││ duration │
│ ││ │
│ ~2260 Rows ││ ~4 Rows │
└───────────────────────────┘└───────────────────────────┘
(41 rows)
所以顯而易見的是,pg_mooncake將數據都加載到了duckdb中,然后去執行了。
對于堆表而言,若當前SQL查詢存在列存表,會走原本pg_duckdb的邏輯,會將元組數據轉換成duckdb的列數據,對應POSTGRES_SEQ_SCAN。(如果當前查詢并不包含列存表,則走的PostgreSQL的默認邏輯)對于列存表而言,走的則是pg_mooncake自己提供的COLUMNSTORE_SCAN,實際上就是parquet_scan。
TableFunction ColumnstoreTable::GetScanFunction(ClientContext &context, unique_ptr &bind_data) {
auto file_names = metadata->DataFilesSearch(oid, &context, &path, &columns);
auto file_paths = GetFilePaths(path, file_names);
if (file_paths.empty()) {
return TableFunction("columnstore_scan", {} /*arguments*/, EmptyColumnstoreScan);
}
TableFunction columnstore_scan = GetParquetScan(context); -- 注意此處
columnstore_scan.name = "columnstore_scan";
columnstore_scan.init_global = ColumnstoreScanInitGlobal;
columnstore_scan.statistics = nullptr;
columnstore_scan.get_multi_file_reader = ColumnstoreScanMultiFileReader::Create;
// 省略代碼...
}
//對應此處
TableFunction GetParquetScan(ClientContext &context) {
return ExtensionUtil::GetTableFunction(*context.db, "parquet_scan") -- 實際是parquet_scan
.functions.GetFunctionByArguments(context, {LogicalType::LIST(LogicalType::VARCHAR)});
}
所以pg_mooncake還是設計的蠻巧妙的。歡迎感興趣的同學可以去點點Star,提提PR。
3.4、不足之處?
這其實倒沒啥好說的了,我能注意到的必然也逃不過pg_mooncake的大佬的法眼。一般的都被記錄到Issues中了。
比方說資源管理之類的,如drop table之后,對應的物理目錄及文件未被及時清除之類的;
比如說性能優化之類的,如https://github.com/Mooncake-Labs/pg_mooncake/issues/82。
聽說cc他們規劃后續會將pg_mooncake做成一個基于PostgreSQL的HTAP數據庫,讓人非常的期待呀,同時歡迎大家加入其中,做做貢獻。
四、推薦閱讀
有意思的是pg_mooncake雖然使用的是duckdb的parquet功能,但是在某些場景下甚至能做到比duckdb還要更快是因為他們做了大量的優化,技術拉滿。
文章鏈接:https://www.mooncake.dev/blog/duckdb-parquet
官方博客:https://www.mooncake.dev/blog
五、聲明
若文中存在錯誤或不當之處,敬請指出,以便我進行修正和完善。希望這篇文章能夠幫助到各位。
文章轉載請聯系,謝謝合作。
六、本公眾號pg文章推薦
(我近期的開源之旅)
(看懂PostgreSQL where子句中表達式的先后執行順序)
特別聲明:以上內容(如有圖片或視頻亦包括在內)為自媒體平臺“網易號”用戶上傳并發布,本平臺僅提供信息存儲服務。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.