ClickHouseInc

Postgres FDW:打破数据孤岛,外部数据如本地!

图片

本文字数:12899;估计阅读时间:33分钟

作者:Kaushik Iska, David Wheeler and Philip Dubé

Image

Postgres 的扩展为 Postgres 自身不具备的功能提供了补充。例如,PostGIS 用于地理空间数据,pgvector 用于向量嵌入(embeddings),而 TimescaleDB 则专注于时序数据。通过扩展,Postgres 生态系统能够不断引入新功能:只需执行 CREATE EXTENSION 即可将新功能集成到 Postgres 内部,之后用户通常无需再对其进行过多关注。

外部数据包装器(Foreign Data Wrapper)扩展,简称 FDW,能够让 Postgres 读取(有时甚至写入)存储在 Postgres 外部的数据。只需简单地声明一个外部表即可:

CREATE FOREIGN TABLE events (...)

SERVER my_clickhouse OPTIONS (table 'events');

然后,执行 SELECT * FROM events 就像查询任何其他常规表一样。在内部,Postgres 会请求 FDW 从外部源获取数据。

我们维护的 pg_clickhouse 是一个 FDW,用于从 ClickHouse 中获取数据。虽然用户通常会选择构建统一数据栈——即 Postgres 处理事务性数据,ClickHouse 负责分析任务——但 pg_clickhouse 却能在两个系统上执行 SQL 查询。在过去六个月的开发过程中,我们发现一个核心问题主导了大部分工程工作:我们应该以 SQL 查询的形式将多少逻辑推送给远程系统,又应该以数据行的形式从远程系统拉取多少数据?

这个问题正是 下推(pushdown)概念的精髓:我们能将多少计算任务“下推”到远程服务执行?答案似乎一目了然:“全部发送!”——无论是 WHERE 子句、GROUP BY 还是 LIMIT。然而,深入探究便会发现其复杂性:有些子句我们可以直接下推;有些通过重写后也能实现近似下推;有些我们曾经下推过,但因结果不准确而停止;还有些子句是任何 FDW 都无法下推的,无论投入多少工程精力都无济于事。

我们发现,做出这些判断的过程是高度迭代的。

为了更好地阐明这一点,我们接下来将深入探讨迭代过程对单个查询的影响:哪些部分被成功下推,哪些未能下推,以及我们如何围绕这一核心问题不断地优化和修改代码。

本文旨在为那些听说过 FDW (Foreign Data Wrapper) 但不了解其工作细节的读者,深入剖析 FDW 的内部机制。无论您是对 ClickHouse 感到好奇的 Postgres 用户,是对 Postgres 感到好奇的 ClickHouse 用户,还是正考虑自行开发 FDW 的开发者,我们都希望您能从本文中有所收获。

Image
80 毫秒与 4 分钟的查询

此查询根据国家和事件名称,对美国、英国和德国过去一周最活跃的 Web 事件进行排名。它将报告事件总量、独立用户数、高级份额、p95 延迟,以及各事件在对应国家的排名,并最终返回前 100 条记录,旨在提供一个概览快照,避免传输所有原始事件数据。

SELECT

  u.country,

  e.event_name,

  count(*) AS n,

  count(DISTINCT e.user_id) AS unique_users,

  count(*) FILTER (WHERE e.properties->>'tier' = 'premium') AS premium_count,

  percentile_cont(0.95) WITHIN GROUP (ORDER BY e.duration_ms) AS p95_ms,

  ROW_NUMBER() OVER (PARTITION BY u.country ORDER BY count(*) DESC) AS rank_in_country

FROM events_ch e JOIN users_ch u USING (user_id)

WHERE e.ts >= now() - interval '7 days'

  AND u.country IN ('US', 'UK', 'DE')

  AND e.properties->>'platform' = 'web'

GROUP BY u.country, e.event_name

ORDER BY n DESC

LIMIT 100;

events_ch 和 users_ch 是由 ClickHouse 支持的外部表。若此查询中的所有子句均能成功下推,它将在大约 80 毫秒内返回 100 条记录。

然而,如果有一个子句无法下推,查询耗时将变为数分钟。我们已实现了对窗口函数、百分位数、JSON 访问和 FILTER 聚合的下推支持;但在此之前,这些功能曾一度无法下推。当某项操作无法下推时,查询的其余部分也将无法下推;原本应在远程聚合的行必须流回 Postgres,以便在本地进行聚合。如此一来,网络传输的流量将从 100 行剧增至数千万乃至数亿行。

下推看似一项功能,实则更像是两种 SQL 语法之间的一项约定,并且会随每个 Postgres 版本的发布而修订。这解释了为何 pg_clickhouse 的发布说明中常出现诸多“修正”:例如撤销不正确的数组函数下推、增加 levenshtein 和 soundex 等更安全的函数,以及遵循诸如 EvalPlanQual 等规划器不变量,即使远程数据库能够更快地执行。

Image
FDW 的实际协商内容

FDW 不会将原始的 Postgres 查询直接发送给远程数据库;因为 SQL 方言的差异会导致错误。理想情况下,除非必要,FDW 也不会像 file_fdw 这类简单的 FDW 那样,将所有数据行全部拉回本地,因为尽管结果是正确的,但执行效率会非常低下。

示意图:外部数据包装器 (Foreign Data Wrapper) 如何作为应用程序与远程数据库之间的桥梁,负责转发 SQL 查询并返回数据行

Postgres 并不会直接决定某个扫描、连接、聚合、排序或限制操作可以在 ClickHouse 中执行。相反,它通过 FDW 规划回调机制,要求 FDW 向规划器贡献外部路径。pg_clickhouse 注册了以下规划回调函数:

routine->GetForeignRelSize    = clickhouseGetForeignRelSize;

routine->GetForeignPaths      = clickhouseGetForeignPaths;

routine->GetForeignPlan       = clickhouseGetForeignPlan;

routine->GetForeignJoinPaths  = clickhouseGetForeignJoinPaths;

routine->GetForeignUpperPaths = clickhouseGetForeignUpperPaths;

GetForeignPaths、GetForeignJoinPaths 和 GetForeignUpperPaths 函数会告知规划器哪些操作可以被下推执行。如果规划器选择其中一条路径,GetForeignPlan 函数将构建执行计划并生成对应的 ClickHouse SQL。

这些回调函数仅作为入口点;pg_clickhouse 的回调函数会验证每个子句或表达式是否能在不改变结果语义的前提下被正确转换。当然,其最初的功能相当简单:仅继承自 postgres_fdw 原始分支的一些子句和表达式。因此,pg_clickhouse 最初可以下推 count(*),但无法下推 count(*) FILTER (...),也无法下推 percentile_cont 或像 properties->>'platform' 这样的 JSON 谓词,因为它当时尚未实现对这些功能的支持。

因此,下推并非针对整个查询的简单“是/否”判断,而是由一系列更小的决策组成:Postgres 是否提供了相应的挂钩(hook),pg_clickhouse 是否能够正确转换该表达式,以及 ClickHouse 能否以相同的语义执行它?

Image
演进之路:一个查询,我们交付的每个阶段

为了解答“为什么 X 不能下推?”这个问题,我们将通过一个真实的查询,详细分析 pg_clickhouse 迄今为止所交付的所有功能。我们将以同一个查询为例,分七个步骤进行讲解。每一步都会将更多操作下推至 ClickHouse 执行;Postgres 则负责在从 ClickHouse 拉取所需数据后,对剩余未下推的部分进行评估。

每个步骤都将分析同一个查询。带有 ✓ 标记的行表示该操作在该步骤中被成功下推,而带有 ✗ 标记的行则表示该操作仍在本地执行。同时,每个步骤还会标出我们项目历史上的一个实际日期,即相关下推功能并入代码库的日期,或我们预计交付的日期。每个步骤都将展示截至该日期,查询计划的具体形态。

步骤 1:扫描 + 简单 WHERE

此功能通过对原始 clickhouse_fdw 的初始移植 e5035bc(2025 年 10 月 2 日)继承而来。自 pg_clickhouse v0.1.0 版本(2025 年 12 月 9 日)发布以来即可使用。

初期的设计理念是小而精:pg_clickhouse 只会下推它确信能够正确转换的谓词 (predicates)。诸如 >= 的比较操作、IN 的成员检查以及简单的时间戳算术都可以安全地下推,因为 Postgres 和 ClickHouse 对这些操作都有明确的对应实现。换言之,这些过滤器 (filters) 会在 ClickHouse 中运行,而不是将所有行收集到 Postgres 中进行评估。

SELECT

  u.country,                                                                -- ✗

  e.event_name,                                                             -- ✗

  count(*) AS n,                                                            -- ✗

  count(DISTINCT e.user_id) AS unique_users,                                -- ✗

  count(*) FILTER (WHERE e.properties->>'tier' = 'premium') AS …,           -- ✗

  percentile_cont(0.95) WITHIN GROUP (ORDER BY e.duration_ms) AS …,         -- ✗

  ROW_NUMBER() OVER (PARTITION BY u.country ORDER BY count(*)) AS …         -- ✗

FROM events_ch e                                                            -- ✓ scan only

  JOIN users_ch u USING (user_id)                                           -- ✗

WHERE e.ts >= now() - interval '7 days'                                     -- ✓

  AND u.country IN ('US','UK','DE')                                         -- ✓

  AND e.properties->>'platform' = 'web'                                     -- ✗

GROUP BY u.country, e.event_name                                            -- ✗

ORDER BY n DESC                                                             -- ✗

LIMIT 100                                                                   -- ✗

目前,只有基表过滤器 (base-table filters) 会被下推。SQL 转换器(内部称为“反解析器”deparser)会为带有时间戳谓词的 events 表发出一次远程扫描 (remote scan),并为带有国家谓词的 users 表发出一次远程扫描。它会将这两个独立的查询发送给 ClickHouse 并收集结果。所有涉及组合行或改变行结构的操作,包括连接 (join)、分组 (grouping)、聚合 (aggregates)、窗口函数 (window function)、排序 (sort) 和限制 (limit),都会在 Postgres 内部针对这些已检索到的行执行。因此,在 Postgres 能将结果减少到 100 行之前,网络必须传输过去七天内所有匹配的 events 行,以及所有匹配的 users 行。

关键在于,即使是最简单的下推也已经是一个转换问题:now() 可以被下推为 now(),而 interval '7 days' 则被转换为 7 * 86400。这些映射逻辑位于 src/custom_types.c 文件中,pg_clickhouse 在此记录了它知道如何安全地为 ClickHouse 转换的表达式。以下概述的步骤将把这种“词汇表”从基本过滤器扩展到连接、聚合、窗口函数和限制。

Step 2: + JOIN pushdown

内连接 (Inner-JOIN) 的反解析功能源自最初的 clickhouse_fdw,但其继承的成本估算 (cost estimates) 只是占位符,Postgres 规划器 (planner) 经常会选择将行拉回本地进行连接,而非下推。提交 b345682(2025 年 12 月 1 日)引入了基于行数的估算,并调整了成本模型,使扫描(下推操作)的成本估算优于本地连接路径,从而确保规划器能够可靠地选择下推计划。此外,提交 6a297ec(2025 年 11 月 13 日)增加了对 join_use_nulls 外连接 (outer-join) 语义的支持。所有这些功能均已随 v0.1.0 版本(2025 年 12 月 9 日)发布。

在此阶段,pg_clickhouse 不再将 events_ch 和 users_ch 视为两个独立的远程扫描。

由于这两张表都位于同一个 ClickHouse 服务器上,并且连接条件在 ClickHouse 中也有等价物,FDW 就能在任何连接结果数据通过网络传输之前,要求 ClickHouse 执行连接操作。

SELECT ...                                                         -- ✗ (still)

FROM events_ch e                                                   -- ✓

  JOIN users_ch u USING (user_id)                                  -- ✓ joins push

WHERE e.ts >= now() - interval '7 days'                            -- ✓

  AND u.country IN ('US','UK','DE')                                -- ✓

  AND e.properties->>'platform' = 'web'                            -- ✗ stays local

GROUP BY u.country, e.event_name                                   -- ✗

ORDER BY n DESC                                                    -- ✗

LIMIT 100                                                          -- ✗

远程 SQL 现在大致如下:SELECT * FROM events ALL INNER JOIN users USING (user_id) WHERE …。ALL 关键字是特意使用的:ClickHouse 的默认 join 只能返回一条匹配记录,而 Postgres 的 inner join 则返回所有匹配记录。指定 ALL INNER JOIN 旨在保留 Postgres 的语义行为。在此阶段,JSON 谓词仍无法下推转换,因此它被排除在远程 WHERE 子句之外,并在本地对返回的行进行过滤。

Postgres 在 9.6 版本中引入了这种 join 下推(join pushdown)能力(通过 Etsuro Fujita 于 2016 年提交的 e4106b25287)。此前,FDW 需要更深入地集成到查询规划器中才能实现 join 下推。

对于我们的查询,仅此一步便将查询时间从约 30 分钟大幅缩短至 30 秒,原因在于 join 操作在数据离开远程服务器之前便完成了行过滤。

步骤 3:+ GROUP BY + 简单聚合函数 + ORDER BY + LIMIT

这些能力也通过 e5035bc 得到继承。自 v0.1.0 版本(发布于 2025 年 12 月 9 日)起,GROUP BY、基本聚合函数(如 count、sum、min、max、avg)、ORDER BY 和 LIMIT 均支持下推。

这一步侧重于结果集的塑形工作,包括行分组、基本聚合计算、分组结果排序以及最终的限制(LIMIT)应用。Postgres 将这些操作作为“上层”规划阶段对外暴露,如果整个阶段的功能都能在 ClickHouse 中实现,FDW 就可以提供这些阶段的远程版本。聚合下推在 Postgres 10 中实现(通过 7012b132d07 的提交),而 ORDER BY 和 LIMIT 下推则随后在 Postgres 12 中实现。

尽管如此,目前 pg_clickhouse 仍无法下推所有这些操作:分组阶段必须完全覆盖 SELECT 列表中的所有项,并且尚未实现对以下三种表达式的下推支持:

• count(*) FILTER (WHERE e.properties->>'tier' = 'premium') : FILTER 子句的主体包含一个 JSON 操作,但尚无对应的下推映射。

• percentile_cont(...) WITHIN GROUP (ORDER BY ...) :ClickHouse 拥有等效功能,但 pg_clickhouse 尚未对其进行映射。

• ROW_NUMBER() OVER (...) :窗口函数(window functions)根本没有映射到 ClickHouse 的等效功能。

值得注意的是,分组下推对于特定的分组结果集而言是“全有或全无”(all-or-nothing)的策略。因此,尽管 count(*)、count(DISTINCT)、GROUP BY、ORDER BY 和 LIMIT 各自单独使用时都能正常下推,但该特定查询涉及分组的部分仍然无法下推,需在本地执行。

SELECT

  u.country, e.event_name,                                          -- ✗ grouped result blocked

  count(*) AS n,                                                    -- ✗ blocked

  count(DISTINCT e.user_id) AS unique_users,                        -- ✗ blocked

  count(*) FILTER (WHERE e.properties->>'tier' = 'premium') AS …,   -- ✗ FILTER body has JSON

  percentile_cont(...) AS p95_ms,                                   -- ✗ no mapping yet

  ROW_NUMBER() OVER (...) AS rank_in_country                        -- ✗ window not remote yet

FROM events_ch e                                                    -- ✓

  JOIN users_ch u USING (user_id)                                   -- ✓

WHERE e.ts >= ... ✓ AND u.country IN (...)                          -- ✓

  AND e.properties->>'platform' = 'web'                             -- ✗

GROUP BY u.country, e.event_name                                    -- ✗ blocked

ORDER BY n DESC                                                     -- ✗ blocked

LIMIT 100                                                           -- ✗ blocked

此查询的一个更简单的子集(移除了 FILTER、percentile 和 ROW_NUMBER)此时可以完全下推:

-- The subset that DOES push down at Step 3:

SELECT u.country, e.event_name, count(*) AS n, count(DISTINCT e.user_id) AS unique_users

FROM events_ch e JOIN users_ch u USING (user_id)

WHERE e.ts >= now() - interval '7 days' AND u.country IN ('US','UK','DE')

GROUP BY u.country, e.event_name

ORDER BY n DESC LIMIT 100;

该子集被反解析为一条独立的 ClickHouse SELECT 语句,其中 count(DISTINCT e.user_id) 被转换为 ClickHouse 的 count(DISTINCT user_id)。而完整、更复杂的查询,则需要另外三个步骤才能完全实现下推。

第 4 步:+ 有序集聚合 (percentile_cont → quantile)

提交 087cfdc(2025 年 11 月 10 日),收录于 v0.1.0 版本。此前,percentile_cont 会阻塞任何使用它的查询的上层关系操作。

这一步为 pg_clickhouse 带来了又一项聚合函数翻译:Postgres 的 percentile_cont(p) WITHIN GROUP (ORDER BY x) 可以转换为 ClickHouse 的 quantile(p)(x)。这项改动将 percentile 从阻碍下推的列表中移除。然而,允许下推的函数列表范围依然有限:例如,pg_clickhouse 仍无法处理 string_agg(... ORDER BY ...),因为 ClickHouse 中最接近的等效函数无法保留相同的组内排序语义。

SELECT

  ...                                             -- ✗ still blocked

  percentile_cont(...) AS p95_ms,                 -- ✓ now shippable

  ROW_NUMBER() OVER (...) AS rank_in_country      -- ✗ window not remote yet

FROM ...                                          -- (everything else unchanged from Step 2-3)

percentile_cont 函数能够单独下推是必要条件,但并非充分条件。分组结果仍无法下推,因为还剩下两个阻碍因素:带有 JSON 的 FILTER 子句和 ROW_NUMBER 窗口函数。

但请留意这些步骤中反复出现的模式:每次翻译都只解除一个下推障碍,而完整查询的下推只有在所有此类障碍都得到解决时才会发生。这解释了 pg_clickhouse 中一系列单一翻译改动,尽管每次改动本身并不起眼,但随着时间的推移会累积起来,最终突然实现了对之前完全在本地运行的完整查询的下推。

第 5 步:+ JSON 子列访问 (->, ->>)

提交 0b4c03e(2026 年 4 月 2 日)和 669924a(4 月 3 日),收录于 v0.1.6 / v0.2.0 版本。此前,每一个 -> / ->> / jsonb_extract_path 操作都必须作为本地过滤器执行;即便 e.properties->>'platform' = 'web' 这样的表达式也无法下推。

这一步将 JSON 字段访问加入了共享的翻译规则集。现在,诸如 e.properties->>'platform' 这样的 Postgres JSON 访问器表达式,可以被转换为 ClickHouse 的子列表达式 e.properties.platform,从而避免 JSON 谓词必须等待行数据被取回到 Postgres 后才能执行。

SELECT

  u.country, e.event_name,                                          -- ✗ still blocked (window)

  count(*) AS n, count(DISTINCT e.user_id) AS unique_users,         -- ✗ blocked (window)

  count(*) FILTER (WHERE e.properties->>'tier' = 'premium') AS …,   -- ✓ FILTER body now lifts

  percentile_cont(...) AS p95_ms,                                   -- ✓

  ROW_NUMBER() OVER (...) AS rank_in_country                        -- ✗ window still blocks

FROM events_ch e                                                    -- ✓

JOIN users_ch u USING (user_id)                                     -- ✓

WHERE ...

  AND e.properties->>'platform' = 'web'                             -- ✓ JSON qual lifts

GROUP BY u.country, e.event_name                                    -- ✗ still blocked

ORDER BY n DESC                                                     -- ✗ blocked

LIMIT 100                                                           -- ✗ blocked

两项变化同时发生:

• properties->>'platform' = 'web' 谓词会被转换为 properties.platform = 'web' ,以便 ClickHouse 能够在数据返回前完成行过滤。

• 过滤聚合函数 count(*) FILTER (WHERE properties->>'tier' = 'premium') 被下推为 countIf(properties.tier = 'premium') ,这利用了 ClickHouse 的条件聚合形式。

分组结果仍无法下推,但现在只剩下一个阻碍:ROW_NUMBER。

第 6 步:支持窗口函数

提交 0caf913(2026 年 4 月 2 日,与 JSON 子列访问的提交在同一天),引入了 v0.1.6 / v0.2.0 版本。在此之前,任何包含 OVER (...) 的子句都会阻碍上层关系表达式的下推。

这一改进使得 pg_clickhouse 能够为窗口函数提供远程执行计划。当其分区键 (partition keys) 和排序键 (order keys) 也能被转换时,ROW_NUMBER、RANK、DENSE_RANK、LEAD、LAG、FIRST_VALUE、LAST_VALUE、NTH_VALUE 以及 MIN/MAX OVER 等所有窗口函数都可以在 ClickHouse 中运行。

在此方面,pg_clickhouse 的下推能力超越了其“祖先” postgres_fdw(后者不支持窗口函数下推)。鉴于 ClickHouse 能在数据源端快速执行这些函数,这项收益远超翻译(SQL 到 ClickHouse 语法)函数带来的开销。

SELECT

  u.country,                                                        -- ✓

  e.event_name,                                                     -- ✓

  count(*) AS n,                                                    -- ✓

  count(DISTINCT e.user_id) AS unique_users,                        -- ✓

  count(*) FILTER (WHERE e.properties->>'tier' = 'premium') AS …,   -- ✓

  percentile_cont(0.95) WITHIN GROUP (ORDER BY e.duration_ms) AS …, -- ✓

  ROW_NUMBER() OVER (PARTITION BY u.country ORDER BY count(*)) AS …,-- ✓

FROM events_ch e                                                    -- ✓

  JOIN users_ch u USING (user_id)                                   -- ✓

WHERE e.ts >= now() - interval '7 days'                             -- ✓

  AND u.country IN ('US','UK','DE')                                 -- ✓

  AND e.properties->>'platform' = 'web'                             -- ✓

GROUP BY u.country, e.event_name                                    -- ✓

ORDER BY n DESC                                                     -- ✓

LIMIT 100                                                           -- ✓

至此,所有下推障碍都已清除。连接、分组、聚合、窗口函数、排序和限制操作都统一为一个 ClickHouse 查询。以前由独立的 Limit / Sort / WindowAgg / Group / Join / Scan / Scan 节点组成的 Postgres 执行计划,现在可以折叠成单个外部扫描 (foreign scan),其远程 SQL 大致如下所示:

-- What lands on the ClickHouse wire:

SELECT

  u.country,

  e.event_name,

  count(*) AS n,

  count(DISTINCT e.user_id) AS unique_users,

  countIf(e.properties.tier = 'premium') AS premium_count,

  quantile(0.95)(e.duration_ms) AS p95_ms,

  ROW_NUMBER() OVER (PARTITION BY u.country ORDER BY count(*) DESC) AS rank_in_country

FROM events e

  ALL INNER JOIN users u USING (user_id)

WHERE e.ts >= now64(9, 'UTC') - INTERVAL 7 DAY

  AND u.country IN ('US','UK','DE')

  AND e.properties.platform = 'web'

GROUP BY u.country, e.event_name

ORDER BY n DESC

LIMIT 100;

(此为近似形式;count(DISTINCT) 和 countIf 的确切反解析 (deparse) 存在此处未展示的边缘情况。)

此时,网络仅传输 100 行数据。Postgres 接收并直接返回这些结果。相较于第 1 步网络传输数千万行的情况,性能提升了几个数量级。

历史背景

我们有时会听到这样的反应:“我很惊讶这个表达式从一开始就没能下推。”如果你在缺乏历史背景的情况下查看 v0.2 版本的变更列表,这便是你可能产生的疑问。但回顾查询的演进过程,会揭示出一些简单变更列表无法体现的事实,例如:

  • 下推(Pushdown)是细粒度的。每个子句都必须独立进行协商。一个看似“显而易见”的子句,可能会因为单个子表达式而无法下推:例如,count(*) FILTER (WHERE json_op) 会受限于 JSON 操作的结果,即使 count 和 FILTER 各自功能完好。

  • 在关系上层(upper-rel)级别,下推是一个“全有或全无”(all-or-nothing)的决策。一个分组查询仅当整个分组关系可以在 ClickHouse SQL 中安全表示时才能被下推。任何一个不受支持的聚合函数或子表达式都可能阻止整个分组计划的下推。换句话说,对一个微小但缺失的翻译功能的支持,就可能彻底解除整个查询的下推限制。

  • 大多数所谓的“改进”,实际上是翻译工作,而非新增功能。例如 percentile_cont、ROW_NUMBER、->> 等函数,Postgres 和 ClickHouse 都支持它们。真正缺失的是连接这两种语法的 deparser 代码。这类改进表面上像是引入了新功能,但其内部本质上只是 custom_types.c 文件中的新映射以及用于实现这些映射的翻译代码。

  • 部分下推规则会被撤销。pg_clickhouse 和 postgres_fdw 都曾撤销过一些下推规则,原因是这些规则被发现会导致数据泄漏或语义错误。在我们的 v0.2.0 版本中,我们撤销了 array_dims、array_lower 及其相关函数的下推。在 postgres_fdw 方面,提交 8cfbac1492b (PG 17) 拒绝了 FETCH FIRST n WITH TIES 的下推;提交 5c571a34d0e (PG 18) 拒绝了在需要向后游标扫描时的 LIMIT 下推。这正是系统正常运行的表现:宁愿撤回下推,也不要为了追求速度而返回不正确的结果。

如果 deparse 契约(contract)遭到违反,我们将直接报错,而非进行推测。

Image
结论

从外部看来,下推(Pushdown)是二元(binary)的:即一个子句要么在远程执行,要么不执行。但在 FDW 内部,它更像是一份契约。Postgres 必须暴露(expose)正确的规划器钩子(planner hook),pg_clickhouse 必须在不改变其语义(semantics)的前提下翻译表达式,而 ClickHouse 必须支持相同的语义。如果这条链条中的任何一个环节缺失,最安全的做法是让操作在本地执行,或者直接抛出错误。

因此,从外部看来,FDW 的工作往往呈现出渐进(incremental)的特性。例如 JSON 子列、percentile_cont、过滤聚合(filtered aggregates)、窗口函数(window functions)、连接语义(join semantics)、DML 支持等,每一个都代表着两个 SQL 系统之间达成的一项小小的协议。但一旦下推的最后一个阻碍被移除,其效果将是巨大的。一个过去需要将数百万行数据流式传输到 Postgres 的查询,现在可以简化为 ClickHouse 执行的单个 SELECT 查询,并仅返回 100 行结果。

下推(Pushdown)并非要一股脑儿地将所有子句都推送到远程执行。有些翻译操作被加入,有些被撤销;有些依赖于 PostgreSQL 规划器的支持,有些则取决于 ClickHouse 引擎的行为。而有些操作则根本不应被下推,因为远程系统无法保证产生与 PostgreSQL 完全相同的结果。

这项工作的目的并非让 ClickHouse 伪装成 PostgreSQL,而是逐个子句地确定这两个系统能在哪些方面达成一致。

避免通过网络传输的每一个字节,都是三方共同同意省略的数据。

/END/

试用阿里云 ClickHouse企业版

轻松节省30%云资源成本?阿里云数据库ClickHouse 云原生架构全新升级,首次购买ClickHouse企业版计算和存储资源组合,首月消费不超过99.58元(包含最大16CCU+450G OSS用量)了解详情:https://t.aliyun.com/Kz5Z0q9G

图片
图片

征稿启示

面向社区长期正文,文章内容包括但不限于关于 ClickHouse 的技术研究、项目实践和创新做法等。建议行文风格干货输出&图文并茂。质量合格的文章将会发布在本公众号,优秀者也有机会推荐到 ClickHouse 官网。请将文章稿件的 WORD 版本发邮件至:[email protected]

图片图片