Nivel 1 · Los 3 mejores productos de cada categoría
¿Es nuevo para ti? Lee primero el concepto: Las ventanas miran otras filas sin colapsarlas
0 de 3
La solicitud

De Jessica Turner, Gerente de Merchandising
Tu objetivo
Usando pedidos completados y líneas con quantity > 0, calcula los ingresos netos por producto (suma de quantity * unit_price * (1 - discount_pct / 100.0)). Devuelve los 3 mejores productos por categoría con: category, product_id, name, net_revenue (redondeado a 2) y category_rank (de 1 a 3, por net_revenue descendente; desempata por product_id ascendente).
- Lee de la tabla
productspendiente - Lee de la tabla
orderspendiente - Muestra exactamente
categoryproduct_idnamenet_revenuecategory_rankpendiente
Tu consulta
Cargando el editor…
Solución de referencia
| product_id | name | category | subcategory | unit_price |
|---|---|---|---|---|
| 101 | Vale Velvet Loveseat | Furniture | Sofas | 1659.00 |
| 102 | Alder Oak Coffee Table | Furniture | Tables | 919.00 |
| 103 | Orchard Rattan Dining Chair | Furniture | Chairs | 789.00 |
| 104 | Upland Oak Bookcase | Furniture | Storage | 429.00 |
Mote revisa tu resultado
Presiona Ejecutar. Cada fila que cumple el objetivo se vuelve una luciérnaga en tu linterna.
Lo que recibe Jessica, y por qué
Tablas
Datos de la tienda Harbor & Pine, listos para consultar.
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 |
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 |
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 |
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 |
La lección
Una función de ventana calcula un valor para cada fila usando un conjunto de filas relacionadas, su
“ventana”, sin colapsarlas. La ventana se define con OVER (...):
ROW_NUMBER() OVER (PARTITION BY category ORDER BY net_revenue DESC)
PARTITION BY categorydivide las filas en grupos, como GROUP BY, pero conserva todas las filas.ORDER BY net_revenue DESCordena las filas dentro de cada partición.ROW_NUMBER()las numera 1, 2, 3, … y vuelve a empezar en 1 en cada partición.
Así cada producto obtiene su posición dentro de su propia categoría, y cada fila de producto sigue ahí.
Top N por grupo
“Los 3 mejores por categoría” es el uso clásico. Con LIMIT solo puedes tomar los 3 mejores en total. Con ROW_NUMBER numeras dentro de cada categoría y luego conservas los números del 1 al 3:
WITH ranked AS (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY category
ORDER BY net_revenue DESC, product_id) AS category_rank
FROM product_revenue
)
SELECT *
FROM ranked
WHERE category_rank <= 3;
Por qué la CTE extra
Las funciones de ventana se evalúan en el paso SELECT, después de WHERE, GROUP BY y HAVING (el orden
lógico del capítulo 4). Por eso WHERE ROW_NUMBER() OVER (...) <= 3 no está permitido: el número todavía
no existe cuando se ejecuta el WHERE. Calcúlalo en una CTE o subconsulta y luego filtra en la consulta
externa. Esto funciona en cualquier base de datos.
DuckDB, BigQuery y Snowflake también tienen QUALIFY, un filtro que se ejecuta después de las funciones de ventana:
SELECT category, product_id, net_revenue
FROM product_revenue
QUALIFY ROW_NUMBER() OVER (PARTITION BY category ORDER BY net_revenue DESC, product_id) <= 3;
Postgres y MySQL no admiten QUALIFY, así que conoce la versión con CTE para las entrevistas.
Funciones de ventana sobre GROUP BY
Puedes aplicar una función de ventana a resultados agrupados en el mismo SELECT, porque los agregados se
calculan antes que las ventanas. RANK() OVER (ORDER BY SUM(revenue) DESC) es válido. Separarlo en una
CTE, como aquí, suele ser más fácil de leer y depurar.
Haz el orden único
ROW_NUMBER siempre da números distintos, incluso en los empates. Si dos productos tienen ingresos
idénticos, cuál es el 3.º y cuál el 4.º es arbitrario y podría cambiar entre ejecuciones, a menos que
añadas un criterio de desempate (product_id). Para “top N incluyendo empates”, usa RANK, que vemos a
continuación.
Nota de entrevista
“Devuelve los 2 empleados mejor pagados de cada departamento” es quizá la pregunta de entrevista de SQL más frecuente. La respuesta esperada es exactamente este patrón: una función de ventana particionada por el grupo, ordenada por la medida, en una CTE, filtrada por fuera. Menciona cómo manejas los empates.
Errores comunes
- Filtrar por la función de ventana en el WHERE.
WHERE ROW_NUMBER() OVER (...) <= 3no está permitido. Calcúlala en una CTE y luego filtra conWHERE category_rank <= 3por fuera. - Usar LIMIT.
ORDER BY net_revenue DESC LIMIT 3da los 3 mejores productos en total, no 3 por categoría. - Olvidar
PARTITION BY category. La numeración corre entonces por todos los productos, así que<= 3conserva solo tres filas en total. - Omitir el criterio de desempate. Dos productos con los mismos ingresos pueden intercambiar el 3.º y el
4.º puesto entre ejecuciones. Añade
product_idal ORDER BY de la ventana.
En SQL Server
No hay QUALIFY, así que la CTE con WHERE rn <= 3 es el camino. SELECT TOP 3 tiene la misma limitación
que LIMIT 3: toma los 3 mejores en total, no por categoría.
¡Linterna encendida!
Practicaste: window functionsrow numberpartition bytop n per groupcte
- Resuelto
- Sin pistas
- Al primer intento
