Curso de Laravel
Consultas Eloquent en Laravel: filtrar, ordenar y agrupar
Por Víctor Peña · Publicado el
Hola, ¿cómo están? Empezamos el módulo de consultas. Hoy vemos Eloquent en profundidad, probando cada ejemplo en Tinker para ver el resultado al instante.
¡Empecemos!
Filtrar con where
Paciente::where('ciudad', 'Cochabamba')->get();
Paciente::where('edad', '>', 18)->get();
Cita::where('costo', '>=', 250)->get();
Cita::where('estado', '!=', 'cancelada')->get();
Cuando el operador es = se puede omitir, que es la forma habitual.
Encadenar condiciones
Cita::where('estado', 'programada')
->where('costo', '>', 200)
->get();
Cada where encadenado equivale a un AND.
Para OR:
Cita::where('estado', 'programada')
->orWhere('estado', 'atendida')
->get();
Y aquí una trampa que hay que conocer. Al mezclar AND y OR, el resultado puede no ser el que esperas:
// Probablemente incorrecto
Cita::where('costo', '>', 200)
->where('estado', 'programada')
->orWhere('estado', 'atendida')
->get();
Eso devuelve las atendidas sin importar el costo. Es el mismo problema de precedencia que vimos en el curso de MySQL, y se resuelve agrupando:
Cita::where('costo', '>', 200)
->where(function ($consulta) {
$consulta->where('estado', 'programada')
->orWhere('estado', 'atendida');
})
->get();
Esa función anónima es el equivalente a los paréntesis en SQL.
Los where especializados
Laravel tiene métodos para los casos frecuentes, y se leen mucho mejor:
Cita::whereIn('estado', ['programada', 'atendida'])->get();
Cita::whereNotIn('doctor_id', [1, 5])->get();
Cita::whereBetween('costo', [200, 400])->get();
Paciente::whereNull('telefono')->get();
Paciente::whereNotNull('correo')->get();
Paciente::whereLike('apellido', 'To%')->get();
Cita::whereDate('fecha', '2026-05-20')->get();
Cita::whereMonth('fecha', 5)->get();
Cita::whereYear('fecha', 2026)->get();
Los tres últimos son muy cómodos para reportes, aunque tienen un costo: como advertimos en la lección de índices de MySQL, aplicar una función sobre la columna impide usar el índice. Para tablas grandes es preferible un rango:
Cita::whereBetween('fecha', ['2026-05-01', '2026-05-31'])->get();
Ordenar y limitar
Paciente::orderBy('apellido')->get();
Paciente::orderBy('created_at', 'desc')->get();
Cita::orderBy('fecha')->orderBy('hora')->get();
Cita::latest()->get(); // por created_at descendente
Cita::latest('fecha')->get(); // por otra columna
Cita::oldest()->get();
Cita::take(10)->get();
Cita::skip(20)->take(10)->get();
Paciente::inRandomOrder()->first();
Obtener resultados
Paciente::get(); // colección
Paciente::first(); // el primero, o null
Paciente::firstOrFail(); // el primero, o 404
Paciente::find(1);
Paciente::findOrFail(1);
Paciente::pluck('nombre'); // solo esa columna
Paciente::pluck('nombre', 'id'); // como clave => valor
Paciente::value('nombre'); // un solo valor
pluck() con dos argumentos es perfecto para llenar un <select>:
$doctores = Doctor::pluck('nombre', 'id');
Seleccionar columnas
Paciente::select('id', 'nombre', 'apellido')->get();
Trae solo lo que necesitas. Con tablas que tienen columnas TEXT pesadas, la diferencia se nota.
Agregaciones
Cita::count();
Cita::where('estado', 'atendida')->sum('costo');
Cita::avg('costo');
Cita::max('costo');
Cita::min('fecha');
Estas se calculan en la base de datos, no en PHP. Es importante: Cita::sum('costo') es muchísimo más eficiente que traer todas las citas y sumarlas con un bucle.
Agrupar
Cita::selectRaw('estado, COUNT(*) as total, SUM(costo) as ingresos')
->groupBy('estado')
->get();
Cuando la consulta se vuelve muy de reporte, muchas veces conviene el Query Builder, que veremos en la siguiente lección.
Filtros condicionales con when
Este método resuelve un patrón que aparece en todos los buscadores.
Sin él, el código se llena de condicionales:
$consulta = Cita::query();
if ($request->filled('estado')) {
$consulta->where('estado', $request->estado);
}
if ($request->filled('doctor_id')) {
$consulta->where('doctor_id', $request->doctor_id);
}
$citas = $consulta->get();
Con when():
$citas = Cita::query()
->when($request->estado, fn ($q, $estado) => $q->where('estado', $estado))
->when($request->doctor_id, fn ($q, $id) => $q->where('doctor_id', $id))
->when($request->desde, fn ($q, $desde) => $q->whereDate('fecha', '>=', $desde))
->get();
La condición solo se aplica si el valor existe. Es el patrón estándar para formularios de búsqueda con filtros opcionales, y lo vas a usar mucho.
Scopes: consultas con nombre
Cuando una condición se repite por todo el proyecto, conviene darle nombre en el modelo:
class Cita extends Model
{
public function scopeProgramadas($consulta)
{
return $consulta->where('estado', 'programada');
}
public function scopeDelMes($consulta, $mes = null)
{
return $consulta->whereMonth('fecha', $mes ?? now()->month);
}
}
Y al usarlos, se omite el prefijo scope:
Cita::programadas()->get();
Cita::programadas()->delMes()->get();
Cita::programadas()->delMes(4)->orderBy('fecha')->get();
Los scopes hacen dos cosas valiosas: evitan repetir la condición y le ponen nombre a una regla del negocio. Cita::programadas() se lee mejor que where('estado', 'programada') repetido en quince archivos, y si mañana «programada» pasa a significar otra cosa, se cambia en un solo sitio.
Paginación
$citas = Cita::orderBy('fecha')->paginate(15);
Y en la vista:
@foreach ($citas as $cita)
{{ $cita->fecha }}
@endforeach
{{ $citas->links() }}
Eso genera los enlaces de paginación completos, respetando los filtros de la URL si añades:
{{ $citas->withQueryString()->links() }}
Sin ese withQueryString(), al pasar a la página 2 se pierden los filtros de búsqueda. Es un detalle que se olvida siempre.
Para tablas muy grandes existe simplePaginate(), que solo genera «anterior» y «siguiente» y evita contar el total, que es lo caro.
Colecciones
Lo que devuelve get() no es un arreglo: es una colección, con métodos propios muy útiles:
$citas = Cita::all();
$citas->count();
$citas->sum('costo');
$citas->groupBy('estado');
$citas->sortBy('fecha');
$citas->filter(fn ($c) => $c->costo > 250);
$citas->map(fn ($c) => $c->costo * 1.13);
$citas->pluck('fecha');
$citas->first();
$citas->isEmpty();
Una advertencia importante: estos métodos operan en memoria, sobre registros ya traídos. No es lo mismo que filtrar en la consulta.
// Mal: trae 50.000 registros y filtra en PHP
Cita::all()->where('estado', 'programada');
// Bien: la base de datos filtra
Cita::where('estado', 'programada')->get();
La regla: filtra en la consulta, transforma en la colección.
Procesar muchos registros
Si necesitas recorrer decenas de miles de filas, traerlas todas agota la memoria:
Cita::chunk(500, function ($citas) {
foreach ($citas as $cita) {
// procesar
}
});
O con un cursor, que trae una fila a la vez:
foreach (Cita::lazy() as $cita) {
// procesar
}
Es la misma idea de los generadores que vimos en el curso de PHP.
Errores comunes
- Mezclar
whereyorWheresin agrupar. Cita::all()->where(...), filtrando en memoria lo que debería filtrar la base.- Sumar con un bucle en lugar de usar
sum(). - Olvidar
withQueryString()y perder los filtros al paginar. - Traer todas las columnas cuando solo necesitas dos.
- Repetir la misma condición en vez de crear un scope.
Para cerrar
Con lo de esta lección ya puedes escribir prácticamente cualquier consulta de lectura de una aplicación. Los dos métodos que más te van a servir en el día a día son when() para los buscadores con filtros y los scopes para las reglas que se repiten.
Y la regla que resume todo: que filtre la base de datos, no PHP.
En la siguiente lección veremos el Query Builder, para cuando Eloquent se queda corto.
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