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 built-in dd() and dump() methods that expose the SQL and its bindings in one call — usually the fastest way to inspect 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 ?.
Executed SQL and builder SQL are snapshots at different moments
toSql() asks the builder what it would run at that point. DB::listen() observes queries that actually execute. That difference matters when scopes, eager loading, pagination, relationship access, or a later builder modification produces additional queries you did not see in the initial toSql() output.
Laravel can expose a bound-query representation for debugging
On current query-builder versions, debugging helpers such as dumpRawSql() / ddRawSql() are designed for the common "show me SQL with bindings substituted" workflow. Use them as a development aid, not as a value to execute later:
User::where('email', $email)->ddRawSql();
The database driver still receives SQL and bindings through its normal parameterized execution path. A printable representation is for humans; it should not become a reason to rebuild a parameterized query as raw SQL.
Be careful logging bindings
Bindings can contain passwords, reset tokens, email addresses, payment-related values, or other sensitive data. A query listener that logs every binding may turn the log system into a second copy of data you intentionally protected elsewhere. In production diagnostics, log only what you need, redact known-sensitive fields, and avoid enabling blanket query logging for long periods.
Use query inspection to answer a specific performance question
Seeing SQL is only the first step. For a slow query, copy a safely reconstructed query into the database client with representative values and inspect its execution plan; for an N+1 problem, count repeated query shapes; for an unexpected condition, inspect bindings. Different symptoms need different evidence, and dumping one builder does not automatically explain the request's database behavior.