Desarrollo Web 5-8 minutos

Laravel 13: Query Builder avanzado — subconsultas y expresiones SQL

Diego Cortés
Diego Cortés
Full Stack Developer & SEO Specialist
Compartir:
Laravel 13: Query Builder avanzado — subconsultas y expresiones SQL
Imagen generada con IA

Con whereExists, selectSub y joinSub, el query builder de Laravel 13 resuelve reportes como "clientes sin pedidos este mes" o "último pedido de cada cliente" en una sola consulta, sin N+1 y sin SQL crudo desordenado.

Qué puede hacer el query builder avanzado por ti

El query builder de Laravel es la API fluida para trabajar con la base de datos: empiezas con DB::table('usuarios') y encadenas métodos para construir casi cualquier consulta, en todos los motores que soporta el framework. Cuando el listado se complica, la tentación es escribir SQL a mano o lanzar una consulta por fila; las subconsultas existen para que no tengas que hacer ni lo uno ni lo otro.

Más allá del SELECT básico: la API fluida de DB::table

La API fluida no es solo para SELECT sencillos. Cada método se puede anidar dentro de otro: donde antes necesitabas una query separada en PHP, ahora colocas un closure o un builder como argumento y Laravel genera la subconsulta por ti. El resultado es código legible que sigue la sintaxis del framework en lugar de fragmentos sueltos de SQL.

Los problemas que resuelven las subconsultas: N+1 y consultas duplicadas

El error clásico es el N+1: cargar cien clientes y lanzar una consulta extra por cada uno para saber su último pedido. Con subconsultas, esa información viaja en la misma consulta principal. También eliminas la duplicación de lógica cuando el mismo agregado aparece en varios puntos del código: lo defines una vez y lo reutilizas.

Subconsultas en el SELECT: selectSub y agregados

El patrón más habitual es añadir una columna calculada al SELECT.

Añadir una columna calculada con selectSub

Con selectSub pasas un closure que define la subconsulta y el alias de la columna: por ejemplo, la fecha del último pedido de cada cliente. Laravel la convierte en una subquery dentro del SELECT y el resultado llega como una columna más de la fila. Es la forma declarativa de resolver "lo más reciente de cada grupo" sin recurrir a SQL crudo.

Azúcar de Eloquent: withSum, withCount, withAvg y withMax

Si trabajas con modelos y relaciones, Eloquent aporta atajos para los agregados más comunes: withSum, withCount, withAvg, withMax y withMin añaden la columna calculada sin escribir la subconsulta a mano. Son ideales para listados con totales: el gasto acumulado de cada cliente o el número de pedidos por categoría salen en una sola query.

ofMany: el último pedido (o el más caro) de cada cliente

Para quedarte con el registro completo que tiene el valor máximo o mínimo de una columna dentro de cada grupo, usa ofMany en la relación: trae el pedido más reciente de cada cliente, o el más caro, con todos sus campos. Si además ordenas por fecha, latestOfMany hace exactamente eso con la semántica de "el último".

Filtrar con subconsultas: whereExists y whereNotExists

Cuando el filtro depende de la existencia de registros relacionados, estas dos cláusulas son la respuesta.

Ejemplo: clientes sin pedidos este mes

whereNotExists recibe un closure con la consulta de la relación y Laravel genera la cláusula NOT EXISTS: filas para las que no existe ningún pedido en el mes. La contraparte whereExists hace lo mismo en positivo, por ejemplo clientes con al menos una compra en los últimos treinta días. Ambos evitan el JOIN con conteo y las filas duplicadas.

whereExists con closures y condiciones dentro de la subconsulta

Dentro del closure tienes un builder completo: puedes filtrar por fechas, limitar, ordenar o añadir más condiciones. Esa flexibilidad convierte a whereExists en la herramienta para consultas de negocio que parecían necesitar SQL a mano, manteniendo todo el encadenado y el binding de parámetros.

joinSub: unir con una subconsulta

No solo se filtran filas: a veces conviene unir datos ya preprocesados.

Una tabla derivada como alias de join

joinSub recibe un closure, un alias y las condiciones del ON, y trata el resultado de la subconsulta como una tabla más: una tabla derivada. Es el patrón para pre-agregar antes de unir, por ejemplo totales por categoría calculados solo sobre pedidos pagados, y luego cruzar con la tabla de categorías.

Cuándo usar joinSub en vez de una tabla real

Úsala cuando la tabla real no tiene la forma que necesitas: agregar, filtrar o transformar columnas antes del JOIN evita traer filas de más o hacer cálculos pesados después. Si la misma derivada se repite en varias consultas, plantéate una vista o un modelo dedicado; para un reporte puntual, joinSub es más simple.

Una subconsulta como tabla base

El mismo truco sirve para el FROM.

from con closure: consultar sobre el resultado de otra consulta

En lugar de un nombre de tabla, from acepta una subconsulta con su alias: consultas sobre el resultado de otra consulta. Es útil cuando necesitas aplicar filtros y agregados en dos niveles, por ejemplo totales sobre un conjunto ya filtrado, sin crear tablas temporales ni views solo para un reporte.

Expresiones SQL crudas: cuándo y cómo

A veces la API fluida no cubre una función de la base de datos; ahí entran las expresiones crudas, con sus reglas.

selectRaw, whereRaw, orderByRaw y havingRaw

selectRaw añade columnas calculadas con SQL, whereRaw y havingRaw filtran con condiciones nativas y orderByRaw ordena con expresiones que la API no traduce. Úsalas para funciones específicas del motor o lógica de ordenación compleja, siempre con la mínima superficie posible y rodeadas de la API fluida.

Seguridad: PDO parameter binding y por qué nunca concatenar

El query builder usa PDO parameter binding: los valores que pasas como bindings se escapan automáticamente, así que no hace falta limpiarlos a mano. El riesgo aparece cuando concatenas valores del usuario dentro de una expresión cruda; en ese caso estás construyendo SQL con entrada no saneada. Regla fija: valores como bindings, nunca pegados al string.

DB::raw para funciones de base de datos no cubiertas por la API

DB::raw envuelve cualquier fragmento SQL para usarlo dentro de otro método. Si una función del motor no existe en la API fluida, este es el escape hatch correcto; si solo necesitas un valor calculado simple, prefiere selectRaw o los agregados de Eloquent para no esparcir SQL por el código.

Union de consultas

Para combinar resultados de varias consultas en una sola respuesta, el query builder ofrece union y unionAll. La primera elimina duplicados y la segunda los conserva, que suele ser más rápida. Es el patrón para listados que mezclan orígenes distintos, como eventos de dos tablas diferentes ordenados por fecha, manteniendo el mismo número de columnas.

Rendimiento: subconsultas eficientes y EXPLAIN

Una subconsulta bien escrita es rápida, pero conviene verificarlo en el plan de ejecución.

Subconsultas correlacionadas vs no correlacionadas

Las subconsultas no correlacionadas se ejecutan una vez y su resultado se reutiliza; las correlacionadas dependen de cada fila de la consulta exterior y pueden volverse lentas con tablas grandes. Cuando puedas elegir, plantea el problema como una no correlacionada o como un agregado en el SELECT, que el optimizador maneja mejor.

Indexar las columnas de WHERE y ON, y revisar el plan con EXPLAIN

Antes de dar por buena una consulta, ejecuta EXPLAIN y revisa si las columnas de las cláusulas WHERE y ON están indexadas. Un índice que falta convierte una subconsulta elegante en un escaneo completo; uno bien puesto suele ser la diferencia entre milisegundos y segundos en los reportes.

Conclusión

El query builder avanzado de Laravel 13 convierte los reportes que parecían exigir SQL crudo en consultas declarativas y seguras: selectSub y los agregados de Eloquent para columnas calculadas, whereExists para filtros de existencia, joinSub para tablas derivadas y unions para combinar resultados, con el binding de PDO protegiendo cada valor. Escribe la primera consulta con whereNotExists esta semana y notarás la diferencia. Sigue leyendo el blog para más tutoriales de Laravel 13.

Categorías