云数据库 PostgreSQL DuckDB 加速功能

云数据库 PostgreSQL 支持 DuckDB 加速功能,这里说明 DuckDB 加速功能的使用指引。

相关产品
云数据库 PostgreSQL 产品页,含版本规格、价格与购买入口。

一、支持版本

v18.4_r1.11及以上版本。

二、前置参数配置

在 postgresql.conf 中设置以下参数,修改完成后需重启实例。

参数 建议值 说明
shared_preload_libraries 包含 pg_extension_base 加载 lake 引擎与 duck server
wal_level logical 必须设置为 logical,replica 级别不足以支撑同步表所需的逻辑复制;若未开启,force_sync 等同步操作将报错
max_replication_slots ≥ 16 同步表共享复制槽池(如 sync_pool_slot_5_0),不是每张表独立一个;实际占用数量远小于同步表数量,预留10 ~ 16通常足够
max_wal_senders ≥ max_replication_slots + 8 与复制槽配套,给同步引擎和其他逻辑复制/主从留出余量
max_worker_processes ≥ 32 1个 database 共用1个 apply worker,并需为全局 sync manager 预留资源

配置示例:

shared_preload_libraries = 'pg_extension_base'wal_level = logicalmax_replication_slots = 64max_wal_senders = 64max_worker_processes = 64

检查当前值

输入:

SELECT name, settingFROM pg_settingsWHERE name IN ('shared_preload_libraries','wal_level', 'max_replication_slots','max_wal_senders','max_worker_processes');

输出:

name | setting | context——————————–+———————————————————————————————————————————————-+———— max_replication_slots | 10 | postmaster max_wal_senders | 27 | postmaster max_worker_processes | 8 | postmaster shared_preload_libraries | pg_stat_statements,pg_stat_log,wal2json,decoderbufs,decoder_raw,rds_server_handler,tencentdb_pwdcheck,auto_explain,pgaudit,pg_extension_base | postmaster wal_level | logical | postmaster(5 rows)

其他约束:

·不支持 TEMP 表、UNLOGGED 表以及分区子表;分区表须对父表调用 create_sync。

·建议同步表具备主键;若无主键且存在 UPDATE / DELETE 场景,须提前设置 REPLICA IDENTITY FULL。

查看当前值

输入:

SELECT name, setting, contextFROM pg_settingsWHERE name IN ('pg_lake_engine.enable_duckdb_features', 'pg_lake_engine.duckdb_replication', 'pg_lake.enabled', 'pg_lake_table.enable_full_query_pushdown', 'pg_lake_table.enable_heap_query_pushdown', 'pg_lake_table.enable_heap_filter_pushdown', 'lake_sync.routing_debug')ORDER BY name;

输出:

name | setting | context——————————————–+———+———— lake_sync.routing_debug | off | user pg_lake.enabled | on | sighup pg_lake_engine.duckdb_replication | on | sighup pg_lake_engine.enable_duckdb_features | on | postmaster pg_lake_table.enable_full_query_pushdown | on | user pg_lake_table.enable_heap_filter_pushdown | on | user pg_lake_table.enable_heap_query_pushdown | on | user(7 rows)

三、创建插件

输入:

CREATE EXTENSION pg_lake CASCADE;

输出:

NOTICE: installing required extension "pg_lake_table"NOTICE: installing required extension "pg_lake_engine"NOTICE: installing required extension "pg_extension_base"NOTICE: installing required extension "pg_map"NOTICE: installing required extension "pg_lake_iceberg"NOTICE: installing required extension "btree_gist"NOTICE: installing required extension "pg_lake_copy"NOTICE: installing required extension "pg_lake_sync"CREATE EXTENSION

CASCADE 会自动安装以下依赖扩展:

·pg_lake_engine — duck_server 管理

·pg_lake_table — 列存表访问方法(USING duckdb)

·pg_lake_sync — 同步表引擎(USING sync / lake_sync.*)

验证安装

输入:

SELECT extname, extversion FROM pg_extension WHERE extname LIKE 'pg_lake%';

输出:

extname | extversion——————-+———— pg_extension_base | 3.3 pg_lake | 3.3 pg_lake_copy | 3.3 pg_lake_engine | 3.5 pg_lake_iceberg | 3.4 pg_lake_sync | 3.2 pg_lake_table | 3.4(7 rows)

查看 pg_lake 版本(比查询 pg_extension 更直接)

输入:

SELECT lake.version();

输出:

version—————- main (2d50ce2)(1 row)

四、如何卸载 pg_lake

可通过级联 DROP 一次卸载所有 pg_lake 相关扩展:

输入(在测试库执行):

DROP EXTENSION pg_lake_engine CASCADE;SELECT extname FROM pg_extension WHERE extname LIKE 'pg_lake%' OR extname LIKE 'pg_extension%' ORDER BY extname;

输出:

NOTICE: drop cascades to 5 other objectsDETAIL: drop cascades to extension pg_lake_icebergdrop cascades to extension pg_lake_tabledrop cascades to extension pg_lake_copydrop cascades to extension pg_lake_syncdrop cascades to extension pg_lake extname——————- pg_extension_base(1 row)

五、创建两种加速表

云数据库 PostgreSQL 提供两种基于 DuckDB 的加速表类型,应根据是否需要 OLTP 事务能力进行选择:

类型 底层 事务 / 写入模型 适用场景
同步表 USING sync Heap(行存) + DuckDB 镜像 事务写入 heap,分析查询走镜像 HTAP:OLTP 与分析并存
列存表 USING duckdb 纯 DuckDB foreign table 支持 INSERT / UPDATE / DELETE / COPY,但不具备行级事务隔离与索引 纯分析场景,数据以批为单位持续追加

六、同步表

同步表 = 一张 heap 表 + 一张 DuckDB 镜像表,写入走 heap,分析走镜像,逻辑复制异步同步。

同步表提供两种创建方式:新建即为同步表,或将已有堆表转换为同步表。

七、方式一:直接新建同步表

输入:

CREATE TABLE orders ( o_id bigint PRIMARY KEY, o_customer text, o_amount numeric(12,2), o_ts timestamptz DEFAULT now()) USING sync;

输出:

CREATE TABLE

heap 侧确认。

输入:

SELECT n.nspname AS schema, c.relname, a.amname AS access_methodFROM pg_class cJOIN pg_namespace n ON n.oid = c.relnamespaceLEFT JOIN pg_am a ON a.oid = c.relamWHERE c.relname = 'orders';

输出:

schema | relname | access_method——–+———+————— public | orders | heap(1 row)

系统自动生成:

·heap 表 orders (业务操作对象)。

·镜像表 orders_duckdb_sync (在 PG 中可见,业务查询无需直接引用)。

八、方式二:将已有堆表转换为同步表

lake_sync.create_sync(regclass, mirror_storage text DEFAULT 'duckdb') 支持两种调用形式,二选一即可:

输入 (默认镜像存储类型即 duckdb,可省略第二参数):

— customers 为普通堆表SELECT lake_sync.create_sync('customers'::regclass);

输出 (函数返回类型为 void,值列显示为空):

create_sync————-(1 row)

或显式指定镜像存储类型 (效果等价):

SELECT lake_sync.create_sync('customers'::regclass, 'duckdb');

注意:说明:

注意:同一张表不能被重复 create_sync。若目标 heap 表已存在对应的 _duckdb_sync 镜像,再次调用会报错:

注意:ERROR: mirror table public.customers_duckdb_sync already exists

注意:若需要重建镜像,请先 lake_sync.reset_sync(oid) 释放当前同步关系,或用 lake_sync.rebuild_sync_tables('duckdb') 全库重建。

转换后校验:heap 侧仍是 heap 表,镜像表已生成。

输入:

SELECT n.nspname AS schema, c.relname, a.amname AS access_methodFROM pg_class cJOIN pg_namespace n ON n.oid = c.relnamespaceLEFT JOIN pg_am a ON a.oid = c.relamWHERE c.relname = 'customers';

输出:

schema | relname | access_method——–+———–+————— public | customers | heap(1 row)

输入:

SELECT heap_table_oid::regclass AS heap_table, iceberg_table_oid::regclass AS mirror_table, sync_state, mirror_storage_typeFROM lake_sync.sync_tablesORDER BY heap_table_oid::regclass::text;

输出:

heap_table | mirror_table | sync_state | mirror_storage_type————+———————–+————+——————— customers | customers_duckdb_sync | ACTIVE | duckdb orders | orders_duckdb_sync | ACTIVE | duckdb(2 rows)

create_sync 执行以下5项操作:

1. 创建 DuckDB 镜像表 customers_duckdb_sync。

2. 按需设置 REPLICA IDENTITY。

3. 锁定 heap 表并全量导入现有数据 (数据量大时存在阻塞窗口,建议在业务低峰期执行)。

4. 将表加入共享逻辑复制槽与 publication。

5. 状态迁移至 ACTIVE,进入增量同步阶段。

九、创建完成后的常规操作

强制镜像追赶至最新 LSN

输入:

SELECT lake_sync.force_sync('orders'::regclass);

输出:

force_sync———— t(1 row)

开启自动路由 (默认关闭。开关只影响通过 heap 表名的 SELECT 是否被自动路由到镜像;直接引用 xxx_duckdb_sync 镜像表的查询任何时候都走 DuckDB,与该开关无关)。

输入:

SET lake_sync.auto_routing_enabled = on;

输出:

SET

追平后校验行数一致

输入:

SELECT 'heap' AS src, count(*) FROM ordersUNION ALLSELECT 'mirror' AS src, count(*) FROM orders_duckdb_sync;

输出:

src | count——–+——- heap | 3 mirror | 3(2 rows)

十一、支持的数据类型

列存表底层由 DuckDB 承载,大部分 PostgreSQL 常见标量与数组类型均可作为列类型。建表前建议先确认字段类型是否在下表范围内:

类别 支持类型
整数 smallint / integer / bigint
浮点 / 定点 real / double precision / numeric(p, s)
文本 text / varchar(n) / char(n)
布尔 boolean
时间 date / time / timestamp / timestamptz
二进制 bytea
JSON json / jsonb
唯一标识 uuid
一维数组 int[] / bigint[] / text[] / numeric[] / varchar[] 等

实测建表与写入(在 de-postgres-7mvblukj 环境):

输入:

CREATE TABLE t_types ( c_int integer, c_bigint bigint, c_num numeric(12,2), c_text text, c_ts timestamp, c_tstz timestamptz, c_bool boolean, c_bytea bytea, c_json json, c_jsonb jsonb, c_uuid uuid, c_arr_int int[], c_arr_text text[]) USING duckdb;INSERT INTO t_types VALUES ( 1, 9999999999, 3.14, 'hello', '2026-07-22 10:00:00', '2026-07-22 10:00:00+08', true, '\\xdeadbeef', '{"a":1}', '{"a":1}', '11111111-1111-1111-1111-111111111111', ARRAY[1,2,3], ARRAY['x','y']);SELECT * FROM t_types;

输出:

CREATE TABLEINSERT 0 1 c_int | c_bigint | c_num | c_text | c_ts | c_tstz | c_bool | c_bytea | c_json | c_jsonb | c_uuid | c_arr_int | c_arr_text——-+————+——-+——–+———————+————————+——–+————+———+———-+————————————–+———–+———— 1 | 9999999999 | 3.14 | hello | 2026-07-22 10:00:00 | 2026-07-22 10:00:00+08 | t | \\xdeadbeef | {"a":1} | {"a": 1} | 11111111-1111-1111-1111-111111111111 | {1,2,3} | {x,y}(1 row)

十二、创建

输入:

CREATE TABLE sales_analytics ( ts timestamp, region text, amount numeric(12,2)) USING duckdb;

输出:

CREATE TABLE

也支持基于查询结果直接创建(CTAS)和基于已有表结构创建(LIKE)。

CTAS:从其他表灌入并落成列存

输入:

CREATE TABLE sales_summary USING duckdb ASSELECT product, sum(price*100) AS totalFROM salesGROUP BY product;SELECT * FROM sales_summary ORDER BY product;

输出:

CREATE TABLE AS product | total———+——— apple | 100.000 banana | 200.000(2 rows)

复用已有表结构

输入:

CREATE TABLE sales_copy (LIKE sales) USING duckdb;

输出:

CREATE TABLE

十三、后续数据写入

建表完成后,支持标准 DML 语法继续追加、修改、删除数据。

操作 是否支持 推荐写法
批量导入 ✅ 推荐 COPY sales_analytics FROM '…' WITH (FORMAT csv);
从其他表灌入 ✅ 推荐 INSERT INTO sales_analytics SELECT … FROM staging;
单行/小批量插入 ✅ 支持,性能一般 INSERT INTO sales_analytics VALUES (…);
更新 ✅ 支持 UPDATE sales_analytics SET amount = … WHERE …;
删除 ✅ 支持 DELETE FROM sales_analytics WHERE …;
清空重灌 ✅ 推荐用于全量刷新 TRUNCATE + INSERT … SELECT
DDL 变更 ✅ 支持 ALTER TABLE … ADD/DROP/RENAME COLUMN

以下逐项实测。

批量插入

输入:

INSERT INTO sales VALUES (1,'apple',1.5), (2,'banana',2.3);SELECT count(*) AS row_cnt FROM sales;

输出:

INSERT 0 2 row_cnt——— 2(1 row)

查询

输入:

SELECT * FROM sales ORDER BY id;

输出:

id | product | price—-+———+——- 1 | apple | 1.5 2 | banana | 2.3(2 rows)

DDL 变更 (RENAME / ADD / ALTER TYPE / DEFAULT)

输入:

ALTER TABLE sales RENAME COLUMN product TO product_name;ALTER TABLE sales ADD COLUMN amount int;ALTER TABLE sales ADD COLUMN category text;ALTER TABLE sales ADD COLUMN status text DEFAULT 'active';ALTER TABLE sales ALTER COLUMN price TYPE numeric(10,2);

输出:

ALTER TABLEALTER TABLEALTER TABLEALTER TABLEALTER TABLE

UPDATE

输入:

UPDATE sales SET price = 2 WHERE product_name = 'apple';SELECT id, product_name, price FROM sales ORDER BY id;

输出:

UPDATE 1 id | product_name | price—-+————–+——- 1 | apple | 2 2 | banana | 2.3(2 rows)

DELETE

输入:

DELETE FROM sales WHERE product_name = 'banana';SELECT id, product_name, price FROM sales ORDER BY id;

输出:

DELETE 1 id | product_name | price—-+————–+——- 1 | apple | 2(1 row)

UPDATE / DELETE 的限制:必须支持 full query pushdown

列存表的 UPDATE / DELETE 必须能够整条 SQL 下推到 DuckDB 执行;若 SET 子句、WHERE 子句里出现不可下推的表达式(比如引用普通 heap 表的相关子查询、WITH … UPDATE、部分不可下推的函数等),会直接报错。

十四、查询

查询由 DuckDB 引擎执行并完成算子下推,常见的过滤、聚合、GROUP BY、JOIN 均可下推。

输入:

EXPLAINSELECT o_customer, sum(o_amount) AS totalFROM orders_duckdb_syncGROUP BY o_customerORDER BY total DESCLIMIT 5;

输出:

QUERY PLAN—————————————————————————– Custom Scan (Query Pushdown) Engine: DuckDB -> PROJECTION -> HASH_GROUP_BY Groups: #0 Aggregates: sum_no_overflow(#1) -> PROJECTION -> SEQ_SCAN Type: Sequential Scan Table: postgres__public__orders_duckdb_sync

十五、系统目录表现

列存表在 pg_class 中的表现:relkind = 'f'(foreign table),amname 为空。

输入:

SELECT n.nspname AS schema, c.relname AS name, c.relkind, COALESCE(a.amname, '-') AS amname, CASE c.relkind WHEN 'r' THEN 'ordinary table' WHEN 'f' THEN 'foreign table' END AS relkind_descFROM pg_class cJOIN pg_namespace n ON n.oid = c.relnamespaceLEFT JOIN pg_am a ON a.oid = c.relamWHERE n.nspname = 'public' AND c.relname IN ('demo_col','demo_heap','demo_sync','demo_sync_duckdb_sync')ORDER BY name;

输出:

schema | name | relkind | amname | relkind_desc——–+———————–+———+——–+—————- public | demo_col | f | – | foreign table public | demo_heap | r | heap | ordinary table public | demo_sync | r | heap | ordinary table public | demo_sync_duckdb_sync | f | – | foreign table(4 rows)

十六、同步表的刷新方式

镜像表默认采用异步逻辑复制。系统提供6种刷新与维护 API:

使用场景 API 行为说明
强制镜像追赶至最新 LSN lake_sync.force_sync(oid) 通知 worker 立即刷新缓冲区并阻塞等待;返回 true 表示成功
暂停指定表的同步(用于大批量写入前) lake_sync.pause_sync(oid) 状态迁移至 PAUSED,后续写入不再同步至镜像
恢复同步(或从 ERROR 状态恢复) lake_sync.resume_sync(oid) 状态迁移至 ACTIVE;从 ERROR 恢复时自动重启 apply
镜像数据出现不一致,需要重建 lake_sync.reset_sync(oid) 清空镜像表并从 heap 全量重导
全库镜像批量重建 lake_sync.rebuild_sync_tables('duckdb' | 'iceberg' | 'all') 全库镜像 drop、重建并全量重导
查询当前同步延迟 lake_sync.get_sync_lag(oid) 返回 interval 类型

下面按 API 逐条演示。

十七、force_sync — 强制镜像追赶至最新 LSN

输入:

INSERT INTO orders SELECT g, 'cust'||g, g*10, now()FROM generate_series(100, 1099) g;SELECT lake_sync.force_sync('orders'::regclass);SELECT 'heap' AS src, count(*) FROM ordersUNION ALLSELECT 'mirror' AS src, count(*) FROM orders_duckdb_sync;

输出:

INSERT 0 1000 force_sync———— t(1 row) src | count——–+——- heap | 1003 mirror | 1003(2 rows)

十八、pause_sync — 暂停指定表的同步

暂停后 heap 继续写入,mirror 停留在暂停前的位置。

输入:

SELECT lake_sync.pause_sync('orders'::regclass);SELECT table_name, sync_state FROM pg_catalog.sync_status WHERE table_name='orders';– 暂停期间写入 1 行INSERT INTO orders VALUES (9999, 'paused_row', 99.99, now());SELECT 'heap' AS src, count(*) FROM ordersUNION ALLSELECT 'mirror' AS src, count(*) FROM orders_duckdb_sync;

输出:

NOTICE: Sync paused for table with OID 17645 pause_sync————(1 row) table_name | sync_state————+———— orders | PAUSED(1 row) src | count——–+——- heap | 1004 mirror | 1003(2 rows)

十九、resume_sync — 恢复同步(或从 ERROR 状态恢复)

输入:

SELECT lake_sync.resume_sync('orders'::regclass);SELECT table_name, sync_state FROM pg_catalog.sync_status WHERE table_name='orders';

输出:

NOTICE: Sync resumed for table with OID 17645 resume_sync————-(1 row) table_name | sync_state————+———— orders | ACTIVE(1 row)

20、reset_sync — 清空镜像并从 heap 全量重导

输入:

SELECT lake_sync.reset_sync('orders'::regclass::oid);SELECT pg_sleep(3);SELECT lake_sync.force_sync('orders'::regclass::oid);SELECT 'heap' AS src, count(*) FROM ordersUNION ALLSELECT 'mirror' AS src, count(*) FROM orders_duckdb_sync;

输出:

NOTICE: Shared replication slot sync_pool_slot_5_0 is not advanced by single-table reset_syncNOTICE: Sync reset completed for table with OID 17645 reset_sync————(1 row) pg_sleep———-(1 row) force_sync———— t(1 row) src | count——–+——- heap | 1004 mirror | 1004(2 rows)

21、rebuild_sync_tables('duckdb') — 全库镜像重建

输入:

SELECT * FROM lake_sync.rebuild_sync_tables('duckdb');

输出:

NOTICE: pg_lake_sync: invalidating DuckDB mirror for public.customersNOTICE: pg_lake_sync: DuckDB mirror invalidated for public.customers, bgworker signaledNOTICE: pg_lake_sync: invalidating DuckDB mirror for public.ordersNOTICE: pg_lake_sync: DuckDB mirror invalidated for public.orders, bgworker signaledNOTICE: pg_lake_sync: invalidate complete: 2 invalidated, 0 skipped, 0 failed schema_name | heap_table | mirror_table | mirror_type | rows_imported | status————-+————+———————–+————-+—————+———————————— public | customers | customers_duckdb_sync | duckdb | 0 | ok: invalidated, bgworker signaled public | orders | orders_duckdb_sync | duckdb | 0 | ok: invalidated, bgworker signaled(2 rows)

22、get_sync_lag — 查询当前同步延迟

输入:

SELECT lake_sync.get_sync_lag('orders'::regclass::oid) AS orders_lag, lake_sync.get_sync_lag('customers'::regclass::oid) AS customers_lag;

输出:

orders_lag | customers_lag—————–+—————– 00:00:03.320283 | 00:00:03.307904(1 row)

23、查询路由

开关(默认关闭)

会话级:

输入:

SET lake_sync.auto_routing_enabled = on;

输出:

SET

持久化(供新会话默认生效):

ALTER DATABASE <你的库名> SET lake_sync.auto_routing_enabled = on;

输出:

ALTER DATABASE

执行后请重新登录再 SHOW lake_sync.auto_routing_enabled 确认值为 on。

24、业务查询使用的表名

业务查询统一使用原表名(如 orders),系统会根据查询谓词自动路由到 heap 或 mirror。不建议在业务代码中直接引用镜像表 orders_duckdb_sync。

25、路由规则

SQL 特征 路由目标
等值条件 + 索引列(点查) heap(索引扫描)
范围条件 / 无 WHERE / 全表扫描 mirror(DuckDB)
聚合操作(count / sum / group by) mirror
多表 JOIN 且带谓词下推 mirror
所有 INSERT / UPDATE / DELETE 始终走 heap(路由仅影响 SELECT)

注意:说明:

注意:对子查询和 CTE 递归应用路由规则:当同一条 SQL 含有子查询或 WITH CTE 时,系统会对每个子查询独立应用上述规则,而不是“整条 SQL 走同一目标”。例如外层是范围扫描走 mirror,内层却是索引点查,内层仍可能被路由至 heap。

26、通过 EXPLAIN 验证路由结果

等值查询 → heap 索引扫描

输入:

SET lake_sync.auto_routing_enabled = on;EXPLAIN (COSTS OFF) SELECT * FROM demo_sync WHERE id = 1;

输出:

QUERY PLAN———————————————- Index Scan using demo_sync_pkey on demo_sync Index Cond: (id = 1)(2 rows)

→ 路由至 heap,使用主键索引扫描。

范围查询 → mirror(DuckDB)

输入:

EXPLAIN (COSTS OFF) SELECT * FROM demo_sync WHERE id > 1;

输出:

QUERY PLAN————————————————————- Custom Scan (Query Pushdown) Engine: DuckDB -> SEQ_SCAN Type: Sequential Scan Table: postgres__public__demo_sync_duckdb_sync Filters: id>1 Estimated Cardinality: 4(7 rows)

→ 路由至 mirror,查询由 DuckDB 执行,Filters: id>1 表示谓词已下推。

聚合查询 → mirror(算子下推)

输入:

EXPLAIN (COSTS OFF) SELECT count(*) FROM demo_sync;

输出:

QUERY PLAN————————————————————————- Custom Scan (Query Pushdown) Engine: DuckDB -> UNGROUPED_AGGREGATE Aggregates: count_star() -> PROJECTION Projections: 42 Estimated Cardinality: 20 -> SEQ_SCAN Type: Sequential Scan Table: postgres__public__demo_sync_duckdb_sync Projections: Estimated Cardinality: 20(12 rows)

→ 路由至 mirror,聚合算子完成下推。

关闭路由后的对比

输入:

RESET lake_sync.auto_routing_enabled; — 或 SET … = offEXPLAIN (COSTS OFF) SELECT count(*) FROM demo_sync;

输出:

QUERY PLAN—————————– Aggregate -> Seq Scan on demo_sync(2 rows)

→ 回退至 heap 全表扫描。

27、手动指定路由目标

强制查询 mirror(直接引用镜像表)

输入:

SELECT o_customer, sum(o_amount) AS totalFROM orders_duckdb_syncGROUP BY o_customerORDER BY total DESCLIMIT 5;

输出:

o_customer | total————+———- cust1099 | 10990.00 cust1098 | 10980.00 cust1097 | 10970.00 cust1096 | 10960.00 cust1095 | 10950.00(5 rows)

强制查询 heap(关闭自动路由)

输入:

SET lake_sync.auto_routing_enabled = off;SELECT o_id, o_customer, o_amount FROM orders WHERE o_id = 100;

输出:

SET o_id | o_customer | o_amount——+————+———- 100 | cust100 | 1000.00(1 row)

28、注意事项

·路由仅对 SELECT 生效;所有 DML 均写入 heap。

·镜像存在同步延迟,对实时性敏感的查询应先调用 force_sync 强制追赶,或显式关闭路由以直接访问 heap。

·该能力还依赖 DuckDB 查询下推开关 pg_lake_engine.enable_duckdb_features = on(默认已启用);关闭该 GUC 后,即使路由把查询指向了 mirror,也会退回到普通 Foreign Scan 而非 Custom Scan (Query Pushdown),列存加速失效。

29、自动路由 vs 查询下推:两个开关的差异

客户最容易混淆的问题:“我明明打开了 auto_routing_enabled,为什么这条 SQL 还是慢?”—— 通常原因是查询下推开关没打开。这两个开关控制的层级不同,组合起来才决定 SQL 的最终执行路径:

开关 GUC 层级 决定的事
自动路由 lake_sync.auto_routing_enabled 表名重写 通过 heap 表名的 SELECT 是否被改写到镜像表 xxx_duckdb_sync
查询下推 pg_lake_table.enable_full_query_pushdown 执行计划 整条 SQL(仅涉及 pg_lake 表且所有算子可下推时)是否打包给 DuckDB 一次执行

三种组合的实测对比(以同步表 t_init_case 为例):

组合 A:auto_routing = on + full_query_pushdown = on → 理想路径,SQL 走镜像并整条下推给 DuckDB。

输入:

SET lake_sync.auto_routing_enabled = on;SET pg_lake_table.enable_full_query_pushdown = on;EXPLAIN (COSTS OFF) SELECT count(*) FROM t_init_case;

输出:

QUERY PLAN———————————————————————- Custom Scan (Query Pushdown) Engine: DuckDB -> UNGROUPED_AGGREGATE Aggregates: count_star() -> PROJECTION -> SEQ_SCAN Type: Sequential Scan Table: postgres__public__t_init_case_duckdb_sync

组合 B:auto_routing = on + full_query_pushdown = off → SQL 已路由到镜像,但每一步都要在 PG 与 DuckDB 之间往返,退化为 Foreign Scan,列存加速失效。

输入:

SET lake_sync.auto_routing_enabled = on;SET pg_lake_table.enable_full_query_pushdown = off;EXPLAIN (COSTS OFF) SELECT count(*) FROM t_init_case;

输出:

QUERY PLAN———————————————————————- Foreign Scan Relations: Aggregate on (t_init_case_duckdb_sync) Engine: DuckDB -> UNGROUPED_AGGREGATE Aggregates: count_star() -> SEQ_SCAN Table: postgres__public__t_init_case_duckdb_sync

组合 C:auto_routing = off → SQL 不路由,直接走 heap。

输入:

SET lake_sync.auto_routing_enabled = off;EXPLAIN (COSTS OFF) SELECT count(*) FROM t_init_case;

输出:

QUERY PLAN————————————————————- Aggregate -> Index Only Scan using t_init_case_pkey on t_init_case

排障建议:如果发现“路由已经开了但性能还是不行”,按此顺序检查:

1. SHOW pg_lake_engine.enable_duckdb_features; 是否 on(总电源)。

2. SHOW pg_lake_table.enable_full_query_pushdown; 是否 on。

3. EXPLAIN 输出的第一行是不是 Custom Scan (Query Pushdown);若是 Foreign Scan,说明下推被拦下了。

30、使用 routing_debug 排查“为什么这条 SQL 没走 mirror”

会话内开启 lake_sync.routing_debug,并把 client_min_messages 提到 DEBUG1,可以看到路由决策的完整链路:

输入:

SET lake_sync.auto_routing_enabled = on;SET lake_sync.routing_debug = on;SET client_min_messages = 'DEBUG1';SELECT count(*) FROM t_init_case WHERE id > 100;

输出:

DEBUG: pg_lake_sync: found sync table t_init_case (heap=49289, iceberg=49297)DEBUG: pg_lake_sync: skipping index for non-equality operator ">"DEBUG: pg_lake_sync: routing t_init_case to iceberg table t_init_case_duckdb_sync (sequential scan preferred)DEBUG: pg_lake_sync: query rewritten to use underlying table

日志能明确告诉你:识别到同步表 → 因为 > 不是等值操作跳过索引 → 决定路由到镜像 → 已改写。

排障完成后建议 RESET ALL 关闭 DEBUG 输出,以免污染业务日志。

31、数据库中同步表 / 列存表的盘点与识别

重要说明:同步表的 heap 侧在 pg_class 中 relkind = 'r'、amname = 'heap',与普通堆表在系统目录层面无法直接区分。识别同步表的权威来源为 lake_sync.sync_tables 或视图 pg_catalog.sync_status。

32、查看全部同步表状态

输入:

SELECT * FROM pg_catalog.sync_status;

输出:

schema_name | table_name | sync_state | last_sync_lsn | current_wal_lsn | last_sync_time | sync_lag_seconds | created_at | replication_slot_name | publication_name————-+————+————+—————+—————–+——————————-+——————+——————————+———————–+——————- public | demo_sync | ACTIVE | 0/713DF50 | 0/7143D78 | 2026-07-22 16:08:33.260193+08 | 7 | 2026-07-22 16:08:33.14298+08 | sync_pool_slot_5_0 | sync_pool_pub_5_0(1 row)

字段说明:

·sync_state — 同步状态,可能值:

·INITIALIZING — 初始化中。在 create_sync / reset_sync / rebuild_sync_tables 执行全量导入阶段短暂出现;当数据量大或后台负载重时窗口更明显。此状态下 apply worker 尚未开始处理增量,业务查询若走镜像可能读到不完整数据。

·ACTIVE — 正常增量同步中(稳态)。

·PAUSED — 已通过 pause_sync 显式暂停,heap 写入不会同步到镜像。

·ERROR — 同步失败(常见:镜像端存在冲突、逻辑复制槽异常),需要 resume_sync 重试,若仍失败则 reset_sync 重建镜像。

·sync_lag_seconds — 当前同步延迟(秒),监控告警的核心指标。

·last_sync_lsn / current_wal_lsn — 镜像最后同步位点与库当前 WAL 位点。

·replication_slot_name / publication_name — 底层复制槽与发布名称。

33、同步表 heap 名与镜像名对照

输入:

SELECT n.nspname AS heap_schema, c.relname AS heap_table, c.relname || '_duckdb_sync' AS mirror_table_name, s.mirror_storage_type, s.sync_state, s.last_sync_time, s.last_sync_lsnFROM lake_sync.sync_tables sJOIN pg_class c ON c.oid = s.heap_table_oidJOIN pg_namespace n ON n.oid = c.relnamespaceORDER BY 1,2;

输出:

heap_schema | heap_table | mirror_table_name | mirror_storage_type | sync_state————-+————+———————–+———————+———— public | demo_sync | demo_sync_duckdb_sync | duckdb | ACTIVE(1 row)

34、全库列存表(USING duckdb)

输入:

SELECT n.nspname AS schema, c.relname AS nameFROM pg_class cJOIN pg_namespace n ON n.oid = c.relnamespaceWHERE c.relkind = 'f' — 列存表与同步镜像均为 foreign table AND c.relname NOT LIKE '%\\_duckdb\\_sync' ESCAPE '\\' — 排除同步镜像 AND n.nspname NOT IN ('pg_catalog','information_schema')ORDER BY 1,2;

输出(演示环境含 demo_col 列存表和 demo_sync_duckdb_sync 镜像,仅保留列存表):

schema | name——–+———- public | demo_col(1 row)

35、全库表类型汇总视图

输入:

SELECT n.nspname AS schema, c.relname AS name, CASE WHEN s.heap_table_oid IS NOT NULL THEN 'sync (heap side)' WHEN c.relname LIKE '%\\_duckdb\\_sync' ESCAPE '\\' THEN 'sync (duckdb mirror)' WHEN c.relkind = 'f' THEN 'duckdb columnstore' WHEN c.relkind = 'r' THEN 'plain heap' ELSE c.relkind::text END AS kindFROM pg_class cJOIN pg_namespace n ON n.oid = c.relnamespaceLEFT JOIN lake_sync.sync_tables s ON s.heap_table_oid = c.oidWHERE n.nspname NOT IN ('pg_catalog','information_schema','pg_toast','lake_sync','lake_engine') AND c.relkind IN ('r','f')ORDER BY 1,2;

输出:

schema | name | kind——–+———————–+———————- public | demo_col | duckdb columnstore public | demo_heap | plain heap public | demo_sync | sync (heap side) public | demo_sync_duckdb_sync | sync (duckdb mirror)(4 rows)

36、常用监控 SQL

延迟超过60秒的同步表

输入:

SELECT schema_name, table_name, sync_lag_secondsFROM pg_catalog.sync_statusWHERE sync_lag_seconds > 60ORDER BY sync_lag_seconds DESC;

输出(演示环境健康,应为空集)

schema_name | table_name | sync_lag_seconds————-+————+——————(0 rows)

处于异常状态的同步表

输入:

SELECT * FROM pg_catalog.sync_status WHERE sync_state <> 'ACTIVE';

输出(演示环境健康,应为空集):

schema_name | table_name | sync_state | …————-+————+————+—–(0 rows)

复制槽占用情况(用于判断是否需要调大 max_replication_slots)

输入:

SELECT count(*) AS used_slots, current_setting('max_replication_slots')::int AS max_slotsFROM pg_replication_slots;

输出:

used_slots | max_slots————+———– 1 | 10(1 row)

38、Q1. force_sync 一直不返回怎么办?

force_sync 内部会通知 apply worker 刷新缓冲区并阻塞等待追赶到当前 LSN。如果长时间不返回,通常是以下两种原因:

·wal_level 未设置为 logical:同步表依赖逻辑复制槽,若 wal_level 为 replica,worker 无法建立复制槽,force_sync 会挂住。

排查:

SHOW wal_level;– 期望输出:logical;若为 replica,需在 postgresql.conf 修改并重启实例

·apply worker 未运行或已崩溃:检查表状态是否为 ERROR,或 max_worker_processes 是否被占满:

SELECT * FROM pg_catalog.sync_status WHERE table_name = 'your_table';SELECT current_setting('max_worker_processes')::int AS max_worker_processes, (SELECT count(*) FROM pg_stat_activity WHERE backend_type = 'background worker') AS running_bgworkers;

若 sync_state = ERROR,调 lake_sync.resume_sync(…) 恢复;仍失败则 lake_sync.reset_sync(…) 重建镜像。

39、Q2. 同步状态显示 ERROR 怎么处理?

同步表进入 ERROR 通常是逻辑复制流程中出现无法自动恢复的冲突(如 heap 表 DDL 改变了列类型、镜像端被外部改动等)。

恢复路径(按优先级):

1. resume_sync:轻量恢复,内部会重启 apply worker、重试上一批变更。适合“暂时性错误”。

输入:

SELECT lake_sync.resume_sync('your_table'::regclass);SELECT sync_state FROM pg_catalog.sync_status WHERE table_name='your_table';

输出:

NOTICE: Sync resumed for table with OID 17645 sync_state———— ACTIVE(1 row)

2. reset_sync:重量级修复,会清空镜像并从 heap 全量重导。适合镜像数据已经和 heap 不一致的情况。

3. rebuild_sync_tables('duckdb'):全库同类型镜像一次性重建(用于批量恢复场景)。

40、Q3. UPDATE 报错“require full query pushdown”怎么改?

报错来自列存表的 UPDATE / DELETE:整条 SQL 必须能下推到 DuckDB 执行。常见触发原因:

·SET 或 WHERE 子句里出现了引用普通 heap 表的相关子查询。

·使用了不可下推的 PG 函数(自定义 PL/pgSQL 函数、部分特殊内置函数)。

·使用了修改型 CTE(WITH … UPDATE … SELECT 之类的嵌套 DML)。

修复思路:

·简化表达式,把不可下推的部分拆到独立 SQL 里完成。

·或者让所有相关表都变成 pg_lake 表(列存表 / 同步表),整条 SQL 就落在 DuckDB 内部。

·或者退回到 heap:直接对同步表的 heap 侧执行 UPDATE(所有 DML 本来就走 heap,只有列存表原生模式才有这个限制)。

41、Q4. 如何确认一条查询是否真的走了 DuckDB 下推?

用 EXPLAIN,重点看第一行:

·Custom Scan (Query Pushdown) → 已经下推,整条 SQL 打包给 DuckDB 执行,列存加速生效。

·Foreign Scan → 只有部分算子在 DuckDB 侧执行,PG 与 DuckDB 之间需要多次往返,通常是 enable_full_query_pushdown 被关掉,或 SQL 中含不可下推算子。

·Seq Scan / Index Scan on <heap_table> → 完全没走镜像,自动路由未生效(检查 lake_sync.auto_routing_enabled)。

进一步排查参见这篇中自动路由 vs 查询下推:两个开关的差异与使用 routing_debug 排查“为什么这条 SQL 没走 mirror”:先看两个开关,再开 routing_debug 抓路由决策日志。

赞 (0)
上一篇 4天前
下一篇 4天前