English version | README em português brasileiro

pg_monetdb

pg_monetdb 是 monetdb_fdw 的一个分支,重点增强了面向 TPC-H 和 TPC-DS 风格分析型查询的 pushdown 能力。

这个分支建立在优秀的 oracle_fdw (https://github.com/laurenz/oracle_fdw.git) 和 postgres_fdw (https://www.postgresql.org/docs/current/postgres-fdw.html) 项目之上。

当前还支持 MonetDB HUGEINT、MonetDB BLOB,以及部分 INTERVAL round trip。已经完成验证的 interval family 包括 interval monthinterval dayinterval second;凡是 MonetDB 端以 sec_interval 为底层的限定 interval,目前导入 PostgreSQL 后仍会表现为 interval second,因此原始 qualifier 暂时还不能保留。

上游致谢

上游仓库地址:https://github.com/HaloTech-Co-Ltd/MonetDB_fdw

特别感谢 monetdb_fdw 的维护者和贡献者,这个分支正是建立在他们的上游工作基础之上。

基准测试

下面的 TPC-H 结果来自仓库内已提交的真实 PostgreSQL heap 基线测试(schema pg)以及与之对应的 pg_monetdb 测试(schema monet),两者都运行在 PostgreSQL 19 上:

Query PostgreSQL heap pg_monetdb Pushdown
Query 1 1272.074 ms 139.990 ms Full
Query 2 298.228 ms 9.162 ms Partial
Query 3 322.203 ms 41.508 ms Partial
Query 4 183.436 ms 21.230 ms Full
Query 5 559.401 ms 23.874 ms Full
Query 6 152.712 ms 9.087 ms Full
Query 7 2448.717 ms 36.381 ms Full
Query 8 246.125 ms 35.699 ms Full
Query 9 1645.422 ms 68.392 ms Full
Query 10 346.119 ms 206.652 ms Full
Query 11 109.532 ms 39.043 ms Partial
Query 12 278.782 ms 11.945 ms Full
Query 13 525.866 ms 61.930 ms Full
Query 14 123.599 ms 4.649 ms Full
Query 15 474.821 ms 36.892 ms Partial
Query 16 182.510 ms 55.098 ms Full
Query 17 596.631 ms 28.372 ms Full
Query 18 1888.438 ms 39.790 ms Full
Query 19 43.837 ms 43.812 ms Full
Query 20 253.526 ms 350.296 ms Partial
Query 21 1474.114 ms 75.057 ms Partial
Query 22 74.176 ms 23.588 ms Partial
Total 13500.269 ms 1362.447 ms -

在这份已提交的 PostgreSQL 19 基准中,pg_monetdb 跑完整套 TPC-H 的总时间约为本地 heap 基线的 9.91x 更快,整体下降约 89.9%

当当前 PostgreSQL 19 工件中的计划形态是纯 FS 时,Pushdown 标记为 Full;如果仍然带有 LOCAL_*INITPLANMIXED 工作,则标记为 Partial

这些总计结果可以通过 scripts/load_pg18_heap_into_pg19.shscripts/run_tpch_all_sql.shscripts/benchmark_tpch_schema.sh 复现。

重要说明:仓库中保留的 tpch_regression_baseline.tsv 仍然只是历史 FDW 工件,它不是 PostgreSQL heap-only 基准,不能直接当作 heap 与 FDW 的对比结果来解读。

上面这些 PostgreSQL 19 已提交基准所使用的环境如下:

  • 操作系统:Ubuntu 26.04 LTS (Resolute Raccoon),内核 7.0.0-15-generic
  • CPU:AMD Ryzen 7 5800H with Radeon Graphics,16 threads
  • 内存:38.5 GiB RAM
  • 上述基准所用 PostgreSQL 版本:postgres (PostgreSQL) 19devel (Ubuntu 19~~devel-3~20260525.0815.g0b8fa5fd37b.pgdg26.04+1)

验证矩阵

仓库中还保留了 PostgreSQL 15 到 19 的版本化 TPC-H 回归工件:

  • tpch_regression_pg15.tsv
  • tpch_regression_pg16.tsv
  • tpch_regression_pg17.tsv
  • tpch_regression_pg18.tsv
  • tpch_regression_pg19.tsv

这些工件构成了当前跨版本验证矩阵的已提交参考,覆盖 PostgreSQL 15、16、17、18 和 19。

支持的操作系统与数据库版本

  • RHEL 8/9, CentOS 8/9, Ubuntu
  • Halo 1.0.14, 1.0.16
  • PostgreSQL 15, 16, 17, 18, 19
  • MonetDB 11.56

使用手册

安装

MonetDB 官方快速安装入口:https://www.monetdb.org/easy-setup/

如果 MonetDB 是通过发行版默认软件包安装的,MONETDB_HOME 的常见取值可以是:

export MONETDB_HOME=/usr
  • 以 PGXS 方式构建
export USE_PGXS=1
export MONETDB_HOME=<MonetDB installation path>
export PATH=$MONETDB_HOME/bin:$PATH
export LD_LIBRARY_PATH=$MONETDB_HOME/lib64:$LD_LIBRARY_PATH
git clone https://github.com/saulojb/pg_monetdb.git
cd pg_monetdb
make && make install
  • 在 PostgreSQL 源码树中构建
export MONETDB_HOME=<MonetDB installation path>
export PATH=$MONETDB_HOME/bin:$PATH
export LD_LIBRARY_PATH=$MONETDB_HOME/lib64:$LD_LIBRARY_PATH
git clone https://github.com/saulojb/pg_monetdb.git <PostgreSQL contrib source path>/pg_monetdb
cd <PostgreSQL contrib source path>/pg_monetdb
make && make install

快速教程

  • 创建 pg_monetdb 扩展
CREATE EXTENSION pg_monetdb;
  • 创建 foreign server
CREATE SERVER foreign_server FOREIGN DATA WRAPPER pg_monetdb
OPTIONS (host '127.0.0.1', port '50000', dbname 'test');
  • 创建用户映射
CREATE USER MAPPING FOR CURRENT_USER SERVER foreign_server OPTIONS (user 'zm', password 'zm');
  • 使用 pg_monetdb_execute 在 MonetDB 中创建示例表 emp
SELECT pg_monetdb_execute('foreign_server', $$CREATE TABLE emp(
  name VARCHAR(20),
  age INTEGER
)$$);
  • 创建 foreign table
CREATE FOREIGN TABLE emp(
  name VARCHAR(20),
  age INTEGER
)
SERVER foreign_server
OPTIONS (schema_name 'zm', table_name 'emp');
  • 之后就可以在 PostgreSQL 中查询 MonetDB 里的 emp
SELECT count(*) FROM emp;
  • 对于临时执行的远端 SQL,monet_query 返回原始文本行,而辅助函数可以把简单标量结果解析成数组或 JSON 对象。
SELECT * FROM monet_query('foreign_server', $$SELECT name, age FROM emp$$);

SELECT * FROM monet_query_to_array('foreign_server', $$SELECT name, age FROM emp$$);

SELECT *
FROM monet_query_to_jsonb(
  'foreign_server',
  $$SELECT name, age FROM emp$$,
  ARRAY['name', 'age']
);
  • 注意:也可以使用 IMPORT FOREIGN SCHEMA 更方便地创建 foreign table
DROP FOREIGN TABLE emp;
IMPORT FOREIGN SCHEMA "zm" limit to (emp) from server foreign_server into public;

支持的操作

  • INSERT
  • DELETE
  • UPDATE
  • SELECT
  • COPY
  • TRUNCATE
  • EXPLAIN
  • IMPORT FOREIGN SCHEMA

支持的类型

类型 支持情况 说明
CHAR Y 参考 PostgreSQL 文档
VARCHAR Y 参考 PostgreSQL 文档
TEXT Y 参考 PostgreSQL 文档。TEXT(x) 不受支持;导入 PostgreSQL 时会转换为 VARCHAR(x)
CLOB Y 底层类型是 TEXT。CLOB(x) 不受支持;导入 PostgreSQL 时会转换为 VARCHAR(x)
STRING Y 底层类型是 TEXT。STRING(x) 不受支持;导入 PostgreSQL 时会转换为 VARCHAR(x)
BLOB Y 底层类型是 bytea;基于 bytea 的域类型如 blob 也受支持
BOOL Y 参考 PostgreSQL 文档
TINYINT Y 底层类型是 smallint
SMALLINT Y 参考 PostgreSQL 文档
INTEGER Y 参考 PostgreSQL 文档
BIGINT Y 参考 PostgreSQL 文档
HUGEINT Y 映射为 PostgreSQL 上基于 numeric(39,0)HUGEINT 域,范围为 -2^127 + 12^127 - 1
DECIMAL Y NUMERIC
REAL Y 参考 PostgreSQL 文档
DOUBLE PRECISION Y 参考 PostgreSQL 文档
FLOAT Y 参考 PostgreSQL 文档
DATE Y 参考 PostgreSQL 文档
TIME Y 参考 PostgreSQL 文档
TIME WITH TIME ZONE Y 参考 PostgreSQL 文档
TIMESTAMP Y 参考 PostgreSQL 文档
TIMESTAMP WITH TIME ZONE Y 参考 PostgreSQL 文档
INTERVAL YEAR Y 导入为 PostgreSQL interval month;round-trip 通过 MonetDB 的 month-based interval family 处理
INTERVAL YEAR TO MONTH Y 导入为 PostgreSQL interval month;round-trip 通过 MonetDB 的 month-based interval family 处理
INTERVAL MONTH Y 导入为 PostgreSQL interval month;已经通过 IMPORT FOREIGN SCHEMA 验证 round-trip
INTERVAL DAY Y 导入为 PostgreSQL interval day;FDW 会在读写时归一化 MonetDB 的原始秒数存储
INTERVAL DAY TO HOUR Partial MonetDB 将其存为 sec_interval;导入 PostgreSQL 后会表现为 interval second,原始 qualifier 不保留
INTERVAL DAY TO MINUTE Partial MonetDB 将其存为 sec_interval;导入 PostgreSQL 后会表现为 interval second,原始 qualifier 不保留
INTERVAL DAY TO SECOND Partial MonetDB 将其存为 sec_interval;导入 PostgreSQL 后会表现为 interval second,原始 qualifier 不保留
INTERVAL HOUR Partial MonetDB 将其存为 sec_interval;导入 PostgreSQL 后会表现为 interval second,原始 qualifier 不保留
INTERVAL HOUR TO MINUTE Partial MonetDB 将其存为 sec_interval;导入 PostgreSQL 后会表现为 interval second,原始 qualifier 不保留
INTERVAL HOUR TO SECOND Partial MonetDB 将其存为 sec_interval;导入 PostgreSQL 后会表现为 interval second,原始 qualifier 不保留
INTERVAL MINUTE Partial MonetDB 将其存为 sec_interval;导入 PostgreSQL 后会表现为 interval second,原始 qualifier 不保留
INTERVAL MINUTE TO SECOND Partial MonetDB 将其存为 sec_interval;导入 PostgreSQL 后会表现为 interval second,原始 qualifier 不保留
INTERVAL SECOND Y 导入为 PostgreSQL interval second;已经通过 IMPORT FOREIGN SCHEMA 验证 round-trip
JSON Y 参考 PostgreSQL 文档
UUID Y 参考 PostgreSQL 文档
URL Y 底层类型是 TEXT
INET Y 参考 PostgreSQL 文档

测试用例请参考 type_support.sql

当前 MonetDB interval 支持状态如下:远端引擎接受 INTERVAL MONTHINTERVAL DAYINTERVAL SECOND 这类带 qualifier 的形式,并在 MonetDB 元数据中呈现为 month_intervalday_intervalsec_intervalIMPORT FOREIGN SCHEMA 会把这些 family 映射成 PostgreSQL 的 interval monthinterval dayinterval second,而 pg_monetdb 已经补齐这些已导入 family 所需的写入格式化与读取归一化逻辑,因此这些 family 已经可以完成端到端 round trip。当前剩余限制仍然是 qualifier fidelity:凡是 MonetDB 底层走 sec_interval 的类型,例如 INTERVAL DAY TO SECOND,导入 PostgreSQL 后目前仍统一表现为 interval second,因此底层 family 可用,但原始 qualifier 暂时不会保留。

手工验证

如果你要针对已导入到 schema monet 的 TPC-H foreign tables,在现有 PostgreSQL 数据库上做 planner 验证,请参考 materialized_cte_manual.sql

重要说明:MonetDB 不接受 ANSI MATERIALIZED / NOT MATERIALIZED CTE 语法。因此 pg_monetdb 不能把 PostgreSQL 的 WITH ... AS MATERIALIZED (...) 子句等价下推成远端 SQL。当前已经验证的安全行为,是把 materialization boundary 保留在 PostgreSQL 本地。

典型执行方式:

sudo -n -u postgres psql -X -p 5433 -d monet_test -f sql/materialized_cte_manual.sql

如果要验证“已下推 grouped CTE 之上再接本地 window stage”的 grouped bridge 场景,请参考 grouped_bridge_window_manual.sql

典型执行方式:

sudo -n -u postgres psql -X -p 5433 -d monet_test -f sql/grouped_bridge_window_manual.sql

对于 INNER JOIN LATERAL 且 lateral 子查询只是一个标量相关聚合的场景,当前 pg_monetdb 仍会把外层 join 保留在本地。在这个特定模式下,可以安全地改写成标量相关 WHERE 形式,并且已经可以完全下推。参考 lateral_scalar_rewrite_manual.sql

典型执行方式:

sudo -n -u postgres psql -X -p 5433 -d monet_test -f sql/lateral_scalar_rewrite_manual.sql

实验性选项:

如果后端 session 在第一次 FDW 查询之前就预加载了 pg_monetdb,当前 planner-hook 实验实现可以自动规范化这个精确的 INNER JOIN LATERAL 标量聚合模式,并生成同样的 fully pushed-down plan。一个测试方式如下:

sudo -n -u postgres env PGOPTIONS='-c session_preload_libraries=pg_monetdb' \
  psql -X -p 5433 -d monet_test -f sql/lateral_scalar_rewrite_manual.sql

这是一个实验性工作流。如果没有 session preload,某个 backend 的第一条 FDW 查询仍然可能错过这次改写,并把原始 JOIN LATERAL 形态保留在本地。在该 backend session 的第一条 FDW 查询之前显式执行 LOAD 'pg_monetdb',也能激活同一条 planner 路径。

改写示例:

-- 原始 INNER JOIN LATERAL 形式
SELECT
  SUM(l.l_extendedprice) / 7.0 AS avg_yearly
FROM
  part p
  JOIN lineitem l ON l.l_partkey = p.p_partkey
  JOIN LATERAL (
    SELECT 0.2 * AVG(l2.l_quantity) AS threshold
    FROM lineitem l2
    WHERE l2.l_partkey = p.p_partkey
  ) aq ON l.l_quantity < aq.threshold
WHERE
  p.p_brand = 'Brand#23'
  AND p.p_container = 'MED BOX';

-- 推荐的、可下推的标量相关 WHERE 改写
SELECT
  SUM(l.l_extendedprice) / 7.0 AS avg_yearly
FROM
  part p
  JOIN lineitem l ON l.l_partkey = p.p_partkey
WHERE
  p.p_brand = 'Brand#23'
  AND p.p_container = 'MED BOX'
  AND l.l_quantity < (
    SELECT 0.2 * AVG(l2.l_quantity)
    FROM lineitem l2
    WHERE l2.l_partkey = p.p_partkey
  );

这里只推荐对这样一种 INNER JOIN LATERAL 形态使用该改写:lateral 侧只返回一行标量聚合结果,相关条件基于外层 relation,而且 join predicate 只是把外层列与该标量结果做比较。

限制

DELETE 和 UPDATE 操作要求远端表存在 Primary Key。