Las funciones de agregación son las herramientas que convierten SQL de un lenguaje para buscar filas en uno para responder preguntas de negocio: ¿cuántos pedidos se hicieron este mes? ¿cuál es el producto más vendido? ¿qué cliente ha gastado más? Sin ellas, extraer información útil de una base de datos requeriría procesar todos los datos en la aplicación. Con ellas, el motor de base de datos hace el trabajo pesado directamente donde viven los datos.
En esta guía aprenderás todas las funciones de agregación principales, cómo usar GROUP BY para agrupar datos, cómo filtrar grupos con HAVING y cómo combinar todo esto para construir consultas analíticas reales.
El esquema de ejemplo
Todos los ejemplos usan estas tres tablas. Créalas para seguir los ejemplos en tu propia base de datos:
CREATE TABLE clientes (
id INT PRIMARY KEY AUTO_INCREMENT,
nombre VARCHAR(100) NOT NULL,
ciudad VARCHAR(50),
pais VARCHAR(50) DEFAULT 'España'
);
CREATE TABLE productos (
id INT PRIMARY KEY AUTO_INCREMENT,
nombre VARCHAR(100) NOT NULL,
categoria VARCHAR(50),
precio DECIMAL(10, 2) NOT NULL,
stock INT DEFAULT 0
);
CREATE TABLE pedidos (
id INT PRIMARY KEY AUTO_INCREMENT,
cliente_id INT REFERENCES clientes(id),
producto_id INT REFERENCES productos(id),
fecha DATE NOT NULL,
cantidad INT NOT NULL,
total DECIMAL(10, 2) NOT NULL,
estado VARCHAR(20) DEFAULT 'completado'
);
-- Datos de ejemplo
INSERT INTO clientes VALUES
(1, 'Ana García', 'Madrid', 'España'),
(2, 'Carlos López', 'Barcelona', 'España'),
(3, 'María Torres', 'Madrid', 'España'),
(4, 'Luis Martín', 'Valencia', 'España'),
(5, 'Sara Jiménez', 'Sevilla', 'España');
INSERT INTO productos VALUES
(1, 'Laptop Pro', 'Electrónica', 1299.99, 15),
(2, 'Teclado RGB', 'Accesorios', 89.99, 50),
(3, 'Monitor 4K', 'Electrónica', 499.99, 20),
(4, 'Ratón inalámbr.', 'Accesorios', 39.99, 80),
(5, 'Auriculares BT', 'Audio', 149.99, 30),
(6, 'Webcam HD', 'Accesorios', 69.99, 40);
INSERT INTO pedidos VALUES
(1, 1, 1, '2026-01-10', 1, 1299.99, 'completado'),
(2, 2, 2, '2026-01-15', 2, 179.98, 'completado'),
(3, 1, 3, '2026-01-20', 1, 499.99, 'completado'),
(4, 3, 4, '2026-02-05', 3, 119.97, 'completado'),
(5, 2, 5, '2026-02-10', 1, 149.99, 'completado'),
(6, 4, 2, '2026-02-14', 1, 89.99, 'completado'),
(7, 1, 6, '2026-02-20', 2, 139.98, 'completado'),
(8, 5, 1, '2026-03-01', 1, 1299.99, 'completado'),
(9, 3, 5, '2026-03-05', 2, 299.98, 'completado'),
(10, 2, 3, '2026-03-12', 1, 499.99, 'cancelado'),
(11, 4, 4, '2026-03-18', 2, 79.98, 'completado'),
(12, 5, 2, '2026-03-22', 1, 89.99, 'completado')COUNT: contar filas
COUNT cuenta filas. Es la función de agregación más usada y tiene tres variantes con comportamientos distintos que conviene entender bien.
-- COUNT(*): cuenta TODAS las filas, incluidas las que tienen NULLs en cualquier columna
SELECT COUNT(*) AS total_pedidos
FROM pedidos;
-- 12
-- COUNT(columna): cuenta solo las filas donde esa columna NO es NULL
SELECT COUNT(cliente_id) AS pedidos_con_cliente
FROM pedidos;
-- 12 (en este caso igual porque no hay NULLs, pero la diferencia importa cuando los hay)
-- COUNT(DISTINCT columna): cuenta valores únicos no nulos
SELECT COUNT(DISTINCT cliente_id) AS clientes_que_han_pedido
FROM pedidos;
-- 5
-- Diferencia entre COUNT(*) y COUNT(columna) con NULLs
-- Si una fila tiene cliente_id = NULL:
-- COUNT(*) → la cuenta
-- COUNT(cliente_id) → NO la cuenta-- Casos de uso reales de COUNT
-- Total de pedidos por estado
SELECT estado, COUNT(*) AS cantidad
FROM pedidos
GROUP BY estado;
-- completado | 11
-- cancelado | 1
-- Cuántos pedidos distintos ha hecho cada cliente
SELECT
c.nombre,
COUNT(p.id) AS num_pedidos
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.id
GROUP BY c.id, c.nombre
ORDER BY num_pedidos DESC;
-- Productos pedidos más de una vez (contar apariciones en pedidos)
SELECT
pr.nombre,
COUNT(pe.id) AS veces_pedido
FROM productos pr
JOIN pedidos pe ON pe.producto_id = pr.id
GROUP BY pr.id, pr.nombre
HAVING COUNT(pe.id) > 1
ORDER BY veces_pedido DESC;SUM: sumar valores
SUM suma los valores de una columna numérica. Ignora los valores NULL automáticamente. Si todas las filas son NULL, devuelve NULL, no cero.
-- Ingresos totales de todos los pedidos completados
SELECT SUM(total) AS ingresos_totales
FROM pedidos
WHERE estado = 'completado';
-- 4249.83
-- Total de unidades vendidas por producto
SELECT
pr.nombre,
SUM(pe.cantidad) AS unidades_vendidas,
SUM(pe.total) AS ingresos_generados
FROM productos pr
JOIN pedidos pe ON pe.producto_id = pr.id
WHERE pe.estado = 'completado'
GROUP BY pr.id, pr.nombre
ORDER BY ingresos_generados DESC;-- Ingresos mensuales: agrupar por año y mes
SELECT
YEAR(fecha) AS anio,
MONTH(fecha) AS mes,
COUNT(*) AS num_pedidos,
SUM(total) AS ingresos
FROM pedidos
WHERE estado = 'completado'
GROUP BY YEAR(fecha), MONTH(fecha)
ORDER BY anio, mes;
-- anio | mes | num_pedidos | ingresos
-- 2026 | 1 | 3 | 1979.96
-- 2026 | 2 | 4 | 579.92
-- 2026 | 3 | 4 | 1689.95-- SUM condicional: ingresos y pérdidas en la misma fila
-- usando CASE WHEN dentro de SUM (técnica muy útil)
SELECT
SUM(CASE WHEN estado = 'completado' THEN total ELSE 0 END) AS ingresos_reales,
SUM(CASE WHEN estado = 'cancelado' THEN total ELSE 0 END) AS pedidos_cancelados,
COUNT(CASE WHEN estado = 'completado' THEN 1 END) AS num_completados,
COUNT(CASE WHEN estado = 'cancelado' THEN 1 END) AS num_cancelados
FROM pedidos;AVG: calcular la media
AVG calcula el promedio aritmético de una columna numérica. Como SUM, ignora los NULL. Es equivalente a SUM(columna) / COUNT(columna), pero más conciso.
-- Importe medio por pedido
SELECT
ROUND(AVG(total), 2) AS ticket_medio
FROM pedidos
WHERE estado = 'completado';
-- 386.35
-- Precio medio por categoría
SELECT
categoria,
ROUND(AVG(precio), 2) AS precio_medio,
MIN(precio) AS precio_minimo,
MAX(precio) AS precio_maximo
FROM productos
GROUP BY categoria
ORDER BY precio_medio DESC;
-- categoria | precio_medio | precio_minimo | precio_maximo
-- Electrónica | 899.99 | 499.99 | 1299.99
-- Audio | 149.99 | 149.99 | 149.99
-- Accesorios | 66.66 | 39.99 | 89.99-- Ticket medio por cliente: quién compra más caro de media
SELECT
c.nombre,
COUNT(p.id) AS num_pedidos,
ROUND(AVG(p.total), 2) AS ticket_medio,
SUM(p.total) AS gasto_total
FROM clientes c
JOIN pedidos p ON p.cliente_id = c.id
WHERE p.estado = 'completado'
GROUP BY c.id, c.nombre
ORDER BY ticket_medio DESC;-- Comparar el precio de cada producto con la media de su categoría
SELECT
nombre,
categoria,
precio,
ROUND(AVG(precio) OVER (PARTITION BY categoria), 2) AS media_categoria,
ROUND(precio - AVG(precio) OVER (PARTITION BY categoria), 2) AS diferencia
FROM productos
ORDER BY categoria, diferencia DESC;MIN y MAX: valor mínimo y máximo
MIN y MAX funcionan con números, fechas y cadenas de texto (orden alfabético). Son especialmente útiles para encontrar extremos dentro de grupos.
-- Fecha del primer y último pedido de cada cliente
SELECT
c.nombre,
MIN(p.fecha) AS primer_pedido,
MAX(p.fecha) AS ultimo_pedido,
DATEDIFF(MAX(p.fecha), MIN(p.fecha)) AS dias_como_cliente
FROM clientes c
JOIN pedidos p ON p.cliente_id = c.id
GROUP BY c.id, c.nombre
ORDER BY primer_pedido;-- Producto más barato y más caro de cada categoría
SELECT
categoria,
MIN(precio) AS mas_barato,
MAX(precio) AS mas_caro,
MAX(precio) - MIN(precio) AS rango_precios
FROM productos
GROUP BY categoria;-- Pedido de mayor importe de cada cliente (sin subconsultas)
-- Usando MAX con CASE para extraer el ID del pedido máximo
SELECT
c.nombre,
MAX(p.total) AS pedido_maximo,
MIN(p.total) AS pedido_minimo,
MAX(p.fecha) AS ultimo_pedido
FROM clientes c
JOIN pedidos p ON p.cliente_id = c.id
WHERE p.estado = 'completado'
GROUP BY c.id, c.nombre;GROUP BY: agrupar filas
GROUP BY divide las filas en grupos y aplica las funciones de agregación a cada grupo por separado. Es la clave para pasar de "el total de todos los pedidos" a "el total de pedidos por cliente" o "por mes" o "por categoría".
La regla fundamental: toda columna que aparezca en el SELECT y no sea una función de agregación debe aparecer también en el GROUP BY.
-- Sin GROUP BY: un único resultado para toda la tabla
SELECT SUM(total) FROM pedidos;
-- 4749.82
-- Con GROUP BY: un resultado por cada valor único de estado
SELECT estado, SUM(total) AS total_por_estado
FROM pedidos
GROUP BY estado;
-- Con múltiples columnas en GROUP BY: un resultado por combinación única
SELECT
YEAR(fecha) AS anio,
MONTH(fecha) AS mes,
estado,
COUNT(*) AS pedidos,
SUM(total) AS total
FROM pedidos
GROUP BY YEAR(fecha), MONTH(fecha), estado
ORDER BY anio, mes, estado;-- Agrupar por expresiones calculadas
-- Pedidos por trimestre
SELECT
YEAR(fecha) AS anio,
QUARTER(fecha) AS trimestre,
COUNT(*) AS num_pedidos,
SUM(total) AS ingresos,
ROUND(AVG(total), 2) AS ticket_medio
FROM pedidos
WHERE estado = 'completado'
GROUP BY YEAR(fecha), QUARTER(fecha)
ORDER BY anio, trimestre;-- Agrupar por rangos de precio (bucketing)
SELECT
CASE
WHEN precio < 50 THEN 'Menos de 50€'
WHEN precio BETWEEN 50 AND 99 THEN '50€ - 99€'
WHEN precio BETWEEN 100 AND 499 THEN '100€ - 499€'
ELSE '500€ o más'
END AS rango_precio,
COUNT(*) AS num_productos,
ROUND(AVG(precio), 2) AS precio_medio
FROM productos
GROUP BY
CASE
WHEN precio < 50 THEN 'Menos de 50€'
WHEN precio BETWEEN 50 AND 99 THEN '50€ - 99€'
WHEN precio BETWEEN 100 AND 499 THEN '100€ - 499€'
ELSE '500€ o más'
END
ORDER BY MIN(precio);HAVING: filtrar grupos
HAVING filtra grupos después de que GROUP BY los haya formado, igual que WHERE filtra filas individuales antes de agrupar. Esta diferencia es crucial: no puedes usar funciones de agregación en el WHERE, pero sí en el HAVING.
-- ERROR: no se puede usar SUM en WHERE
SELECT cliente_id, SUM(total)
FROM pedidos
WHERE SUM(total) > 500 -- ← esto falla
GROUP BY cliente_id;
-- CORRECTO: usar HAVING para filtrar sobre el resultado de la agregación
SELECT cliente_id, SUM(total) AS gasto_total
FROM pedidos
WHERE estado = 'completado' -- WHERE filtra filas ANTES de agrupar
GROUP BY cliente_id
HAVING SUM(total) > 500; -- HAVING filtra grupos DESPUÉS de agrupar-- Clientes con más de un pedido y gasto total superior a 300€
SELECT
c.nombre,
COUNT(p.id) AS num_pedidos,
SUM(p.total) AS gasto_total
FROM clientes c
JOIN pedidos p ON p.cliente_id = c.id
WHERE p.estado = 'completado'
GROUP BY c.id, c.nombre
HAVING COUNT(p.id) > 1
AND SUM(p.total) > 300
ORDER BY gasto_total DESC;-- Categorías con precio medio superior a 100€ y más de un producto
SELECT
categoria,
COUNT(*) AS num_productos,
ROUND(AVG(precio), 2) AS precio_medio,
SUM(stock) AS stock_total
FROM productos
GROUP BY categoria
HAVING AVG(precio) > 100
AND COUNT(*) > 1;-- Meses con más de 2 pedidos completados
SELECT
YEAR(fecha) AS anio,
MONTH(fecha) AS mes,
COUNT(*) AS pedidos,
SUM(total) AS total_mes
FROM pedidos
WHERE estado = 'completado'
GROUP BY YEAR(fecha), MONTH(fecha)
HAVING COUNT(*) > 2
ORDER BY anio, mes;El orden correcto de las cláusulas SQL
SQL tiene un orden de escritura fijo y un orden de ejecución diferente. Confundirlos es la fuente de muchos errores al principio.
| Orden de escritura | Cláusula | Orden de ejecución | Qué hace |
|---|---|---|---|
| 1 | SELECT | 6 | Define qué columnas devolver |
| 2 | FROM | 1 | De qué tabla o tablas leer |
| 3 | JOIN | 2 | Combinar tablas |
| 4 | WHERE | 3 | Filtrar filas individuales |
| 5 | GROUP BY | 4 | Agrupar filas en grupos |
| 6 | HAVING | 5 | Filtrar grupos |
| 7 | ORDER BY | 7 | Ordenar el resultado final |
| 8 | LIMIT | 8 | Limitar el número de filas devueltas |
Por eso no puedes usar un alias del SELECT en el WHERE o en el GROUP BY: el SELECT se evalúa después. Sí puedes usarlo en el ORDER BY porque este se evalúa al final.
-- ERROR: el alias 'gasto_total' no existe todavía cuando se evalúa HAVING en algunos motores
SELECT cliente_id, SUM(total) AS gasto_total
FROM pedidos
GROUP BY cliente_id
HAVING gasto_total > 500; -- funciona en MySQL pero no en SQL estándar
-- CORRECTO y portable: repetir la expresión
SELECT cliente_id, SUM(total) AS gasto_total
FROM pedidos
GROUP BY cliente_id
HAVING SUM(total) > 500;
-- Sí funciona en ORDER BY (se evalúa al final)
SELECT cliente_id, SUM(total) AS gasto_total
FROM pedidos
GROUP BY cliente_id
ORDER BY gasto_total DESC; -- ← aquí el alias sí está disponibleROLLUP y CUBE: totales y subtotales automáticos
GROUP BY ROLLUP genera automáticamente subtotales y el total general además de los grupos normales. Es muy útil para informes.
-- Ingresos por ciudad y mes, con subtotales por ciudad y total general
SELECT
COALESCE(c.ciudad, 'TOTAL GENERAL') AS ciudad,
COALESCE(CAST(MONTH(p.fecha) AS CHAR), 'Subtotal') AS mes,
COUNT(*) AS pedidos,
SUM(p.total) AS ingresos
FROM pedidos p
JOIN clientes c ON c.id = p.cliente_id
WHERE p.estado = 'completado'
GROUP BY c.ciudad, MONTH(p.fecha) WITH ROLLUP
ORDER BY ciudad, mes;
-- Resultado incluye:
-- Madrid | 1 | ... | ... ← datos reales
-- Madrid | 2 | ... | ...
-- Madrid | NULL | ... | ... ← subtotal de Madrid
-- TOTAL GENERAL | NULL | ... | ... ← total globalFunciones de ventana: agregación sin perder filas
Las funciones de ventana (window functions) aplican agregaciones sin colapsar las filas en grupos. Con GROUP BY obtienes una fila por grupo; con las funciones de ventana obtienes todas las filas con el resultado de la agregación añadido como columna extra.
-- Comparar el total de cada pedido con el total global y el total del cliente
SELECT
c.nombre,
p.fecha,
p.total,
SUM(p.total) OVER () AS total_global,
SUM(p.total) OVER (PARTITION BY c.id) AS total_cliente,
ROUND(p.total / SUM(p.total) OVER () * 100, 1) AS pct_del_total
FROM pedidos p
JOIN clientes c ON c.id = p.cliente_id
WHERE p.estado = 'completado'
ORDER BY c.nombre, p.fecha;-- Ranking de clientes por gasto dentro de cada ciudad
SELECT
c.nombre,
c.ciudad,
SUM(p.total) AS gasto_total,
RANK() OVER (
PARTITION BY c.ciudad
ORDER BY SUM(p.total) DESC
) AS ranking_en_ciudad
FROM clientes c
JOIN pedidos p ON p.cliente_id = c.id
WHERE p.estado = 'completado'
GROUP BY c.id, c.nombre, c.ciudad
ORDER BY c.ciudad, ranking_en_ciudad;-- Total acumulado de ingresos por fecha (running total)
SELECT
fecha,
SUM(total) AS ingresos_dia,
SUM(SUM(total)) OVER (ORDER BY fecha) AS ingresos_acumulados
FROM pedidos
WHERE estado = 'completado'
GROUP BY fecha
ORDER BY fecha;Consultas analíticas completas
Aquí tienes tres consultas que integran todo lo aprendido y resuelven preguntas de negocio reales.
Informe completo de clientes
SELECT
c.nombre,
c.ciudad,
COUNT(p.id) AS total_pedidos,
SUM(p.total) AS gasto_total,
ROUND(AVG(p.total), 2) AS ticket_medio,
MIN(p.fecha) AS primer_pedido,
MAX(p.fecha) AS ultimo_pedido,
DATEDIFF(MAX(p.fecha), MIN(p.fecha)) AS dias_activo,
COUNT(CASE WHEN p.estado = 'cancelado' THEN 1 END) AS pedidos_cancelados
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.id
GROUP BY c.id, c.nombre, c.ciudad
ORDER BY gasto_total DESC;Análisis de productos: ventas, ingresos y rotación de stock
SELECT
pr.nombre,
pr.categoria,
pr.precio,
pr.stock,
COALESCE(COUNT(pe.id), 0) AS veces_pedido,
COALESCE(SUM(pe.cantidad), 0) AS unidades_vendidas,
COALESCE(SUM(pe.total), 0) AS ingresos_generados,
ROUND(
COALESCE(SUM(pe.cantidad), 0) /
NULLIF(pr.stock + COALESCE(SUM(pe.cantidad), 0), 0) * 100, 1
) AS tasa_rotacion_pct
FROM productos pr
LEFT JOIN pedidos pe ON pe.producto_id = pr.id
AND pe.estado = 'completado'
GROUP BY pr.id, pr.nombre, pr.categoria, pr.precio, pr.stock
ORDER BY ingresos_generados DESC;Dashboard mensual con variación respecto al mes anterior
WITH metricas_mensuales AS (
SELECT
YEAR(fecha) AS anio,
MONTH(fecha) AS mes,
COUNT(*) AS pedidos,
SUM(total) AS ingresos,
ROUND(AVG(total), 2) AS ticket_medio,
COUNT(DISTINCT cliente_id) AS clientes_activos
FROM pedidos
WHERE estado = 'completado'
GROUP BY YEAR(fecha), MONTH(fecha)
)
SELECT
anio,
mes,
pedidos,
ingresos,
ticket_medio,
clientes_activos,
LAG(ingresos) OVER (ORDER BY anio, mes) AS ingresos_mes_anterior,
ROUND(
(ingresos - LAG(ingresos) OVER (ORDER BY anio, mes))
/ NULLIF(LAG(ingresos) OVER (ORDER BY anio, mes), 0) * 100, 1
) AS variacion_pct
FROM metricas_mensuales
ORDER BY anio, mes;Errores frecuentes y cómo evitarlos
Columna en SELECT que no está en GROUP BY. Es el error más común al empezar. Si seleccionas una columna que no está en GROUP BY y no es una función de agregación, el resultado es ambiguo (¿qué valor de esa columna debería mostrar el grupo?). MySQL lo permite por defecto pero da resultados impredecibles. PostgreSQL y SQL estándar lo rechazan con un error.
-- ❌ Mal: nombre no está en GROUP BY ni es una agregación
SELECT nombre, ciudad, COUNT(*) AS pedidos
FROM clientes c
JOIN pedidos p ON p.cliente_id = c.id
GROUP BY ciudad; -- ¿qué nombre mostrar si hay varios por ciudad?
-- ✅ Bien: incluir todas las columnas no agregadas en GROUP BY
SELECT c.nombre, c.ciudad, COUNT(p.id) AS pedidos
FROM clientes c
JOIN pedidos p ON p.cliente_id = c.id
GROUP BY c.id, c.nombre, c.ciudad;Usar WHERE en lugar de HAVING para condiciones sobre agregaciones.
-- ❌ Mal: WHERE no puede usar SUM()
SELECT cliente_id, SUM(total)
FROM pedidos
WHERE SUM(total) > 500
GROUP BY cliente_id;
-- ✅ Bien
SELECT cliente_id, SUM(total) AS gasto
FROM pedidos
GROUP BY cliente_id
HAVING SUM(total) > 500;Olvidar que AVG ignora los NULL. Si quieres incluir los ceros en el promedio, debes convertir los NULL en cero con COALESCE.
-- Si algunos clientes no tienen pedidos (NULL tras el LEFT JOIN):
-- AVG calcula solo sobre los que SÍ tienen pedidos
SELECT AVG(p.total) FROM clientes c LEFT JOIN pedidos p ON p.cliente_id = c.id;
-- Para incluir los clientes sin pedidos como 0 en la media:
SELECT AVG(COALESCE(p.total, 0))
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.id;COUNT(*) con LEFT JOIN cuenta filas aunque no haya match. Si usas LEFT JOIN y quieres contar solo las filas que sí tienen coincidencia, usa COUNT(columna_de_la_tabla_derecha) en lugar de COUNT(*).
-- ❌ Cuenta 1 para clientes sin pedidos (la fila existe, con NULL en columnas de pedidos)
SELECT c.nombre, COUNT(*) AS pedidos
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.id
GROUP BY c.id, c.nombre;
-- ✅ COUNT(p.id) devuelve 0 para clientes sin pedidos (porque p.id es NULL)
SELECT c.nombre, COUNT(p.id) AS pedidos
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.id
GROUP BY c.id, c.nombre;Resumen
COUNT(*)cuenta todas las filas.COUNT(columna)excluye NULLs.COUNT(DISTINCT columna)cuenta valores únicos.SUMsuma valores numéricos ignorando NULLs. UsaCASE WHENdentro deSUMpara sumas condicionales.AVGcalcula la media ignorando NULLs. UsaCOALESCEsi quieres incluir los ceros en el promedio.MINyMAXfuncionan con números, fechas y texto. Útiles para encontrar extremos dentro de grupos.GROUP BYagrupa filas. Toda columna no agregada delSELECTdebe estar en elGROUP BY.WHEREfiltra filas antes de agrupar.HAVINGfiltra grupos después de agrupar. Nunca uses funciones de agregación en elWHERE.- Las funciones de ventana (
OVER) aplican agregaciones sin colapsar las filas, permitiendo totales acumulados, rankings y comparativas en la misma consulta. - Las CTEs (
WITH) hacen las consultas analíticas complejas mucho más legibles y mantenibles.
No hay comentarios todavía. Sé el primero en compartir tu opinión.