skip to content

Why is the placeholder syntax in database/sql driver-specific — ? for some databases and $1 for others?

level: middleimportance: should knowfreq 58%

answer

  1. the package is dialect-blind
  2. text goes to the driver unchanged
  3. positional token versus numbered token
  4. sql.Named needs driver support

basics

~20 s

database/sql passes the query text to the driver unchanged and has no SQL parser, so the placeholder token is the database's own syntax: ? for MySQL and SQLite, $1 for PostgreSQL, :name or @name elsewhere.

solid answer

~40 s

`database/sql` is a thin, dialect-blind layer: the query string you write is handed to the driver as-is, and the placeholder token belongs to the database's own syntax. MySQL and SQLite use positional `?`; PostgreSQL uses numbered `$1`, `$2`; some other engines use named `:id` or `@id`. The consequence is that query text is not portable across engines — swapping drivers means rewriting placeholders. The numbering also matters: `$1` may appear several times in a statement and consume one argument, while `?` is strictly positional, so reusing a value means writing `?` again and passing the value again. For engines whose drivers support named parameters, `sql.Named("id", 7)` attaches a name to an argument; a driver that does not support named parameters rejects it with an error rather than falling back to positional order.

code

go · 9 lines
go
// ?-style dialects: strictly positional
_, err := db.ExecContext(ctx, "UPDATE orders SET state = ? WHERE id = ?", state, id)

// $N-style dialects: numbered, and $1 may be repeated in the text
_, err = db.ExecContext(ctx, "UPDATE orders SET state = $1 WHERE id = $2", state, id)

// drivers that support named parameters
_, err = db.ExecContext(ctx, "UPDATE orders SET state = :s WHERE id = :id",
	sql.Named("s", state), sql.Named("id", id))

go deeper

for a junior

Know that the placeholder token depends on the database you are talking to, and that copying a query between two engines usually means editing every placeholder. Check which token your project's statements already use before writing a new one.

for a middle

Explain why: database/sql passes the text through unchanged and has no SQL parser, so the token is the database's syntax. Be able to contrast strictly positional tokens with numbered ones that may repeat, and say what sql.Named requires from the driver.

for a senior

Show judgment about portability — where a per-dialect token helper earns its keep, why per-dialect statement sets beat a homegrown rewriter, and how you catch argument-order mistakes in tests when the types happen to line up and the query silently returns wrong rows.

for a principal

Decide whether multi-engine support is a requirement at all, since it prices every statement your team writes. If it is, own where that abstraction lives and what it is forbidden from doing — notably, never growing into a text rewriter that reintroduces the parsing problem the standard library refused.

## A deliberately thin layer `database/sql` is not an abstraction over SQL. It is a connection pool plus a small contract with drivers: give me a statement and some argument values, hand me back rows. It contains no SQL parser, no dialect table and no rewriting step. What you write in the query string is exactly what the driver receives, and — for most drivers — exactly what the server parses. So the placeholder token is not a Go concept at all. It is part of each database's own syntax for a bind parameter: - `?` — positional, used by MySQL and SQLite dialects. - `$1`, `$2` — numbered, used by PostgreSQL. - `:name` or `@name` — named, used by several other engines. A package that normalised these would have to parse SQL well enough to know which `?` is a placeholder and which sits inside a string literal or a comment, in every dialect, forever. Go's standard library declines that job. ## Positional versus numbered With `?`, arguments are consumed strictly left to right. Two conditions that need the same value need two `?` and the value passed twice: ``` WHERE created_at >= ? AND updated_at >= ? // value passed twice ``` With `$N` the number selects the argument, so a single argument can be referenced from several places: ``` WHERE created_at >= $1 AND updated_at >= $1 // one argument ``` This is a real source of bugs when a statement is ported between dialects: converting `$1 ... $1` into `? ... ?` silently changes the argument count, and converting `? ... ?` into `$1 ... $1` silently drops an argument. When the driver reports its expected parameter count, `database/sql` catches the mismatch before the statement is sent; otherwise the server rejects it. ## Named arguments `sql.Named(name string, value any) NamedArg` builds a named argument you can pass in the ordinary `args ...any` position. It is useful where a statement references the same value in many places or where the dialect's placeholders are names to begin with. Two constraints matter. First, the driver must support named parameters; the ones whose dialect is purely positional return an error rather than silently mapping names onto positions. Second, the names in the statement text and the names you pass must agree — there is no compile-time check tying them together. ## What this means for portable code Teams that must support more than one engine have three honest options, and it is worth being able to name them: 1. **Pick a dialect and stay there.** The overwhelmingly common choice. Statements are written for one engine and tested against it. 2. **Generate the placeholder token in one place.** A tiny helper that returns `"?"` or `"$" + strconv.Itoa(n)` for the configured dialect, used by the code that builds variable-length clauses. This only helps for generated fragments; hand-written statements still need rewriting. 3. **Keep two sets of statements.** Duplicated text, selected at startup. Verbose, but obvious, and each statement stays testable against its own engine. Whatever the choice, the rule underneath is unchanged: only placeholder tokens and program-owned SQL fragments are ever concatenated into the text, never request data. Dialect differences change which token you emit; they never change whether values are bound. ## Related gotchas Some engines treat a literal `?` inside a string literal or a JSON operator as significant, which is another reason nothing tries to rewrite the text for you. And the pattern of writing a statement, counting its placeholders by eye, and passing the arguments in a different order is the most common self-inflicted bug here — the types often still match, so the query runs and returns wrong rows rather than failing.

  • What happens if you pass sql.Named to a driver whose dialect is purely positional?
    The driver reports that it does not support named parameters and the call fails with an error. It does not quietly fall back to positional order, which is the safe behaviour: silently mapping names onto positions would run a statement binding the wrong values to the wrong slots.
  • A statement is ported from $1-style to ?-style placeholders. What is the classic bug?
    A value referenced twice as `$1` needs two `?` tokens and the argument passed twice. Convert mechanically and you end up with one argument for two placeholders — caught as an argument-count error if the driver reports its count, otherwise rejected by the server. The mirror bug going the other way passes a duplicate argument that no placeholder consumes.
  • How do teams that support two engines handle the placeholder difference?
    Usually by choosing one dialect and testing against it. When both are genuinely required, the workable pattern is a single helper that emits the right token for the configured dialect and is used by the code that builds generated clauses, with hand-written statements kept in per-dialect sets. Nothing in the standard library rewrites them for you.

saying these in an interview costs you the question

  • Thinks database/sql rewrites ? into $1 for the driver
  • Assumes query text is portable between database engines
  • Believes sql.Named works with any driver
  • Converts $1 used twice into a single ? without adding an argument
  • Says the placeholder token is part of Go's syntax