SQL · Capítulo 7 · Lección de conceptos · 6 min de lectura

Una consulta es una tabla que puedes reutilizar

Toda consulta devuelve una tabla, así que cualquier consulta puede ir dentro de otra como un valor, una prueba de sí o no, o una tabla propia. Un CTE solo le pone nombre a esa tabla interna.

La idea

Toda consulta devuelve una tabla. Una tabla de una fila y una columna es un número. Una tabla de una columna es una lista de valores. Una tabla con varias columnas es... una tabla. Así que en cualquier lugar donde SQL espera un número, una lista o una tabla, puedes poner una consulta entre paréntesis.

Eso es una subconsulta: una consulta usada como parte de otra. La consulta interna se ejecuta, se convierte en un valor, una lista o una tabla, y la consulta externa sigue como si hubieras escrito ese resultado a mano.

Figura 1

Una subconsulta se ejecuta primero y devuelve un número

products
product_idnameunit_price
156Willow Acacia Knife Block53.99
183Glen Glass Canvas Print189.00
216Tidewater Concrete String Lights379.00
(SELECT AVG(unit_price) FROM products)
avg_price
270.16
Resultado
nameunit_price
Tidewater Concrete String Lights379.00

Paso 1 de 4 La pregunta: ¿qué productos cuestan más que el producto promedio? La consulta externa necesita un número que todavía no tiene.

  1. La pregunta: ¿qué productos cuestan más que el producto promedio? La consulta externa necesita un número que todavía no tiene.
  2. La consulta interna, entre paréntesis, se ejecuta primero y sola, sobre los 120 productos. Devuelve una tabla de una fila y una columna: 270.16.
  3. Ahora la consulta externa ve WHERE unit_price > 270.16, una comparación normal. Revisa cada producto contra ese único número.
  4. Solo pasan las luces de cuerda. La consulta interna se ejecutó una vez, no una por producto.

Por qué existe

Muchas preguntas tienen un número que todavía no conoces. "Productos con precio superior al promedio" necesita el promedio. "Los productos premium de cada categoría" necesita un promedio por categoría. "El valor de vida de un cliente" necesita los ingresos por pedido antes de sumar los pedidos por cliente.

Sin subconsultas ejecutarías la primera consulta, copiarías la respuesta y la pegarías en la segunda. Eso se rompe el día que cambian los datos, y no puedes pasarle el resultado a nadie más. Una subconsulta mantiene ambas preguntas en una sola instrucción, así que la respuesta se actualiza sola y la lógica queda escrita.

Cómo funciona por dentro

Tres lugares donde puede ir una subconsulta

Como un solo valor (subconsulta escalar). Debe devolver exactamente una fila y una columna. La usas en WHERE unit_price > (SELECT AVG(unit_price) FROM products) o en la lista del SELECT. Si devuelve dos filas, obtienes un error, lo cual es mejor que una respuesta equivocada en silencio.

Como una lista o una prueba. IN (SELECT …) comprueba si un valor está en la lista devuelta. EXISTS (SELECT …) comprueba si la consulta interna devuelve alguna fila. Las columnas que selecciona no importan, por eso la gente escribe SELECT 1.

Como una tabla. Una subconsulta en FROM se trata como una tabla con alias. Así agregas primero y unes después: FROM (SELECT order_id, SUM(…) AS total FROM order_items GROUP BY order_id) t.

Correlacionada o no

Una subconsulta que no menciona a la consulta externa se evalúa una vez, como muestra el primer diagrama. Una subconsulta correlacionada hace referencia a la fila externa (WHERE o.customer_id = c.customer_id). Conceptualmente se evalúa otra vez por cada fila externa, y cada vez usa los valores de esa fila. "Los productos premium de cada categoría" es correlacionada: el promedio con el que compara depende de la categoría del producto.

Figura 2

EXISTS hace una pregunta de sí o no por fila y se detiene en la primera coincidencia

order_items
order_item_idorder_idquantity
1310042
1410051
1510054
returns
return_idorder_item_idreason
114damaged
242wrong_item
Resultado
order_item_idorder_id
141005

Paso 1 de 4 Objetivo: las líneas de pedido que se devolvieron. Para cada línea pregunta: ¿existe una fila en returns cuyo order_item_id sea el de esta línea?

  1. Objetivo: las líneas de pedido que se devolvieron. Para cada línea pregunta: ¿existe una fila en returns cuyo order_item_id sea el de esta línea?
  2. Línea 13: nada en returns apunta a ella. La respuesta es no, y la fila se descarta.
  3. Línea 14: la fila 1 de returns apunta a ella. Una coincidencia basta para responder que sí, así que la base de datos deja de buscar y conserva la línea.
  4. La línea 15 no tiene devolución. A diferencia de un JOIN, EXISTS nunca repite una línea, tenga las devoluciones que tenga, porque solo responde sí o no.

EXISTS es un sí o no por fila, así que nunca multiplica filas. Un JOIN con returns produciría una fila por devolución. Si solo necesitas saber si existe una devolución, EXISTS es más seguro y a menudo más claro.

CTE: ponerle nombre a los pasos

Una expresión de tabla común (CTE) es una subconsulta con nombre, escrita al principio con WITH. Las matemáticas no cambian. Lo que cambia es que puedes leer la consulta de arriba abajo, como una receta, y probar cada paso por separado.

Figura 3

Una cadena de CTE: cada paso es una consulta pequeña que puedes ejecutar sola

  1. order_items una fila por línea
  2. order_totals AS (…) una fila por pedido
  3. customer_ltv AS (…) una fila por cliente
  4. SELECT … FROM customer_ltv la respuesta

Paso 1 de 4 El pedido 1049 tiene tres líneas en order_items: 378.00, 107.98 y 1,137.00. Ese es el nivel de detalle del que partimos.

  1. El pedido 1049 tiene tres líneas en order_items: 378.00, 107.98 y 1,137.00. Ese es el nivel de detalle del que partimos.
  2. order_totals suma las líneas por pedido. Las tres filas se pliegan en una: 1,622.98. Ahora hay una fila por pedido.
  3. customer_ltv suma los totales de pedido por cliente. Como lee pedidos y no líneas, nada se cuenta dos veces.
  4. El SELECT final lee el último paso como cualquier tabla. Si un número se ve mal, ejecuta por separado el CTE de arriba y revísalo ahí.

El hábito más útil que esto te da es agregar antes de unir. El costo de envío vive en orders, las líneas de pedido viven en order_items, y unirlas directamente repite cada costo una vez por línea. Si sumas primero las líneas en una fila por pedido y luego unes, el nivel de detalle se mantiene en una fila por pedido. Un CTE hace visible ese paso en lugar de esconderlo dentro de un join largo.

Dos cosas más. Un CTE existe solo para la instrucción a la que pertenece; no se guarda. Y un CTE puede usar los CTE escritos antes que él, así se construye una cadena de pasos. Los CTE recursivos (un CTE que se refiere a sí mismo) generan filas, por ejemplo un calendario con una fila por día, para poder mostrar los días con cero pedidos.

Leer una consulta anidada

Léela de adentro hacia afuera. Busca los paréntesis más internos, ejecuta esa parte sola y mira qué devuelve: un número, una lista, una tabla. Luego reemplázala mentalmente por ese resultado y lee la siguiente capa hacia afuera. Así también se depura: cuando una consulta larga da una respuesta rara, pega cada pieza interna en su propia pestaña y compara su número de filas con algo en lo que confíes. La mayoría de los errores en consultas anidadas son un problema de nivel de detalle escondido en una capa, como un paso que devuelve una fila por línea de pedido cuando el siguiente suponía una fila por pedido.

Cuál elegir

Para un número, usa una subconsulta escalar. Para "existe una fila relacionada", usa EXISTS (y NOT EXISTS para "no existe"). Para todo lo que tenga dos o más pasos, o donde querrás revisar un resultado intermedio, usa un CTE.

Errores comunes

"Una subconsulta es más lenta, así que siempre debo usar un JOIN"

Las bases de datos modernas reescriben muchas subconsultas como joins por su cuenta, y la elección tiene más que ver con lo que se lee mejor y con responder la pregunta correcta. EXISTS suele ser mejor que un join porque no puede crear filas duplicadas. Optimiza cuando la consulta ya sea correcta y hayas medido que es lenta.

"Un CTE guarda su resultado como una tabla"

Un CTE es un nombre para una consulta, válido para una sola instrucción. No se guarda nada, y la siguiente instrucción no lo ve. Si necesitas que el resultado permanezca, creas una tabla o una vista, que es otra herramienta.

"NOT IN y NOT EXISTS significan lo mismo"

Coinciden en todas las filas sin NULL y se separan en cuanto la lista interna contiene un NULL: x NOT IN (1, NULL) nunca es verdadero, así que la consulta no devuelve nada. NOT EXISTS no tiene ese problema. Cuando quieras "filas sin coincidencia", usa NOT EXISTS o un LEFT JOIN … IS NULL.

Ponte a prueba

Una pregunta. Al responderla, la lección queda marcada en el mapa; nunca bloquea un nivel.

Quieres los clientes que han hecho al menos un pedido. ¿Qué afirmación sobre WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id) es verdadera?

Elige qué puede ejecutarse en este sitio. Puedes cambiarlo cuando quieras desde "Configurar cookies", al pie de cada página.