skip to content

Fluent Query Builder

DB::table() chains build SQL with where clauses, joins, aggregates and raw expressions bound as parameters. Interviewers check grouping closures, raw-string injection and row locks.

on this pageshow

explore

questions

6

In Laravel's query builder, what do DB::table('orders')->get(), ->first() and ->value('total') return, including when no row matches?

level: juniorimportance: must knowfreq 62%

answer

  1. plain rows, not Eloquent models
  2. Collection of stdClass objects
  3. first() adds limit 1
  4. null, not an exception
  5. firstOrFail throws RecordNotFoundException

basics

~10 s

get() returns an Illuminate\Support\Collection of stdClass rows, empty when nothing matches; first() returns one stdClass or null; value('total') returns that column from the first row or null. Only firstOrFail() and sole() throw.

solid answer

~40 s

`DB::table()` works with plain rows, not models. `get()` returns an `Illuminate\Support\Collection` of `stdClass` objects, and an empty collection when nothing matches, so check it with `isEmpty()`, never with `=== null`. `first()` adds `limit 1` and returns one `stdClass` or `null`. `value('total')` selects only that column from the first row and returns the bare value or `null`. None of these throw on a miss: `firstOrFail()` throws `RecordNotFoundException`, and `sole()` throws `RecordsNotFoundException` for zero rows or `MultipleRecordsFoundException` for more than one. Laravel's exception handler turns both not-found exceptions into a 404. Aggregates are terminal too: `count()` returns an int, and `sum()` returns 0 when no rows match.

code

php · 15 lines
php
<?php

use Illuminate\Support\Facades\DB;

$paid = DB::table('orders')->where('status', 'paid')->get();
// Collection of stdClass; empty Collection when no paid orders

$latest = DB::table('orders')->latest()->first();
// stdClass or null

$total = DB::table('orders')->where('id', 42)->value('total');
// the column value or null

$order = DB::table('orders')->where('id', 42)->firstOrFail();
// throws RecordNotFoundException (rendered as a 404) on a miss

go deeper

for a junior

Recall the three return shapes: a Collection of stdClass from get(), one stdClass or null from first(), and a bare value or null from value().

for a middle

Explain why a miss is null or an empty Collection rather than an exception, and when firstOrFail() or sole() is the better call.

for a senior

Show when plain rows beat models, such as reports and bulk jobs, and what you give up: casts, hidden attributes and persistence.

for a principal

Frame a team convention for when data access may bypass Eloquent, and how to keep response shapes from leaking raw columns.

## What `DB::table()` gives you `DB::table('orders')` asks the `DB` facade for the default database connection and returns an `Illuminate\Database\Query\Builder` for that table. The builder is a **fluent object**: every `where`, `join` or `orderBy` call adds a piece to an in-memory description of the SQL and returns the same builder, so nothing runs yet. SQL only reaches the database when you call a **terminal method** such as `get()`, `first()`, `value()`, `pluck()`, `count()` or `exists()`. What those methods return is the part interviewers probe, because the query builder does **not** return Eloquent models. Each row is a PHP `stdClass` object whose properties are the selected column names. There are no casts, no accessors, no relationships and no `save()` method. ## The terminal methods side by side | Call | Returns when rows match | Returns when nothing matches | |---|---|---| | `get()` | `Illuminate\Support\Collection` of `stdClass` | an **empty** Collection | | `first()` | one `stdClass` (runs with `limit 1`) | `null` | | `value('total')` | the column's value from the first row | `null` | | `find(5)` | the row whose `id` is 5 | `null` | | `pluck('total', 'id')` | Collection of values, optionally keyed | empty Collection | | `count()` | an `int` | `0` | | `sum('total')` | the database's sum | `0` | | `exists()` | `true` | `false` | A few details sit behind that table: - `first()` is literally `limit(1)->get()->first()`, so it is still a real query; calling it in a loop is one round trip per iteration. - `value()` selects **only** the named column, which makes it cheaper than `first()->total` on wide tables. - `find()` always compares against a column named `id`; a table with another primary key needs `where()` and `first()` instead. - `sum()` is written as the aggregate result `?: 0`, while `avg()`, `min()` and `max()` return `null` for an empty set. ## When nothing matches None of `get()`, `first()`, `value()` or `find()` throws on a miss. That is deliberate, but it moves the check into your code: 1. For `get()`, test `$rows->isEmpty()`. An empty Collection is an object, so `if ($rows)` is always true. 2. For `first()` and `find()`, test for `null` before reading a property. Reading `->total` on `null` raises a PHP warning, which Laravel's error handler converts into an `ErrorException`, so the request fails with a 500 instead of a clean 404. 3. When a missing row should end the request, call `firstOrFail()`. It throws `Illuminate\Database\RecordNotFoundException`, which the framework's exception handler converts into a 404 response. 4. When exactly one row must exist, call `sole()`. It throws `RecordsNotFoundException` for zero rows and `MultipleRecordsFoundException` for two or more, because it fetches with `limit 2` to detect duplicates. ## Rows are not models Because each result is a `stdClass`, several things Eloquent users take for granted are absent: - **No casts.** A `created_at` column comes back as the string the driver produced, not a Carbon instance, and a JSON column is a string you decode yourself. - **No hidden attributes.** Returning query-builder rows straight from a controller serialises every selected column, so select only what the response needs. - **No persistence.** Changing `$row->status` changes a PHP object and nothing else; writing back takes an explicit `DB::table('orders')->where('id', $row->id)->update([...])`. That trade-off is why teams reach for `DB::table()` in reports and bulk jobs, where hydrating thousands of models would cost memory and time for features the code never uses. ## Choosing the right terminal - Need a list to loop over or return: `get()`, selecting only the columns you use. - Need one row that may be missing: `first()` plus a `null` check, or `firstOrFail()` when a miss is a 404. - Need one scalar, such as a status or a total: `value()`. - Need an id-to-name map for a dropdown: `pluck('name', 'id')`. - Need a yes/no answer: `exists()`, which avoids fetching rows at all. ## What interviewers listen for A junior answer names the three return shapes. A stronger one adds that a miss is signalled by `null` or an empty Collection rather than an exception, names `firstOrFail()` and `sole()` as the throwing variants, and notes that the rows are plain objects with no model behaviour.

  • How do firstOrFail() and sole() differ on the Laravel query builder?
    `firstOrFail()` runs `first()` and throws `RecordNotFoundException` only when no row matches; it happily returns the first of many. `sole()` fetches with `limit 2` and throws `RecordsNotFoundException` for zero rows or `MultipleRecordsFoundException` for two or more, so it also asserts uniqueness. Use `sole()` when a duplicate would mean corrupt data.
  • Why might you pick value('total') over first()->total?
    `value('total')` selects only that column, so a wide row is never transferred, and it returns `null` cleanly on a miss. `first()->total` selects every column, and when `first()` returns `null` it reads a property on `null`: a PHP warning that Laravel's error handler turns into an `ErrorException`.

saying these in an interview costs you the question

  • Says DB::table()->get() returns Eloquent models with casts
  • Checks an empty get() result with === null
  • Believes first() throws when no row matches
  • Thinks value('total') returns the whole first row
  • Expects changing a returned row object to update the database
open as a page

In Laravel's query builder, why does chaining where('shop_id', 3)->where('status', 'paid')->orWhere('status', 'refunded') return other shops' orders, and how do you fix it?

level: middleimportance: must knowfreq 58%

basics

~20 s

The chain compiles to shop_id = 3 and status = 'paid' or status = 'refunded'; SQL evaluates and before or, so every refunded order from any shop matches. Group the alternatives in a closure passed to where(), or use whereIn().

open as a page

In Laravel's query builder, how would you build a coffee-shop chain's monthly revenue-per-shop report with a join, aggregates and grouping, keeping shops that sold nothing?

level: middleimportance: should knowfreq 42%

basics

~20 s

Start from shops, leftJoin orders with the month and status conditions inside the join closure, selectRaw a coalesced sum, and groupBy the shop columns. A date filter in where() would drop zero-sale shops, and calling sum() returns one number.

open as a page

In Laravel's query builder, how does when() apply an optional filter, and why can ->when($request->input('shop_id'), ...) skip the filter for shop 0?

level: middleimportance: should knowfreq 34%

basics

~20 s

when($value, $callback) runs the callback only if $value is truthy, passing the builder and the value. The string "0" is falsy in PHP, so a shop_id of 0 skips the filter and returns every shop's rows.

open as a page

In Laravel, a gift-card redemption uses DB::table('gift_cards')->where('id', $id)->lockForUpdate()->first() yet still double-spends a balance under load; what are the likely causes?

level: seniorimportance: should knowfreq 38%

basics

~10 s

lockForUpdate() only appends FOR UPDATE; without DB::transaction() around the read and the write the lock ends with the statement. On SQLite, the skeleton default, the grammar emits no lock clause at all.

open as a page

In Laravel's query builder, which parts of a query are bound as parameters and which are pasted in verbatim, and how do selectRaw, whereRaw and DB::raw change that?

level: seniorimportance: should knowfreq 48%

basics

~20 s

Values passed to where(), whereIn(), insert() and update() are bound as PDO parameters. Column and table names are only quoted, and DB::raw() or selectRaw/whereRaw/orderByRaw strings are pasted verbatim, so user input there must go through their ? bindings or an allowlist.

open as a page