Proposal: opt-in automatic DuckDB execution for analytical PostgreSQL table queries #1057
jsuchane-cz
started this conversation in
Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Hi pg_duckdb maintainers,
I would like to discuss a possible opt-in auto execution mode for analytical queries over regular PostgreSQL tables. I am not opening a PR yet, because I would first like to check whether this direction fits pg_duckdb’s design.
If the idea is acceptable, I would be happy to start with a small MVP PR as described below.
Proposal: opt-in automatic DuckDB execution for analytical queries on PostgreSQL tables
Summary
An opt-in negative filter for uncontrolled analytical SQL workloads. It avoids routing obviously non-analytical queries to DuckDB, is conservative by default, does not double-plan, and does not change any existing behavior. The feature would be disabled by default and would not affect existing users unless they explicitly enable it.
Problem
Today, for queries that touch only regular PostgreSQL base tables, pg_duckdb effectively offers two execution-control modes:
duckdb.force_execution = false(default): analytical queries never use DuckDB execution.duckdb.force_execution = true(e.g. viaALTER ROLE ... SETorSET LOCAL): every allowedSELECTwith aFROMclause is attempted in DuckDB.The second option already falls back to PostgreSQL when DuckDB planning fails, so the problem is not robustness. The problem is that many queries plan successfully in DuckDB but should never run there. An index-selective query like:
plans fine in DuckDB, so no fallback is triggered — yet it may avoid the efficient PostgreSQL index access path the normal planner would have chosen, and become orders of magnitude slower.
force_executionper user/connection therefore penalizes every selective query issued by that user.This matters most where SQL is not under the developer's control and cannot be routed per statement:
SETanythingWhat is missing is a negative filter: "try DuckDB, but only for queries that look analytical and touch enough data."
Proposal
A new opt-in GUC:
When enabled, and only when
NeedsDuckdbExecution()andduckdb.force_executiondid not already decide, the planner hook attempts DuckDB execution for a query iff all of the following hold:SELECTwith aFROMclause and passes the existingIsAllowedStatement()checks (no modifying CTEs, no catalog tables, not inside functions unless allowed).GROUP BYclause — checked recursively viaquery_tree_walker, covering common BI-generated wrappers such as outer subqueries and view-expandedRTE_SUBQUERYentries. Set-operation handling can be included where it fits cleanly into the analysed query tree traversal.GROUP BYwithout aggregates is intentionally included because BI tools generate it as a deduplication pattern; it is effectively similar toDISTINCT, which could be added later once the initial behavior is validated.pg_class.reltuplesas a cheap negative filter — not as a selectivity or cost estimate — exceeds the configured threshold. Relations with unknown or clearly unavailable statistics (such asreltuples = -1) are treated conservatively as below the threshold. The exact relation collection strategy is open for review; the intent is to keep this check cheap and conservative rather than complete.COUNT(*)queries with aWHEREclause stay in PostgreSQL, because this pattern is often index-selective (see open questions). Here "plainCOUNT(*)withWHERE" means a single count aggregate withoutGROUP BY,HAVING,DISTINCT, or window functions — soSELECT customer_id, count(*) ... WHERE ... GROUP BY customer_idremains a normal candidate. AggregateFILTERclauses are a semantically similar selective pattern and could be excluded as well, depending on maintainer preference.If DuckDB planning fails, the query falls back to PostgreSQL exactly as
force_executiondoes today — but in the auto path only, the failure is logged atDEBUG1instead ofWARNING. Withforce_executionthe user explicitly requested DuckDB, so a client-visible warning is useful there; in auto mode it would be noise for BI workloads.The existing exclusions in
ShouldTryToUseDuckdbExecution()(materialized view refresh, background worker) apply to the auto path as well.Resulting decision order — current behavior is unchanged unless the new GUC is enabled:
NeedsDuckdbExecution()→ DuckDB (as today)duckdb.force_execution = true→ try DuckDB, fall back (as today)duckdb.auto_execution = onand query is an analytical candidate → try DuckDB, fall back (new)Expected decisions
SELECT * FROM orders WHERE id = ?SELECT sum(amount) FROM large_orders GROUP BY customer_idSELECT * FROM (SELECT customer_id, sum(amount) FROM large_orders GROUP BY 1) tSELECT customer_id FROM large_orders GROUP BY customer_idSELECT count(*) FROM small_lookupSELECT row_number() OVER (...) FROM large_tableSELECT count(*) FROM large_table WHERE id = ?SELECT customer_id, count(*) FROM large_table WHERE ... GROUP BY 1INSERT / UPDATE / DELETE ...Non-goals
To keep this reviewable and low-risk, the following are explicitly out of scope:
EXPLAIN-based inspection of the PostgreSQL planduckdb.auto_execution = offby default)Safety and eligibility
The auto path should not bypass any eligibility, permission, or RLS-related checks that apply to the existing
force_executionpath. It reuses the existing statement eligibility checks and only narrows the eligible set by requiring analytical shape and sufficient data volume.Known limitations (acknowledged upfront)
WHEREcould still be routed to DuckDB. This is the same trade-off users accept today withforce_execution, but scoped to far fewer queries; theCOUNT(*)-with-WHERErule above removes the most common class of these.force_executioninteracts with caching today.force_execution; auto mode does not widen the per-query semantics gap. Hence: off by default, documented clearly.Phasing
Following the contribution guidelines (small PRs, discuss first):
duckdb.auto_execution+duckdb.auto_min_total_relation_rowsGUCs, recursive analytical-query classifier, reltuples threshold, fallback at DEBUG1 in the auto path, regression tests. Small enough to review, but complete enough to be a usable routing policy.EXPLAINoutput, DEBUG logging, or per-session counters, depending on maintainer preference.COUNT(*)-with-WHERErule configurable,DISTINCTas an analytical signal.Questions
force_execution+ fallback the intended answer for mixed workloads?duckdb.auto_execution, or should I align it with any broader execution-mode naming you already have in mind? The existingforce_executionbehavior stays unchanged either way.DEBUG1rather thanWARNINGfor DuckDB planning failures in the auto path only?COUNT(*)with aWHEREclause in PostgreSQL in the first version, or would you prefer it to be routed by the threshold like any other aggregate?If the direction is acceptable, I will start with the MVP PR as scoped above, with initial benchmark results to justify the proposed default threshold.
All reactions