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. 1.

    En la tabla sales, quieres ver solo las órdenes del mes de marzo de 2024 y ordenarlas de mayor a menor por amount.

    sales
    +---------+------------+--------+
    | order_id| sale_date  | amount |
    +---------+------------+--------+
    1012024-03-021200
    1022024-02-28800
    1032024-03-151500
    1042024-03-20700
    +---------+------------+--------+

    ¿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;

    La correcta usa WHERE para filtrar marzo completo y ORDER BY amount DESC para 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.

    Aprende más sobre SQL
  2. 2.

    Tienes estas tablas:

    customers
    +-------------+--------+
    | customer_id | name   |
    +-------------+--------+
    1Ana
    2Bruno
    3Carla
    +-------------+--------+
    
    orders
    +----------+-------------+--------+
    | order_id | customer_id  | amount |
    +----------+-------------+--------+
    101300
    111200
    123150
    +----------+-------------+--------+

    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;

    LEFT JOIN conserva todas las filas de customers y trae las coincidencias de orders, por eso Bruno aparece aunque no tenga pedido. INNER JOIN elimina 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.

    Aprende más sobre SQL Joins
  3. 3.

    En la tabla sales, quieres ver las regiones cuya venta total supere 1000.

    sales
    +---------+--------+
    | region  | amount |
    +---------+--------+
    Norte600
    Norte500
    Sur700
    Este300
    Este800
    +---------+--------+

    ¿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;

    La correcta agrupa por region y filtra el total con HAVING, que se aplica después de GROUP BY. La segunda opción usa WHERE 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 de GROUP BY y HAVING.

    Aprende más sobre GROUP BY and HAVING
  4. 4.

    Tienes esta tabla sales y quieres asignar un rango de ventas dentro de cada región, desde el monto más alto al más bajo.

    sales
    +---------+---------+
    | region  | amount  |
    +---------+---------+
    Norte500
    Norte900
    Norte700
    Sur400
    Sur1000
    +---------+---------+

    ¿Qué consulta hace eso?

    Ver respuesta

    Respuesta correcta: SELECT region, amount, RANK() OVER (PARTITION BY region ORDER BY amount DESC) AS rnk FROM sales;

    La correcta usa RANK() con PARTITION BY region para reiniciar el ranking en cada región y ORDER BY amount DESC para 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 usa ROW_NUMBER(), que no maneja empates igual que RANK(). El concepto clave es la función de ventana.

    Aprende más sobre SQL Window Functions
  5. 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 |
    +------------+-------+
    Cuaderno35
    Mochila80
    Lámpara50
    Lápiz10
    +------------+-------+

    ¿Qué consulta devuelve ese resultado?

    Ver respuesta

    Respuesta correcta: SELECT product, price FROM products WHERE price >= 50 ORDER BY price ASC;

    La correcta filtra con WHERE price >= 50 y luego ordena con ORDER 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 porque WHERE debe ir antes de ORDER BY. Es un ejercicio básico de filtro y ordenamiento que suele aparecer en entrevistas.

    Aprende más sobre SQL
  6. 6.

    Tienes la tabla sales y quieres saber cuántos pedidos hizo cada cliente, pero solo mostrar los clientes con más de 2 pedidos.

    sales
    +---------+-------------+
    | sale_id | customer_id |
    +---------+-------------+
    110
    210
    310
    411
    511
    612
    +---------+-------------+

    ¿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;

    La correcta usa COUNT(*) para contar filas por cliente y HAVING para filtrar los grupos con más de 2 pedidos. La segunda opción es inválida porque COUNT(*) > 2 no se usa en WHERE; los agregados se filtran con HAVING. La tercera mezcla SUM(*), que no existe, con COUNT(*), así que no produce el resultado esperado. Aquí se practica conteo por grupo, un caso típico de entrevista.

    Aprende más sobre GROUP BY and HAVING

Intermedio · 5 preguntas

  1. 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  |
    +----------+-------------+--------+------------+
    101802024-03-01
    1111202024-03-05
    122902024-03-02
    132602024-03-07
    1412002024-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;

    ROW_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.

    Aprende más sobre SQL Window Functions
  2. 8.

    Tienes esta tabla sales y 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  |
    +----------+------------+--------+------------+
    1A1102024-04-01
    2A1202024-04-03
    3A1402024-04-05
    4A1302024-04-08
    5B2502024-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;

    La 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.

    Aprende más sobre SQL Window Functions
  3. 9.

    Tienes estas tablas:

    customers
    +-------------+--------+
    | customer_id | name   |
    +-------------+--------+
    1Ana
    2Bruno
    3Carla
    +-------------+--------+
    
    orders
    +----------+-------------+--------+
    | order_id | customer_id | amount |
    +----------+-------------+--------+
    101120
    11180
    122200
    +----------+-------------+--------+

    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;

    La correcta combina LEFT JOIN, GROUP BY y COALESCE para convertir el NULL del cliente sin pedidos en 0 después de sumar. INNER JOIN elimina a Carla por completo. La opción con HAVING total = 0 filtraría solo grupos sin ventas, no devuelve los totales de todos. Y IFNULL(o.amount, 0) no reemplaza la suma agregada; solo actúa fila por fila, así que no resuelve el total por cliente.

    Aprende más sobre SQL Joins
  4. 10.

    En sales, quieres calcular el total acumulado de amount por region, ordenado por sale_date dentro de cada región.

    sales
    +---------+------------+--------+
    | region  | sale_date  | amount |
    +---------+------------+--------+
    Norte2024-06-01100
    Norte2024-06-03250
    Norte2024-06-0550
    Sur2024-06-02200
    Sur2024-06-04100
    +---------+------------+--------+

    ¿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;

    La correcta usa SUM(amount) OVER (...) con PARTITION BY region y ORDER 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 con GROUP BY no 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.

    Aprende más sobre SQL Window Functions
  5. 11.

    Tienes estas tablas:

    customers
    +-------------+--------+
    | customer_id | name   |
    +-------------+--------+
    1Ana
    2Bruno
    3Carla
    +-------------+--------+
    
    orders
    +----------+-------------+--------+
    | order_id | customer_id | amount |
    +----------+-------------+--------+
    101100
    11150
    12280
    +----------+-------------+--------+

    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;

    Aquí se necesita GROUP BY para sumar por cliente y HAVING para filtrar después de la agregación. WHERE SUM(o.amount) > 100 es inválido porque WHERE no puede usar un agregado. COUNT(o.amount) cuenta pedidos, no dinero, y o.amount sin sumar devolvería cada fila individual, no el total del cliente.

    Aprende más sobre GROUP BY and HAVING

Avanzado · 6 preguntas

  1. 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 |
    +----------+-------------+
    10011
    10011
    10021
    10032
    10042
    10042
    +----------+-------------+
    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;

    La correcta usa una CTE para deduplicar primero las combinaciones order_id, customer_id y luego cuenta las filas únicas por cliente. La opción sin DISTINCT sigue contando duplicados. COUNT(DISTINCT customer_id) devuelve 1 para cada cliente, no el número de pedidos. HAVING COUNT(*) > 1 solo filtra grupos y no corrige la duplicación.

    Aprende más sobre Common Table Expressions
  2. 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 distintos customer_id + order_date que aparecen más de una vez?

    orders
    +------------+-------------+------------+--------+
    | order_id   | customer_id | order_date | status |
    +------------+-------------+------------+--------+
    10112024-05-01paid
    10212024-05-01paid
    10312024-05-02paid
    10422024-05-01paid
    10522024-05-01paid
    10622024-05-03paid
    10732024-05-01paid
    +------------+-------------+------------+--------+
    Ver respuesta

    Respuesta correcta: SELECT COUNT(*) FROM (SELECT customer_id, order_date FROM orders GROUP BY customer_id, order_date HAVING COUNT(*) > 1) x;

    La correcta agrupa por customer_id y order_date, filtra con HAVING COUNT(*) > 1 y luego cuenta esos pares en la subconsulta. La opción tentadora con GROUP BY ... HAVING devuelve 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.

    Aprende más sobre SQL
  3. 14.

    En orders tienes una columna promo_code, pero algunos pedidos tienen códigos vacíos o NULL. 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     |
    +----------+--------+------------+------------+
    1webSAVE10120
    2webNULL80
    3app95
    4appAPP20140
    5webSAVE1060
    6appNULL50
    +----------+--------+------------+------------+

    ¿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;

    La 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) ignora NULL, pero no excluye cadenas vacías, así que daría un porcentaje inflado en app. SUM(promo_code) es inválido porque promo_code es texto. La última opción invierte el denominador y cambia por completo la métrica. Esta pregunta mide manejo de NULL y blancos en agregaciones.

    Aprende más sobre GROUP BY and HAVING
  4. 15.

    Tienes estas tablas:

    employees
    +------------+-----------+-----------+
    | emp_id     | dept_id   | salary    |
    +------------+-----------+-----------+
    1103000
    2104500
    3205000
    4302800
    +------------+-----------+-----------+
    
    departments
    +---------+-----------+
    | dept_id | dept_name |
    +---------+-----------+
    10Ventas
    20Finanzas
    30Soporte
    +---------+-----------+

    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;

    La 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.

    Aprende más sobre GROUP BY and HAVING
  5. 16.

    Te muestran esta tabla:

    sales
    +---------+------------+--------+
    | sale_id | sale_date  | amount |
    +---------+------------+--------+
    12024-01-10100
    22024-01-1550
    32024-02-02200
    42024-02-2080
    +---------+------------+--------+

    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;

    La 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.

    Aprende más sobre SQL Window Functions
  6. 17.

    Una tienda registra pagos en esta tabla:

    payments
    +------------+-----------+--------+
    | payment_id | order_id  | amount |
    +------------+-----------+--------+
    110120
    21080
    31150
    412200
    +------------+-----------+--------+

    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;

    La 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.

    Aprende más sobre Common Table Expressions
17 preguntas

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