All Products
Search
Document Center

ApsaraDB RDS:How to accelerate queries with DuckDB?

Last Updated:Jun 12, 2026

The rds_duckdb extension for ApsaraDB RDS for PostgreSQL automatically offloads analytical SELECT queries to the DuckDB column-store engine. This process accelerates complex queries without requiring you to change your application's SQL. This topic explains how to offload queries, use advanced features, and troubleshoot common issues, assuming you have already created the extension and required DuckDB tables.

For inquiries, discussions, or feedback about the extension, you can join the ApsaraDB RDS for PostgreSQL extension community on DingTalk (ID: 103525002795).

Prerequisites

Before you use this feature, ensure that your instance meets the following requirements:

  • The primary instance must run ApsaraDB RDS for PostgreSQL 13 to 18 with a minor engine version of 20260130 or later. To accelerate queries on a read-only instance, it must run version 16 to 18, also with a minor engine version of 20260130 or later.

  • To use the partitioned table synchronization or automatic DuckDB table creation features, the minor engine version must be 20260330 or later.

  • The rds_duckdb extension has been created.

Usage notes

  • The rds_duckdb extension currently accelerates only read-only SELECT queries. Data Manipulation Language (DML) operations such as INSERT, UPDATE, and DELETE, Data Definition Language (DDL) operations, and queries that involve tables without a corresponding DuckDB table automatically fall back to PostgreSQL.

  • Hints only support setting rds_duckdb.execution and do not apply to other parameters.

  • Because DMS can rewrite SQL statements, use a hint to enable acceleration.

  • Simple queries, such as point queries and small-range scans, might run slower on DuckDB due to forwarding and startup overhead. Use the rds_duckdb.plan_cost_threshold parameter to filter out low-cost queries. For more information, see 4.5 Execution cost threshold.

Procedure

The query acceleration process involves three steps: enabling acceleration, verifying that queries are offloaded, and viewing execution logs.

Step 1: Enable DuckDB acceleration

To offload SELECT queries to DuckDB, use one of the following methods.

  1. Method 1: Use a hint (statement-level)

    Add a hint to a SELECT statement to accelerate only that query. This method is ideal for temporary validation or accelerating a specific slow query:

    /*+ set(rds_duckdb.execution on) */ SELECT * FROM my_table WHERE id = 1;
  2. Method 2: Set a session-level parameter

    Run the following command in the current session. All eligible queries within this session are then offloaded to DuckDB:

    SET rds_duckdb.execution = on;

Step 2: Verify offloading

Use the execution plan to verify if a SQL statement was offloaded to DuckDB.

  1. Example 1: A single-table query is offloaded to DuckDB

    /*+ set(rds_duckdb.execution on) */ EXPLAIN SELECT * FROM test_hint;

    The expected plan contains Custom Scan (DuckDBScan) and DuckDB Execution Plan:

                             QUERY PLAN
    ------------------------------------------------------------
     Custom Scan (DuckDBScan)  (cost=0.00..0.00 rows=0 width=0)
       DuckDB Execution Plan:
    
     ┌───────────────────────────┐
     │         SEQ_SCAN          │
     │    ────────────────────   │
     │      Table: test_hint     │
     │   Type: Sequential Scan   │
     │       Projections: a      │
     │                           │
     │          ~0 Rows          │
     └───────────────────────────┘
  2. Example 2: The plan reverts to the native PostgreSQL plan after offloading is disabled

    /*+ set(rds_duckdb.execution off) */ EXPLAIN SELECT * FROM test_hint;

    Expected plan:

          QUERY PLAN
    -----------------------
     Seq Scan on test_hint
    (1 row)

Step 3: View DuckDB logs (optional)

The rds_duckdb.enable_log_warning parameter controls whether WARNING-level messages are sent to the client. This helps you identify why a query was not offloaded to DuckDB.

  1. Enable WARNING output. This change takes effect immediately at the session level.

    SET rds_duckdb.enable_log_warning = on;
  2. Observe the client output. When enable_log_warning = on, a WARNING is output in the following scenarios:

    • The query falls back to PostgreSQL: Fallback postgres due to ...

    • Write operations are not supported: Modification operations on DuckDB tables are currently not supported, fallback to PG.

    • The statement does not need to be processed by DuckDB: Statements don't need to be handed over to DuckDB, fallback to PG.

When enable_log_warning = off (default), this information is written to the log at the DEBUG1 level and does not appear on the client.

Advanced features

The following features help you optimize acceleration or expand the scope of data synchronization. You can enable them based on your business requirements.

4.1 Automatic DDL synchronization

The rds_duckdb extension can automatically synchronize table structure changes (DDL) from ApsaraDB RDS for PostgreSQL to DuckDB.

Parameter

Description

Default

rds_duckdb.enable_ddl_replication

Specifies whether to enable DDL synchronization.

on

rds_duckdb.ddl_replication_fail_action

Behavior after synchronization failure: refresh (Automatically performs a full refresh), conflict (Marks a conflict and stops synchronization), or noop (No action is taken).

refresh

Supported DDL types:

-- Add a column
ALTER TABLE tbl ADD COLUMN extra_1 int;
-- Drop a column
ALTER TABLE tbl DROP COLUMN extra_1;
-- Add a column with a default value
ALTER TABLE tbl ADD COLUMN extra_1 int DEFAULT 0;
-- Drop the default value of a column
ALTER TABLE tbl ALTER COLUMN extra_1 DROP DEFAULT;
-- Modify the data type of a column
ALTER TABLE tbl ALTER COLUMN extra_1 TYPE varchar;
-- Rename a column
ALTER TABLE tbl RENAME COLUMN extra_1 TO extra_2;
-- Rename a table
ALTER TABLE tbl RENAME TO tbl_new;
-- DDL within a transaction
BEGIN;
ALTER TABLE tbl ADD COLUMN extra_3 int;
INSERT INTO tbl VALUES (..., 1);
COMMIT;
-- Roll back DDL with a savepoint
BEGIN;
SAVEPOINT s1;
ALTER TABLE tbl DROP COLUMN extra_1;
ROLLBACK TO SAVEPOINT s1;
COMMIT;

Examples of unsupported DDL operations that trigger a fallback:

-- Change the table schema (triggers an automatic full refresh or a conflict, depending on the fail_action setting)
ALTER TABLE tbl SET SCHEMA nsp1;

-- Drop the primary key (causes synchronization to stop and queries to fall back to PostgreSQL)
ALTER TABLE tbl DROP CONSTRAINT tbl_pkey;

After you delete the primary key, the status of the table in duckdb_sync_stat changes to not syncing, and queries are no longer routed through DuckDB. This state persists until you restore the primary key or REPLICA IDENTITY and manually refresh the table.

4.2 Partitioned table synchronization

Note

This feature is supported only in minor engine version 20260330 and later.

The rds_duckdb extension can synchronize PostgreSQL partitioned tables, including single-level and multi-level partitions, to DuckDB.

Synchronize a PostgreSQL partitioned table: To synchronize a partitioned table, you must create DuckDB tables for the root partition and all leaf partitions. The following example uses a single-level partitioned table:

-- Create a PostgreSQL partitioned table.
CREATE TABLE test_partition (
    id int,
    age int,
    primary key (id, age)
) PARTITION BY RANGE (age);

CREATE TABLE test_partition_a PARTITION OF test_partition FOR VALUES FROM (0) TO (18);
CREATE TABLE test_partition_b PARTITION OF test_partition FOR VALUES FROM (18) TO (30);
CREATE TABLE test_partition_c PARTITION OF test_partition FOR VALUES FROM (30) TO (60);

INSERT INTO test_partition SELECT i, i FROM generate_series(0, 59) i;

-- Synchronize the root partition and all leaf partitions to DuckDB.
SELECT rds_duckdb.create_duckdb_tables('{test_partition, test_partition_a, test_partition_b, test_partition_c}');

Example of querying the root partition:

/*+ set(rds_duckdb.execution on) */ EXPLAIN SELECT * FROM test_partition WHERE age >= 10 AND age < 20;

Expected plan (pruned to scan only the matching partitions):

 Custom Scan (DuckDBScan)  (cost=0.00..0.00 rows=0 width=0)
   DuckDB Execution Plan:

 ┌───────────────────────────┐
 │           UNION           ├──────────────┐
 └─────────────┬─────────────┘              │
 ┌─────────────┴─────────────┐┌─────────────┴─────────────┐
 │         SEQ_SCAN          ││         SEQ_SCAN          │
 │           Table:          ││           Table:          │
 │      test_partition_a     ││      test_partition_b     │
 │   Type: Sequential Scan   ││   Type: Sequential Scan   │
 │       Projections:        ││       Projections:        │
 │          Filters:         ││          Filters:         │
 │     age>=10 AND age<20││     age>=10 AND age<20│
 └───────────────────────────┘└───────────────────────────┘

Multi-level partitioned tables are also supported, as shown in the following example:

CREATE TABLE test_multi_partition (
    id serial,
    sale_id int NOT NULL,
    sale_date date NOT NULL,
    amount numeric(15,2) NOT NULL,
    primary key(sale_id, sale_date)
) PARTITION BY RANGE (sale_date);

CREATE TABLE test_multi_partition_a_l1 PARTITION OF test_multi_partition
    FOR VALUES FROM ('2024-1-1') TO ('2025-1-1') PARTITION BY RANGE (sale_date);
CREATE TABLE test_multi_partition_b_l1 PARTITION OF test_multi_partition
    FOR VALUES FROM ('2025-1-1') TO ('2026-1-1') PARTITION BY RANGE (sale_date);

CREATE TABLE test_multi_partition_a_l2_1 PARTITION OF test_multi_partition_a_l1
    FOR VALUES FROM ('2024-1-1') TO ('2024-7-1');
CREATE TABLE test_multi_partition_a_l2_2 PARTITION OF test_multi_partition_a_l1
    FOR VALUES FROM ('2024-7-1') TO ('2025-1-1');
CREATE TABLE test_multi_partition_b_l2_1 PARTITION OF test_multi_partition_b_l1
    FOR VALUES FROM ('2025-1-1') TO ('2025-7-1');
CREATE TABLE test_multi_partition_b_l2_2 PARTITION OF test_multi_partition_b_l1
    FOR VALUES FROM ('2025-7-1') TO ('2026-1-1');

INSERT INTO test_multi_partition (sale_id, sale_date, amount)
SELECT (random() * 100)::int, '2024-01-1'::date + i, (random() * 1000)::numeric(15,2)
FROM generate_series(1, 730) i;

-- Synchronize all partition levels.
SELECT rds_duckdb.create_duckdb_tables('{
    test_multi_partition,
    test_multi_partition_a_l1, test_multi_partition_b_l1,
    test_multi_partition_a_l2_1, test_multi_partition_a_l2_2,
    test_multi_partition_b_l2_1, test_multi_partition_b_l2_2
}');
Note

If a leaf partition is not synchronized, a query on the root partition may trigger a fallback because a table is missing.

DDL changes on partitioned tables, such as ATTACH/DETACH PARTITION, are also controlled by enable_ddl_replication, and the failure behavior is determined by ddl_replication_fail_action.

4.3 Automatic DuckDB table creation

Note

This feature is supported only in minor engine version 20260330 and later.

In addition to manually calling create_duckdb_table(), rds_duckdb supports automatic creation. When this feature is enabled, the system automatically creates a corresponding DuckDB table and starts incremental synchronization if you execute CREATE TABLE in PostgreSQL and the synchronization conditions are met.

-- Enable automatic creation (USERSET level, can be controlled at the session level).
SET rds_duckdb.auto_create_duckdb_table = on;

-- Create a PostgreSQL table. A DuckDB table is automatically created.
CREATE TABLE auto_tbl(id int primary key, val text);
INSERT INTO auto_tbl VALUES (1, 'hello');

-- Directly query the DuckDB data.
/*+ set(rds_duckdb.execution on) */ SELECT * FROM auto_tbl;

Expected result (if synchronization is normal):

  sync_table     | sync_status_description | sync_error_description
-----------------+-------------------------+------------------------
 public.auto_tbl | data syncing            | no errors

Behavior notes:

  • This feature applies only to regular tables. Automatic creation for partitioned tables is not currently supported.

  • The table must have a primary key or a REPLICA IDENTITY. Otherwise, it cannot enter the data syncing state.

  • Automatic creation is not triggered if no DuckDB tables have been created in the current database.

4.4 Autonomous fallback

The rds_duckdb.enable_fallback parameter controls the system's behavior when a SQL statement cannot be executed by DuckDB:

  • on (default): Automatically falls back to PostgreSQL for execution. You can use enable_log_warning to view the reason for the fallback.

  • off: Throws an error directly, which forces the issue to be addressed.

The extension also supports fallback based on synchronization latency. The rds_duckdb.wait_sync_timeout parameter controls how long a query waits for the incremental synchronization position before timing out. The unit is milliseconds.

Value

Description

-1 (default)

The query is executed immediately without checking the synchronization position.

0

The LSN is compared immediately without waiting.

> 0

The system waits for the specified number of milliseconds. If the DuckDB synchronization position has not caught up to the commit position of the current transaction within the timeout period, a fallback is triggered or an error is reported.

Triggering scenario: In environments with frequent write operations and high synchronization latency, queries that require strong consistency may fall back due to a timeout.

Log example (enable_log_warning = on):

WARNING:  Fallback postgres due to waiting for incremental synchronization timeout

If enable_fallback = off, an error is reported:

ERROR:  RDS DuckDB: canceling statement due to waiting for incremental synchronization timeout

Check latency:

-- Check the synchronization latency.
SELECT slot_name, pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) AS lag_bytes
FROM pg_replication_slots
WHERE slot_name LIKE 'rds_duckdb_slot%';

4.5 Execution cost threshold (plan_cost_threshold)

The rds_duckdb.plan_cost_threshold parameter ensures that only queries with a sufficiently high estimated cost are forwarded to DuckDB for execution, preventing simple queries from slowing down due to the overhead of forwarding and startup.

Parameter

Default

Description

rds_duckdb.plan_cost_threshold

0

The cost threshold, which has the same unit as the cost in PostgreSQL EXPLAIN. A value of 0 indicates that the cost is not evaluated, and all eligible SELECT statements attempt to use DuckDB.

Typical use case:

  • Your workload includes both complex analytical SQL queries (involving joins, aggregations, and large scans) and simple point queries or small-range scans. The simple queries often run faster in PostgreSQL because of the fixed overhead of offloading and connection initialization in DuckDB.

  • Setting a reasonable threshold, such as 1000 or 10000, allows low-cost, simple queries to continue running in PostgreSQL, while only pushing down high-cost, large queries to DuckDB for acceleration.

Configuration example:

-- Set the cost threshold to 5000. Queries are offloaded to DuckDB only if their PostgreSQL plan cost exceeds 5000.
SET rds_duckdb.plan_cost_threshold = 5000;

-- Example 1: A simple point query with a low PostgreSQL plan cost remains on PostgreSQL.
/*+ set(rds_duckdb.execution on) */ EXPLAIN SELECT * FROM my_table WHERE id = 1;

-- Example 2: A complex aggregate query with a high PostgreSQL plan cost is offloaded to DuckDB.
/*+ set(rds_duckdb.execution on) */ EXPLAIN SELECT count(*), avg(amount) FROM my_table GROUP BY region;
Note

This parameter affects only the cost assessment phase and does not change the fallback mechanism. Even if a query is offloaded to DuckDB, it will still fall back to PostgreSQL if an error occurs within the DuckDB executor.

Setting the threshold too high may prevent medium-cost queries that could benefit from acceleration from being offloaded. Setting it too low may fail to filter out simple queries. Adjust the threshold gradually based on your actual workload.

FAQ

Why isn't my query offloaded?

Use the rds_duckdb.enable_fallback parameter to determine why a query was not offloaded to DuckDB.

Troubleshooting steps:

-- Step 1: Disable fallback to expose the root cause.
SET rds_duckdb.enable_fallback = off;

-- Step 2: Run the target SQL statement.
/*+ set(rds_duckdb.execution on) */ EXPLAIN SELECT * FROM my_table;

Potential errors and their meanings:

Error message

Description

"my_table" is not a duckdb table or not in syncing status, and rds_duckdb.enable_fallback is set to off.

The table does not have a corresponding DuckDB column-store table, or the table is not in the syncing state.

RDS DuckDB: canceling statement due to not in syncing status.

query_syncing_table = on indicates that a table has not yet completed synchronization.

RDS DuckDB: canceling statement due to waiting for incremental synchronization timeout

The synchronization latency exceeded the wait_sync_timeout value.

Modification operations on DuckDB tables are currently not supported, fallback to PG.

An INSERT, UPDATE, or DELETE operation was attempted.

Summary of common fallback scenarios:

Scenario

Description

The table is not a DuckDB table.

create_duckdb_table() was not called.

The table is not in the syncing state.

The initial synchronization is incomplete, a DDL conflict occurred, or the table has no primary key.

Incremental synchronization timed out.

Waiting for synchronization exceeded the time specified by rds_duckdb.wait_sync_timeout.

The syntax is not supported by DuckDB.

Some SQL features are not supported by DuckDB, which triggers an automatic fallback.

The query includes non-DuckDB tables.

In a JOIN query, some of the tables do not have DuckDB replicas.

You can query the rds_duckdb.duckdb_sync_stat view to check the synchronization status of each DuckDB table. This action requires a privileged account. Example:

SELECT sync_table,
       sync_status_description,
       sync_error_description
FROM rds_duckdb.duckdb_sync_stat;

 sync_table        | sync_status_description | sync_error_description
-------------------+-------------------------+------------------------------------------
 test_schema.test1 | not syncing             | no primary key or replica identity index

If you want to query data from tables that have not been incrementally synchronized, you can set the rds_duckdb.query_syncing_table = off parameter.

Related links