Saltar al contenido principal

SQL — Avanzado 🟡

Window Functions

Las funciones de ventana operan sobre un conjunto de filas relacionadas sin colapsar el resultado.

-- ROW_NUMBER: número de fila dentro de la partición
SELECT
Nombre,
Precio,
CategoriaId,
ROW_NUMBER() OVER (PARTITION BY CategoriaId ORDER BY Precio DESC) AS RankEnCategoria
FROM Productos;

-- RANK vs DENSE_RANK
-- RANK: 1, 2, 2, 4 (salta números si hay empate)
-- DENSE_RANK: 1, 2, 2, 3 (no salta)
SELECT
Nombre,
Precio,
RANK() OVER (ORDER BY Precio DESC) AS Ranking,
DENSE_RANK() OVER (ORDER BY Precio DESC) AS RankingDenso
FROM Productos;

-- Agregar con OVER (sin GROUP BY)
SELECT
Nombre,
Precio,
AVG(Precio) OVER () AS PromedioGlobal,
AVG(Precio) OVER (PARTITION BY CategoriaId) AS PromedioCategoria,
Precio - AVG(Precio) OVER (PARTITION BY CategoriaId) AS DiferenciaDelPromedio
FROM Productos;

-- LAG y LEAD: acceder a filas anteriores/posteriores
SELECT
Fecha,
Ventas,
LAG(Ventas, 1) OVER (ORDER BY Fecha) AS VentasAyer,
Ventas - LAG(Ventas, 1) OVER (ORDER BY Fecha) AS Diferencia
FROM VentasDiarias;

CTEs (Common Table Expressions)

-- CTE básica
WITH ProductosCostosos AS (
SELECT Id, Nombre, Precio
FROM Productos
WHERE Precio > 500
),
CategoriasConCostosos AS (
SELECT DISTINCT c.Nombre AS Categoria
FROM Categorias c
INNER JOIN ProductosCostosos p ON p.CategoriaId = c.Id
)
SELECT * FROM CategoriasConCostosos;

-- CTE recursiva (jerarquías, árboles)
WITH EmpleadosJerarquia AS (
-- Caso base: empleados sin jefe (CEO)
SELECT Id, Nombre, JefeId, 0 AS Nivel
FROM Empleados
WHERE JefeId IS NULL

UNION ALL

-- Caso recursivo
SELECT e.Id, e.Nombre, e.JefeId, h.Nivel + 1
FROM Empleados e
INNER JOIN EmpleadosJerarquia h ON e.JefeId = h.Id
)
SELECT * FROM EmpleadosJerarquia ORDER BY Nivel, Nombre;

Índices avanzados

-- Índice cubriente (covering index) — include columns
-- El query puede resolverse solo con el índice sin acceder a la tabla
CREATE INDEX IX_Productos_Cat_Precio
ON Productos (CategoriaId, Precio)
INCLUDE (Nombre, Stock); -- columnas adicionales en las hojas del B-tree

-- Filtered index — para columnas con muchos NULLs o valores específicos
CREATE INDEX IX_Pedidos_Pendientes
ON Pedidos (FechaCreacion)
WHERE Estado = 'Pendiente'; -- solo indexa pendientes

-- Índice columnstore — para analítica y reportes
CREATE COLUMNSTORE INDEX IX_Ventas_Columnstore
ON Ventas (Fecha, ProductoId, Cantidad, Monto);

Cómo analizar performance de queries

-- Ver el plan de ejecución
SET STATISTICS IO ON;
SET STATISTICS TIME ON;

SELECT * FROM Productos WHERE CategoriaId = 1 AND Precio > 100;

-- Buscar: Table Scan (malo) vs Index Seek (bueno)
-- Ver: Estimated Rows, Actual Rows (si difieren mucho, estadísticas desactualizadas)

-- Actualizar estadísticas
UPDATE STATISTICS Productos;

Stored Procedures vs Queries Dinámicas

-- Stored Procedure
CREATE PROCEDURE sp_BuscarProductos
@Termino NVARCHAR(100),
@CategoriaId INT = NULL,
@PrecioMin DECIMAL(10,2) = 0,
@PrecioMax DECIMAL(10,2) = 9999999
AS
BEGIN
SELECT p.Id, p.Nombre, p.Precio, c.Nombre AS Categoria
FROM Productos p
INNER JOIN Categorias c ON p.CategoriaId = c.Id
WHERE p.Nombre LIKE '%' + @Termino + '%'
AND (@CategoriaId IS NULL OR p.CategoriaId = @CategoriaId)
AND p.Precio BETWEEN @PrecioMin AND @PrecioMax;
END

-- Ejecución
EXEC sp_BuscarProductos @Termino = 'laptop', @CategoriaId = 1;

Optimización de queries comunes

-- ❌ SELECT * (trae columnas innecesarias)
SELECT * FROM Productos;

-- ✅ Solo las columnas necesarias
SELECT Id, Nombre, Precio FROM Productos;

-- ❌ Función en WHERE (no puede usar índice)
SELECT * FROM Pedidos WHERE YEAR(FechaCreacion) = 2024;

-- ✅ Rango de fechas (usa índice)
SELECT * FROM Pedidos
WHERE FechaCreacion >= '2024-01-01' AND FechaCreacion < '2025-01-01';

-- ❌ OR puede deshabilitar índices
SELECT * FROM Clientes WHERE Nombre = 'Juan' OR Email = 'juan@example.com';

-- ✅ UNION para permitir uso de índices separados
SELECT * FROM Clientes WHERE Nombre = 'Juan'
UNION
SELECT * FROM Clientes WHERE Email = 'juan@example.com';

Niveles de aislamiento de transacciones

NivelDirty ReadNon-Repeatable ReadPhantom Read
Read Uncommitted✅ posible✅ posible✅ posible
Read Committed (default)✅ posible✅ posible
Repeatable Read✅ posible
Serializable
Snapshot
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; -- default SQL Server
SET TRANSACTION ISOLATION LEVEL SNAPSHOT; -- MVCC, menos bloqueos

Preguntas frecuentes de entrevista 🎯

1. ¿Cuál es la diferencia entre un índice clustered y non-clustered?

Clustered: determina el orden físico de los datos en disco (solo uno por tabla, generalmente la PK). Non-clustered: estructura separada con punteros a los datos (puede haber varios). Lecturas por PK son muy rápidas en clustered.

2. ¿Qué es un deadlock y cómo se previene?

Ocurre cuando dos transacciones se bloquean mutuamente esperando recursos que la otra tiene. Se previene: accediendo a recursos en el mismo orden, usando transacciones cortas, con NOLOCK (cuidado con dirty reads), o con niveles de aislamiento SNAPSHOT.

3. ¿Cuándo usarías una CTE vs una subquery vs una tabla temporal?

CTE: legibilidad, queries recursivas, una sola vez. Subquery: simple, una sola referencia. Tabla temporal (#tabla): cuando necesitas referenciar múltiples veces el mismo resultado o realizar operaciones sobre él.

4. ¿Qué es el problema de N+1 en SQL?

Ejecutar 1 query principal y luego N queries adicionales (una por cada resultado). Ej: traer 100 pedidos y luego hacer 100 queries para los items de cada pedido. Se soluciona con JOINs o eager loading.