pg_clickhouse 强力更新:JSONB 支持,复杂数据查询更高效!
本文字数:5920;估计阅读时间:15 分钟
作者:David Wheeler
我们对社区对pg\_clickhouse的欢迎感到欣慰,它是一个用于从 Postgres 查询 ClickHouse 数据库的扩展。近期大量的使用产生了许多反馈,我们一直在最近的几个版本中积极地解决这些问题。这些改动遵循了我们对 pg\_clickhouse 一贯的宗旨:下推,下推,再下推!让我们快速了解一下。
如果您想跟着操作,以下示例使用了这个 ClickHouse 表:
CREATE TABLE events (id UInt32,event String,tags Array(String),at DateTime64,props JSON) ENGINE = MergeTree ORDER BY (event, id);INSERT INTO events VALUES(1, 'order', ['a', 'b', 'c'], '2025-12-28 10:42:35.342','{"cid": "C100", "address": {"city": "Paris, France", "code": "75001"}}'),(2, 'order', ['d', 'e', 'f'], '2026-02-22 05:26:26.982','{"cid": "C200", "address": {"city": "London, UK", "code": "SW1A"}}'),(3, 'return', ['😀', '⚽️'], now64() - 86400 * 2,'{"cid": "C200", "address": {"city": "Manchester, UK", "code": "M2 1AB"}}'),(4, 'order', ['x', 'y', 'z'], now64() - 86400,'{"cid": "C300", "address": {"city": "New York, USA", "code": "10030"}}'),(5, 'deliver', [], now64(),'{"cid": "C500", "address": {"city": "Portland, USA", "code": "97212"}}');
以及 Postgres 中以下 pg\_clickhouse 外部表配置:
CREATE SERVER ch FOREIGN DATA WRAPPER clickhouse_fdw OPTIONS(driver 'http');CREATE USER MAPPING FOR CURRENT_USER SERVER ch;CREATE EXTENSION pg_clickhouse;CREATE SCHEMA customer;IMPORT FOREIGN SCHEMA "default" FROM SERVER ch INTO customer;
对于映射到 ClickHouse JSON 类型的 Postgres JSONB 列,pg\_clickhouse v0.1.10 增加了 JSONB 访问器运算符和函数在 SELECT 子句之外(通常在 WHERE、ORDER BY 和 HAVING 子句中)的下推功能。它通过将 JSON 属性访问器转换为 ClickHouse 子列语法来实现此功能。
例如,在使用 ->> 比较 JSON 对象值与字符串时,来自这个详细 EXPLAIN 的 Remote SQL 输出如下:
EXPLAIN (VERBOSE, COSTS OFF)SELECT id, event, propsFROM customer.eventsWHERE props ->> 'cid' = 'C200';
QUERY PLAN------------------------------------------------------------------------------------------Foreign Scan on customer.eventsOutput: id, event, propsRemote SQL: SELECT id, event, props FROM "default".events WHERE ((props.cid = 'C200'))(3 rows)
这使得 ClickHouse 能够直接过滤“C100”。输出结果正如您所预期:
SELECT id, event, propsFROM customer.eventsWHERE props ->> 'cid' = 'C200';
id | event | props----+--------+--------------------------------------------------------------------------2 | order | {"cid": "C200", "address": {"city": "London, UK", "code": "SW1A"}}3 | return | {"cid": "C200", "address": {"city": "Manchester, UK", "code": "M2 1AB"}}(2 rows)
对于返回 JSONB 值的 -> 运算符,pg\_clickhouse 让 ClickHouse 将 子列语法返回的值转换为 JSON,以便像 Postgres 那样进行值比较:
EXPLAIN (VERBOSE, COSTS OFF)SELECT id, event, propsFROM customer.eventsWHERE props -> 'cid' = '"C300"'::jsonb;
QUERY PLAN----------------------------------------------------------------------------------------------------------Foreign Scan on customer.eventsOutput: id, event, propsRemote SQL: SELECT id, event, props FROM "default".events WHERE ((toJSONString(props.cid) = '"C300"'))(3 rows)
执行查询返回预期结果:
SELECT id, event, propsFROM customer.eventsWHERE props -> 'cid' = '"C300"'::jsonb;
id | event | props----+-------+------------------------------------------------------------------------4 | order | {"cid": "C300", "address": {"city": "New York, USA", "code": "10030"}}(1 row)
同样,JSONB 函数 jsonb_extract_path() 和 jsonb_extract_path_text() 也遵循相同的模式,它们同样支持通过多条路径访问嵌套值。这一点可以在此计划的 Remote SQL 中清晰可见:
EXPLAIN (VERBOSE, COSTS OFF)SELECT id, event, props FROM customer.eventsWHERE jsonb_extract_path_text(props, 'address', 'city') = 'Paris, France';
QUERY PLAN------------------------------------------------------------------------------------------------------------Foreign Scan on customer.eventsOutput: id, event, propsRemote SQL: SELECT id, event, props FROM "default".events WHERE ((props.address.city = 'Paris, France'))(3 rows)
当然,执行结果符合预期:
SELECT id, event, propsFROM customer.eventsWHERE jsonb_extract_path_text(props, 'address', 'city') = 'Paris, France';
id | event | props----+-------+------------------------------------------------------------------------1 | order | {"cid": "C100", "address": {"city": "Paris, France", "code": "75001"}}(1 row)
同样,在使用 jsonb_extract_path() 下推 JSON 值比较时也适用:
EXPLAIN (VERBOSE, COSTS OFF)SELECT id, event, propsFROM customer.eventsWHERE jsonb_extract_path(props, 'address', 'city') = '"New York, USA"';
QUERY PLAN----------------------------------------------------------------------------------------------------------------------------Foreign Scan on customer.eventsOutput: id, event, propsRemote SQL: SELECT id, event, props FROM "default".events WHERE ((toJSONString(props.address.city) = '"New York, USA"'))(3 rows)
SELECT id, event, propsFROM customer.eventsWHERE jsonb_extract_path(props, 'address', 'city') = '"New York, USA"';
id | event | props----+-------+------------------------------------------------------------------------4 | order | {"cid": "C300", "address": {"city": "New York, USA", "code": "10030"}}(1 row)
我们的一位客户在将查询从 Postgres 迁移到 pg\_clickhouse 表后,发现在使用某些 日期和时间函数(如 CURRENT_DATE 和 CURRENT_TIMESTAMP)时遇到了一些故障。由于 pg\_clickhouse 未能下推这些函数,当它们与 date_part() 和 date_trunc() 等(这些函数能够下推)配合使用时便会产生问题。
pg\_clickhouse v0.2.0改进了所有“当前”类型日期和时间函数的下推能力,现在这些函数都能被下推,并且能比以前更准确地根据本地 Postgres 配置生成相应的值。
例如,要查询 `CURRENT_DATE` 之前的记录时,pg\_clickhouse 会生成以下计划:
EXPLAIN (VERBOSE, COSTS OFF)SELECT id FROM customer.events WHERE AT < CURRENT_DATE;
它利用 Postgres 会话中当前设置的时区,以确保日期相对于预期的时区。对于 CURRENT_TIMESTAMP 也是如此,它同时指定了精度 6,这是 Postgres 时间戳的默认精度:
EXPLAIN (VERBOSE, COSTS OFF)SELECT idFROM customer.eventsWHERE AT < CURRENT_TIMESTAMP;
QUERY PLAN-------------------------------------------------------------------------------------------Foreign Scan on customer.eventsOutput: idRemote SQL: SELECT id FROM "default".events WHERE ((at < now64(6, 'America/New_York')))(3 rows)
自然地,它会传递一个显式精度:
EXPLAIN (VERBOSE, COSTS OFF)SELECT idFROM customer.eventsWHERE AT < CURRENT_TIMESTAMP(3);
QUERY PLAN-------------------------------------------------------------------------------------------Foreign Scan on customer.eventsOutput: idRemote SQL: SELECT id FROM "default".events WHERE ((at < now64(3, 'America/New_York')))(3 rows)
除了这些 SQL 标准的当前日期和时间关键字外,我们还为 Postgres 特有的时间戳函数 clock_timestamp()、statement_timestamp() 和 transaction_timestamp() 增加了下推能力,它们都能被下推到最接近的 ClickHouse 等效函数 nowInBlock64:
EXPLAIN (VERBOSE, COSTS OFF)SELECT idFROM customer.eventsWHERE AT < clock_timestamp();
QUERY PLAN--------------------------------------------------------------------------------------------------Foreign Scan on customer.eventsOutput: idRemote SQL: SELECT id FROM "default".events WHERE ((at < nowInBlock64(6, 'America/New_York')))(3 rows)
这些函数能与 date_part 等其他下推函数正常配合使用:
EXPLAIN (VERBOSE, COSTS OFF)SELECT id, at FROM customer.eventsWHERE date_part('year', at) < date_part('year', CURRENT_DATE);
QUERY PLAN----------------------------------------------------------------------------------------------------------------------------------------------Foreign Scan on customer.eventsOutput: id, atRemote SQL: SELECT id, at FROM "default".events WHERE ((toYear(at) < toYear(cast(toDate(now('America/New_York')), 'Nullable(DateTime)'))))(3 rows)
SELECT id, atFROM customer.eventsWHERE date_part('year', at) < date_part('year', CURRENT_DATE);
id | at----+----------------------------1 | 2025-12-28 05:42:35.342-05(1 row)
同样,它们也能与 date_trunc 正常配合,即便加入了某些间隔日期运算:
EXPLAIN (VERBOSE, COSTS OFF)SELECT id, atFROM customer.eventsWHERE date_trunc('day', at) >= date_trunc('day', CURRENT_DATE) - INTERVAL '1 day';
QUERY PLAN-----------------------------------------------------------------------------------------------------------------------------------------Foreign Scan on customer.eventsOutput: id, atRemote SQL: SELECT id, at FROM "default".events WHERE ((toStartOfDay(at) >= (toStartOfDay(toDate(now('America/New_York'))) - 86400)))(3 rows)
SELECT id, atFROM customer.eventsWHERE date_trunc('day', at) >= date_trunc('day', CURRENT_DATE) - INTERVAL '1 day';
id | at----+----------------------------5 | 2026-04-17 17:29:47.046-044 | 2026-04-16 17:29:47.046-04(2 rows)
继 v0.1.4中 HTTP 驱动数组解析得到改进之后,pg_clickhouse v0.2.0 增加了对一系列 数组函数的下推 (pushdown) 支持。例如,array_cat 映射到 arrayConcat:
EXPLAIN (VERBOSE, COSTS OFF)SELECT id, tags FROM customer.events WHERE array_cat(tags, ARRAY['🥏']) = ARRAY['😀','⚽️','🥏'];
QUERY PLAN------------------------------------------------------------------------------------------------------------Foreign Scan on customer.eventsOutput: id, tagsRemote SQL: SELECT id, tags FROM "default".events WHERE ((arrayConcat(tags, ['🥏']) = ['😀','⚽️','🥏']))(3 rows)
SELECT id, tagsFROM customer.eventsWHERE array_cat(tags, ARRAY['🥏']) = ARRAY['😀','⚽️','🥏'];
id | tags----+---------3 | {😀,⚽️}(1 row)
array_to_string 映射到 arrayStringConcat:
EXPLAIN (VERBOSE, COSTS OFF)SELECT id, tagsFROM customer.eventsWHERE array_to_string(tags, '|') = 'a|b|c';
QUERY PLAN------------------------------------------------------------------------------------------------------Foreign Scan on customer.eventsOutput: id, tagsRemote SQL: SELECT id, tags FROM "default".events WHERE ((arrayStringConcat(tags, '|') = 'a|b|c'))(3 rows)
SELECT id, tagsFROM customer.eventsWHERE array_to_string(tags, '|') = 'a|b|c';
id | tags----+---------1 | {a,b,c}(1 row)
此外,string_to_array 映射到 splitByString,此处与 前述 JSONB 访问器结合使用:
EXPLAIN (VERBOSE, COSTS OFF)SELECT id, event, jsonb_extract_path_text(props, 'address', 'code')FROM customer.eventsWHERE string_to_array(jsonb_extract_path_text(props, 'address', 'city'), ', ') = ARRAY['Portland', 'USA'];
QUERY PLAN----------------------------------------------------------------------------------------------------------------------------------------------Foreign Scan on customer.eventsOutput: id, event, tags, at, propsRemote SQL: SELECT id, event, tags, at, props FROM "default".events WHERE ((splitByString(', ', props.address.city) = ['Portland','USA']))(3 rows)
SELECT id, event, jsonb_extract_path_text(props, 'address', 'code')FROM customer.eventsWHERE string_to_array(jsonb_extract_path_text(props, 'address', 'city'), ', ') = ARRAY['Portland', 'USA'];
id | event | jsonb_extract_path_text----+---------+-------------------------5 | deliver | 97212(1 row)
我们还支持了更多映射!有关更多功能,请查阅 完整列表(https://pgxn.org/dist/pg_clickhouse/doc/pg_clickhouse.html#Pushdown.Functions "pg_clickhouse Docs: Pushdown Functions"),探索其广泛的可能性。
当然,我们并非只关注下推 (pushdown);有时,我们也需要处理所谓的“回推 (pushback)”。
默认情况下,当 Postgres 外部数据包装器 (foreign data wrapper) 执行外部查询时,它会在将所有结果返回给调用方之前,将它们全部收集到内存中。这对于 ClickHouse 典型聚合查询返回的小型结果集非常有效。但有时应用程序需要自行处理大量数据,这可能导致内存压力问题,因为 Postgres 会将整个数据集加载到内存中。警惕 OOM Killer 的出现!
在 pg\_clickhouse v0.1.10中,我们为 HTTP 驱动程序添加了查询结果流式传输 (query result streaming) 功能。该功能默认在内存中缓冲约 50MB 的结果批次,并在内存被下一批次数据重用前将其返回。为了实际演示其效果,我们首先将 NYC taxi data set加载到 ClickHouse 中,然后启动了一个未启用流式传输功能的 pg\_clickhouse v0.6.1 OCI 镜像 (image),并使用 http 驱动程序将 trips_small 表导入到 nyc_taxi 模式:
docker run --name pg_clickhouse -p 6432:5432 -e POSTGRES_HOST_AUTH_METHOD=trust -d ghcr.io/clickhouse/pg_clickhouse:18docker exec -it pg_clickhouse bash -c 'apt-get update && apt-get install ca-certificates'psql -U postgres -h localhost -p 6432 <<EOFCREATE EXTENSION pg_clickhouse;CREATE SERVER my_ch FOREIGN DATA WRAPPER clickhouse_fdw OPTIONS(driver 'http', host 'abcdefghij.us-east-1.aws.clickhouse.cloud', port '8443');CREATE USER MAPPING FOR CURRENT_USER SERVER my_ch OPTIONS(user 'default', password 'xxxxxxxxxxxxx');CREATE SCHEMA nyc_taxi;IMPORT FOREIGN SCHEMA nyc_taxi FROM SERVER my_ch INTO nyc_taxi;EOF
我们启动了一个进程,持续测量该 OCI 容器的内存消耗:
while true; dodocker stats --no-stream --format "{{.MemUsage}}" pg_clickhouse | \cut -d '/' -f 1 | xargs printf "%s%s\n" ",[object Object]," | sed -e 's/MiB//g'done
最后,我们运行了以下查询:
psql -U postgres -h localhost -p 6432 -c 'SELECT * FROM nyc_taxi.trips_small' > /dev/null
随后,我们使用已启用流式传输功能的 pg\_clickhouse v0.2.0 OCI 镜像 (image)重复了上述步骤,并对结果进行了比较。下图清晰地展示了两者差异:
以下是经过时间对齐处理后的数据,同样印证了这一结果:
可以看到,v0.6.0 版本的内存消耗飙升至 600MiB 以上,而启用流式传输功能的 v0.2.0 版本从未超过 86 MiB。同时,v0.2.0 的性能也略有提升!显然,结果集越大,内存节省越显著。我们还计划在未来的版本中,在二进制驱动程序中引入流式传输功能。
我们还有更多内容正在开发中。敬请关注此空间,获取有关 window function pushdown 和正则表达式兼容性的更多进展。在此期间,欢迎加入我们的 [PGConf.dev](https://2026.pgconf.dev/ "PGConf.dev 2026") 大会,了解我们在 构建 Foreign Data Wrapper过程中所学到的经验。
/END/
试用阿里云 ClickHouse企业版
轻松节省30%云资源成本?阿里云数据库ClickHouse 云原生架构全新升级,首次购买ClickHouse企业版计算和存储资源组合,首月消费不超过99.58元(包含最大16CCU+450G OSS用量)了解详情:https://t.aliyun.com/Kz5Z0q9G
征稿启示
面向社区长期正文,文章内容包括但不限于关于 ClickHouse 的技术研究、项目实践和创新做法等。建议行文风格干货输出&图文并茂。质量合格的文章将会发布在本公众号,优秀者也有机会推荐到 ClickHouse 官网。请将文章稿件的 WORD 版本发邮件至:[email protected]