InterviewHack.ai
Start free
Blog/Preguntas de entrevista de SQL con ejercicios y respuestas (45+)

Preguntas de entrevista de SQL con ejercicios y respuestas (45+)

16 de septiembre de 2026

sqlbase-de-datos

Artículo completo de preguntas de entrevista SQL con 47 preguntas numeradas, respuestas detalladas y código real

Preguntas de entrevista de SQL con ejercicios y respuestas (45+)

Si estás preparando una entrevista técnica y saben que te van a evaluar SQL, este artículo es para vos. Acá encontrás más de 45 preguntas reales — las que aparecen en entrevistas de analista de datos, data engineer, backend y BI — con respuestas detalladas, código que podés correr y explicaciones de por qué cada solución funciona.

No hay preguntas de relleno. Cada una tiene un nivel de dificultad, el contexto donde aparece y código SQL real.


Cómo usar este recurso

Las preguntas están organizadas en cuatro bloques:

  1. 1Fundamentos (preguntas 1-12) — las bases que todo el mundo da por sentado que sabés
  2. 2Consultas intermedias (preguntas 13-25) — JOINs, agregaciones, subconsultas
  3. 3SQL avanzado (preguntas 26-38) — window functions, CTEs, optimización
  4. 4Diseño y escenarios reales (preguntas 39-47) — modelado, performance, casos prácticos

Bloque 1: Fundamentos

1. ¿Qué diferencia hay entre WHERE y HAVING?

Nivel: Junior | Aparece en: casi toda entrevista técnica

WHERE filtra filas antes de que se aplique el GROUP BY. HAVING filtra los grupos resultantes después de la agregación.

sql
-- WHERE filtra filas individuales antes de agrupar
SELECT department, COUNT(*) AS total
FROM employees
WHERE salary > 50000
GROUP BY department;

-- HAVING filtra el resultado del GROUP BY
SELECT department, COUNT(*) AS total
FROM employees
GROUP BY department
HAVING COUNT(*) > 10;

Por qué importa en la entrevista: muchos candidatos confunden los dos. La regla es simple: si tu condición usa una función de agregación (SUM, COUNT, AVG), necesitás HAVING. Si no, usá WHERE.


2. ¿Qué es un índice y cuándo conviene crearlo?

Nivel: Junior-Medio | Aparece en: backend, data engineering

Un índice es una estructura auxiliar que acelera las búsquedas a costa de espacio en disco y mayor tiempo de escritura.

sql
-- Crear un índice simple
CREATE INDEX idx_employees_email ON employees(email);

-- Índice compuesto (el orden importa)
CREATE INDEX idx_orders_user_date ON orders(user_id, created_at);

Conviene crear índices cuando:

  • La columna aparece frecuentemente en cláusulas WHERE, JOIN ON, ORDER BY
  • La tabla tiene muchas filas y la selectividad es alta (pocos valores repetidos)

No conviene cuando:

  • La tabla se escribe muy seguido (los inserts y updates son más lentos)
  • La columna tiene baja cardinalidad (ej: una columna status con 3 valores posibles)

3. Explicá los tipos de JOIN con un ejemplo

Nivel: Junior | Aparece en: toda entrevista SQL

sql
-- Datos de ejemplo
-- tabla users: id, name
-- tabla orders: id, user_id, amount

-- INNER JOIN: solo los usuarios que tienen pedidos
SELECT u.name, o.amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id;

-- LEFT JOIN: todos los usuarios, tengan pedido o no
SELECT u.name, o.amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;
-- Los usuarios sin pedidos aparecen con NULL en o.amount

-- RIGHT JOIN: todos los pedidos, aunque el usuario no exista
SELECT u.name, o.amount
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id;

-- FULL OUTER JOIN: todos los registros de ambas tablas
SELECT u.name, o.amount
FROM users u
FULL OUTER JOIN orders o ON u.id = o.user_id;

Tip de entrevista: si te preguntan "¿cuándo usarías un LEFT JOIN?", la respuesta es cuando querés mantener todos los registros de la tabla izquierda aunque no haya coincidencia. Ejemplo clásico: "todos los usuarios y, si tienen pedidos, mostrarlos."


4. ¿Qué es NULL en SQL y cómo se maneja?

Nivel: Junior | Aparece en: analítica, QA de datos

NULL no es cero ni cadena vacía. Es la ausencia de valor. Toda comparación con NULL devuelve NULL (no TRUE ni FALSE).

sql
-- INCORRECTO: esto nunca devuelve filas
SELECT * FROM users WHERE phone = NULL;

-- CORRECTO
SELECT * FROM users WHERE phone IS NULL;
SELECT * FROM users WHERE phone IS NOT NULL;

-- COALESCE: devuelve el primer valor no-NULL
SELECT name, COALESCE(phone, 'sin teléfono') AS phone_display
FROM users;

-- NULLIF: devuelve NULL si los dos valores son iguales (útil para evitar división por cero)
SELECT revenue / NULLIF(visits, 0) AS revenue_per_visit
FROM campaigns;

5. ¿Qué diferencia hay entre UNION y UNION ALL?

Nivel: Junior | Aparece en: analítica, reporting

sql
-- UNION: elimina duplicados (más lento, hace un DISTINCT implícito)
SELECT email FROM users_argentina
UNION
SELECT email FROM users_colombia;

-- UNION ALL: mantiene duplicados (más rápido)
SELECT email FROM users_argentina
UNION ALL
SELECT email FROM users_colombia;

Usá UNION ALL siempre que puedas, especialmente en tablas grandes. Si sabés que los conjuntos son disjuntos (sin filas repetidas), UNION ALL es la elección correcta. UNION implica un paso extra de deduplicación que puede ser muy costoso.


6. ¿Qué son las constraints y cuáles conocés?

Nivel: Junior | Aparece en: diseño de bases de datos, backend

sql
CREATE TABLE employees (
  id          SERIAL PRIMARY KEY,           -- PRIMARY KEY: unicidad + NOT NULL
  email       VARCHAR(255) UNIQUE NOT NULL,  -- UNIQUE: sin duplicados
  department_id INT REFERENCES departments(id), -- FOREIGN KEY: integridad referencial
  salary      NUMERIC CHECK (salary > 0),   -- CHECK: validación de dominio
  status      VARCHAR(20) DEFAULT 'active'  -- DEFAULT: valor por defecto
);

En entrevistas suelen preguntar: "¿qué pasa si intentás insertar un registro con una foreign key que no existe?" → La base lanza un error de violación de integridad referencial (a menos que la FK esté marcada como ON DELETE CASCADE o SET NULL).


7. ¿Qué es una transacción y para qué sirve ACID?

Nivel: Junior-Medio | Aparece en: backend, ingeniería de datos

Una transacción agrupa operaciones para que se ejecuten todas juntas o ninguna.

sql
BEGIN;

UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

COMMIT;
-- Si algo falla entre las dos líneas, hacemos ROLLBACK

ACID son las propiedades que garantiza una base de datos transaccional:

  • Atomicidad: todo o nada
  • Consistencia: la base pasa de un estado válido a otro válido
  • Aislamiento: las transacciones concurrentes no se interfieren
  • Durabilidad: lo que fue commiteado sobrevive a fallas

8. Diferencia entre DELETE, TRUNCATE y DROP

Nivel: Junior | Aparece en: backend, administración

sql
-- DELETE: borra filas que cumplan la condición (se puede hacer ROLLBACK)
DELETE FROM logs WHERE created_at < '2025-01-01';

-- TRUNCATE: borra TODAS las filas, más rápido que DELETE, no hace log por fila
-- En PostgreSQL, se puede hacer ROLLBACK. En MySQL, no.
TRUNCATE TABLE logs;

-- DROP: elimina la tabla completamente (estructura + datos)
DROP TABLE logs;

La diferencia clave en entrevista: DELETE es DML (Data Manipulation Language) y se puede deshacer dentro de una transacción. TRUNCATE es DDL en la mayoría de los motores. DROP elimina el objeto entero.


9. ¿Qué es un subquery correlacionado?

Nivel: Medio | Aparece en: analítica avanzada

Un subquery correlacionado hace referencia a la consulta exterior. Se ejecuta una vez por cada fila del resultado exterior.

sql
-- Para cada empleado, encontrar si su salario es mayor al promedio de su departamento
SELECT name, salary, department_id
FROM employees e1
WHERE salary > (
  SELECT AVG(salary)
  FROM employees e2
  WHERE e2.department_id = e1.department_id  -- referencia a la consulta exterior
);

Advertencia de performance: los subqueries correlacionados pueden ser lentos en tablas grandes porque se re-ejecutan por cada fila. En muchos casos conviene reescribirlos con un JOIN o una window function.


10. ¿Cómo encontrás duplicados en una tabla?

Nivel: Junior-Medio | Aparece en: QA de datos, ingeniería

sql
-- Encontrar emails duplicados
SELECT email, COUNT(*) AS cantidad
FROM users
GROUP BY email
HAVING COUNT(*) > 1;

-- Ver los registros completos de los duplicados
SELECT *
FROM users
WHERE email IN (
  SELECT email
  FROM users
  GROUP BY email
  HAVING COUNT(*) > 1
)
ORDER BY email;

11. ¿Qué diferencia hay entre una vista (VIEW) y una tabla?

Nivel: Junior | Aparece en: BI, reporting, backend

sql
-- Una vista es una consulta guardada, no almacena datos
CREATE VIEW active_users AS
SELECT id, name, email
FROM users
WHERE status = 'active';

-- Se usa igual que una tabla
SELECT * FROM active_users;

Diferencias clave:

  • Una vista no almacena datos (salvo vistas materializadas)
  • Siempre refleja el estado actual de las tablas subyacentes
  • No podés indexar una vista regular (sí una materializada)
  • Las vistas materializadas almacenan el resultado y hay que refrescarlas

12. ¿Qué es una vista materializada?

Nivel: Medio | Aparece en: data engineering, BI

sql
-- PostgreSQL
CREATE MATERIALIZED VIEW monthly_revenue AS
SELECT
  DATE_TRUNC('month', created_at) AS month,
  SUM(amount) AS total
FROM orders
GROUP BY 1;

-- Refrescar manualmente
REFRESH MATERIALIZED VIEW monthly_revenue;

-- Refrescar sin bloquear lecturas (PostgreSQL)
REFRESH MATERIALIZED VIEW CONCURRENTLY monthly_revenue;

Una vista materializada guarda el resultado de la consulta en disco. Es mucho más rápida de leer que calcular todo de vuelta, pero los datos no son en tiempo real — hay que refrescarlos.


Bloque 2: Consultas intermedias

13. Encontrá el segundo salario más alto

Nivel: Medio | Aparece en: casi toda entrevista técnica de SQL

Esta es una de las preguntas más clásicas. Hay varias formas de resolverla.

sql
-- Opción 1: con subconsulta
SELECT MAX(salary)
FROM employees
WHERE salary < (SELECT MAX(salary) FROM employees);

-- Opción 2: con OFFSET (más limpio)
SELECT DISTINCT salary
FROM employees
ORDER BY salary DESC
LIMIT 1 OFFSET 1;

-- Opción 3: con window function (la más robusta para el N-ésimo)
SELECT salary
FROM (
  SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk
  FROM employees
) ranked
WHERE rnk = 2;

Por qué la opción 3 es la mejor: DENSE_RANK maneja correctamente los empates. Si dos empleados tienen el mismo salario más alto, DENSE_RANK le asigna el mismo rango a ambos y el siguiente valor diferente recibe el rango 2.


14. Escribí una consulta para calcular el total acumulado de ventas por día

Nivel: Medio | Aparece en: analítica, data engineering

sql
SELECT
  order_date,
  daily_revenue,
  SUM(daily_revenue) OVER (ORDER BY order_date) AS cumulative_revenue
FROM (
  SELECT
    DATE(created_at) AS order_date,
    SUM(amount) AS daily_revenue
  FROM orders
  GROUP BY DATE(created_at)
) daily_totals
ORDER BY order_date;

15. ¿Cómo encontrás usuarios que no hicieron ningún pedido?

Nivel: Medio | Aparece en: analítica, CRM, marketing

Hay tres formas equivalentes. Cada entrevistador prefiere una diferente.

sql
-- Opción 1: LEFT JOIN con filtro NULL
SELECT u.id, u.name
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL;

-- Opción 2: NOT EXISTS (generalmente más eficiente en tablas grandes)
SELECT id, name
FROM users u
WHERE NOT EXISTS (
  SELECT 1 FROM orders o WHERE o.user_id = u.id
);

-- Opción 3: NOT IN (cuidado con NULLs)
SELECT id, name
FROM users
WHERE id NOT IN (
  SELECT DISTINCT user_id FROM orders WHERE user_id IS NOT NULL
);

Truco de entrevista: mencioná que NOT IN tiene una trampa con NULL. Si la subconsulta devuelve algún NULL, la consulta entera no devuelve ningún resultado. NOT EXISTS no tiene ese problema.


16. Escribí una consulta que calcule el porcentaje de cada categoría sobre el total

Nivel: Medio | Aparece en: BI, reporting

sql
SELECT
  category,
  COUNT(*) AS quantity,
  ROUND(
    COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (),
    2
  ) AS percentage
FROM products
GROUP BY category
ORDER BY percentage DESC;

SUM(COUNT(*)) OVER () es una window function sin partición: suma el total de todos los grupos, lo que permite calcular el porcentaje sin una subconsulta adicional.


17. Encontrá los clientes que compraron en todos los meses del último año

Nivel: Medio-Avanzado | Aparece en: analítica de retención

sql
SELECT user_id
FROM orders
WHERE created_at >= DATE_TRUNC('year', CURRENT_DATE - INTERVAL '1 year')
  AND created_at < DATE_TRUNC('year', CURRENT_DATE)
GROUP BY user_id
HAVING COUNT(DISTINCT DATE_TRUNC('month', created_at)) = 12;

18. ¿Cómo calculás la mediana en SQL?

Nivel: Medio | Aparece en: analítica, ciencia de datos

AVG es fácil, pero la mediana no tiene una función estándar en todos los motores.

sql
-- PostgreSQL tiene PERCENTILE_CONT
SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY salary) AS mediana
FROM employees;

-- Para MySQL o SQLite (sin esa función):
SELECT AVG(salary) AS mediana
FROM (
  SELECT salary
  FROM (
    SELECT salary, ROW_NUMBER() OVER (ORDER BY salary) AS rn, COUNT(*) OVER () AS total
    FROM employees
  ) ranked
  WHERE rn IN (FLOOR((total + 1) / 2.0), CEIL((total + 1) / 2.0))
) median_rows;

19. Escribí una consulta para encontrar el top 3 de productos más vendidos por categoría

Nivel: Medio-Avanzado | Aparece en: e-commerce, analítica

sql
WITH ranked_products AS (
  SELECT
    p.category,
    p.name,
    SUM(oi.quantity) AS total_sold,
    RANK() OVER (PARTITION BY p.category ORDER BY SUM(oi.quantity) DESC) AS rnk
  FROM order_items oi
  JOIN products p ON oi.product_id = p.id
  GROUP BY p.category, p.name
)
SELECT category, name, total_sold
FROM ranked_products
WHERE rnk <= 3
ORDER BY category, rnk;

20. ¿Cómo harías un self-join y para qué sirve?

Nivel: Medio | Aparece en: estructuras jerárquicas, HR, organización

sql
-- Estructura de empleados con managers
-- tabla: employees (id, name, manager_id)

SELECT
  e.name AS empleado,
  m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;

Los self-joins aparecen en:

  • Organigramas y jerarquías
  • Relaciones de "amigos" o "seguidores"
  • Comparaciones de filas dentro de la misma tabla (ej: precio de hoy vs. precio de ayer)

21. Detectá sesiones de usuario: agrupa eventos consecutivos del mismo usuario separados por más de 30 minutos

Nivel: Avanzado | Aparece en: analítica de producto, data engineering

sql
WITH events_with_prev AS (
  SELECT
    user_id,
    event_time,
    LAG(event_time) OVER (PARTITION BY user_id ORDER BY event_time) AS prev_event_time
  FROM user_events
),
session_starts AS (
  SELECT
    user_id,
    event_time,
    CASE
      WHEN prev_event_time IS NULL
        OR event_time - prev_event_time > INTERVAL '30 minutes'
      THEN 1
      ELSE 0
    END AS is_new_session
  FROM events_with_prev
),
sessions AS (
  SELECT
    user_id,
    event_time,
    SUM(is_new_session) OVER (PARTITION BY user_id ORDER BY event_time) AS session_id
  FROM session_starts
)
SELECT user_id, session_id, MIN(event_time) AS session_start, MAX(event_time) AS session_end
FROM sessions
GROUP BY user_id, session_id;

22. ¿Cómo pivotás una tabla en SQL (rows a columns)?

Nivel: Medio-Avanzado | Aparece en: reporting, BI

sql
-- Convertir filas de categorías en columnas
-- Tabla: sales (month, category, amount)

SELECT
  month,
  SUM(CASE WHEN category = 'electronics' THEN amount ELSE 0 END) AS electronics,
  SUM(CASE WHEN category = 'clothing'    THEN amount ELSE 0 END) AS clothing,
  SUM(CASE WHEN category = 'food'        THEN amount ELSE 0 END) AS food
FROM sales
GROUP BY month
ORDER BY month;

En PostgreSQL también podés usar la extensión tablefunc con crosstab(), pero el CASE WHEN es más portable y más fácil de leer en una entrevista.


23. Calculá el churn mensual de usuarios

Nivel: Avanzado | Aparece en: SaaS, analytics de producto

sql
WITH monthly_users AS (
  SELECT DISTINCT
    DATE_TRUNC('month', event_date) AS month,
    user_id
  FROM user_activity
),
user_months AS (
  SELECT
    user_id,
    month,
    LAG(month) OVER (PARTITION BY user_id ORDER BY month) AS prev_month
  FROM monthly_users
)
SELECT
  month,
  COUNT(DISTINCT CASE WHEN prev_month IS NULL OR
    prev_month < month - INTERVAL '1 month' THEN user_id END) AS new_or_returned,
  COUNT(DISTINCT user_id) AS active_users
FROM user_months
GROUP BY month
ORDER BY month;

24. Encontrá las filas duplicadas y eliminá todas menos una

Nivel: Medio | Aparece en: limpieza de datos, data engineering

sql
-- En PostgreSQL, usando ctid (identificador interno de fila)
DELETE FROM users
WHERE ctid NOT IN (
  SELECT MIN(ctid)
  FROM users
  GROUP BY email  -- o los campos que definen el duplicado
);

-- Alternativa con CTE (más legible)
WITH duplicates AS (
  SELECT id,
    ROW_NUMBER() OVER (PARTITION BY email ORDER BY created_at DESC) AS rn
  FROM users
)
DELETE FROM users
WHERE id IN (
  SELECT id FROM duplicates WHERE rn > 1
);

25. Escribí una consulta para calcular la retención de cohortes (cohort retention)

Nivel: Avanzado | Aparece en: analítica de producto, growth

sql
WITH cohorts AS (
  SELECT
    user_id,
    DATE_TRUNC('month', MIN(created_at)) AS cohort_month
  FROM users
  GROUP BY user_id
),
activity AS (
  SELECT
    e.user_id,
    c.cohort_month,
    DATE_TRUNC('month', e.event_date) AS activity_month,
    EXTRACT(EPOCH FROM (DATE_TRUNC('month', e.event_date) - c.cohort_month)) / (30 * 86400) AS months_since_signup
  FROM events e
  JOIN cohorts c ON e.user_id = c.user_id
)
SELECT
  cohort_month,
  months_since_signup::INT AS month_number,
  COUNT(DISTINCT user_id) AS retained_users,
  FIRST_VALUE(COUNT(DISTINCT user_id)) OVER (
    PARTITION BY cohort_month ORDER BY months_since_signup
  ) AS cohort_size,
  ROUND(
    COUNT(DISTINCT user_id) * 100.0 /
    FIRST_VALUE(COUNT(DISTINCT user_id)) OVER (
      PARTITION BY cohort_month ORDER BY months_since_signup
    ), 1
  ) AS retention_rate
FROM activity
GROUP BY cohort_month, months_since_signup
ORDER BY cohort_month, month_number;

Bloque 3: SQL avanzado

26. Explicá las window functions y sus componentes

Nivel: Medio-Avanzado | Aparece en: toda entrevista de datos senior

Una window function realiza un cálculo sobre un conjunto de filas relacionadas con la fila actual, sin colapsar el resultado (a diferencia de GROUP BY).

sql
SELECT
  name,
  department,
  salary,
  -- OVER () sin PARTITION: toda la tabla es la ventana
  AVG(salary) OVER () AS company_avg,

  -- PARTITION BY: ventana por departamento
  AVG(salary) OVER (PARTITION BY department) AS dept_avg,

  -- ORDER BY dentro de la ventana: acumulado
  SUM(salary) OVER (PARTITION BY department ORDER BY hire_date) AS cumulative_payroll,

  -- Funciones de ranking
  ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS row_num,
  RANK()       OVER (PARTITION BY department ORDER BY salary DESC) AS rank_num,
  DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dense_rank_num
FROM employees;

Diferencias entre funciones de ranking:

| Salarios | ROW_NUMBER | RANK | DENSE_RANK |

|----------|-----------|------|-----------|

| 100, 100, 90 | 1, 2, 3 | 1, 1, 3 | 1, 1, 2 |

RANK deja "huecos" después de un empate. DENSE_RANK no.


27. ¿Qué es un CTE y cuándo lo usás en lugar de una subconsulta?

Nivel: Medio | Aparece en: toda entrevista SQL media-avanzada

Un CTE (Common Table Expression) define una consulta temporal con nombre que se puede referenciar una o más veces.

sql
-- Sin CTE: difícil de leer
SELECT *
FROM (
  SELECT user_id, COUNT(*) AS orders, SUM(amount) AS revenue
  FROM orders
  GROUP BY user_id
) user_stats
WHERE revenue > 1000;

-- Con CTE: mucho más legible
WITH user_stats AS (
  SELECT user_id, COUNT(*) AS orders, SUM(amount) AS revenue
  FROM orders
  GROUP BY user_id
)
SELECT *
FROM user_stats
WHERE revenue > 1000;

Ventajas de CTE sobre subconsulta:

  • Legibilidad (se lee de arriba a abajo)
  • Se puede referenciar múltiples veces (vs. repetir la subconsulta)
  • Permite CTEs recursivos (para jerarquías y grafos)

28. Escribí un CTE recursivo para recorrer una jerarquía de empleados

Nivel: Avanzado | Aparece en: data engineering, backend con estructuras de árbol

sql
WITH RECURSIVE org_chart AS (
  -- Caso base: el CEO (sin manager)
  SELECT id, name, manager_id, 0 AS level, name::TEXT AS path
  FROM employees
  WHERE manager_id IS NULL

  UNION ALL

  -- Caso recursivo: los reportes de cada manager
  SELECT e.id, e.name, e.manager_id, oc.level + 1, oc.path || ' > ' || e.name
  FROM employees e
  JOIN org_chart oc ON e.manager_id = oc.id
)
SELECT level, name, path
FROM org_chart
ORDER BY path;

29. ¿Qué es el EXPLAIN ANALYZE y cómo lo usás?

Nivel: Avanzado | Aparece en: backend, data engineering, DBA

sql
EXPLAIN ANALYZE
SELECT u.name, COUNT(o.id) AS total_orders
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name;

El output muestra:

  • El plan de ejecución (qué pasos sigue la base)
  • Costo estimado vs. costo real
  • Tiempo de ejecución real
  • Cantidad de filas por paso

Lo que buscás cuando optimizás:

  • Seq Scan en tablas grandes → falta un índice
  • Hash Join vs Nested Loop → el join puede ser ineficiente
  • Diferencia grande entre filas estimadas y reales → estadísticas desactualizadas (correr ANALYZE)

30. ¿Qué son los isolation levels y qué problemas resuelven?

Nivel: Avanzado | Aparece en: backend, entrevistas de sistemas distribuidos

Los niveles de aislamiento controlan qué puede ver una transacción de los cambios de otras transacciones concurrentes.

| Nivel | Dirty Read | Non-repeatable Read | Phantom Read |

|-------|-----------|---------------------|--------------|

| READ UNCOMMITTED | posible | posible | posible |

| READ COMMITTED | no | posible | posible |

| REPEATABLE READ | no | no | posible |

| SERIALIZABLE | no | no | no |

sql
-- Cambiar el nivel de aislamiento en PostgreSQL
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- ... tus queries
COMMIT;

31. ¿Cómo funcionan los índices compuestos? ¿Qué es el "leftmost prefix rule"?

Nivel: Avanzado | Aparece en: performance, backend

sql
-- Índice compuesto
CREATE INDEX idx_orders_user_date_status ON orders(user_id, created_at, status);

-- USA el índice (empieza desde la izquierda)
SELECT * FROM orders WHERE user_id = 42;
SELECT * FROM orders WHERE user_id = 42 AND created_at > '2025-01-01';
SELECT * FROM orders WHERE user_id = 42 AND created_at > '2025-01-01' AND status = 'paid';

-- NO usa el índice (salta el primer campo)
SELECT * FROM orders WHERE created_at > '2025-01-01';
SELECT * FROM orders WHERE status = 'paid';

La regla: un índice compuesto (A, B, C) solo ayuda si la condición incluye A. Puede incluir A+B o A+B+C, pero no B solo ni C solo.


32. Escribí una consulta para detectar gaps en una secuencia numérica

Nivel: Avanzado | Aparece en: auditoría, facturación, ingeniería

sql
-- Encontrar números faltantes en una secuencia
WITH consecutive AS (
  SELECT
    id,
    LEAD(id) OVER (ORDER BY id) AS next_id
  FROM invoices
)
SELECT
  id + 1 AS gap_start,
  next_id - 1 AS gap_end
FROM consecutive
WHERE next_id > id + 1;

33. ¿Qué es el problema N+1 en SQL y cómo lo evitás?

Nivel: Avanzado | Aparece en: backend, ORM, performance

El problema N+1 ocurre cuando ejecutás 1 query para obtener N registros y luego N queries adicionales para obtener datos relacionados de cada uno.

sql
-- MALO: 1 query para usuarios, N queries para los pedidos de cada uno
-- SELECT * FROM users;
-- Para cada usuario: SELECT * FROM orders WHERE user_id = ?

-- BUENO: un solo JOIN
SELECT u.id, u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name;

En ORMs como ActiveRecord, Sequelize o SQLAlchemy, esto se resuelve con includes, eager_load o joinedload.


34. ¿Cómo calculás el promedio móvil de 7 días?

Nivel: Avanzado | Aparece en: analítica, finanzas, growth

sql
SELECT
  order_date,
  daily_revenue,
  AVG(daily_revenue) OVER (
    ORDER BY order_date
    ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
  ) AS moving_avg_7d
FROM (
  SELECT DATE(created_at) AS order_date, SUM(amount) AS daily_revenue
  FROM orders
  GROUP BY DATE(created_at)
) daily
ORDER BY order_date;

ROWS BETWEEN 6 PRECEDING AND CURRENT ROW define el frame de la ventana: la fila actual más las 6 anteriores = 7 días.


35. Escribí una consulta para encontrar usuarios activos en los últimos 30 días que no lo estuvieron en los 30 días anteriores (usuarios reactivados)

Nivel: Avanzado | Aparece en: analítica de producto, growth

sql
WITH recent AS (
  SELECT DISTINCT user_id
  FROM events
  WHERE event_date >= CURRENT_DATE - INTERVAL '30 days'
),
previous AS (
  SELECT DISTINCT user_id
  FROM events
  WHERE event_date >= CURRENT_DATE - INTERVAL '60 days'
    AND event_date < CURRENT_DATE - INTERVAL '30 days'
)
SELECT r.user_id
FROM recent r
LEFT JOIN previous p ON r.user_id = p.user_id
WHERE p.user_id IS NULL;

36. ¿Qué diferencia hay entre ROW_NUMBER(), RANK() y DENSE_RANK() con un caso concreto?

Nivel: Medio | Aparece en: toda entrevista de SQL analítico

sql
SELECT
  name,
  score,
  ROW_NUMBER() OVER (ORDER BY score DESC) AS row_num,
  RANK()       OVER (ORDER BY score DESC) AS rank,
  DENSE_RANK() OVER (ORDER BY score DESC) AS dense_rank
FROM results;

/*
name    | score | row_num | rank | dense_rank
--------|-------|---------|------|------------
Ana     | 95    | 1       | 1    | 1
Carlos  | 95    | 2       | 1    | 1
Lucia   | 87    | 3       | 3    | 2
Pedro   | 80    | 4       | 4    | 3
*/
  • ROW_NUMBER: siempre único, aunque haya empates
  • RANK: empate = mismo número, luego salta (1, 1, 3...)
  • DENSE_RANK: empate = mismo número, sin saltos (1, 1, 2...)

37. ¿Cómo funciona PARTITION BY dentro de una window function?

Nivel: Medio-Avanzado | Aparece en: analítica, reporting

sql
SELECT
  department,
  name,
  salary,
  -- Promedio POR departamento (no de toda la empresa)
  ROUND(AVG(salary) OVER (PARTITION BY department), 2) AS dept_avg,
  -- Posición dentro de su departamento
  RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dept_rank,
  -- Posición en la empresa entera
  RANK() OVER (ORDER BY salary DESC) AS company_rank
FROM employees;

PARTITION BY es como un GROUP BY pero para la ventana: calcula el resultado dentro de cada grupo sin colapsar las filas.


38. Escribí una consulta para calcular el LTV (lifetime value) promedio por canal de adquisición

Nivel: Avanzado | Aparece en: marketing analytics, growth

sql
WITH user_revenue AS (
  SELECT
    u.id AS user_id,
    u.acquisition_source,
    SUM(o.amount) AS total_revenue
  FROM users u
  LEFT JOIN orders o ON u.id = o.user_id
  GROUP BY u.id, u.acquisition_source
)
SELECT
  acquisition_source,
  COUNT(*) AS total_users,
  ROUND(AVG(total_revenue), 2) AS avg_ltv,
  ROUND(PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY total_revenue), 2) AS median_ltv,
  SUM(total_revenue) AS total_revenue
FROM user_revenue
GROUP BY acquisition_source
ORDER BY avg_ltv DESC;

Bloque 4: Diseño y escenarios reales

39. ¿Cómo diseñarías el esquema de una app de e-commerce?

Nivel: Avanzado | Aparece en: entrevistas de sistema design + SQL

sql
CREATE TABLE users (
  id         SERIAL PRIMARY KEY,
  email      VARCHAR(255) UNIQUE NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE products (
  id          SERIAL PRIMARY KEY,
  name        VARCHAR(255) NOT NULL,
  price       NUMERIC(10,2) NOT NULL CHECK (price >= 0),
  stock       INT NOT NULL DEFAULT 0,
  category_id INT REFERENCES categories(id)
);

CREATE TABLE orders (
  id         SERIAL PRIMARY KEY,
  user_id    INT NOT NULL REFERENCES users(id),
  status     VARCHAR(50) DEFAULT 'pending',
  created_at TIMESTAMPTZ DEFAULT NOW(),
  total      NUMERIC(10,2)
);

CREATE TABLE order_items (
  id         SERIAL PRIMARY KEY,
  order_id   INT NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
  product_id INT NOT NULL REFERENCES products(id),
  quantity   INT NOT NULL CHECK (quantity > 0),
  unit_price NUMERIC(10,2) NOT NULL  -- precio al momento de la compra
);

Puntos a mencionar en la entrevista:

  • unit_price en order_items guarda el precio histórico (el precio del producto puede cambiar)
  • ON DELETE CASCADE en order_items para mantener consistencia
  • Los índices que agregarías: orders(user_id), orders(status, created_at), order_items(order_id), order_items(product_id)

40. ¿Qué es la normalización y cuándo desnormalizás?

Nivel: Avanzado | Aparece en: diseño de bases, data warehousing

Normalización es el proceso de organizar datos para reducir redundancia. Las tres primeras formas normales son:

  • 1NF: sin grupos repetidos, cada celda tiene un valor atómico
  • 2NF: 1NF + sin dependencias parciales (cada columna depende de la clave completa, no de parte de ella)
  • 3NF: 2NF + sin dependencias transitivas (las columnas no-clave dependen solo de la clave)

Cuándo desnormalizás (intencionalmente):

  • Data warehouses / OLAP (las lecturas masivas son más lentas con muchos JOINs)
  • Caches y tablas de reporting pre-agregadas
  • Cuando el costo de los JOINs supera el costo de la redundancia

41. ¿Cómo manejarías el historial de precios de un producto?

Nivel: Avanzado | Aparece en: e-commerce, finanzas, auditoría

sql
-- Tabla de historial de precios
CREATE TABLE product_prices (
  id         SERIAL PRIMARY KEY,
  product_id INT NOT NULL REFERENCES products(id),
  price      NUMERIC(10,2) NOT NULL,
  valid_from TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  valid_to   TIMESTAMPTZ  -- NULL significa "precio actual"
);

-- Consultar el precio en un momento dado
SELECT price
FROM product_prices
WHERE product_id = 42
  AND valid_from <= '2025-06-15 10:00:00'
  AND (valid_to IS NULL OR valid_to > '2025-06-15 10:00:00');

-- Actualizar el precio (dejar rastro)
UPDATE product_prices
SET valid_to = NOW()
WHERE product_id = 42 AND valid_to IS NULL;

INSERT INTO product_prices (product_id, price, valid_from)
VALUES (42, 29.99, NOW());

42. ¿Qué es un deadlock y cómo lo prevenís?

Nivel: Avanzado | Aparece en: backend, sistemas distribuidos

Un deadlock ocurre cuando dos transacciones se bloquean mutuamente esperando recursos que la otra tiene.

sql
-- Transacción A          | Transacción B
-- UPDATE accounts SET... |
-- WHERE id = 1;          | UPDATE accounts SET...
--                        | WHERE id = 2;
-- UPDATE accounts SET... | -- espera a A
-- WHERE id = 2; -- espera a B  |

Cómo prevenirlo:

  1. 1Bloquear recursos siempre en el mismo orden (ej: siempre actualizar id más bajo primero)
  2. 2Mantener transacciones cortas
  3. 3Usar SELECT ... FOR UPDATE SKIP LOCKED para procesamiento de colas
sql
-- Adquirir el lock en orden ascendente de id
BEGIN;
SELECT * FROM accounts WHERE id IN (1, 2) ORDER BY id FOR UPDATE;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

43. ¿Cómo implementarías soft delete?

Nivel: Medio | Aparece en: casi todo producto con datos de usuario

sql
-- Agregar columna de soft delete
ALTER TABLE users ADD COLUMN deleted_at TIMESTAMPTZ;

-- Eliminar (marcar como borrado)
UPDATE users SET deleted_at = NOW() WHERE id = 42;

-- Consultar solo activos
SELECT * FROM users WHERE deleted_at IS NULL;

-- Índice parcial para que las queries de activos sean rápidas
CREATE INDEX idx_users_active ON users(id) WHERE deleted_at IS NULL;

-- Vista para simplificar las queries
CREATE VIEW active_users AS
SELECT * FROM users WHERE deleted_at IS NULL;

44. Escribí una consulta para encontrar el "nth" registro de cada grupo

Nivel: Avanzado | Aparece en: reporting, analítica

sql
-- El tercer pedido de cada cliente (por fecha)
WITH ranked_orders AS (
  SELECT
    user_id,
    id AS order_id,
    amount,
    created_at,
    ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) AS order_number
  FROM orders
)
SELECT user_id, order_id, amount, created_at
FROM ranked_orders
WHERE order_number = 3;

45. ¿Cómo optimizarías una query lenta?

Nivel: Avanzado | Aparece en: toda entrevista de performance

El proceso paso a paso que vas a mencionar en la entrevista:

  1. 1Correr EXPLAIN ANALYZE para ver el plan de ejecución real
  2. 2Buscar Seq Scans en tablas grandes — agregar índice en la columna del WHERE/JOIN
  3. 3Verificar selectividad — un índice en una columna con 2 valores distintos no ayuda
  4. 4Revisar los JOINs — asegurarse de que las columnas del ON estén indexadas en ambos lados
  5. 5Evitar funciones en columnas indexadas en el WHERE
sql
-- MALO: la función impide usar el índice
SELECT * FROM orders WHERE DATE(created_at) = '2025-01-15';

-- BUENO: rango que puede usar el índice
SELECT * FROM orders
WHERE created_at >= '2025-01-15'
  AND created_at < '2025-01-16';

-- MALO: cálculo en columna indexada
SELECT * FROM users WHERE LOWER(email) = 'test@example.com';

-- BUENO: índice funcional
CREATE INDEX idx_users_email_lower ON users(LOWER(email));
SELECT * FROM users WHERE LOWER(email) = 'test@example.com';

46. ¿Qué diferencia hay entre OLTP y OLAP?

Nivel: Avanzado | Aparece en: data engineering, system design

| | OLTP | OLAP |

|---|---|---|

| Propósito | Operacional (transacciones) | Analítico (reportes, BI) |

| Consultas | Muchas, cortas, por PK | Pocas, largas, sobre millones de filas |

| Escrituras | Frecuentes | Pocas (batch loads) |

| Esquema | Normalizado (3NF) | Desnormalizado (star schema, snowflake) |

| Ejemplo | PostgreSQL, MySQL | Redshift, BigQuery, Snowflake |

| Índices | B-tree por clave | Columnar, particionado |


47. Explicá el modelo estrella (star schema) con un ejemplo

Nivel: Avanzado | Aparece en: data warehousing, BI, data engineering

sql
-- Tabla de hechos (fact table): las métricas
CREATE TABLE fact_sales (
  sale_id      BIGINT PRIMARY KEY,
  date_id      INT REFERENCES dim_date(date_id),
  product_id   INT REFERENCES dim_product(product_id),
  customer_id  INT REFERENCES dim_customer(customer_id),
  store_id     INT REFERENCES dim_store(store_id),
  quantity     INT,
  revenue      NUMERIC(12,2)
);

-- Dimensiones: el contexto
CREATE TABLE dim_date (
  date_id    INT PRIMARY KEY,
  date       DATE,
  year       INT,
  quarter    INT,
  month      INT,
  week       INT,
  day_name   VARCHAR(10)
);

CREATE TABLE dim_product (
  product_id   INT PRIMARY KEY,
  name         VARCHAR(255),
  category     VARCHAR(100),
  brand        VARCHAR(100)
);

Por qué el star schema? Las queries analíticas se simplifican (un join a cada dimensión desde el fact table) y los motores columnar lo pueden optimizar mejor que un esquema normalizado.


Bonus: preguntas cortas para revisión rápida

Estas aparecen como preguntas de calentamiento o cierre:

¿Qué hace COALESCE(a, b, c)? — Devuelve el primer valor no-NULL de la lista.

¿Qué diferencia hay entre CHAR y VARCHAR? — CHAR(n) tiene longitud fija y rellena con espacios. VARCHAR(n) es variable hasta n caracteres. En la práctica, usás VARCHAR o TEXT.

¿Cuándo usás EXISTS en lugar de IN? — EXISTS es más eficiente cuando la subconsulta devuelve muchas filas, porque se detiene al encontrar la primera coincidencia.

¿Qué es una clave compuesta? — Una clave primaria formada por más de una columna. Útil en tablas de relación muchos-a-muchos.

¿Qué hace GROUP BY ROLLUP? — Genera subtotales y el gran total en una sola consulta.

sql
SELECT region, category, SUM(revenue)
FROM sales
GROUP BY ROLLUP(region, category);
-- Devuelve: total por (region, category), total por region, y gran total

¿Qué es un full table scan? — Cuando la base lee todas las filas de una tabla porque no puede usar un índice. Inofensivo en tablas pequeñas, costoso en tablas grandes.

¿Qué hace DISTINCT ON en PostgreSQL? — Devuelve la primera fila de cada grupo definido por las columnas del DISTINCT ON, ordenado por el ORDER BY.

sql
-- El pedido más reciente de cada cliente
SELECT DISTINCT ON (user_id) user_id, id, created_at
FROM orders
ORDER BY user_id, created_at DESC;

Cómo prepararte para la parte práctica de la entrevista

En la mayoría de las entrevistas técnicas de datos vas a tener un editor SQL en vivo. Estos son los hábitos que separan a los candidatos que pasan de los que no:

Pensá en voz alta. Antes de escribir, explicá qué querés lograr. "Primero voy a encontrar los usuarios activos del último mes, después los voy a cruzar con los de dos meses atrás para ver cuáles son nuevos." Los entrevistadores quieren ver el proceso, no solo el resultado.

Empezá simple. Escribí la query más básica que funcione y después agregale complejidad. No intentés hacer todo en una sola query compleja desde el inicio.

Verificá con datos de ejemplo. Si podés, creá un par de filas mentales y trazá tu query paso a paso.

Conocé los errores comunes. El NULL en NOT IN, el orden de WHERE vs HAVING, los empates en RANK() — son las trampas que los entrevistadores ponen a propósito.

Preguntá por el motor. PostgreSQL, MySQL, BigQuery y Spark SQL tienen diferencias. Preguntar "¿estamos en Postgres?" es una señal de madurez, no de inseguridad.


*Seguí practicando con casos reales. Las entrevistas de SQL evalúan tanto la sintaxis como la capacidad de modelar un problema de negocio en una query — eso solo se mejora resolviendo ejercicios reales.*

FAQ

¿Cuáles son las preguntas de SQL más frecuentes en entrevistas?+

Las más frecuentes son: diferencia entre WHERE y HAVING, tipos de JOIN, cómo encontrar duplicados, el segundo salario más alto, window functions (ROW_NUMBER, RANK, DENSE_RANK), CTEs vs subconsultas, y cómo optimizar queries lentas. Estas aparecen en el 80% de las entrevistas técnicas para roles de datos y backend.

¿Qué son las window functions en SQL?+

Las window functions realizan cálculos sobre un conjunto de filas relacionadas con la fila actual, sin colapsar el resultado como lo hace GROUP BY. Las más usadas son ROW_NUMBER(), RANK(), DENSE_RANK(), LAG(), LEAD(), SUM() OVER(), AVG() OVER(). Se definen con la cláusula OVER (PARTITION BY ... ORDER BY ...).

¿Cuál es la diferencia entre RANK y DENSE_RANK?+

Ambas asignan rankings, pero difieren en cómo manejan los empates. RANK deja huecos después de un empate (1, 1, 3, 4...). DENSE_RANK no deja huecos (1, 1, 2, 3...). Si dos empleados tienen el mismo salario más alto, RANK les asigna 1 a ambos y el siguiente es 3. DENSE_RANK les asigna 1 a ambos y el siguiente es 2.

¿Cómo encontrás el segundo valor más alto en SQL?+

La forma más robusta es con DENSE_RANK: SELECT salary FROM (SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk FROM employees) ranked WHERE rnk = 2. También podés usar LIMIT/OFFSET: SELECT DISTINCT salary FROM employees ORDER BY salary DESC LIMIT 1 OFFSET 1.

¿Cuándo usar NOT EXISTS en lugar de NOT IN?+

NOT EXISTS es más seguro cuando la subconsulta puede devolver NULLs. NOT IN con NULLs en la subconsulta devuelve cero filas porque toda comparación con NULL resulta en NULL (no en TRUE). NOT EXISTS no tiene este problema. Además, NOT EXISTS suele ser más eficiente en tablas grandes porque se detiene al encontrar la primera coincidencia.

¿Qué diferencia hay entre un CTE y una subconsulta?+

Un CTE (WITH ...) es una subconsulta con nombre que mejora la legibilidad y puede referenciarse múltiples veces en la misma query sin repetir el código. Los CTEs también permiten recursividad para recorrer jerarquías y grafos. La diferencia de performance depende del motor: en PostgreSQL un CTE se puede materializar (ejecutar una sola vez), mientras que una subconsulta se puede reescribir por el optimizador.

¿Cómo optimizás una query SQL lenta?+

El proceso es: 1) Correr EXPLAIN ANALYZE para ver el plan real de ejecución. 2) Buscar Seq Scans en tablas grandes y agregar índices. 3) Verificar que las columnas de JOIN estén indexadas en ambas tablas. 4) Evitar funciones sobre columnas indexadas en el WHERE (usan índices funcionales o reescribís la condición). 5) Preferir rangos de fechas sobre funciones de fecha. 6) Actualizar estadísticas con ANALYZE si hay diferencia grande entre estimaciones y filas reales.

Related articles

Cómo usar el método STAR en entrevistas (con ejemplos reales)

Aprende a responder preguntas difíciles usando el método STAR en entrevistas. Consejos y ejemplos concretos para roles remotos tech de LATAM.

Cómo conseguir trabajo remoto en dólares desde LATAM: guía real

Descubre consejos concretos para conseguir trabajo remoto en dólares desde LATAM: estrategias de búsqueda, preparación y entrevista para roles tecnológicos.

Las mejores preguntas para hacerle al entrevistador al final

Descubre las mejores preguntas para hacerle al entrevistador al final, útiles para entrevistas tech remotas, diferenciándote y logrando roles en dólares.

Cómo preparar entrevistas de desarrollo sin experiencia previa

Consejos prácticos para enfrentar entrevistas de tu primer trabajo como desarrollador, incluso sin experiencia. Técnicas para destacar y convencer en cada etapa.

Prepare for your real interview

Paste your job link: we research who's interviewing you and rehearse you live.

Start free →

Have an interview coming up? Install the live copilot →

InterviewHack.ai

Prepare for the exact interview: who's interviewing you, a tailored CV, and a real coach.

Product

JobsFree ATS checkerInterview-English checkSalary checkLATAM salary reportFree coursesBlogTailored CVSpoken practiceIt's free

Remote jobs

ReactPythonFull-StackLATAMArgentinaMexicoSee all →

Prepare

Spoken practiceFrontendBackendAI EngineerBy companySell with your CV

Company

For employersAboutContactPrivacyTerms

© 2026 InterviewHack.ai · Your CV is yours. Never used to train anything. · A product of IA-PTY