Subqueries vs CTEs: cuándo usar cada uno
5 min de lecturaEl problema: queries que crecen
Las consultas de SEO rara vez son simples. En cuanto se necesita cruzar datos de GA4 con GSC, comparar periodos, calcular métricas derivadas o filtrar por condiciones complejas, una sola consulta plana con SELECT/FROM/WHERE no alcanza. Hay que componer la lógica en pasos.
SQL ofrece dos mecanismos para esto: subqueries (subconsultas anidadas) y CTEs (Common Table Expressions). Ambos producen el mismo resultado, pero difieren en legibilidad y mantenimiento.
Subqueries: consultas dentro de consultas
Una subquery es una consulta completa encerrada entre paréntesis que se usa como si fuera una tabla o un valor:
-- Subquery en el FROM (como tabla temporal)
SELECT url, clics
FROM (
SELECT
url,
SUM(clicks) AS clics
FROM `your-project.searchconsole.searchdata_url_impression`
WHERE data_date BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 28 DAY) AND CURRENT_DATE()
GROUP BY url
) AS datos_gsc
WHERE clics > 100
ORDER BY clics DESC
La consulta interna calcula los clics por URL. La consulta externa filtra solo las que tienen más de 100 clics. Funciona, pero cuando hay dos o tres niveles de anidamiento, la legibilidad se degrada rápidamente.
Las subqueries también se usan en el WHERE para filtrar basándose en los resultados de otra consulta:
-- URLs que tienen clics en GSC pero no aparecen en GA4
SELECT url, SUM(clicks) AS clics
FROM gsc_data
WHERE url NOT IN (
SELECT DISTINCT url_normalizada
FROM ga4_data
)
GROUP BY url
CTEs: consultas con nombre
Un CTE (Common Table Expression) usa la sintaxis WITH nombre AS (...) para definir consultas con nombre que se pueden referenciar después, como si fueran tablas temporales:
-- La misma lógica del ejemplo anterior, pero con CTEs
WITH datos_gsc AS (
SELECT
url,
SUM(clicks) AS clics
FROM `your-project.searchconsole.searchdata_url_impression`
WHERE data_date BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 28 DAY) AND CURRENT_DATE()
GROUP BY url
)
SELECT url, clics
FROM datos_gsc
WHERE clics > 100
ORDER BY clics DESC
El resultado es idéntico, pero la estructura es más clara: primero se define el paso intermedio (datos_gsc), luego se usa en la consulta final.
CTEs múltiples: el patrón estrella para SEO
La ventaja real de los CTEs aparece cuando hay múltiples pasos. Se pueden encadenar varios CTEs separados por comas, y cada uno puede referenciar a los anteriores:
-- Cruce GSC + GA4 con CTEs encadenados
WITH gsc_data AS (
-- Paso 1: datos de GSC
SELECT url, query, SUM(clicks) AS clics_gsc
FROM `your-project.searchconsole.searchdata_url_impression`
WHERE data_date BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 28 DAY) AND CURRENT_DATE()
GROUP BY url, query
),
ga4_data AS (
-- Paso 2: datos de GA4
SELECT
REGEXP_EXTRACT(page_location, r'(https?://[^?#]+)') AS url_normalizada,
COUNT(DISTINCT session_id) AS sesiones
FROM eventos_organicos
GROUP BY url_normalizada
)
-- Paso 3: cruzar ambas fuentes
SELECT
g.query, g.url, g.clics_gsc, a.sesiones
FROM gsc_data g
INNER JOIN ga4_data a ON g.url = a.url_normalizada
ORDER BY g.clics_gsc DESC
LIMIT 50
Este patrón de «definir cada fuente de datos como CTE, luego cruzarlas al final» es exactamente el que usan todas las queries de cruce del catálogo de Queryteca. Es el estándar de facto para análisis que combinan múltiples fuentes.
Comparación directa
La misma lógica escrita con subqueries anidadas vs CTEs:
Con subqueries anidadas (difícil de leer):
SELECT url, clics_gsc, sesiones
FROM (
SELECT url, SUM(clicks) AS clics_gsc
FROM gsc_table
GROUP BY url
) gsc
JOIN (
SELECT url_normalizada, COUNT(*) AS sesiones
FROM (
SELECT REGEXP_EXTRACT(page_location, r'(https?://[^?#]+)') AS url_normalizada
FROM ga4_table
WHERE medium = 'organic'
)
GROUP BY url_normalizada
) ga4 ON gsc.url = ga4.url_normalizada
Con CTEs (claro y mantenible):
WITH gsc AS (
SELECT url, SUM(clicks) AS clics_gsc
FROM gsc_table
GROUP BY url
),
ga4 AS (
SELECT
REGEXP_EXTRACT(page_location, r'(https?://[^?#]+)') AS url_normalizada,
COUNT(*) AS sesiones
FROM ga4_table
WHERE medium = 'organic'
GROUP BY url_normalizada
)
SELECT gsc.url, gsc.clics_gsc, ga4.sesiones
FROM gsc
JOIN ga4 ON gsc.url = ga4.url_normalizada
El resultado es idéntico. La diferencia es que con CTEs cada paso tiene un nombre descriptivo y se lee de arriba a abajo, como una receta de cocina. Con subqueries, hay que leer de adentro hacia afuera, lo que se vuelve confuso con más de dos niveles.
Cuándo usar cada uno
- Subqueries: aceptables para operaciones simples de un solo nivel, como un filtro
IN (SELECT ...)o un valor escalar en elSELECT. - CTEs: recomendables para cualquier consulta con dos o más pasos lógicos, especialmente cruces de datos, comparaciones de periodos y cálculos que requieren pasos intermedios.
En BigQuery, los CTEs no tienen penalización de rendimiento respecto a las subqueries. El optimizador de consultas trata ambas de la misma forma internamente. La elección es puramente de legibilidad y mantenimiento.
Una buena práctica
Al estructurar queries complejas, conviene dar nombres descriptivos a los CTEs: gsc_data, ga4_conversiones, urls_normalizadas. Un nombre como t1 o temp ahorra segundos al escribir pero cuesta minutos al releer la query semanas después.
Con los conceptos clave cubiertos, el bloque de Buenas prácticas cubre los aspectos metodológicos: cómo controlar los costos, evitar errores frecuentes y mantener las queries organizadas para reutilización.
Queries para practicar
Tráfico orgánico por país y ciudad
Desglosa el tráfico orgánico por ubicación geográfica. Permite identificar mercados principales y oportunidades de contenido localizado.
Top 100 keywords por clics en los últimos 28 días
Obtiene las 100 keywords con más clics en los últimos 28 días. Permite conocer los términos que generan mayor volumen de tráfico orgánico real al sitio.
Performance por país en GSC
Desglosa el rendimiento de búsqueda por país. Permite identificar mercados geográficos donde el sitio tiene presencia y detectar oportunidades de expansión internacional.
¿Listo para practicar? Explora el catálogo de queries
Ver catálogo