Reader Stacks

Raw SQL Queries in Laravel: Writing, Debugging, and Printing Them Safely

A raw query is only as safe as its parameter binding — concatenating a variable directly into a raw string reopens the exact SQL injection risk Eloquent otherwise protects against automatically.

Raw SQL Queries in Laravel: Writing, Debugging, and Printing Them Safely

Dropping down to raw SQL in Laravel is sometimes genuinely necessary — a complex query the query builder can't express cleanly, or a database-specific function — but it must be done with proper parameter binding, or it reopens exactly the SQL injection risk Eloquent otherwise protects against automatically.

Running a raw select query, safely, with bound parameters

$users = DB::select('select * from users where active = ? and role = ?', [true, 'admin']);

The ? placeholders and the separate bindings array are what make this safe — the database driver treats the bound values purely as data, never as part of the SQL syntax itself, the same protection prepared statements provide when using plain PDO directly.

Using named bindings instead of positional ones

$users = DB::select('select * from users where active = :active and role = :role', [
    'active' => true,
    'role' => 'admin',
]);

Mixing raw expressions into an otherwise normal Eloquent query

$products = Product::select('*')
    ->selectRaw('(price * 1.08) as price_with_tax')
    ->where('is_active', true)
    ->get();

selectRaw() is the standard way to add a computed column expression that the query builder's normal fluent methods don't have a dedicated function for — the rest of the query (the where() clause here) still works entirely through normal Eloquent methods.

The critical mistake: concatenating a variable directly into a raw string

// NEVER DO THIS — vulnerable to SQL injection
$role = $request->input('role');
$users = DB::select("select * from users where role = '{$role}'");

This is precisely the same vulnerability covered in the plain-PHP PDO article elsewhere on this site — wrapping something in DB::raw() or a raw select string provides zero automatic protection; the safety comes entirely from using ?/named placeholders with a separate bindings array, never from string interpolation.

Printing the actual SQL a query builder call generates, without executing it

$query = Product::where('category_id', 3)->where('price', '>', 100);

echo $query->toSql(); // "select * from products where category_id = ? and price > ?"
dd($query->getBindings()); // [3, 100]

toSql() shows the query's structure with placeholders, not the actual final values — getBindings() separately returns the values that get substituted in; combining both is necessary to see the complete, real query that would actually run.

Getting the fully substituted SQL string, values included

DB::enableQueryLog();

Product::where('category_id', 3)->get();

dd(DB::getQueryLog());

DB::getQueryLog() captures every query executed during the request, including the actual bound values — genuinely more useful than toSql() alone when debugging a specific query's real, final form as it was actually sent to the database.

Logging every query to the log file, for ongoing debugging

DB::listen(function ($query) {
    Log::info($query->sql, $query->bindings);
});

DB::listen(), typically registered in a service provider's boot() method during local development, logs every query as it runs across the entire application — useful for spotting an unexpected N+1 query pattern or an unusually slow query, though it should be disabled or heavily filtered in production due to the sheer log volume it generates.

Using dd() directly on a query builder instance, as a quick shortcut

Product::where('category_id', 3)->dd(); // dumps SQL + bindings and halts execution

dd() called directly on a query builder instance is a genuinely convenient shortcut during active debugging — it combines toSql() and getBindings() into one readable dump and halts execution immediately, without needing three separate lines of debugging code.