> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Query builder

> Guía del query builder de Laravel a partir de DB::table(), desde lo básico hasta usos avanzados. Aprende a construir SQL directamente sin Eloquent y cuándo conviene una u otra opción.

## Qué es el query builder

El query builder de Laravel es un mecanismo para construir y ejecutar consultas de base de datos mediante una interfaz fluida. Se parte de `DB::table()` y se encadenan métodos para armar la consulta.

```php theme={null}
use Illuminate\Support\Facades\DB;

$users = DB::table('users')->get();
```

Internamente utiliza binding de parámetros de PDO, así que la protección contra inyecciones SQL es automática.

<Info>
  El query builder funciona con todas las bases de datos soportadas por Laravel (MySQL, MariaDB, PostgreSQL, SQLite, SQL Server). Puedes cambiar de motor sin modificar el código.
</Info>

## Query builder frente a Eloquent

| Situación                                     | Recomendado   |
| --------------------------------------------- | ------------- |
| Necesitas modelos y relaciones                | Eloquent      |
| Agregados o informes complejos                | Query builder |
| Rendimiento crítico con muchos datos          | Query builder |
| Operaciones sencillas sobre tablas existentes | Query builder |
| Migraciones y seeders                         | Query builder |

## Obtener datos

### Todos los registros

```php theme={null}
$users = DB::table('users')->get();

foreach ($users as $user) {
    echo $user->name;
}
```

`get()` devuelve una `Illuminate\Support\Collection`. Cada registro es un `stdClass` de PHP.

### Un solo registro

```php theme={null}
// Primer registro (null si no lo encuentra)
$user = DB::table('users')->where('name', 'Ana García')->first();

// Lanza excepción (respuesta 404 automática) si no lo encuentra
$user = DB::table('users')->where('name', 'Ana García')->firstOrFail();

// Solo el valor de una columna
$email = DB::table('users')->where('name', 'Ana García')->value('email');

// Búsqueda por ID
$user = DB::table('users')->find(3);
```

### Lista de valores de una columna

```php theme={null}
// Colección con los valores de email
$emails = DB::table('users')->pluck('email');

// Colección asociativa: name → email
$emailByName = DB::table('users')->pluck('email', 'name');
```

### Procesar grandes volúmenes por partes

```php theme={null}
use Illuminate\Support\Collection;

// Procesar en bloques de 100
DB::table('users')->orderBy('id')->chunk(100, function (Collection $users) {
    foreach ($users as $user) {
        // Procesar...
    }
});

// Cuando modificas datos durante el chunk, usa chunkById
DB::table('users')->where('active', false)
    ->chunkById(100, function (Collection $users) {
        foreach ($users as $user) {
            DB::table('users')
                ->where('id', $user->id)
                ->update(['active' => true]);
        }
    });
```

<Warning>
  Si actualizas o eliminas registros durante el chunk, usa `chunkById()` en lugar de `chunk()`. Con `chunk()` podrían saltarse registros.
</Warning>

### Streaming (LazyCollection)

```php theme={null}
DB::table('users')->orderBy('id')->lazy()->each(function (object $user) {
    // Procesar registro a registro
});
```

## Agregados

```php theme={null}
$count   = DB::table('users')->count();
$maxAge  = DB::table('users')->max('age');
$minAge  = DB::table('users')->min('age');
$avgAge  = DB::table('users')->avg('age');
$total   = DB::table('orders')->sum('amount');

// Agregado condicional
$avgPremium = DB::table('orders')
    ->where('plan', 'premium')
    ->avg('amount');
```

### Comprobar existencia de registros

```php theme={null}
if (DB::table('orders')->where('finalized', 1)->exists()) {
    // Existe
}

if (DB::table('orders')->where('finalized', 1)->doesntExist()) {
    // No existe
}
```

## Cláusula SELECT

```php theme={null}
// Columnas específicas
$users = DB::table('users')
    ->select('name', 'email as user_email')
    ->get();

// Sin duplicados
$users = DB::table('users')->distinct()->get();

// Añadir columnas después
$query = DB::table('users')->select('name');
$users = $query->addSelect('age')->get();
```

## Cláusula WHERE

### Condiciones básicas

```php theme={null}
// Igualdad (el = se puede omitir)
$users = DB::table('users')->where('votes', 100)->get();

// Operador de comparación
$users = DB::table('users')->where('votes', '>=', 100)->get();
$users = DB::table('users')->where('name', 'like', 'Ana%')->get();

// Varias condiciones (AND)
$users = DB::table('users')
    ->where('status', 'active')
    ->where('age', '>', 20)
    ->get();

// OR
$users = DB::table('users')
    ->where('votes', '>', 100)
    ->orWhere('name', 'Ana García')
    ->get();
```

### Agrupar condiciones

```php theme={null}
use Illuminate\Database\Query\Builder;

// Agrupar OR y combinarlo con AND
$users = DB::table('users')
    ->where('active', true)
    ->where(function (Builder $query) {
        $query->where('role', 'admin')
              ->orWhere('role', 'moderator');
    })
    ->get();
// WHERE active = 1 AND (role = 'admin' OR role = 'moderator')
```

### whereIn / whereBetween / whereNull

```php theme={null}
// IN
$users = DB::table('users')
    ->whereIn('id', [1, 2, 3])
    ->get();

$users = DB::table('users')
    ->whereNotIn('id', [1, 2, 3])
    ->get();

// BETWEEN
$users = DB::table('users')
    ->whereBetween('age', [20, 40])
    ->get();

// Comprobar NULL
$users = DB::table('users')->whereNull('deleted_at')->get();
$users = DB::table('users')->whereNotNull('email_verified_at')->get();
```

### whereLike (coincidencia de patrón)

```php theme={null}
// Por defecto no distingue mayúsculas
$users = DB::table('users')
    ->whereLike('name', '%Ana%')
    ->get();

// Distinguir mayúsculas y minúsculas
$users = DB::table('users')
    ->whereLike('name', '%Ana%', caseSensitive: true)
    ->get();
```

### whereAny / whereAll (misma condición en varias columnas)

```php theme={null}
// Al menos una columna coincide
$users = DB::table('users')
    ->where('active', true)
    ->whereAny(['name', 'email', 'bio'], 'like', '%Laravel%')
    ->get();

// Todas las columnas coinciden
$posts = DB::table('posts')
    ->whereAll(['title', 'content'], 'like', '%Laravel%')
    ->get();
```

### whereNullSafeEquals (igualdad NULL-safe)

`whereNullSafeEquals` y `orWhereNullSafeEquals` comparan una columna con un valor tratando dos `NULL` como iguales.

Con el operador `=` normal, `NULL = NULL` es `false`; en cambio `whereNullSafeEquals` considera `NULL` igual a `NULL`. Se corresponde con `<=>` en MySQL y con `IS NOT DISTINCT FROM` en PostgreSQL.

```php theme={null}
$lastLoginIp = $request->input('last_login_ip');

// Aunque $lastLoginIp sea null, encuentra los usuarios con last_login_ip IS NULL
$users = DB::table('users')
    ->whereNullSafeEquals('last_login_ip', $lastLoginIp)
    ->get();
```

<Info>
  `where('column', null)` se convierte en `WHERE column IS NULL`, pero `whereNullSafeEquals('column', $value)` se comporta de la misma manera independientemente de que el valor bindeado sea `null` o no. Es especialmente útil cuando el valor de entrada del usuario puede ser `null`.
</Info>

## JOIN

```php theme={null}
// INNER JOIN
$users = DB::table('users')
    ->join('orders', 'users.id', '=', 'orders.user_id')
    ->select('users.name', 'orders.amount')
    ->get();

// LEFT JOIN
$users = DB::table('users')
    ->leftJoin('orders', 'users.id', '=', 'orders.user_id')
    ->get();

// JOIN de varias tablas
$users = DB::table('users')
    ->join('contacts', 'users.id', '=', 'contacts.user_id')
    ->join('orders', 'users.id', '=', 'orders.user_id')
    ->select('users.*', 'contacts.phone', 'orders.amount')
    ->get();
```

### JOIN con subconsulta

```php theme={null}
// JOIN con una subconsulta
$latestOrders = DB::table('orders')
    ->select('user_id', DB::raw('MAX(created_at) as last_order_at'))
    ->groupBy('user_id');

$users = DB::table('users')
    ->joinSub($latestOrders, 'latest_orders', function ($join) {
        $join->on('users.id', '=', 'latest_orders.user_id');
    })
    ->get();
```

## Orden, agrupación y límites

```php theme={null}
// Ordenar
$users = DB::table('users')
    ->orderBy('name', 'asc')
    ->get();

// Varias columnas
$users = DB::table('users')
    ->orderBy('last_name')
    ->orderBy('first_name', 'desc')
    ->get();

// Aleatorio
$users = DB::table('users')->inRandomOrder()->get();

// Agrupar
$orders = DB::table('orders')
    ->select('status', DB::raw('COUNT(*) as count'))
    ->groupBy('status')
    ->get();

// HAVING
$orders = DB::table('orders')
    ->select('user_id', DB::raw('SUM(amount) as total'))
    ->groupBy('user_id')
    ->having('total', '>', 10000)
    ->get();

// LIMIT y OFFSET
$users = DB::table('users')
    ->skip(10)   // OFFSET
    ->take(5)    // LIMIT
    ->get();
```

## Subconsultas

```php theme={null}
// Subconsulta en el WHERE
$activeUsers = DB::table('users')->select('id')->where('is_active', 1);

$comments = DB::table('comments')
    ->whereIn('user_id', $activeUsers)
    ->get();

// Subconsulta en el SELECT
$users = DB::table('users')
    ->select('name')
    ->selectSub(function ($query) {
        $query->from('orders')
              ->selectRaw('COUNT(*)')
              ->whereColumn('orders.user_id', 'users.id');
    }, 'order_count')
    ->get();
```

## Expresiones Raw

<Warning>
  Las expresiones Raw se insertan literalmente en la consulta. Nunca pases datos del usuario directamente: usa siempre bindings para evitar inyecciones SQL.
</Warning>

```php theme={null}
// DB::raw() incrusta una expresión SQL arbitraria
$users = DB::table('users')
    ->select(DB::raw('count(*) as user_count, status'))
    ->groupBy('status')
    ->get();

// selectRaw
$orders = DB::table('orders')
    ->selectRaw('price * ? as price_with_tax', [1.10])
    ->get();

// whereRaw
$orders = DB::table('orders')
    ->whereRaw('price > IF(state = "JP", ?, 100)', [500])
    ->get();

// havingRaw
$orders = DB::table('orders')
    ->select('department', DB::raw('SUM(amount) as total'))
    ->groupBy('department')
    ->havingRaw('SUM(amount) > ?', [100000])
    ->get();

// orderByRaw
$orders = DB::table('orders')
    ->orderByRaw('updated_at - created_at DESC')
    ->get();
```

## INSERT / UPDATE / DELETE

### INSERT

```php theme={null}
// Insertar 1 registro
DB::table('users')->insert([
    'email' => 'ana@example.com',
    'name'  => 'Ana García',
]);

// Insertar varios
DB::table('users')->insert([
    ['email' => 'ana@example.com', 'name' => 'Ana García'],
    ['email' => 'luis@example.com', 'name' => 'Luis Pérez'],
]);

// Obtener el ID auto-incremental tras insertar
$id = DB::table('users')->insertGetId([
    'email' => 'maria@example.com',
    'name'  => 'María López',
]);
```

### UPSERT (INSERT o UPDATE)

```php theme={null}
// Si existe se actualiza; si no, se inserta
DB::table('users')->upsert(
    [
        ['email' => 'ana@example.com', 'name' => 'Ana García', 'votes' => 5],
        ['email' => 'luis@example.com', 'name' => 'Luis Pérez', 'votes' => 10],
    ],
    uniqueBy: ['email'],    // Columna para detectar duplicados
    update: ['name', 'votes'] // Columnas a actualizar
);
```

### UPDATE

```php theme={null}
// Actualizar con condición
$affected = DB::table('users')
    ->where('id', 1)
    ->update(['name' => 'Ana López', 'updated_at' => now()]);

// Incrementar y decrementar
DB::table('users')->where('id', 1)->increment('votes');       // +1
DB::table('users')->where('id', 1)->increment('votes', 5);    // +5
DB::table('users')->where('id', 1)->decrement('votes');       // -1
DB::table('users')->where('id', 1)->decrement('balance', 100); // -100
```

### DELETE

```php theme={null}
// Con condición
$deleted = DB::table('users')->where('status', 'inactive')->delete();

// Vaciar toda la tabla (resetea el auto-incremental)
DB::table('users')->truncate();
```

## Consultas condicionales (`when`)

Para aplicar condiciones dinámicamente, `when()` mantiene el código limpio.

```php theme={null}
$status = request('status');
$sortBy = request('sort', 'name');

$users = DB::table('users')
    ->when($status, function ($query, $status) {
        $query->where('status', $status);
    })
    ->when($sortBy === 'email', function ($query) {
        $query->orderBy('email');
    }, function ($query) {
        $query->orderBy('name');
    })
    ->get();
```

## Depuración

```php theme={null}
// Ver la SQL generada
$sql = DB::table('users')->where('active', true)->toSql();
// "select * from `users` where `active` = ?"

// SQL y bindings
$bindings = DB::table('users')->where('active', true)->getBindings();

// Ejecuta y muestra la query, y continúa
DB::table('users')->where('active', true)->dump();

// Ejecuta, muestra la query y termina
DB::table('users')->where('active', true)->dd();
```

<Tip>
  `dd()` es útil en depuración, pero no lo uses jamás en producción. `toSql()` y `getBindings()` son opciones seguras.
</Tip>

## Resumen

<AccordionGroup>
  <Accordion title="Métodos habituales">
    | Método            | Descripción                      |
    | ----------------- | -------------------------------- |
    | `get()`           | Todos los registros (Collection) |
    | `first()`         | Primer registro                  |
    | `find($id)`       | Por ID                           |
    | `value($column)`  | Un valor de columna              |
    | `pluck($column)`  | Lista de valores                 |
    | `count()`         | Número de registros              |
    | `sum($col)`       | Suma                             |
    | `avg($col)`       | Media                            |
    | `max($col)`       | Máximo                           |
    | `min($col)`       | Mínimo                           |
    | `exists()`        | Comprueba existencia             |
    | `insert([...])`   | Inserta                          |
    | `update([...])`   | Actualiza                        |
    | `delete()`        | Elimina                          |
    | `chunk($n, fn)`   | Procesamiento por bloques        |
    | `when($cond, fn)` | Consulta condicional             |
    | `toSql()`         | SQL generada                     |
  </Accordion>

  <Accordion title="Query builder frente a Eloquent">
    El query builder es más bajo nivel: devuelve `stdClass` en lugar de instancias de modelo.
    Cuando no necesitas relaciones ni observers, el query builder es más sencillo y rápido.

    ```php theme={null}
    // Eloquent: devuelve instancias del modelo User
    $users = User::where('active', true)->get();
    echo $users[0]->name; // Objeto User

    // Query builder: devuelve stdClass
    $users = DB::table('users')->where('active', true)->get();
    echo $users[0]->name; // Objeto stdClass
    ```
  </Accordion>
</AccordionGroup>


## Related topics

- [MongoDB](/es/mongodb.md)
- [Configuración de la base de datos](/es/database.md)
- [Atributos de PHP](/es/advanced/php-attributes.md)
- [Trait ForwardsCalls](/es/advanced/forwards-calls.md)
- [Trait Conditionable](/es/advanced/conditionable.md)
