La solicitud

De Jordan Reyes, Director de Marketing
Tu objetivo
Paso 1 de 3. Devuelve source, sessions (número de sesiones), conversions (sesiones convertidas) y conversion_rate_pct (conversions * 100 / sessions, redondeado a 2). Ordena por conversion_rate_pct de forma descendente.
- Lee de la tabla
web_sessionspendiente - Muestra exactamente
sourcesessionsconversionsconversion_rate_pctpendiente - 5 filas, ordenadas como se pidependiente
Paso 2 de 3. Devuelve una fila por cada fuente de sesión con: source, conversions, spend (gasto total de 2025 en marketing_spend.amount para ese canal, redondeado a 2; NULL cuando la fuente no tiene gasto) y cost_per_conversion (spend / conversions, redondeado a 2; NULL cuando no hay gasto). Ordena por source de forma ascendente.
- Lee de la tabla
web_sessionspendiente - Lee de la tabla
marketing_spendpendiente - Muestra exactamente
sourceconversionsspendcost_per_conversionpendiente
Paso 3 de 3. Solo para el canal de pago (web_sessions.source = 'paid', marketing_spend.channel = 'paid'), devuelve month (texto 'YYYY-MM'), spend (redondeado a 2), sessions, conversions y cost_per_conversion (spend / conversions, redondeado a 2; NULL en un mes sin conversiones). Una fila por cada mes de 2025, ordenada por month.
- Lee de la tabla
web_sessionspendiente - Lee de la tabla
marketing_spendpendiente - Muestra exactamente
monthspendsessionsconversionscost_per_conversionpendiente
Este caso se desbloquea cuando terminas:
La solicitud y la lección ya se pueden leer. Resuelve todos los niveles de esos capítulos, en cualquier orden, y este caso se abre.
Paso 1 · tu consulta
Cargando el editor…
Solución de referencia
| session_id | customer_id | started_at | device | source |
|---|---|---|---|---|
| 1 | NULL | 2023-01-01 23:02:24 | mobile | paid |
| 2 | 18 | 2023-01-01 15:07:49 | desktop | paid |
| 3 | NULL | 2023-01-01 16:55:46 | desktop | direct |
| 4 | NULL | 2023-01-01 12:43:28 | mobile | organic |
Mote revisa tu resultado
Presiona Ejecutar. Cada fila que cumple el objetivo se vuelve una luciérnaga en tu linterna.
Paso 2 · tu consulta
Cargando el editor…
Solución de referencia
| session_id | customer_id | started_at | device | source |
|---|---|---|---|---|
| 1 | NULL | 2023-01-01 23:02:24 | mobile | paid |
| 2 | 18 | 2023-01-01 15:07:49 | desktop | paid |
| 3 | NULL | 2023-01-01 16:55:46 | desktop | direct |
| 4 | NULL | 2023-01-01 12:43:28 | mobile | organic |
Mote revisa tu resultado
Presiona Ejecutar. Cada fila que cumple el objetivo se vuelve una luciérnaga en tu linterna.
Paso 3 · tu consulta
Cargando el editor…
Solución de referencia
| session_id | customer_id | started_at | device | source |
|---|---|---|---|---|
| 1 | NULL | 2023-01-01 23:02:24 | mobile | paid |
| 2 | 18 | 2023-01-01 15:07:49 | desktop | paid |
| 3 | NULL | 2023-01-01 16:55:46 | desktop | direct |
| 4 | NULL | 2023-01-01 12:43:28 | mobile | organic |
Mote revisa tu resultado
Presiona Ejecutar. Cada fila que cumple el objetivo se vuelve una luciérnaga en tu linterna.
Lo que recibe Jordan, y por qué
Tablas
Datos de la tienda Harbor & Pine, listos para consultar.
web_sessions40,000 filas · 7 columnas
session_idintegercustomer_idintegerstarted_attimestampdevicevarcharsourcevarcharpages_viewedintegerconvertedboolean
| session_id | customer_id | started_at | device | source | pages_viewed | converted |
|---|---|---|---|---|---|---|
| 1 | NULL | 2023-01-01 23:02:24 | mobile | paid | 1 | false |
| 2 | 18 | 2023-01-01 15:07:49 | desktop | paid | 1 | false |
| 3 | NULL | 2023-01-01 16:55:46 | desktop | direct | 7 | false |
| 4 | NULL | 2023-01-01 12:43:28 | mobile | organic | 3 | false |
| 5 | NULL | 2023-01-01 13:06:21 | mobile | social | 2 | false |
marketing_spend3,276 filas · 3 columnas
spend_datedatechannelvarcharamountdecimal
| spend_date | channel | amount |
|---|---|---|
| 2023-01-01 | paid | 492.99 |
| 2023-01-01 | 37.38 | |
| 2023-01-01 | social | 200.53 |
| 2023-01-02 | paid | 709.13 |
| 2023-01-02 | 64.13 |
customers2,018 filas · 9 columnas
customer_idintegerfirst_namevarcharlast_namevarcharemailvarcharcityvarcharstatevarcharsignup_datedatesegmentvarcharreferral_sourcevarchar
| customer_id | first_name | last_name | city | state | signup_date | segment | referral_source | |
|---|---|---|---|---|---|---|---|---|
| 1 | Owen | Hudson | owen.hudson@icloud.com | San Francisco | CA | 2022-06-02 | consumer | search |
| 2 | Douglas | Mendoza | dmendoza69@gmail.com | Atlanta | GA | 2022-06-02 | consumer | NULL |
| 3 | Henry | Brooks | hbrooks@lakesidecafe.com | Los Angeles | CA | 2022-06-04 | business | NULL |
| 4 | Brian | Graham | bgraham@bluecedarhomes.com | Seattle | WA | 2022-06-06 | business | |
| 5 | Kyle | Jimenez | kjimenez27@yahoo.com | Indianapolis | IN | 2022-06-07 | consumer | NULL |
products120 filas · 8 columnas
product_idintegernamevarcharcategoryvarcharsubcategoryvarcharunit_pricedecimalunit_costdecimallaunched_ondateis_activeboolean
| product_id | name | category | subcategory | unit_price | unit_cost | launched_on | is_active |
|---|---|---|---|---|---|---|---|
| 101 | Vale Velvet Loveseat | Furniture | Sofas | 1659.00 | NULL | 2023-09-11 | true |
| 102 | Alder Oak Coffee Table | Furniture | Tables | 919.00 | 503.50 | 2024-05-16 | true |
| 103 | Orchard Rattan Dining Chair | Furniture | Chairs | 789.00 | 429.87 | 2023-05-27 | true |
| 104 | Upland Oak Bookcase | Furniture | Storage | 429.00 | 214.70 | 2022-12-20 | true |
| 105 | Avery Leather Bed Frame | Furniture | Beds | 449.00 | 232.70 | 2021-08-31 | true |
orders12,000 filas · 7 columnas
order_idintegercustomer_idintegerorder_datetimestampstatusvarcharchannelvarcharshipping_feedecimalcoupon_codevarchar
| order_id | customer_id | order_date | status | channel | shipping_fee | coupon_code |
|---|---|---|---|---|---|---|
| 1001 | 125 | 2023-01-01 20:51:19 | completed | web | 0.00 | NULL |
| 1002 | 156 | 2023-01-02 07:58:46 | completed | web | 0.00 | NULL |
| 1003 | 22 | 2023-01-02 10:25:27 | completed | marketplace | 4.99 | WELCOME10 |
| 1004 | 48 | 2023-01-02 11:21:51 | completed | marketplace | 4.99 | WELCOME10 |
| 1005 | 82 | 2023-01-02 11:43:36 | completed | web | 0.00 | WELCOME10 |
order_items30,338 filas · 6 columnas
order_item_idintegerorder_idintegerproduct_idintegerquantityintegerunit_pricedecimaldiscount_pctdecimal
| order_item_id | order_id | product_id | quantity | unit_price | discount_pct |
|---|---|---|---|---|---|
| 1 | 1001 | 121 | 9 | 31.99 | 0.00 |
| 2 | 1001 | 154 | 3 | 11.99 | 0.00 |
| 3 | 1002 | 154 | 4 | 11.99 | 0.00 |
| 4 | 1002 | 194 | 1 | 149.00 | 0.00 |
| 5 | 1002 | 193 | 2 | 19.99 | 0.00 |
returns1,673 filas · 5 columnas
return_idintegerorder_item_idintegerreturned_ondatereasonvarcharrefund_amountdecimal
| return_id | order_item_id | returned_on | reason | refund_amount |
|---|---|---|---|---|
| 1 | 14 | 2023-01-10 | damaged | 83.69 |
| 2 | 42 | 2023-01-10 | wrong_item | 19.99 |
| 3 | 105 | 2023-01-14 | wrong_item | 10.99 |
| 4 | 24 | 2023-01-16 | changed_mind | 26.99 |
| 5 | 126 | 2023-01-16 | late | 20.99 |
employees60 filas · 8 columnas
employee_idintegerfirst_namevarcharlast_namevarchardepartmentvarchartitlevarcharmanager_idintegerhire_datedatesalaryinteger
| employee_id | first_name | last_name | department | title | manager_id | hire_date | salary |
|---|---|---|---|---|---|---|---|
| 1 | Catherine | Hale | Executive | Chief Executive Officer | NULL | 2016-03-01 | 285000 |
| 2 | Robert | Kim | Executive | Chief Operating Officer | 1 | 2017-01-09 | 235000 |
| 3 | Megan | Collins | Sales | Head of Sales | 1 | 2018-04-16 | 172000 |
| 4 | Chloe | Gutierrez | Sales | Account Executive | 3 | 2020-01-21 | 72000 |
| 5 | Frank | Bell | Sales | Account Executive | 3 | 2023-05-21 | 73000 |
La lección
Cómo lee un analista este ticket
Jordan necesita tres números por canal: cuántas visitas trajo, cuántas de esas convirtieron y cuánto costó. Dos tablas los contienen, y ninguna está al grano de la respuesta:
web_sessionstiene una fila por visita.sourcedice de dónde vino la visita yconverteddice si terminó en una compra.marketing_spendtiene una fila por día y por canal. Sus valores dechannel(paid,email,social) coinciden conweb_sessions.source, peroorganicydirectno tienen ningún gasto, porque nadie paga por ellos.
La respuesta es una fila por fuente (o por fuente y mes). Así que el plan es: agrega cada tabla a ese grano por su cuenta y después une los dos resultados pequeños.
Paso 1: tasa de conversión
Una tabla, un GROUP BY. El COUNT(*) FILTER (WHERE converted) de DuckDB cuenta solo las filas
convertidas; SUM(CASE WHEN converted THEN 1 ELSE 0 END) es la forma portable de escribirlo.
Multiplica por 100.0 antes de dividir para que la división sea decimal.
Ten cuidado con cómo describes el resultado. En este conjunto de datos las sesiones no están vinculadas a pedidos, así que esta es una tasa de conversión de sesiones tal como la reportó la herramienta de analítica web, no un conteo de pedidos.
Paso 2: la trampa de la duplicación y su solución
La consulta tentadora une las tablas en bruto:
SELECT w.source, SUM(m.amount) ...
FROM web_sessions AS w
JOIN marketing_spend AS m ON m.channel = w.source
Cada sesión de paid coincide con cada día de gasto de paid, así que el gasto se multiplica por el número de sesiones. Los números salen en miles de millones y nadie lo nota hasta que lo nota el director financiero. Incluso unir también por fecha repite el gasto de cada día una vez por cada sesión de ese día.
La solución es el patrón del capítulo de CTE: preagrega antes de unir. Una CTE reduce las
sesiones a una fila por fuente, otra reduce el gasto a una fila por canal, y entonces el JOIN es uno
a uno. Parte del lado de las sesiones con un LEFT JOIN para que organic y direct se queden en
la respuesta con gasto NULL. Dejarlas fuera ocultaría que aportan gratis una gran parte de las
conversiones.
NULLIF(a, b) devuelve NULL cuando a es igual a b, y a en caso contrario. Así,
NULLIF(c.conversions, 0) convierte un cero en NULL, y cualquier cosa dividida entre NULL es
NULL:
SELECT 5000 / NULLIF(25, 0) AS per_conversion, -- 200.0
5000 / NULLIF(0, 0) AS no_conversions; -- NULL, not an error
Un canal con gasto y sin conversiones da entonces un costo por conversión NULL. Postgres y la
mayoría de los almacenes de datos lanzan un error de división entre cero sin esto, así que escríbelo
de forma explícita.
Paso 3: mes a mes para paid
La misma receta, con month añadido al grano en ambos lados. strftime(spend_date, '%Y-%m') crea
la etiqueta de mes en texto ('2025-03'), exactamente como en el caso de las fiestas; constrúyela
igual en ambas CTE para que las claves del JOIN coincidan. Las dos CTE tienen una fila por mes, así
que aquí un JOIN interno es seguro. Las cifras mensuales de paid son ruidosas porque las
conversiones por mes son números pequeños; fíjate en la dirección, no en los decimales.
SQL y pandas lado a lado
| Idea | SQL | pandas |
|---|---|---|
| contar donde es verdadero | COUNT(*) FILTER (WHERE converted) |
("converted", "sum") |
| preagregar | una CTE con GROUP BY |
groupby(...).agg(...) en un nuevo DataFrame |
| conservar fuentes sin gasto | LEFT JOIN |
merge(how="left") |
| división segura | NULLIF(x, 0) |
dividir entre 0 da inf; reemplázalo por NaN |
Errores comunes
- Unir tablas en bruto con granos distintos. Pregúntate siempre «¿cuántas filas por clave tiene cada lado?» antes de un JOIN.
- Días de gasto faltantes. Faltan doce filas de día y canal en
marketing_spendpor caídas del seguimiento. Los totales quedan ligeramente bajos; dilo en el informe en lugar de inventar valores. - Filtrar el año solo en un lado. Ambas tablas necesitan el filtro de 2025, o el gasto de tres años se divide entre las conversiones de uno.
- Atribución. «Fuente de la sesión» es atribución de último contacto. Es el punto de partida habitual y la primera pregunta habitual de Finanzas; menciónalo.
¡Linterna encendida!
Practicaste: ctepre aggregateleft joinnullifconversion ratecost per conversion
- Resuelto
- Sin pistas
- Al primer intento
