Curso de Laravel
Query Builder en Laravel: el constructor de consultas SQL
Por Víctor Peña · Publicado el
Hola, ¿cómo están? Continuando con el módulo de consultas, hoy veremos el Query Builder: la capa que hay debajo de Eloquent y que puedes usar directamente.
¡Empecemos!
Qué es y en qué se diferencia de Eloquent
El Query Builder construye consultas SQL con una interfaz fluida, sin necesidad de modelos. Trabaja directamente sobre las tablas.
use Illuminate\Support\Facades\DB;
// Eloquent: necesita el modelo, devuelve objetos Paciente
Paciente::where('ciudad', 'Cochabamba')->get();
// Query Builder: sin modelo, devuelve objetos genéricos
DB::table('pacientes')->where('ciudad', 'Cochabamba')->get();
De hecho, Eloquent usa el Query Builder por debajo. Cuando escribes Paciente::where(...), quien resuelve es el Query Builder.
La diferencia práctica: Eloquent devuelve modelos, con sus relaciones, casts y eventos. El Query Builder devuelve datos en crudo, y por eso es algo más rápido.
Consultas básicas
DB::table('pacientes')->get();
DB::table('pacientes')->where('activo', true)->get();
DB::table('pacientes')->find(1);
DB::table('pacientes')->first();
DB::table('pacientes')->count();
DB::table('pacientes')->pluck('nombre');
La sintaxis es prácticamente la misma que en Eloquent, así que lo que aprendiste en la lección anterior se aplica igual.
Insertar, actualizar y eliminar
DB::table('pacientes')->insert([
'cedula' => '1234567',
'nombre' => 'Ana',
'created_at' => now(),
'updated_at' => now(),
]);
$id = DB::table('pacientes')->insertGetId([...]);
DB::table('citas')
->where('fecha', '<', now())
->update(['estado' => 'cancelada']);
DB::table('citas')->where('id', 5)->delete();
Fíjate en created_at y updated_at: aquí hay que ponerlos a mano. El Query Builder no pasa por Eloquent, así que no gestiona las marcas de tiempo ni los casts.
Y para insertar muchos registros de una vez, que es donde brilla:
DB::table('pacientes')->insert($milesDeRegistros);
Joins
Aquí es donde el Query Builder se vuelve realmente útil:
$citas = DB::table('citas')
->join('pacientes', 'citas.paciente_id', '=', 'pacientes.id')
->join('doctores', 'citas.doctor_id', '=', 'doctores.id')
->join('especialidades', 'doctores.especialidad_id', '=', 'especialidades.id')
->select(
'citas.fecha',
'citas.hora',
'pacientes.nombre as paciente',
'doctores.nombre as doctor',
'especialidades.nombre as especialidad',
'citas.costo'
)
->where('citas.estado', 'atendida')
->orderBy('citas.fecha', 'desc')
->get();
Es exactamente el JOIN que vimos en el curso de MySQL, escrito en PHP.
Y las variantes:
->leftJoin('citas', 'pacientes.id', '=', 'citas.paciente_id')
->rightJoin(...)
->crossJoin(...)
Agrupaciones y reportes
Este es el caso donde el Query Builder suele ganarle a Eloquent:
$reporte = DB::table('citas')
->join('doctores', 'citas.doctor_id', '=', 'doctores.id')
->join('especialidades', 'doctores.especialidad_id', '=', 'especialidades.id')
->select(
'especialidades.nombre as especialidad',
DB::raw('COUNT(citas.id) as total_citas'),
DB::raw('SUM(citas.costo) as ingresos'),
DB::raw('ROUND(AVG(citas.costo), 2) as promedio')
)
->where('citas.estado', 'atendida')
->groupBy('especialidades.id', 'especialidades.nombre')
->having('total_citas', '>', 5)
->orderByDesc('ingresos')
->get();
Cuando la consulta devuelve columnas calculadas que no corresponden a ningún modelo, forzarla a través de Eloquent no aporta nada.
Cuidado con DB::raw
DB::raw() inserta SQL sin procesar. Es potente y es la única parte del Query Builder donde puedes crear una vulnerabilidad:
// NUNCA: inyección SQL
DB::raw("SUM(costo) as total WHERE ciudad = '{$request->ciudad}'")
// Correcto: los valores van como parámetros
DB::table('citas')
->selectRaw('SUM(costo) as total')
->whereRaw('ciudad = ?', [$request->ciudad])
->get();
La regla es la misma del curso de PHP: todo dato que venga de fuera va como parámetro, nunca concatenado.
Subconsultas
$citas = DB::table('citas')
->whereIn('doctor_id', function ($consulta) {
$consulta->select('id')
->from('doctores')
->where('especialidad_id', 2);
})
->get();
Y una subconsulta como columna:
$doctores = DB::table('doctores')
->select('nombre')
->selectSub(
DB::table('citas')
->selectRaw('COUNT(*)')
->whereColumn('doctor_id', 'doctores.id'),
'total_citas'
)
->get();
Aquí conviene recordar lo que vimos en la lección de subconsultas de MySQL: una subconsulta correlacionada se ejecuta una vez por fila. Con muchos doctores, un JOIN con GROUP BY rinde mejor.
SQL directo
Cuando ni el Query Builder alcanza:
$resultados = DB::select('SELECT * FROM citas WHERE estado = ? AND costo > ?', ['atendida', 250]);
DB::insert('INSERT INTO logs (mensaje) VALUES (?)', ['Proceso iniciado']);
DB::update('UPDATE citas SET estado = ? WHERE id = ?', ['atendida', 5]);
DB::delete('DELETE FROM logs WHERE created_at < ?', [now()->subYear()]);
DB::statement('ALTER TABLE citas ADD INDEX idx_fecha (fecha)');
Fíjate en los ? con sus valores en el arreglo. Siempre parámetros.
Es el equivalente al DB::select() que vimos en el curso de Laravel Gohu.
Transacciones
DB::transaction(function () {
DB::table('citas')->where('id', 5)->update(['estado' => 'cancelada']);
DB::table('citas')->insert([
'paciente_id' => 1,
'doctor_id' => 1,
'fecha' => '2026-06-01',
'created_at' => now(),
'updated_at' => now(),
]);
});
Si algo dentro falla, se revierte todo automáticamente. Es la forma recomendada, más segura que hacer beginTransaction() y commit() a mano.
Lo veremos a fondo en la lección de transacciones del módulo avanzado.
Ver el SQL generado
DB::table('citas')->where('estado', 'atendida')->toSql();
DB::table('citas')->where('estado', 'atendida')->toRawSql();
Y para registrar todas las consultas de una petición, lo que vimos en la lección de configuración con DB::listen().
Query Builder o Eloquent
| Situación | Preferible |
|---|---|
| CRUD sobre una entidad | Eloquent |
| Necesitas relaciones, casts o eventos | Eloquent |
| Reportes con agrupaciones y cálculos | Query Builder |
| Joins de cuatro o más tablas | Query Builder |
| Insertar miles de registros | Query Builder |
| Tablas sin modelo, como una pivote | Query Builder |
| Consultas muy optimizadas | Query Builder |
La regla práctica: Eloquent por defecto, Query Builder para reportes y operaciones masivas.
Y algo importante: no son excluyentes. Puedes usar Eloquent en el 90 % de la aplicación y bajar al Query Builder en las tres consultas de reportes donde hace falta.
De hecho, puedes empezar desde el modelo y bajar cuando convenga:
Cita::query()
->join('pacientes', 'citas.paciente_id', '=', 'pacientes.id')
->selectRaw('pacientes.ciudad, COUNT(*) as total')
->groupBy('pacientes.ciudad')
->get();
Errores comunes
- Olvidar
created_atyupdated_atal insertar. - Concatenar variables en
DB::raw(). Es la puerta a la inyección SQL. - Usar Query Builder para todo y perder relaciones, casts y borrado lógico.
- Usar Eloquent para reportes con muchas tablas y cálculos, cuando el Query Builder es más directo.
- Esperar que dispare los eventos del modelo. No pasa por Eloquent.
Para cerrar
El Query Builder es la herramienta para cuando Eloquent estorba: reportes, joins complejos y operaciones masivas. Saber que existe y cuándo bajar a él es parte de escribir Laravel con criterio.
Y la regla de seguridad que no cambia nunca: los datos van como parámetros.
En la siguiente lección veremos las relaciones entre modelos, que es lo que hace que Eloquent valga la pena.
Saludos y éxitos.
Norvic Software
Desarrollamos el software que tu empresa necesita
Somos una fábrica de software en Bolivia. Construimos sistemas a medida y aplicaciones móviles, y llevamos Inteligencia Artificial a las empresas que ya tienen un sistema funcionando.
- Chatbots con Inteligencia Artificial
- Creación de agentes de IA
- Integración de IA en tus sistemas
- Desarrollo de software a medida
- Aplicaciones móviles iOS y Android
- Consultoría y asesoramiento técnico
Cotización sin costo · Respuesta directa por WhatsApp