En esta página
Funciones de agregación
Funciones de agregación
Las funciones de agregación transforman un conjunto de filas en un único valor resumen. Son la base de cualquier sistema de reportes, dashboard analítico o estadística de negocio. Con ellas puedes responder preguntas como: "¿cuántos pedidos se realizaron este mes?", "¿cuál es el producto más vendido?", "¿qué categoría genera más ingresos?".
Las cinco funciones de agregación fundamentales
-- Datos de ejemplo: tabla ventas
CREATE TABLE ventas (
id SERIAL PRIMARY KEY,
producto_id INTEGER NOT NULL,
cantidad INTEGER NOT NULL CHECK (cantidad > 0),
precio_unit NUMERIC(10,2) NOT NULL,
vendedor_id INTEGER,
vendido_en TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
INSERT INTO ventas (producto_id, cantidad, precio_unit, vendedor_id) VALUES
(1, 2, 1299.99, 1), (2, 5, 199.99, 1), (3, 1, 89.99, 2),
(4, 10, 19.99, 2), (5, 3, 59.99, 1), (6, 7, 35.99, NULL),
(7, 2, 42.99, 3), (8, 4, 34.99, 3), (9, 6, 24.99, 1),
(10, 2, 119.99, 2),(1, 1, 1299.99, 3),(2, 3, 199.99, 1);COUNT — Contar filas
-- Contar TODAS las filas (incluyendo NULLs en otras columnas)
SELECT COUNT(*) AS total_ventas FROM ventas;
-- Contar valores no-NULL en una columna específica
SELECT COUNT(vendedor_id) AS ventas_con_vendedor FROM ventas;
-- Omite las filas donde vendedor_id es NULL
-- COUNT DISTINCT: contar valores únicos
SELECT COUNT(DISTINCT producto_id) AS productos_distintos_vendidos FROM ventas;
SELECT COUNT(DISTINCT vendedor_id) AS vendedores_activos FROM ventas;
-- Contar con filtro (veremos FILTER más adelante)
SELECT
COUNT(*) AS total,
COUNT(*) FILTER (WHERE vendedor_id IS NOT NULL) AS con_vendedor,
COUNT(*) FILTER (WHERE vendedor_id IS NULL) AS sin_vendedor
FROM ventas;SUM — Sumar valores
-- Total de unidades vendidas
SELECT SUM(cantidad) AS total_unidades FROM ventas;
-- Total de ingresos (precio × cantidad)
SELECT SUM(cantidad * precio_unit) AS total_ingresos FROM ventas;
-- SUM ignora NULLs (si vendedor_id tuviera comisión, NULL no cuenta)
-- Para reemplazar NULL con 0 antes de sumar, usa COALESCE
SELECT SUM(COALESCE(vendedor_id, 0)) FROM ventas; -- No muy útil aquí, solo ilustrativoAVG — Promedio
-- Precio promedio de venta
SELECT AVG(precio_unit) AS precio_promedio FROM ventas;
SELECT ROUND(AVG(precio_unit), 2) AS precio_promedio FROM ventas;
-- Promedio de unidades por transacción
SELECT ROUND(AVG(cantidad), 1) AS promedio_unidades FROM ventas;MIN y MAX — Mínimo y máximo
-- Rango de precios en las ventas
SELECT
MIN(precio_unit) AS precio_minimo,
MAX(precio_unit) AS precio_maximo,
MAX(precio_unit) - MIN(precio_unit) AS diferencia
FROM ventas;
-- Cuándo fue la primera y última venta
SELECT
MIN(vendido_en) AS primera_venta,
MAX(vendido_en) AS ultima_venta
FROM ventas;GROUP BY — Agrupar resultados
GROUP BY divide las filas en grupos y aplica la función de agregación a cada grupo por separado:
-- Ventas por producto
SELECT
producto_id,
COUNT(*) AS num_transacciones,
SUM(cantidad) AS total_unidades,
SUM(cantidad * precio_unit) AS ingresos_totales,
ROUND(AVG(precio_unit), 2) AS precio_promedio
FROM ventas
GROUP BY producto_id
ORDER BY ingresos_totales DESC;
-- Ventas por vendedor (incluyendo NULL como "sin vendedor")
SELECT
COALESCE(vendedor_id::TEXT, 'Sin vendedor') AS vendedor,
COUNT(*) AS num_ventas,
SUM(cantidad * precio_unit) AS total_vendido
FROM ventas
GROUP BY vendedor_id
ORDER BY total_vendido DESC NULLS LAST;[!warning] Regla de ORO de GROUP BY: toda columna en el SELECT que NO sea una función de agregación DEBE aparecer en el GROUP BY. PostgreSQL lanzará un error si violas esta regla. Si quieres mostrar el nombre del vendedor junto con las estadísticas, necesitas hacer un JOIN con la tabla de vendedores, o usar una subconsulta.
GROUP BY con múltiples columnas
-- Agrupar por producto Y vendedor
SELECT
producto_id,
vendedor_id,
SUM(cantidad * precio_unit) AS ingresos
FROM ventas
GROUP BY producto_id, vendedor_id
ORDER BY producto_id, ingresos DESC;HAVING — Filtrar grupos
WHERE filtra filas individuales antes de agrupar. HAVING filtra grupos después de agrupar:
-- Solo productos con más de 2 transacciones
SELECT
producto_id,
COUNT(*) AS num_transacciones,
SUM(cantidad * precio_unit) AS ingresos
FROM ventas
GROUP BY producto_id
HAVING COUNT(*) > 2
ORDER BY ingresos DESC;
-- Vendedores con más de 500€ en ventas totales
SELECT
vendedor_id,
SUM(cantidad * precio_unit) AS total
FROM ventas
WHERE vendedor_id IS NOT NULL -- WHERE filtra ANTES de agrupar
GROUP BY vendedor_id
HAVING SUM(cantidad * precio_unit) > 500 -- HAVING filtra DESPUÉS de agrupar
ORDER BY total DESC;
-- Combinando WHERE y HAVING
SELECT
producto_id,
SUM(cantidad) AS unidades
FROM ventas
WHERE vendido_en >= NOW() - INTERVAL '30 days' -- Solo ventas del último mes
GROUP BY producto_id
HAVING SUM(cantidad) >= 3 -- Con al menos 3 unidades
ORDER BY unidades DESC;FILTER — Agregación condicional
FILTER permite calcular múltiples agregaciones con diferentes condiciones en una sola consulta:
-- Dashboard de ventas por período en una sola consulta
SELECT
COUNT(*) AS total_ventas,
COUNT(*) FILTER (WHERE vendido_en >= CURRENT_DATE - 7) AS ventas_ultima_semana,
SUM(cantidad * precio_unit) AS ingresos_totales,
SUM(cantidad * precio_unit)
FILTER (WHERE vendedor_id IS NOT NULL) AS ingresos_con_vendedor,
ROUND(AVG(precio_unit), 2) AS precio_promedio,
ROUND(AVG(precio_unit) FILTER (WHERE precio_unit > 100), 2) AS precio_promedio_premium
FROM ventas;Funciones de texto
PostgreSQL ofrece decenas de funciones para manipular cadenas de texto:
-- Funciones básicas de texto
SELECT
UPPER('hola mundo') AS mayusculas, -- 'HOLA MUNDO'
LOWER('HOLA MUNDO') AS minusculas, -- 'hola mundo'
LENGTH('PostgreSQL') AS longitud, -- 10
TRIM(' espacios ') AS sin_espacios, -- 'espacios'
LTRIM(' izquierda') AS sin_izq, -- 'izquierda'
RTRIM('derecha ') AS sin_der, -- 'derecha'
LPAD('42', 5, '0') AS relleno_izq, -- '00042'
RPAD('SQL', 6, '.') AS relleno_der, -- 'SQL...'
SUBSTRING('PostgreSQL', 1, 4) AS subcadena, -- 'Post'
REPLACE('hola mundo', 'mundo', 'SQL') AS reemplazo,-- 'hola SQL'
SPLIT_PART('a,b,c', ',', 2) AS parte, -- 'b'
CONCAT('Hola', ' ', 'mundo') AS concatenar, -- 'Hola mundo'
'Hola' || ' ' || 'mundo' AS concat_operador; -- 'Hola mundo'
-- Búsqueda y posición
SELECT
POSITION('SQL' IN 'PostgreSQL') AS posicion, -- 8
STRPOS('PostgreSQL', 'gre') AS posicion2, -- 4
STARTS_WITH('PostgreSQL', 'Post') AS comienza_con; -- true
-- Ejemplo práctico: formatear nombres de productos
SELECT
nombre,
INITCAP(LOWER(nombre)) AS nombre_formateado, -- Capitaliza cada palabra
LEFT(nombre, 20) || CASE WHEN LENGTH(nombre) > 20 THEN '...' ELSE '' END AS nombre_corto
FROM productos;Funciones de fecha y hora
-- Funciones de fecha actuales
SELECT
NOW() AS ahora_con_tz,
CURRENT_TIMESTAMP AS timestamp_actual,
CURRENT_DATE AS solo_fecha,
CURRENT_TIME AS solo_hora;
-- Extracción de partes de una fecha
SELECT
vendido_en,
EXTRACT(YEAR FROM vendido_en) AS año,
EXTRACT(MONTH FROM vendido_en) AS mes,
EXTRACT(DOW FROM vendido_en) AS dia_semana, -- 0=domingo, 6=sábado
DATE_PART('hour', vendido_en) AS hora,
DATE_TRUNC('month', vendido_en) AS inicio_mes,
DATE_TRUNC('week', vendido_en) AS inicio_semana
FROM ventas;
-- Aritmética de fechas
SELECT
NOW() + INTERVAL '7 days' AS en_una_semana,
NOW() - INTERVAL '1 month' AS hace_un_mes,
NOW()::DATE - '2024-01-01' AS dias_desde_inicio_año,
AGE(NOW(), '1990-05-15') AS edad_desde_fecha;
-- Ventas agrupadas por mes
SELECT
DATE_TRUNC('month', vendido_en) AS mes,
COUNT(*) AS num_ventas,
SUM(cantidad * precio_unit) AS ingresos
FROM ventas
GROUP BY DATE_TRUNC('month', vendido_en)
ORDER BY mes DESC;COALESCE — Manejar valores NULL
COALESCE devuelve el primer valor no-NULL de su lista de argumentos:
-- Reemplazar NULL con un valor por defecto
SELECT
nombre,
COALESCE(descripcion, 'Sin descripción') AS descripcion,
COALESCE(descripcion, nombre) AS desc_o_nombre -- Usa nombre si no hay desc
FROM productos;
-- Muy útil en cálculos donde NULL propagaría el error
SELECT
vendedor_id,
COALESCE(vendedor_id, 0) + 100 AS id_ajustado
FROM ventas;
-- En agregaciones con valores opcionales
SELECT
SUM(COALESCE(descuento, 0)) AS total_descuentos
FROM pedidos;NULLIF — Evitar división por cero
NULLIF(a, b) devuelve NULL si a = b, de lo contrario devuelve a. El uso más común es evitar divisiones por cero:
-- Sin NULLIF: error si total_ventas = 0
-- SELECT ingresos / total_ventas FROM resumen;
-- Con NULLIF: devuelve NULL en lugar de error
SELECT
producto_id,
SUM(cantidad * precio_unit) AS ingresos,
COUNT(*) AS ventas,
SUM(cantidad * precio_unit) / NULLIF(COUNT(*), 0) AS promedio_por_venta
FROM ventas
GROUP BY producto_id;CASE WHEN — Lógica condicional
CASE WHEN es el equivalente SQL de if-else y permite crear columnas calculadas con lógica condicional:
-- CASE simple (comparación de igualdad)
SELECT
nombre,
precio,
CASE categoria_id
WHEN 1 THEN 'Electrónica'
WHEN 2 THEN 'Ropa'
WHEN 3 THEN 'Libros'
ELSE 'Otra'
END AS categoria_nombre
FROM productos;
-- CASE buscado (condiciones arbitrarias)
SELECT
nombre,
precio,
CASE
WHEN precio < 30 THEN 'Económico'
WHEN precio < 100 THEN 'Rango medio'
WHEN precio < 500 THEN 'Premium'
ELSE 'Lujo'
END AS segmento_precio,
CASE
WHEN stock = 0 THEN 'Agotado'
WHEN stock < 10 THEN 'Stock bajo'
WHEN stock BETWEEN 10 AND 50 THEN 'Stock normal'
ELSE 'Stock alto'
END AS estado_stock
FROM productos
ORDER BY precio DESC;
-- CASE en agregaciones: pivot manual
SELECT
DATE_TRUNC('month', vendido_en) AS mes,
SUM(CASE WHEN vendedor_id = 1 THEN cantidad * precio_unit ELSE 0 END) AS vendedor_1,
SUM(CASE WHEN vendedor_id = 2 THEN cantidad * precio_unit ELSE 0 END) AS vendedor_2,
SUM(CASE WHEN vendedor_id = 3 THEN cantidad * precio_unit ELSE 0 END) AS vendedor_3,
SUM(CASE WHEN vendedor_id IS NULL THEN cantidad * precio_unit ELSE 0 END) AS sin_vendedor,
SUM(cantidad * precio_unit) AS total
FROM ventas
GROUP BY DATE_TRUNC('month', vendido_en)
ORDER BY mes;[!tip] CASE WHEN en GROUP BY: puedes usar expresiones CASE dentro de GROUP BY para crear categorías dinámicas. Por ejemplo,
GROUP BY CASE WHEN precio < 100 THEN 'bajo' ELSE 'alto' ENDagrupa los productos en dos segmentos sin necesidad de tener esa columna en la tabla.
Consulta analítica completa
Combinando todo lo aprendido, aquí un reporte de ventas completo:
-- Reporte ejecutivo de ventas por producto
SELECT
p.id AS producto_id,
p.nombre AS producto,
c.nombre AS categoria,
COUNT(v.id) AS num_transacciones,
COALESCE(SUM(v.cantidad), 0) AS total_unidades,
COALESCE(ROUND(SUM(v.cantidad * v.precio_unit), 2), 0) AS ingresos_totales,
COALESCE(ROUND(AVG(v.precio_unit), 2), 0) AS precio_promedio,
CASE
WHEN COALESCE(SUM(v.cantidad * v.precio_unit), 0) = 0 THEN 'Sin ventas'
WHEN SUM(v.cantidad * v.precio_unit) < 100 THEN 'Bajo'
WHEN SUM(v.cantidad * v.precio_unit) < 1000 THEN 'Medio'
ELSE 'Alto'
END AS nivel_ventas
FROM productos p
LEFT JOIN categorias c ON p.categoria_id = c.id
LEFT JOIN ventas v ON p.id = v.producto_id
GROUP BY p.id, p.nombre, c.nombre
HAVING COALESCE(SUM(v.cantidad * v.precio_unit), 0) > 0
ORDER BY ingresos_totales DESC;Las funciones de agregación junto con GROUP BY y HAVING son la base del análisis de datos en SQL. En la siguiente lección aprenderemos los JOINs, que nos permiten combinar datos de múltiples tablas en una sola consulta.
nextSteps:
- slug: joins-completo label: "Siguiente: JOINs completo"
Inicia sesión para guardar tu progreso