AI Features and extensions pgvector
David Sterling  

pg_clickhouse 0.11: Query ClickHouse From Postgres, No ETL

EnterpriseDB announced on November 5 that it has acquired Altinity, the main independent provider of enterprise support and managed services for ClickHouse, with integrated enterprise ClickHouse capabilities promised for Q4 2026. You can read the vendor claims yourself in the EDB press release.

For a Postgres DBA the M&A is background noise. The useful signal is that the transactional-plus-analytical argument keeps getting louder, and you do not have to wait for a vendor bundle to act on it. pg_clickhouse, the open-source foreign data wrapper maintained by ClickHouse, lets a plain Postgres session read ClickHouse tables with ordinary SQL — no ETL job, no second client library, no rewriting dashboards into a new dialect.

This post covers what shipped in the current 0.11 release, how to wire it up, how to confirm the work is actually happening on the ClickHouse side, and where the design falls over.

What pg_clickhouse actually is

It is an FDW in the classic Postgres sense: CREATE SERVER, CREATE USER MAPPING, IMPORT FOREIGN SCHEMA, then query. The extension talks to ClickHouse 23.3 or later over the native binary protocol or HTTP, imports ClickHouse tables as foreign tables, and deparses the parts of your query it understands into ClickHouse SQL so they execute remotely instead of dragging rows across the network.

Pushdown is the whole product. Filters, joins, semi-joins, aggregations, and a long list of functions are translated; v0.10 added pushdown for correlated and uncorrelated scalar subqueries, EXISTS, and single-column IN/NOT IN in WHERE and HAVING — the TPC-H Q2, Q11, Q15, Q17, Q20 and Q22 shapes — provided the ClickHouse server is 25.8 or newer. Anything outside that set runs partly or wholly in Postgres, which for a billion-row table is the slow path you were trying to avoid. ClickHouse’s own documentation claims 14 of 22 TPC-H queries fully pushed down with more than 60x over the same queries on local Postgres tables; treat that as a vendor benchmark on a laptop, not a promise about your schema.

Wiring it up

Install the extension on the Postgres host with make install or the packaged builds, then, from a superuser session in the database that needs the data:

CREATE EXTENSION pg_clickhouse;

CREATE SERVER ch_analytics
  FOREIGN DATA WRAPPER clickhouse_fdw
  OPTIONS (driver 'binary', host 'ch.internal', port '9000', dbname 'telemetry');

CREATE USER MAPPING FOR reporting
  SERVER ch_analytics
  OPTIONS (user 'reporting');   -- password via ClickHouse users.xml grants, not a shared superuser

Import only what analysts need. IMPORT FOREIGN SCHEMA in 0.11 is noticeably better at types than it used to be — Array(Decimal(12,6)) arrives as numeric(12,6)[], UInt64 as numeric(20,0) instead of erroring, FixedString(N) as unconstrained text, and DateTime64 keeps up to six digits of precision:

CREATE SCHEMA ch;
IMPORT FOREIGN SCHEMA telemetry
  FROM SERVER ch_analytics INTO ch
  LIMIT TO (device_events, daily_rollups);

Then query it like any other table:

SELECT device_id,
       count(*)                               AS events,
       round(avg(pressure_psi)::numeric, 2)   AS avg_psi
FROM ch.device_events
WHERE event_time >= now() - interval '24 hours'
GROUP BY device_id
ORDER BY events DESC
LIMIT 20;

Verify pushdown before you trust it

The failure mode is silent: the query works, returns correct rows, and takes forty seconds because a Foreign Scan pulled 900 million rows into a local HashAggregate. Always check the plan:

EXPLAIN (VERBOSE, COSTS OFF)
SELECT device_id, count(*)
FROM ch.device_events
WHERE event_time >= now() - interval '24 hours'
GROUP BY device_id;

If WHERE and the GROUP BY appear in the remote SQL and the local plan is a single Foreign Scan, the aggregate is running in ClickHouse. If you see Aggregate or Filter nodes above the scan, something in the expression is un-translatable — a Postgres-only function, a cast ClickHouse rejects, a join against a local table — and you should rewrite it or materialise an intermediate table locally.

Two administration functions make operational checks cheap. clickhouse_query(server, sql) returns rows from arbitrary ClickHouse SQL, and clickhouse_perform(server, sql) runs statements that return nothing:

SELECT clickhouse_server_version('ch_analytics');

SELECT * FROM clickhouse_query('ch_analytics',
  'SELECT count() FROM telemetry.device_events');

CALL clickhouse_perform('ch_analytics',
  'OPTIMIZE TABLE telemetry.device_events FINAL');

Upgrade notes for anyone on 0.10 or earlier

Release 0.11.0 (30 September 2026) is binary-compatible with 0.10, so a reload picks up most fixes, but three things will bite:

  • PostgreSQL 13 support is gone. If you are still on 13, this extension no longer builds for you — that is a pg_upgrade conversation, not an extension conversation.
  • clickhouse_raw_query() was removed. Replace it with clickhouse_query(server, sql) or CALL clickhouse_perform(server, sql). Both take a foreign-server name rather than a connection string.
  • fetch_size is gone. Both drivers stream results by ClickHouse Native block now, so there is nothing to tune.

0.11 also validates text and JSON encoding on the way back and will raise an error when the server returns bytes invalid for your database encoding. Any server with dirty legacy data needs ALTER SERVER ch_analytics OPTIONS (ADD encoding_check 'replace'); where the accepted values are fail, replace, remove and truncate.

Where this design hurts

Foreign tables are read-mostly in practice. Writes go through the binary driver, but DML pushdown is still on the roadmap — lightweight UPDATE/DELETE is not there yet — so treat ClickHouse as an append-only analytics target that your Postgres app does not mutate row by row.

There are no cross-engine transactions. If your Postgres write and the ClickHouse copy come from CDC (ClickPipes or PeerDB), the analytics side lags by design, and a dashboard that joins a live Postgres table to a replicated ClickHouse table can show numbers that never coexisted in reality. Row-level security does not apply to foreign tables either, so the security boundary is the user mapping plus ClickHouse grants, not your Postgres policies. And a second engine means a second backup and restore discipline — the same reasoning as any restore verification runbook, applied twice.

For Houston teams this pattern fits a narrow, familiar set of problems: fleet and equipment telemetry from Gulf Coast logistics operations, wellhead and SCADA sensor history, high-volume clickstream or API request logs — anything where Postgres holds the authoritative record and someone needs to slice years of events by the hour without slowing the OLTP path. It is the same foreign-data-wrapper pattern we examined in pg_pinecone vs. pgvector: Postgres stays the system of record and the query interface, while a specialised engine does the heavy aggregate work.

What to do next

Start with one table and one real analyst query. Import it, run EXPLAIN (VERBOSE), confirm the aggregate pushes down, and measure against the same query on a local copy. If the numbers hold, the integration is a day of work; if the plan will not push down, you learned that before building a pipeline around it.

If you are deciding whether ClickHouse belongs next to your Postgres cluster, or you already run both and want the foreign tables, pushdown coverage and access control reviewed before analysts depend on them, Postgres and ClickHouse integration review for Houston teams is the place to start.

Leave A Comment