fix/duckdb/failed-to-execute-prepared-statement
DuckDB error

Failed to execute prepared statement

Updated Aug 27, 20266-min read
TL;DR

A wrapper-level error your client (Go database/sql, dbt, sqlx, node, JDBC) prints when the underlying DuckDB Prepare or Exec call returned an error. The real problem is always in the wrapped DuckDB message underneath: a parameter binding mismatch, a statement type that cannot be prepared, or a type DuckDB could not infer at bind time.

  • ·Parameter count in the SQL does not match the number of args passed at Exec time
  • ·The SQL is a CREATE VIEW, PIVOT, or another statement type that DuckDB refuses to prepare
  • ·Multiple statements were sent to Prepare in one string (DuckDB accepts only one)
  • ·A parameter is used inside an expression (WHERE id = $1 + 1) so DuckDB infers UNKNOWN and the driver cannot bind it
  • ·A nested type (LIST, STRUCT, MAP) or BIT / HUGEINT value is being bound, and parameter binding does not support those
  • ·The statement handle was already closed, or the connection was reused across goroutines without protection
CHECK FIRSTRead the line under 'failed to execute prepared statement:'. That inner message (Binder Error, Parser Error, Invalid Input Error, Parameter count mismatch) is the actual DuckDB error and tells you which cause below applies.

What you're seeing

The wording differs slightly by client but the shape is always the same: a wrapper prefix followed by the DuckDB message it caught. Match the inner message, not the prefix.

text
# Go (database/sql + go-duckdb / duckdb-go), dbt-duckdb logs, sqlx wrappers:
failed to execute prepared statement: Binder Error: Prepared statement needs 2 parameters, 1 given

# Same wrapper, different underlying cause:
failed to execute prepared statement: Invalid Input Error: Cannot prepare multiple statements at once!

# Python client (duckdb):
duckdb.PreparedStatementException: Prepared statement needs 2 parameters, 1 given

# Node / JDBC wrappers commonly surface:
Error: Failed to execute prepared statement: Binder Error: Unexpected prepared parameter.
  This type of statement can't be prepared!
Also seen as: duckdb failed to execute prepared statement, PreparedStatementException, could not prepare query, Cannot prepare multiple statements at once, Prepared statement needs N parameters, M given

What's causing this

Ranked most-likely first.

  1. 1

    Parameter count does not match placeholders in the SQL

    The most common cause. Your SQL has two `?` or `$1, $2` placeholders and Exec was called with one argument (or three). DuckDB rejects at bind time with `Prepared statement needs N parameters, M given`, and the wrapper prints its `failed to execute prepared statement` prefix in front.

  2. 2

    The statement type is not preparable

    DuckDB refuses to prepare CREATE VIEW with parameters, PIVOT statements (parsed as multiple statements internally), and a handful of DDL variants. The inner message reads `Unexpected prepared parameter. This type of statement can't be prepared!` or `Cannot prepare multiple statements at once!`.

  3. 3

    Multiple SQL statements passed to Prepare

    Prepare accepts a single statement. Sending `INSERT ...; SELECT ...;` in one call trips `Cannot prepare multiple statements at once!`. Splitting on semicolons and preparing each half individually is the fix.

  4. 4

    Parameter appears inside an expression, and DuckDB infers UNKNOWN

    `WHERE id = $1 + 1` leaves DuckDB unable to infer the parameter type during Prepare. Drivers that require a resolved type at bind time (pg_duckdb, some Go/Node wrappers) surface this as `Could not convert DuckDB type: UNKNOWN to <target> type` under the wrapper prefix.

  5. 5

    Binding an unsupported value type

    Parameter binding does not accept nested types (LIST, STRUCT, MAP) or BIT and HUGEINT / BIGNUM values in most clients. Passing one triggers `could not bind parameter` from the driver, wrapped as `failed to execute prepared statement` upstream.

  6. 6

    The statement handle was closed, or the connection was misused

    Calling Exec after Close, sharing a `*sql.Stmt` across goroutines without the connection being safe for concurrent use, or executing after the parent connection was returned to the pool all produce this. In go-duckdb the inner message is one of `closed statement`, `uninitialized statement`, or `ExecContext or QueryContext with active Rows`.

How to fix it

Step 1: read the inner error, not the wrapper prefix

Everything after `failed to execute prepared statement:` is the real DuckDB error. `Binder Error`, `Parser Error`, `Invalid Input Error`, `Parameter/argument count mismatch` each map to a different fix. Do not tune anything until you have identified which one.

text
failed to execute prepared statement: <THIS PART IS THE REAL ERROR>

# Examples of the inner message you should be matching against:
Binder Error: Prepared statement needs 2 parameters, 1 given
Invalid Input Error: Cannot prepare multiple statements at once!
Binder Error: Unexpected prepared parameter. This type of statement can't be prepared!
Conversion Error: Could not convert DuckDB type: UNKNOWN to Postgres type

Step 2: match the placeholder count to the argument count

Count `?` (or `$1..$N`) in the SQL and match Exec args exactly. DuckDB counts every placeholder occurrence separately, so `WHERE a = ? OR b = ?` is two parameters even if you meant to reuse one value.

python
# Wrong: one placeholder, two args.
con.execute("SELECT * FROM t WHERE id = ?", [42, "extra"])

# Right: one placeholder, one arg.
con.execute("SELECT * FROM t WHERE id = ?", [42])

# Reusing a value? Bind it twice or switch to named parameters:
con.execute("SELECT * FROM t WHERE a = $1 OR b = $1", {"1": 42})

Step 3: give the parameter an explicit type so DuckDB can bind it

If the inner message mentions `UNKNOWN` or `could not determine data type of parameter`, cast the placeholder in the SQL. Prepare then sees a concrete type and the driver can bind normally.

sql
-- Instead of an expression that leaves the type unresolved:
SELECT * FROM events WHERE id = $1 + 1;

-- Cast the parameter so Prepare knows what type to expect:
SELECT * FROM events WHERE id = ($1::BIGINT) + 1;

-- Same fix for VARCHAR:
SELECT * FROM events WHERE label = $1::VARCHAR;

Step 4: prepare one statement at a time

If the inner message is `Cannot prepare multiple statements at once!`, split the SQL on semicolons and prepare each statement in its own call. Same rule for PIVOT: either run it directly with Exec/Query (no parameters), or rewrite it into a plain SELECT with a GROUP BY.

text
// Go: wrong. Two statements in one Prepare.
stmt, err := db.Prepare("INSERT INTO t VALUES (?); SELECT COUNT(*) FROM t;")

// Right: one Prepare per statement.
insertStmt, err := db.Prepare("INSERT INTO t VALUES (?)")
countStmt, err  := db.Prepare("SELECT COUNT(*) FROM t")

Step 5: do not prepare statement types DuckDB refuses to prepare

CREATE VIEW with `$1` placeholders, PIVOT, and a few DDL variants raise `Unexpected prepared parameter. This type of statement can't be prepared!`. Either build the SQL as a string with the value already interpolated (safe for trusted internal values), or restructure. A view can wrap a SELECT that takes parameters at query time instead of at CREATE time.

sql
-- Not preparable:
CREATE VIEW recent AS SELECT * FROM events WHERE ts > $1;

-- Preparable: create the view once with no parameter,
-- then parameterize the SELECT against it:
CREATE VIEW recent AS SELECT * FROM events;
-- Later, at query time:
SELECT * FROM recent WHERE ts > $1;

Step 6: bind supported types, or cast complex values on the way in

Parameter binding accepts primitives, VARCHAR, BLOB, DATE / TIMESTAMP, and DECIMAL. LIST, STRUCT, MAP, BIT, and HUGEINT are commonly rejected. Serialize complex values into a supported type at the boundary (JSON as VARCHAR is the usual escape hatch), then cast inside the SQL.

sql
-- Instead of binding a LIST directly:
-- (fails with "could not bind parameter" in most clients)
SELECT * FROM t WHERE tags = $1;

-- Bind JSON as VARCHAR, cast on the DuckDB side:
SELECT * FROM t WHERE tags = CAST($1::VARCHAR AS VARCHAR[]);

Step 7: check the statement and connection lifecycle

If the inner message is `closed statement`, `uninitialized statement`, or `ExecContext or QueryContext with active Rows`, the SQL itself is fine. The wrapper caught a lifecycle bug. Rows must be fully iterated or Closed before the next Exec on the same statement, and a `*sql.Stmt` should not be shared across goroutines without a fresh connection.

text
// Go (go-duckdb): forgetting to close Rows before the next call:
rows, err := stmt.Query(1)
if err != nil { return err }
// Missing: defer rows.Close()
_, err = stmt.Exec(2) // errActiveRows -> "failed to execute prepared statement"

// Correct:
rows, err := stmt.Query(1)
if err != nil { return err }
defer rows.Close()
for rows.Next() { /* ... */ }
rows.Close()
_, err = stmt.Exec(2) // safe now

Prevention

Log the full driver error, not just the wrapper prefix. If your code writes `log.Printf("failed to execute prepared statement: %v", err)` you already have the underlying message, so display it. Truncating at the colon is how teams end up debugging this error for hours.

Prefer named parameters (`$name`) over positional `?` in code paths where the SQL is edited often. Placeholder-count drift is the number-one source of this error, and named parameters make the mismatch obvious at the call site.

For any statement type you know DuckDB will not prepare (CREATE VIEW with parameters, PIVOT), skip Prepare entirely and use the driver's direct Exec / Query path with a built SQL string. Prepared statements are for hot query paths, not for one-shot DDL.

Debug DuckDB faster

1bench is a native GUI for DuckDB. Inspect queries, connections, and settings without leaving the app. See what's happening before you have to Google it.

Open DuckDB in 1bench