pg_clickhouse 与 chdb 更新:编码、嵌套与类型
发布日期:2026-09-30T16:29:40.227Z
作者:David Wheeler
分类:产品
摘要:最新的 pg_clickhouse 和 chdb 扩展版本改进了 ClickHouse 与 Postgres 之间的字符编码、间隔处理、嵌套数据、JSON 和类型映射。
pg_clickhouse 与 chdb 更新:编码、嵌套与类型
现已在 GitHub 和 PGXN 上发布,pg_clickhouse v0.11.0 和 chdb 扩展 v0.1.2 继续我们坚定不移地专注于跨数据库兼容性。其中大量增强源自我们的仅头文件 C 库 clickhouse-c 和 pg-clickhouse-c。让我们看看这些版本中仅有的三项更改。
好一个字符 {#what_a_character}
首先是字符编码。在开发 chdb 扩展文章的基准测试过程中,我发现 pg-clickhouse-c 没有验证文本列的字符编码。我的同事 Philip 迅速修补了该库,使其在任何基于文本或 JSON 的类型[1](#1)包含违反数据库编码的字节时抛出异常。
此修复随 chdb v0.1.1 发布,但我们推迟了 pg_clickhouse 的发布,以避免对任何拥有读取无效编码数据的现有外部表的用户造成错误。pg_clickhouse v0.11.0 添加了一个新的外部服务器选项 check_encoding,它提供编码错误处理器。选项包括:
fail(默认):抛出错误remove:移除无效字节replace:在 UTF-8 编码下,将无效字节替换为 Unicode 替换字符(�);对于其他编码,等同于removetruncate:在第一个无效字节处截断文本
chdb_hook v0.1.2 为其 COPY 和 CREATE TABLE 命令提供了相同的选项。两者都允许你解决类似以下的错误:
shell
ERROR: invalid byte sequence for encoding "UTF8": 0x81
更改 pg_clickhouse 服务器配置 check_encoding 以消除错误。最易读的将是 replace:
ALTER SERVER ch_server_name OPTIONS (ADD check_encoding 'replace');
对于 chdb_hook,将其作为 COPY 或 CREATE TABLE 选项传入:
CREATE TABLE logs () WITH (
copy_from = 's3://chdb-lakedata-public/logs/logs-2026-08-26.csv',
format = 'CSVWithNames',
check_encoding = 'replace'
);
对于 UTF-8 编码的数据库,无效字节将被替换为 �:
shell
try=# SELECT * FROM ch_table ORDER BY id;
id | name
----+---------
1 | Barrack
2 | Ale�y
3 | Leopold
4 | An�n�e
对于其他数据库编码,有问题的字符将被直接移除:
shell
try=# SELECT * FROM ch_table ORDER BY id;
id | name
----+---------
1 | Barrack
2 | Aley
3 | Leopold
4 | Ann
另一方面,如果你需要保留字节兼容性,则需要将有问题的列映射为 bytea:
ALTER FOREIGN TABLE ch_table ALTER name TYPE bytea;
这将保留逐字节的二进制数据:
shell
try=# SELECT * FROM ch_table ORDER BY id;
id | name
----+------------------
1 | \x4261727261636b
2 | \x416c650079
3 | \x4c656f706f6c64
4 | \x416e006e8165
(4 rows)
但请注意,转换为文本将会失败。
间隔有效 {#intervalid}
ClickHouse 支持众多间隔类型:IntervalNanosecond、IntervalHour、IntervalDay、IntervalYear,以及介于两者之间的所有类型。在以前的版本中,pg_clickhouse 不支持这些类型;尝试导入使用其中一种类型的 ClickHouse 表会返回错误。
不再如此。pg_clickhouse v0.11.0 和 chdb_hook 0.1.2 将这些类型导入为 Postgres interval 列。因此,给定一个使用例如 IntervalMillisecond 的 ClickHouse 表,如下面 duration 列所示:
CREATE TABLE logs (
req_id Int64 NOT NULL,
start_at DateTime64(6, 'UTC') NOT NULL,
duration IntervalMillisecond NOT NULL,
resource Text NOT NULL,
method Enum8('GET' = 1, 'HEAD', 'POST', 'PUT', 'DELETE', 'PATCH') NOT NULL,
node_id Int64 NOT NULL,
response Int32 NOT NULL
) ENGINE = MergeTree
ORDER BY start_at;
导入时,pg_clickhouse 会创建一个带有 duration interval 列的表:
| 列 | 类型 | 可空 |
|---|---|---|
| req_id | bigint | not null |
| start_at | timestamp(6) with time zone | not null |
| duration | interval | not null |
| resource | text | not null |
| method | text | not null |
| node_id | bigint | not null |
| response | integer | not null |
当然,下推也有效。假设你想统计昨天一天结束前完成的所有事务。只需将持续时间加到开始时间:
shell
try=# EXPLAIN (VERBOSE, COSTS OFF)
SELECT COUNT(*)
FROM logs
WHERE start_at + duration < date_trunc('day', now());
QUERY PLAN
-----------------------------------------------------------------------------------------------------------
Foreign Scan
Output: (count(*))
Relations: Aggregate on (logs)
Remote SQL: SELECT count(*) FROM "default".logs WHERE (((start_at + duration) < toStartOfDay(now64())))
(4 rows)
EXPLAIN (VERBOSE) 输出显示了在 ClickHouse 上执行的远程查询,它显然将 start_at + duration 下推到 ClickHouse 中执行(当然,还有 COUNT() 聚合[2](#2))。
同样的模式适用于 chdb_hook v0.1.2:它将 chDB 间隔类型导入为 Postgres interval 值。两个扩展也都允许将间隔类型改为导入为 bigint。只需创建带有 duration bigint 的外部表或复制目标表,扩展会完成其余工作。
嵌套本能 {#nesting_instinct}
pg_clickhouse 的 http 驱动自 v0.1 起就支持 JSON 类型,二进制驱动自 v0.3 起也支持。然而,尽管它会下推 JSON 属性访问器,例如:
SELECT * FROM things ORDER BY data ->> 'name';
ClickHouse 会返回错误:
shell
DB::Exception: Data types Variant/Dynamic are not allowed in ORDER BY keys, because it can lead to unexpected results.
Consider using a subcolumn with a specific data type instead
此错误源于 ClickHouse JSON 对象的实现:ClickHouse 想要知道 JSON 属性存在才能排序。ClickHouse 25.3+ 参数化子列保证了属性存在。一个示例:
CREATE TABLE things (
id Int32 NOT NULL,
data JSON(
id UInt32,
name String,
size Enum('small', 'medium', 'large'),
stocked Bool
) NOT NULL
) ENGINE = MergeTree PARTITION BY id ORDER BY (id);
以前,pg_clickhouse 无法导入参数化 JSON 列,但 v0.11.0(以及 chdb_hook v0.1.2)简单地将其映射为 jsonb(或 json),现在对属性的 ORDER BY 可以正确下推:
shell
try=# SELECT * FROM things ORDER BY data ->> 'name';
id | data
----+-----------------------------------------------------------------
4 | {"id": 4, "name": "doodad", "size": "large", "stocked": false}
3 | {"id": 3, "name": "gizmo", "size": "medium", "stocked": true}
2 | {"id": 2, "name": "sprocket", "size": "small", "stocked": true}
1 | {"id": 1, "name": "widget", "size": "large", "stocked": true}
(4 rows)
类似地,pg_clickhouse v0.11.0 和 chdb_hook v0.1.2 改进了对未展平 Nested 类型的支持,如下例所示:
CREATE TABLE visits(
visit_id UInt64,
user_id UInt64,
goals Nested(
serial UInt32,
order_id String
)
) ENGINE = MergeTree ORDER BY visit_id SETTINGS flatten_nested = 0;
flatten_nested=0 指示 ClickHouse 创建一个单一的 goals 列,格式为 Tuple(serial UInt32, order_id String) 的数组(而不是为每个字段单独的数组列)。以前,两个扩展都不支持这种结构。现在它们提供两种映射。
默认情况下,IMPORT FOREIGN SCHEMA 和 chdb_hook 的 COPY 与 CREATE TABLE 命令将未展平的 Nested 列映射为二维文本数组:
| 列 | 类型 |
|---|---|
| visit_id | numeric(20,0) |
| user_id | numeric(20,0) |
| goals | text\[\]\[\] |
这会将 Nested 值中的每一项映射为每种类型文本表示的数组:
shell
try=# SELECT * FROM nest_bin.visits WHERE visit_id < 3 ORDER BY visit_id;
visit_id | user_id | goals
----------+---------+-----------------
1 | 1 | {{1,xx},{2,yy}}
(1 row)
goals 数组包含两个数组,每个数组有两个文本值,第一个用于 serial,第二个用于 order_id。这种结构以牺牲数据类型为代价保留了数据,不过在 pg_clickhouse 表上执行 INSERT 会在插入 ClickHouse 之前正确转换类型:
INSERT INTO visits
VALUES (2, 2, ARRAY[ ['3', 'aa'], ['4', 'bb'] ]);
但我们可以做得更好。pg_clickhouse v0.11.0 还允许将 Nested 值映射到自定义复合类型,只要顺序、类型和命名完全一致。给定为 goals 定义的 Nested 类型:
shell
Tuple(serial UInt32, order_id String)
我们可以创建一个具有对应名称和类型的类型,并将其放入外部表:
CREATE TYPE goal_type AS (serial bigint, order_id text);
ALTER FOREIGN TABLE visits ALTER goals TYPE goal_type[];
现在 Nested 元组转换为复合类型:
shell
try=# SELECT * FROM nest_bin.visits WHERE visit_id < 3 ORDER BY visit_id;
visit_id | user_id | goals
----------+---------+---------------------
1 | 1 | {"(1,xx)","(2,yy)"}
2 | 2 | {"(3,aa)","(4,bb)"}
当然我们也可以按此格式 INSERT 数据:
INSERT INTO visits
VALUES (3, 3, ARRAY[row(5, 'jj'), row(6, 'zz')]::goal_type[]);
同样的模式适用于 chdb_hook v0.1.2:在处理从 ClickHouse 或 chDB 导出的嵌套数据时,Postgres 目标表可以使用多维值数组或适当结构的复合类型数组:
CREATE TYPE event_status AS ENUM ('new', 'done');
CREATE TYPE event_point AS (x integer, y integer);
CREATE TYPE event_label AS (key text, value bigint);
CREATE TYPE event_item AS (id integer, name text);
CREATE TABLE events (
status event_status,
point event_point,
labels event_label[],
items event_item[]
);
然后在 COPY 查询中使用相应的数据类型定义(或依赖某个 *WithNamesAndTypes 格式)来导入数据。
COPY events FROM 's3://chdb-lakedata-public/examples/events.parquet' (
structure $$
status Enum8('new' = 1, 'done' = 2),
point Tuple(Int32, Int32),
labels Map(String, Int64),
items Array(Tuple(id Int32, name String))
$$
);
这里我们为嵌套类型使用了 Array();如果数据是从 ClickHouse 以未展平(flatten_nested=0)结构导出的,你可以改用 Nested:
COPY events FROM 's3://chdb-lakedata-public/examples/events.parquet' (
structure $$
status Enum8('new' = 1, 'done' = 2),
point Tuple(Int32, Int32),
labels Map(String, Int64),
items Nested(id Int32, name String)
$$
);
零碎事项 {#odds_and_ends}
pg_clickhouse v0.11.0 还附带了许多其他值得一提的改进:
-
眼尖的读者无疑已经注意到,除了间隔映射之外,大整数类型现在映射到适当的 Postgres numeric,许多其他 ClickHouse 数据类型现在也映射到适当的 Postgres 对应类型:
ClickHouse PostgreSQL Int128 numeric(39,0) Int256 numeric(77,0) UInt64 numeric(20,0) UInt128 numeric(39,0) UInt256 numeric(78,0) BFloat16 float4 Time time Time64§ time Tuple(...) text\[\] Map(K,V) text\[\]\[\] LineString path MultiLineString path\[\] MultiPolygon polygon\[\]\[\] Point point Ring polygon Polygon polygon\[\] 相同的映射适用于 chdb 扩展 v0.1.2。
-
最初的
clickhouse_raw_query()函数已在 v0.10.0 中弃用,现已被移除。请更新你的代码,使用clickhouse_query(server, sql)读取行,使用CALL clickhouse_perform(server, sql)运行不返回结果的语句。 -
此版本放弃了对 PostgreSQL 13 的支持,该版本自 2025 年 9 月起已不再受 Postgres 社区支持。
-
一项社区贡献添加了对 PostgreSQL
sha224()、sha256()、sha384()和sha512()函数的下推支持,以及对 pgcrypto 扩展digest()函数支持的常量算法调用。
查看完整的 pg_clickhouse 更改和 chdb 更改以了解更多细节,包括错误修复。然后从通常的地方获取它们。对于 pg_clickhouse:
对于 chdb 扩展:
立即开始使用 ClickHouse Managed Postgres
有兴趣了解 ClickHouse Managed Postgres 如何在你的数据上运作吗?几分钟内开始使用 ClickHouse Cloud,并获得 300 美元免费额度。
-
是的,JSON 按定义偏好 UTF-8,除非它并非如此。Postgres 中的 JSON 数据必须始终使用数据库编码。 ↩︎
-
不幸的是,ClickHouse 间隔类型本身尚不支持聚合,因此例如
avg(duration)会失败。但请关注 26.10 中的avg和sum支持。 ↩︎