Ejercicios de SQL resueltos para entrevistas: 17 preguntas
Practica SQL para entrevistas con 17 ejercicios resueltos de básico a avanzado sobre SELECT, JOIN, GROUP BY, ventanas y CTEs.
Qué evalúan en entrevistas y pruebas de SQL
En entrevistas para data analyst y en tests take-home, SQL no se mide solo por memorizar sintaxis: suelen revisar si entiendes cómo filtrar datos, combinar tablas sin duplicar filas, resumir información con agregaciones y detectar errores comunes como nulos, empates o resultados inflados por joins. También importa que puedas leer una tabla, interpretar el resultado esperado y elegir una consulta que realmente responda la pregunta de negocio.
Cómo está organizado este test
Este práctica test tiene 17 preguntas agrupadas por nivel: básico, intermedio y avanzado. En las primeras verás SELECT, WHERE, ORDER BY y agregaciones simples. Luego aparecen JOINs, GROUP BY con HAVING y manejo de NULL. En el nivel avanzado trabajas funciones de ventana, CTEs, deduplicación y casos donde un join puede multiplicar filas y cambiar el resultado.
Cómo usarlo
Primero intenta responder cada pregunta sin mirar la solución. Piensa qué devolvería la consulta o qué query produciría el resultado pedido. Después revisa la respuesta y la explicación para confirmar tu lógica o detectar en qué paso te equivocaste. Si fallas una, vuelve a leer la tabla y verifica el filtro, la clave de unión o el nivel de agrupación antes de pasar a la siguiente.
Básico · 6 preguntas
- 1.
En la tabla
sales, quieres ver solo las órdenes del mes de marzo de 2024 y ordenarlas de mayor a menor poramount.sales +---------+------------+--------+ | order_id| sale_date | amount | +---------+------------+--------+101 2024-03-02 1200 102 2024-02-28 800 103 2024-03-15 1500 104 2024-03-20 700 +---------+------------+--------+¿Cuál consulta devuelve ese resultado?
Ver respuesta
Respuesta correcta: SELECT order_id, sale_date, amount FROM sales WHERE sale_date BETWEEN '2024-03-01' AND '2024-03-31' ORDER BY amount DESC;
Aprende más sobre SQLLa correcta usa
WHEREpara filtrar marzo completo yORDER BY amount DESCpara dejar primero los importes más altos. La segunda opción sí filtra desde marzo, pero incluye también abril en adelante porque no pone límite superior. La tercera ordena al revés, de menor a mayor, así que no entrega el mismo resultado solicitado. Aquí se evalúa filtrado por fecha y ordenamiento básico en SQL. - 2.
Tienes estas tablas:
customers +-------------+--------+ | customer_id | name | +-------------+--------+1 Ana 2 Bruno 3 Carla +-------------+--------+ orders +----------+-------------+--------+ | order_id | customer_id | amount | +----------+-------------+--------+10 1 300 11 1 200 12 3 150 +----------+-------------+--------+Quieres listar todos los clientes con sus pedidos, incluyendo a quienes no tienen pedidos. ¿Qué consulta es la correcta?
Ver respuesta
Respuesta correcta: SELECT c.name, o.order_id, o.amount FROM customers c LEFT JOIN orders o ON c.customer_id = o.customer_id;
Aprende más sobre SQL JoinsLEFT JOINconserva todas las filas decustomersy trae las coincidencias deorders, por eso Bruno aparece aunque no tenga pedido.INNER JOINelimina clientes sin coincidencia, así que no cumple el requisito. La tercera opción invierte la tabla base: conserva todas las órdenes, no todos los clientes, y por eso puede omitir clientes sin pedidos. El punto clave es entender qué lado del JOIN se preserva. - 3.
En la tabla
sales, quieres ver las regiones cuya venta total supere 1000.sales +---------+--------+ | region | amount | +---------+--------+Norte 600 Norte 500 Sur 700 Este 300 Este 800 +---------+--------+¿Cuál consulta devuelve solo esas regiones?
Ver respuesta
Respuesta correcta: SELECT region, SUM(amount) AS total_amount FROM sales GROUP BY region HAVING SUM(amount) > 1000;
Aprende más sobre GROUP BY and HAVINGLa correcta agrupa por
regiony filtra el total conHAVING, que se aplica después deGROUP BY. La segunda opción usaWHERE amount > 1000, pero eso filtra filas individuales antes de sumar, así que elimina todos los registros de este ejemplo. La tercera cuenta filas en vez de sumar montos, por lo que responde otra pregunta distinta. Aquí se evalúa el uso correcto deGROUP BYyHAVING. - 4.
Tienes esta tabla
salesy quieres asignar un rango de ventas dentro de cada región, desde el monto más alto al más bajo.sales +---------+---------+ | region | amount | +---------+---------+Norte 500 Norte 900 Norte 700 Sur 400 Sur 1000 +---------+---------+¿Qué consulta hace eso?
Ver respuesta
Respuesta correcta: SELECT region, amount, RANK() OVER (PARTITION BY region ORDER BY amount DESC) AS rnk FROM sales;
Aprende más sobre SQL Window FunctionsLa correcta usa
RANK()conPARTITION BY regionpara reiniciar el ranking en cada región yORDER BY amount DESCpara que el monto mayor reciba el rango 1. La segunda opción mezcla todas las regiones en una sola clasificación, así que no separa los grupos. La tercera sí particiona, pero ordena ascendente y además usaROW_NUMBER(), que no maneja empates igual queRANK(). El concepto clave es la función de ventana. - 5.
En la tabla
products, quieres obtener solo los nombres cuyo precio sea mayor o igual a 50, ordenados de menor a mayor precio.products +------------+-------+ | product | price | +------------+-------+Cuaderno 35 Mochila 80 Lámpara 50 Lápiz 10 +------------+-------+¿Qué consulta devuelve ese resultado?
Ver respuesta
Respuesta correcta: SELECT product, price FROM products WHERE price >= 50 ORDER BY price ASC;
Aprende más sobre SQLLa correcta filtra con
WHERE price >= 50y luego ordena conORDER BY price ASC, por lo que incluye a Lámpara y Mochila. La segunda opción excluye el precio exacto de 50, así que pierde una fila válida. La tercera es sintácticamente incorrecta porqueWHEREdebe ir antes deORDER BY. Es un ejercicio básico de filtro y ordenamiento que suele aparecer en entrevistas. - 6.
Tienes la tabla
salesy quieres saber cuántos pedidos hizo cada cliente, pero solo mostrar los clientes con más de 2 pedidos.sales +---------+-------------+ | sale_id | customer_id | +---------+-------------+1 10 2 10 3 10 4 11 5 11 6 12 +---------+-------------+¿Qué consulta devuelve ese resultado?
Ver respuesta
Respuesta correcta: SELECT customer_id, COUNT(*) AS total_orders FROM sales GROUP BY customer_id HAVING COUNT(*) > 2;
Aprende más sobre GROUP BY and HAVINGLa correcta usa
COUNT(*)para contar filas por cliente yHAVINGpara filtrar los grupos con más de 2 pedidos. La segunda opción es inválida porqueCOUNT(*) > 2no se usa enWHERE; los agregados se filtran conHAVING. La tercera mezclaSUM(*), que no existe, conCOUNT(*), así que no produce el resultado esperado. Aquí se practica conteo por grupo, un caso típico de entrevista.
Intermedio · 5 preguntas
- 7.
Tienes esta tabla y quieres numerar las ventas de cada cliente de la más reciente a la más antigua, sin mezclar clientes entre sí. ¿Qué consulta hace eso?
sales +----------+-------------+--------+------------+ | sale_id | customer_id | amount | sale_date | +----------+-------------+--------+------------+10 1 80 2024-03-01 11 1 120 2024-03-05 12 2 90 2024-03-02 13 2 60 2024-03-07 14 1 200 2024-03-09 +----------+-------------+--------+------------+Ver respuesta
Respuesta correcta: SELECT sale_id, customer_id, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY sale_date DESC) AS rn FROM sales;
Aprende más sobre SQL Window FunctionsROW_NUMBER() con PARTITION BY customer_id reinicia la numeración para cada cliente, y ORDER BY sale_date DESC pone primero la venta más reciente. La versión sin PARTITION BY mezcla todos los clientes en una sola secuencia. RANK() podría servir para empates, pero aquí no asigna números consecutivos iguales de la misma forma que se pide. Particionar por sale_id no tiene sentido porque cada fila queda en su propio grupo.
- 8.
Tienes esta tabla
salesy quieres calcular el promedio móvil de 3 filas por producto, ordenado por fecha. ¿Qué consulta lo hace correctamente?sales +----------+------------+--------+------------+ | sale_id | product_id | amount | sale_date | +----------+------------+--------+------------+1 A1 10 2024-04-01 2 A1 20 2024-04-03 3 A1 40 2024-04-05 4 A1 30 2024-04-08 5 B2 50 2024-04-02 +----------+------------+--------+------------+Ver respuesta
Respuesta correcta: SELECT sale_id, product_id, AVG(amount) OVER (PARTITION BY product_id ORDER BY sale_date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS avg_3_rows FROM sales;
Aprende más sobre SQL Window FunctionsLa ventana correcta usa AVG() con PARTITION BY product_id para separar productos y una frame clause de ROWS BETWEEN 2 PRECEDING AND CURRENT ROW para limitar el cálculo a las tres filas más recientes. La versión sin frame calcula un promedio acumulado, no de 3 filas. Sin PARTITION BY mezcla productos distintos. SUM() devuelve un total móvil, no un promedio móvil, aunque la estructura de la ventana sea similar.
- 9.
Tienes estas tablas:
customers +-------------+--------+ | customer_id | name | +-------------+--------+1 Ana 2 Bruno 3 Carla +-------------+--------+ orders +----------+-------------+--------+ | order_id | customer_id | amount | +----------+-------------+--------+10 1 120 11 1 80 12 2 200 +----------+-------------+--------+Quieres listar todos los clientes y sumar sus pedidos, pero también mostrar a los que no tienen pedidos con total 0. ¿Qué consulta corresponde?
Ver respuesta
Respuesta correcta: SELECT c.name, COALESCE(SUM(o.amount), 0) AS total FROM customers c LEFT JOIN orders o ON c.customer_id = o.customer_id GROUP BY c.name;
Aprende más sobre SQL JoinsLa correcta combina
LEFT JOIN,GROUP BYyCOALESCEpara convertir elNULLdel cliente sin pedidos en 0 después de sumar.INNER JOINelimina a Carla por completo. La opción conHAVING total = 0filtraría solo grupos sin ventas, no devuelve los totales de todos. YIFNULL(o.amount, 0)no reemplaza la suma agregada; solo actúa fila por fila, así que no resuelve el total por cliente. - 10.
En
sales, quieres calcular el total acumulado deamountporregion, ordenado porsale_datedentro de cada región.sales +---------+------------+--------+ | region | sale_date | amount | +---------+------------+--------+Norte 2024-06-01 100 Norte 2024-06-03 250 Norte 2024-06-05 50 Sur 2024-06-02 200 Sur 2024-06-04 100 +---------+------------+--------+¿Qué consulta lo hace correctamente?
Ver respuesta
Respuesta correcta: SELECT region, sale_date, amount, SUM(amount) OVER (PARTITION BY region ORDER BY sale_date) AS running_total FROM sales;
Aprende más sobre SQL Window FunctionsLa correcta usa
SUM(amount) OVER (...)conPARTITION BY regionyORDER BY sale_date, lo que reinicia el acumulado por región y respeta el orden temporal. Sin la partición, el acumulado mezcla todas las regiones en una sola secuencia. La versión conGROUP BYno produce un acumulado fila a fila, sino totales por grupo. Y particionar por fecha no cumple el requisito, porque separa por día en lugar de por región. - 11.
Tienes estas tablas:
customers +-------------+--------+ | customer_id | name | +-------------+--------+1 Ana 2 Bruno 3 Carla +-------------+--------+ orders +----------+-------------+--------+ | order_id | customer_id | amount | +----------+-------------+--------+10 1 100 11 1 50 12 2 80 +----------+-------------+--------+Quieres obtener el total comprado por cliente, pero solo mostrar los clientes cuyo total sea mayor a 100. ¿Qué consulta es la correcta?
Ver respuesta
Respuesta correcta: SELECT c.name, SUM(o.amount) AS total_spent FROM customers c JOIN orders o ON c.customer_id = o.customer_id GROUP BY c.name HAVING SUM(o.amount) > 100;
Aprende más sobre GROUP BY and HAVINGAquí se necesita
GROUP BYpara sumar por cliente yHAVINGpara filtrar después de la agregación.WHERE SUM(o.amount) > 100es inválido porqueWHEREno puede usar un agregado.COUNT(o.amount)cuenta pedidos, no dinero, yo.amountsin sumar devolvería cada fila individual, no el total del cliente.
Avanzado · 6 preguntas
- 12.
En esta base quieres contar cuántos pedidos distintos tiene cada cliente, pero debes ignorar pedidos duplicados que llegaron repetidos en la tabla. ¿Qué consulta es la adecuada?
orders +----------+-------------+ | order_id | customer_id | +----------+-------------+1001 1 1001 1 1002 1 1003 2 1004 2 1004 2 +----------+-------------+Ver respuesta
Respuesta correcta: WITH pedidos_unicos AS (SELECT DISTINCT order_id, customer_id FROM orders) SELECT customer_id, COUNT(*) AS pedidos FROM pedidos_unicos GROUP BY customer_id;
Aprende más sobre Common Table ExpressionsLa correcta usa una CTE para deduplicar primero las combinaciones
order_id, customer_idy luego cuenta las filas únicas por cliente. La opción sinDISTINCTsigue contando duplicados.COUNT(DISTINCT customer_id)devuelve 1 para cada cliente, no el número de pedidos.HAVING COUNT(*) > 1solo filtra grupos y no corrige la duplicación. - 13.
En la tabla
orders, quieres detectar clientes que hicieron más de un pedido en el mismo día, pero sin contar filas duplicadas idénticas. ¿Qué consulta devuelve el número de pares distintoscustomer_id+order_dateque aparecen más de una vez?orders +------------+-------------+------------+--------+ | order_id | customer_id | order_date | status | +------------+-------------+------------+--------+101 1 2024-05-01 paid 102 1 2024-05-01 paid 103 1 2024-05-02 paid 104 2 2024-05-01 paid 105 2 2024-05-01 paid 106 2 2024-05-03 paid 107 3 2024-05-01 paid +------------+-------------+------------+--------+Ver respuesta
Respuesta correcta: SELECT COUNT(*) FROM (SELECT customer_id, order_date FROM orders GROUP BY customer_id, order_date HAVING COUNT(*) > 1) x;
Aprende más sobre SQLLa correcta agrupa por
customer_idyorder_date, filtra conHAVING COUNT(*) > 1y luego cuenta esos pares en la subconsulta. La opción tentadora conGROUP BY ... HAVINGdevuelve una fila por grupo, no el total de pares repetidos. Además,COUNT(DISTINCT customer_id, order_date)cuenta combinaciones únicas, que es lo contrario de buscar duplicados. Aquí el detalle clave es separar la detección de duplicados del conteo final. - 14.
En
orderstienes una columnapromo_code, pero algunos pedidos tienen códigos vacíos oNULL. Quieres saber qué porcentaje de pedidos por canal usó un código promocional válido, sin contar vacíos como uso de promoción.orders +----------+--------+------------+------------+ | order_id | channel | promo_code | amount | +----------+--------+------------+------------+1 web SAVE10 120 2 web NULL 80 3 app 95 4 app APP20 140 5 web SAVE10 60 6 app NULL 50 +----------+--------+------------+------------+¿Cuál consulta calcula correctamente ese porcentaje por canal?
Ver respuesta
Respuesta correcta: SELECT channel, 100.0 * SUM(CASE WHEN promo_code IS NOT NULL AND promo_code <> '' THEN 1 ELSE 0 END) / COUNT(*) AS pct_with_promo FROM orders GROUP BY channel;
Aprende más sobre GROUP BY and HAVINGLa correcta cuenta solo códigos no nulos y no vacíos con un
CASE WHEN, luego divide entre el total de pedidos por canal.COUNT(promo_code)ignoraNULL, pero no excluye cadenas vacías, así que daría un porcentaje inflado enapp.SUM(promo_code)es inválido porquepromo_codees texto. La última opción invierte el denominador y cambia por completo la métrica. Esta pregunta mide manejo deNULLy blancos en agregaciones. - 15.
Tienes estas tablas:
employees +------------+-----------+-----------+ | emp_id | dept_id | salary | +------------+-----------+-----------+1 10 3000 2 10 4500 3 20 5000 4 30 2800 +------------+-----------+-----------+ departments +---------+-----------+ | dept_id | dept_name | +---------+-----------+10 Ventas 20 Finanzas 30 Soporte +---------+-----------+Quieres mostrar cada departamento con el promedio de salario, pero solo los que tengan promedio mayor a 3500. ¿Qué consulta es correcta?
Ver respuesta
Respuesta correcta: SELECT d.dept_name, AVG(e.salary) AS avg_salary FROM departments d JOIN employees e ON e.dept_id = d.dept_id GROUP BY d.dept_name HAVING AVG(e.salary) > 3500;
Aprende más sobre GROUP BY and HAVINGLa consulta correcta agrupa por departamento y usa HAVING para aplicar el filtro sobre AVG(e.salary), que es un agregado. WHERE no puede evaluar un promedio calculado por grupo, por eso la segunda opción es inválida. Agrupar por e.salary fragmenta los datos por sueldo y rompe la lógica del reporte. Filtrar por e.salary en WHERE cambia la base antes del promedio y da otro resultado.
- 16.
Te muestran esta tabla:
sales +---------+------------+--------+ | sale_id | sale_date | amount | +---------+------------+--------+1 2024-01-10 100 2 2024-01-15 50 3 2024-02-02 200 4 2024-02-20 80 +---------+------------+--------+Quieres calcular el total acumulado por orden de fecha, pero reiniciando el acumulado para cada mes. ¿Qué consulta lo hace?
Ver respuesta
Respuesta correcta: SELECT sale_id, sale_date, amount, SUM(amount) OVER (PARTITION BY DATE_FORMAT(sale_date, '%Y-%m') ORDER BY sale_date) AS running_total FROM sales;
Aprende más sobre SQL Window FunctionsLa correcta usa una ventana con PARTITION BY por mes y ORDER BY por fecha para reiniciar el acumulado en cada periodo. La opción sin partición acumula toda la serie completa y no reinicia en febrero. Particionar por sale_date hace que casi cada fila quede sola, salvo fechas repetidas. La cláusula ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING calcula una suma hacia adelante, no un acumulado histórico.
- 17.
Una tienda registra pagos en esta tabla:
payments +------------+-----------+--------+ | payment_id | order_id | amount | +------------+-----------+--------+1 10 120 2 10 80 3 11 50 4 12 200 +------------+-----------+--------+Quieres obtener el total por pedido usando una CTE para dejar clara la lógica, y luego quedarte solo con pedidos cuyo total sea mayor a 100. ¿Qué consulta es correcta?
Ver respuesta
Respuesta correcta: WITH order_totals AS (SELECT order_id, SUM(amount) AS total_amount FROM payments GROUP BY order_id) SELECT order_id, total_amount FROM order_totals WHERE total_amount > 100;
Aprende más sobre Common Table ExpressionsLa CTE primero calcula el total por order_id y la consulta externa filtra después, lo cual separa la agregación del filtro y hace el proceso fácil de leer. La opción con WHERE total_amount dentro de la CTE es inválida porque ese alias todavía no existe en esa fase. Agrupar por amount rompe la lógica del pedido. HAVING fuera de un GROUP BY en la consulta final no corresponde a este patrón.
Errores que más se repiten
El fallo más común en estos ejercicios es confundir el resultado de una consulta con lo que “parece” correcto a simple vista. En SQL, un filtro mal puesto, un JOIN con la clave equivocada o un GROUP BY incompleto puede cambiar por completo la respuesta. También es muy frecuente olvidar cómo se tratan los NULL, usar COUNT(*) cuando se necesita COUNT de una columna, o asumir que un join uno a muchos devuelve una sola fila por registro.
Cómo prepararte en los días previos
Repasa SELECT, WHERE, ORDER BY y agregaciones hasta poder leer una consulta sin detenerte. Luego practica JOINs con dos y tres tablas, porque ahí aparecen la mayoría de los errores de entrevista. Después enfócate en GROUP BY y HAVING, y asegúrate de entender cuándo filtrar antes o después de agrupar. Si ya dominas eso, dedica tiempo a window functions y CTEs, que suelen aparecer en pruebas de mayor nivel y ayudan a resolver problemas de ranking, acumulados y deduplicación.
Antes de responder, acostúmbrate a leer siempre las columnas clave, contar filas y pensar si un join puede duplicar resultados. Esa disciplina te ahorra muchos errores en entrevistas y en pruebas escritas.
¿Quieres más preguntas como estas?
Practica preguntas con tiempo a tu nivel, mide tu puntaje por habilidad y encuentra lo que te falta antes de la entrevista. Gratis.
Practicar más preguntas