Unique and exists rules — how they hit the database
Concept
The unique and exists validation rules both issue real SQL queries against your database during the validation pass. Understanding exactly what queries they generate — and how to constrain them — is critical for writing correct, secure validation.
unique:table,column checks that no row in the specified table has the given value in the specified column. The underlying implementation is in Illuminate\Validation\DatabasePresenceVerifier::getCount(), which builds a raw SELECT COUNT(*) AS aggregate FROM {table} WHERE {column} = ? query using a separate database connection instance (not Eloquent). The validator passes if the count is zero.
exists:table,column does the opposite: it passes if the count is one or more. It is used to verify that a foreign key value actually exists in a related table before saving it.
Both rules accept additional constraint parameters in their fluent Rule::unique() / Rule::exists() API, which is far more readable and safe than the raw string form. The fluent builder lets you add extra where clauses and, crucially for unique, ignore a specific row by its primary key — essential for update operations where the current record's own value should not be treated as a conflict.
The ignore($id) method in Rule::unique() appends AND id != ? to the generated query. Internally it calls DatabasePresenceVerifier::addWhere() with a NOT IN clause. Never pass user-controlled input directly to ignore() — always use the authenticated user's ID or a model instance, since a crafted ignore value could bypass the uniqueness check entirely.
| Rule form | Generated SQL (approximate) |
|---|---|
unique:users,email | SELECT COUNT(*) FROM users WHERE email = ? |
unique:users,email + ->ignore($user) | SELECT COUNT(*) FROM users WHERE email = ? AND id != ? |
exists:countries,code | SELECT COUNT(*) FROM countries WHERE code = ? |
exists:roles,id + ->where('active', 1) | SELECT COUNT(*) FROM roles WHERE id = ? AND active = ? |
Code Example
<?php
use Illuminate\Validation\Rule;
use App\Models\User;
// --- CREATE: email must not already exist ---
$request->validate([
'email' => [
'required',
'email',
Rule::unique('users', 'email'),
],
]);
// --- UPDATE: ignore the current user's own row ---
// $user is the authenticated User model instance (never pass $request->input('id'))
$user = auth()->user();
$request->validate([
'email' => [
'required',
'email',
Rule::unique('users', 'email')->ignore($user->id),
// or pass the model itself — Laravel uses $model->getKey()
// Rule::unique('users', 'email')->ignore($user),
],
'username' => [
'required',
'string',
Rule::unique('users', 'username')
->ignore($user->id)
->whereNull('deleted_at'), // honour soft deletes
],
]);
// --- exists: verify foreign key references real, active record ---
$request->validate([
'country_id' => [
'required',
'integer',
Rule::exists('countries', 'id')->where('active', true),
],
'role_id' => [
'required',
Rule::exists('roles', 'id'),
],
]);
// --- Custom table + column via string shorthand (simple cases only) ---
$request->validate([
'invite_code' => 'required|exists:invite_codes,code',
]);Interview Q&A
Q: Why is it unsafe to pass user-supplied input directly to Rule::unique()->ignore()?
The ignore($id) method appends AND {primaryKey} NOT IN (?) to the uniqueness query. If an attacker controls the ignored ID — for example by sending ?id=1 in the request body and passing $request->id to ignore() — they can bypass the uniqueness check entirely for any existing row, effectively allowing them to steal another user's email address. Always source the ignored ID from a trusted, server-side value such as auth()->id() or the model fetched via route model binding, never from user-supplied input.
Q: The unique and exists rules use a DatabasePresenceVerifier. How does this relate to Eloquent, and what are the implications?
The DatabasePresenceVerifier (bound in ValidationServiceProvider) uses a raw Illuminate\Database\Connection instance, not Eloquent models. This means global scopes, model observers, and SoftDeletes are bypassed. A unique:users,email rule will find soft-deleted rows and count them as conflicts. To exclude soft-deleted records you must add an explicit constraint: Rule::unique('users', 'email')->whereNull('deleted_at'). This is a common production bug: restoring a soft-deleted user whose email "already exists" fails uniqueness validation even though the active-user table has no conflict.
Q: How does Laravel's exists rule handle array inputs in nested validation?
When validating an array such as tag_ids.*, each element is validated individually by the presence verifier. The query is still a SELECT COUNT(*) WHERE column = ? per element — there is no batching. For large arrays with many foreign key checks this can generate N queries, one per element. A more performant alternative is a custom rule that fetches the full set in one WHERE id IN (?) query and validates the entire array at once, or using Rule::exists() combined with the array wildcard (tag_ids.*), accepting the N-query cost.