Hoja de referencia
Hoja de referencia de pandas
Hoja de referencia de pandas agrupada por tarea: filtrar filas, groupby, merge, valores faltantes, limpieza de texto y fechas. Cada fragmento se ejecuta sobre un conjunto de datos real de una tienda.
Hoja de referencia · 12 tareas · 47 fragmentos, todos probados en DuckDB
Las consultas que los analistas escriben cada semana, agrupadas por el trabajo que necesitas resolver. Cada fragmento se ejecuta tal como está escrito sobre las tablas de la tienda Harbor & Pine, y cada grupo enlaza con el nivel que lo enseña.
Dialecto: DuckDB, que sigue a PostgreSQL (¿vienes de SQL Server? mira la tabla del final). Tablas: customers, products, orders, order_items, returns, employees, web_sessions, marketing_spend. Los ingresos de una línea son quantity × unit_price × (1 − discount_pct / 100).
SELECT *
FROM products
LIMIT 10; SELECT name, category, unit_price
FROM products; SELECT name,
unit_price - unit_cost AS margin,
ROUND((unit_price - unit_cost) / unit_price * 100, 1) AS margin_pct
FROM products; Cualquier operación aritmética con NULL da NULL: los productos sin unit_cost tienen un margen NULL.
SELECT DISTINCT category, subcategory
FROM products
ORDER BY category, subcategory; SELECT order_id, order_date, channel
FROM orders
WHERE status = 'refunded'; SELECT name, category, unit_price
FROM products
WHERE category IN ('Decor', 'Kitchen')
AND (unit_price < 50 OR NOT is_active); AND se evalúa antes que OR. Pon entre paréntesis cada OR que mezcles con AND.
SELECT order_id, order_date
FROM orders
WHERE order_date >= DATE '2024-03-01'
AND order_date < DATE '2024-04-01'; BETWEEN '2024-03-01' AND '2024-03-31' descarta sin avisar los pedidos hechos después de la medianoche del día 31.
SELECT customer_id, email
FROM customers
WHERE email ILIKE '%@gmail.com'; ILIKE ignora mayúsculas y minúsculas (PostgreSQL, DuckDB). En otros motores: LOWER(email) LIKE ...
SELECT customer_id, email
FROM customers
WHERE referral_source IS NULL; = NULL nunca es verdadero. Usa IS NULL / IS NOT NULL.
SELECT COALESCE(referral_source, 'unknown') AS source,
COUNT(*) AS customers
FROM customers
GROUP BY 1; SELECT COUNT(*) AS customers,
COUNT(referral_source) AS with_source
FROM customers; SELECT channel,
SUM(amount) / NULLIF(COUNT(*), 0) AS avg_daily_spend
FROM marketing_spend
GROUP BY channel; SELECT name, unit_cost
FROM products
ORDER BY unit_cost NULLS FIRST; SELECT product_id, name, unit_price
FROM products
ORDER BY unit_price DESC, product_id; SELECT order_id, order_date
FROM orders
ORDER BY order_date DESC
LIMIT 10; SELECT product_id, name
FROM products
ORDER BY product_id
LIMIT 10 OFFSET 10; SELECT COUNT(*) AS lines,
SUM(quantity) AS units,
AVG(unit_price) AS avg_price,
MIN(unit_price), MAX(unit_price)
FROM order_items; SELECT channel, status, COUNT(*) AS orders
FROM orders
GROUP BY channel, status
ORDER BY channel, orders DESC; SELECT channel, COUNT(DISTINCT customer_id) AS buyers
FROM orders
GROUP BY channel; SELECT customer_id, COUNT(*) AS orders
FROM orders
WHERE status = 'completed'
GROUP BY customer_id
HAVING COUNT(*) >= 20; Orden de ejecución: FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY, LIMIT. WHERE filtra filas, HAVING filtra grupos.
SELECT CASE WHEN unit_price < 50 THEN 'under 50'
WHEN unit_price < 200 THEN '50-199'
ELSE '200+' END AS band,
COUNT(*) AS products
FROM products
GROUP BY 1; CASE se detiene en el primer WHEN verdadero, así que ordena las pruebas de lo más estrecho a lo más amplio.
SELECT channel,
COUNT(*) FILTER (WHERE status = 'cancelled') AS cancelled,
ROUND(AVG(CASE WHEN status = 'cancelled' THEN 1.0 ELSE 0 END) * 100, 1) AS cancel_pct
FROM orders
GROUP BY channel; FILTER funciona en PostgreSQL y DuckDB; SUM(CASE WHEN ... THEN 1 ELSE 0 END) funciona en todas partes.
SELECT p.category,
ROUND(SUM(oi.quantity * oi.unit_price * (1 - oi.discount_pct / 100)), 2) AS revenue
FROM order_items AS oi
JOIN products AS p ON p.product_id = oi.product_id
GROUP BY p.category
ORDER BY revenue DESC; SELECT c.customer_id, c.email
FROM customers AS c
LEFT JOIN orders AS o ON o.customer_id = c.customer_id
WHERE o.order_id IS NULL; SELECT c.customer_id, COUNT(o.order_id) AS app_orders
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id AND o.channel = 'app'
WHERE c.segment = 'business'
GROUP BY c.customer_id; Si pones o.channel = 'app' en WHERE, los clientes sin pedidos de la app desaparecen: el LEFT JOIN se convierte en un inner join.
SELECT o.channel,
COUNT(*) AS lines,
COUNT(DISTINCT o.order_id) AS orders
FROM orders AS o
JOIN order_items AS oi ON oi.order_id = o.order_id
GROUP BY o.channel; Tras unir las líneas de pedido, cada pedido aparece una vez por línea (multiplicación de filas). Nunca hagas SUM de una columna a nivel de pedido, como shipping_fee, aquí.
SELECT e.first_name || ' ' || e.last_name AS employee,
m.first_name || ' ' || m.last_name AS manager
FROM employees AS e
LEFT JOIN employees AS m ON m.employee_id = e.manager_id; SELECT DATE_TRUNC('month', order_date) AS month,
COUNT(*) AS orders
FROM orders
WHERE order_date >= DATE '2025-01-01'
GROUP BY 1
ORDER BY 1; SELECT EXTRACT(year FROM order_date) AS yr,
EXTRACT(month FROM order_date) AS mo,
EXTRACT(dow FROM order_date) AS weekday, -- 0 = Sunday
COUNT(*) AS orders
FROM orders
GROUP BY 1, 2, 3; SELECT r.return_id,
r.returned_on - CAST(o.order_date AS DATE) AS days_to_return
FROM returns AS r
JOIN order_items AS oi ON oi.order_item_id = r.order_item_id
JOIN orders AS o ON o.order_id = oi.order_id; DATE menos DATE da días completos en PostgreSQL y DuckDB. SQL Server y BigQuery usan DATEDIFF / DATE_DIFF.
SELECT COUNT(*) AS orders
FROM orders
WHERE order_date >= TIMESTAMP '2025-12-31' - INTERVAL '30 days'; SELECT UPPER(TRIM(state)) AS state, COUNT(*) AS customers
FROM customers
GROUP BY 1
ORDER BY customers DESC; SELECT SPLIT_PART(email, '@', 2) AS domain, COUNT(*) AS customers
FROM customers
GROUP BY 1
ORDER BY customers DESC; SELECT LOWER(TRIM(email)) AS email_key,
COUNT(*) AS records,
MIN(customer_id) AS keep_id
FROM customers
GROUP BY 1
HAVING COUNT(*) > 1; SELECT first_name || ' ' || last_name AS full_name
FROM employees; CONCAT(a, b) también funciona y trata NULL como vacío; || devuelve NULL si cualquiera de los dos lados es NULL.
SELECT name, unit_price
FROM products
WHERE unit_price > (SELECT AVG(unit_price) FROM products); SELECT c.customer_id, c.email
FROM customers AS c
WHERE EXISTS (
SELECT 1
FROM orders AS o
JOIN order_items AS oi ON oi.order_id = o.order_id
JOIN returns AS r ON r.order_item_id = oi.order_item_id
WHERE o.customer_id = c.customer_id
); NOT IN (subconsulta) no devuelve ninguna fila si la subconsulta contiene un NULL. NOT EXISTS es seguro.
WITH order_totals AS (
SELECT order_id,
SUM(quantity * unit_price * (1 - discount_pct / 100)) AS merch
FROM order_items
GROUP BY order_id
)
SELECT o.customer_id,
COUNT(*) AS orders,
ROUND(SUM(t.merch), 2) AS lifetime_value,
ROUND(AVG(t.merch), 2) AS avg_order_value
FROM orders AS o
JOIN order_totals AS t ON t.order_id = o.order_id
WHERE o.status = 'completed'
GROUP BY o.customer_id; WITH merch AS (
SELECT order_id, SUM(quantity * unit_price) AS merch
FROM order_items
GROUP BY order_id
)
SELECT o.channel,
SUM(o.shipping_fee) AS shipping,
ROUND(SUM(m.merch), 2) AS merchandise
FROM orders AS o
LEFT JOIN merch AS m ON m.order_id = o.order_id
GROUP BY o.channel; WITH ranked AS (
SELECT category, name, unit_price,
ROW_NUMBER() OVER (PARTITION BY category
ORDER BY unit_price DESC, product_id) AS rn
FROM products
)
SELECT category, name, unit_price
FROM ranked
WHERE rn <= 3; No puedes filtrar por una función de ventana en WHERE: primero envuélvela en una CTE o una subconsulta.
SELECT name, unit_price,
ROW_NUMBER() OVER (ORDER BY unit_price DESC) AS row_num,
RANK() OVER (ORDER BY unit_price DESC) AS rnk, -- 1, 2, 2, 4
DENSE_RANK() OVER (ORDER BY unit_price DESC) AS dense_rnk -- 1, 2, 2, 3
FROM products; WITH daily AS (
SELECT CAST(order_date AS DATE) AS day, SUM(shipping_fee) AS fees
FROM orders
WHERE order_date >= DATE '2025-01-01'
GROUP BY 1
)
SELECT day, fees,
SUM(fees) OVER (ORDER BY day) AS fees_ytd
FROM daily; WITH monthly AS (
SELECT DATE_TRUNC('month', order_date) AS month, COUNT(*) AS orders
FROM orders
GROUP BY 1
)
SELECT month, orders,
LAG(orders) OVER (ORDER BY month) AS prev,
ROUND((orders - LAG(orders) OVER (ORDER BY month)) * 100.0
/ LAG(orders) OVER (ORDER BY month), 1) AS mom_pct
FROM monthly; WITH daily AS (
SELECT CAST(order_date AS DATE) AS day, COUNT(*) AS orders
FROM orders
GROUP BY 1
)
SELECT day, orders,
AVG(orders) OVER (ORDER BY day
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS avg_7d
FROM daily; ROWS cuenta filas, no días: un día sin pedidos no tiene fila, así que completa primero el calendario si los huecos importan.
SELECT ROUND(SUM(oi.quantity * oi.unit_price * (1 - oi.discount_pct / 100))
/ COUNT(DISTINCT o.order_id), 2) AS aov
FROM orders AS o
JOIN order_items AS oi ON oi.order_id = o.order_id
WHERE o.status = 'completed'; SELECT source,
COUNT(*) AS sessions,
ROUND(AVG(CASE WHEN converted THEN 1.0 ELSE 0 END) * 100, 2) AS conversion_pct
FROM web_sessions
GROUP BY source
ORDER BY conversion_pct DESC; SELECT p.category,
ROUND(COUNT(r.return_id) * 100.0 / COUNT(*), 1) AS return_pct
FROM order_items AS oi
JOIN products AS p ON p.product_id = oi.product_id
LEFT JOIN returns AS r ON r.order_item_id = oi.order_item_id
GROUP BY p.category; La mayoría de los cursos y muchas oficinas usan Microsoft SQL Server. Las ideas son idénticas; estas son las escrituras que cambian. Todo lo de la columna de la derecha se ejecuta en este sitio.
| Tarea | SQL Server | Este sitio (DuckDB) |
|---|---|---|
| Primeras 10 filas | SELECT TOP 10 * FROM orders | SELECT * FROM orders LIMIT 10 |
| Fecha de hoy | CAST(GETDATE() AS date) | CURRENT_DATE |
| Reemplazar NULL | ISNULL(referral_source, 'unknown') | COALESCE(referral_source, 'unknown') |
| Sumar 30 días | DATEADD(day, 30, signup_date) | signup_date + INTERVAL '30 days' |
| Días entre dos fechas | DATEDIFF(day, signup_date, GETDATE()) | DATE_DIFF('day', signup_date, CURRENT_DATE) |
| Primer día del mes | DATEFROMPARTS(YEAR(d), MONTH(d), 1) | DATE_TRUNC('month', signup_date) |
| Año de una fecha | YEAR(signup_date) | YEAR(signup_date) o EXTRACT(year FROM signup_date) |
| Unir texto | first_name + ' ' + last_name | first_name || ' ' || last_name |
| Longitud de un texto | LEN(city) -- ignora los espacios finales | LENGTH(city) -- los cuenta |
| Cambiar un tipo | CONVERT(int, '42') o CAST('42' AS int) | CAST('42' AS INTEGER) o '42'::INTEGER |
| 7 / 2 con números enteros | 3 (división entera) | 3.5; escribe 7 // 2 para obtener 3 |
| Buscar texto sin distinguir mayúsculas | LIKE '%gmail%' (no distingue mayúsculas por defecto) | ILIKE '%gmail%' (LIKE sí distingue mayúsculas) |
| Columna verdadero / falso | bit: 1 / 0 | BOOLEAN: true / false |
https://analystquest.com/es/resources/sql-cheat-sheet/ · Niveles gratis para practicar cada fragmento: analystquest.com
Hoja de referencia
Hoja de referencia de pandas agrupada por tarea: filtrar filas, groupby, merge, valores faltantes, limpieza de texto y fechas. Cada fragmento se ejecuta sobre un conjunto de datos real de una tienda.
Hoja de referencia
Fórmulas de Excel para analistas, agrupadas por tarea: SUMIFS, COUNTIFS, XLOOKUP, IF, limpieza de texto, fechas y duplicados, con los atajos que más tiempo ahorran.
Preparación para entrevistas
Preguntas de entrevistas reales de analista, cada una con una respuesta modelo, la respuesta que resta puntos y un nivel para practicarla. Los términos que no tengas claros están en el glosario.