There are two different things people mean by "see the SQL" — the query shape with placeholders, and the actual query with real values substituted in — and they need different tools.
Query shape only: toSql()
$query = User::where('active', true)->where('created_at', '>', now()->subDays(7));
echo $query->toSql();
// select * from `users` where `active` = ? and `created_at` > ?
Note the ? placeholders — this shows the query's structure, not the real values bound to it.
Getting the actual bound values
dd($query->getBindings());
// [true, "2026-08-24 10:00:00"]
The shortcut: dd() straight on the query
User::where('active', true)->dd();
Laravel's query builder has a built-in dd() and dump() that dumps SQL, bindings, and the connection name together in one call — usually the fastest way to check a single query while developing.
Capturing every query the app runs
To see every query for a request, not just one you're inspecting — useful for hunting an N+1 problem — register a listener, typically in AppServiceProvider::boot():
use Illuminate\Support\Facades\DB;
DB::listen(function ($query) {
logger($query->sql, $query->bindings);
// also available: $query->time (milliseconds)
});
This logs every executed query for as long as the listener is active — genuinely useful for finding N+1 patterns, but noisy enough that it should be gated behind a local-only or debug-only condition, not left running in production.
Reading bindings back into the query manually
If you need one printable string with values substituted in (for pasting into a DB client, not for production logging — values aren't safely escaped for that purpose), Laravel's query log entries include both pieces separately for exactly this reason: combine them yourself rather than trusting a naive string-replace, which can break on values containing ?.