Running simple SQL queries safelyfor Drupal 8 , 9 , 10 , and 11

Last updated :  

The fluent select() builder from the last two lessons is the everyday tool, but sometimes you already know the exact SQL you want to run — a fixed query with no dynamic joins or conditions to assemble. For that case, Drupal gives you a more direct route: Connection::query(), which runs a literal SQL string with placeholders, safely.

What you'll learn in this lesson

  • How to run a literal SQL string with Connection::query()
  • Why placeholders are non-negotiable even in a "static" query
  • When a static query is the right choice over the dynamic builder

A real static query

From the Drupal Examples project's own documentation comment:

// SELECT * FROM {dbtng_example} WHERE uid = 0 AND name = 'John'
\Drupal::database()->query(
  "SELECT * FROM {dbtng_example} WHERE uid = :uid and name = :name",
  [':uid' => 0, ':name' => 'John']
)->execute();

Two details make this a genuinely Drupal-flavored static query rather than plain PDO:

Curly braces around the table name

Notice {dbtng_example}, not just dbtng_example. Drupal wraps table names in curly braces specifically so its database layer can rewrite them at query time — this is what makes Drupal's multisite table-prefixing feature work transparently, even inside a hand-written SQL string.

Named placeholders, never string concatenation

:uid and :name are placeholders, resolved from the second array argument — not interpolated into the string. This is the one rule that must never be broken: the moment you concatenate a variable directly into a SQL string ("WHERE name = '$name'"), you've opened a SQL injection hole, "static" query or not. The placeholder syntax is what keeps this method just as safe as the dynamic query builder.

When to actually reach for this instead of select(): static queries make sense when the SQL is genuinely fixed and unlikely to ever need conditional joins or optional filters — a known, simple lookup. The moment a query needs to branch (add a condition only if a parameter was passed, join a table only sometimes), the dynamic select() builder from the previous lessons is the better fit, since building that logic inside a raw string gets unreadable fast.

Quick check: would \Drupal::database()->query("SELECT * FROM {dbtng_example} WHERE uid = " . $uid) be safe if $uid came from user input? (No — that's direct string concatenation, not a placeholder, so it's exactly the SQL injection pattern this lesson warns against, regardless of which method name you called it through.)

Key takeaways

  • Connection::query() runs a literal SQL string — the right tool when the query shape is genuinely fixed.
  • Wrap table names in curly braces ({table_name}) so Drupal's multisite prefixing still applies inside raw SQL.
  • Always use named placeholders (:name) with a parameters array — never concatenate variables into the SQL string, ever.
  • Reach for the dynamic select() builder instead the moment your query needs conditional logic.

Coming up next

You've now read data three different ways. The last piece of the Database API puzzle is making sure multiple writes succeed or fail together — that's what transactions are for, up next.