Saltar al contenido
funciones-agregadas.sql · devschool

COUNT, SUM, AVG, MIN y MAX en SQL

Lección 8 de 22 · 10 min de lectura · Actualizado el

En esta lección
  1. COUNT: contar
  2. SUM: sumar
  3. AVG: media
  4. MIN y MAX
  5. Varias a la vez
  6. Los agregados y NULL
  7. El error de mezclar columnas sueltas
  8. Errores frecuentes
  9. Resumen

Las funciones de agregación convierten muchas filas en un solo valor: cuántas hay, cuánto suman, cuál es la media, cuál es la mayor. Son la base de cualquier informe: ventas del mes, nota media de la clase, número de socios del gimnasio, último pedido de un cliente.

A diferencia de las funciones de texto, números y fechas, que devuelven un resultado por cada fila, estas resumen todas las filas en una. En esta lección las verás de una en una, cómo se comportan con textos, fechas y NULL, y el error más típico al usarlas.

COUNT: contar

SELECT COUNT(*) FROM clientes;
COUNT(*)
5

COUNT tiene tres formas, y cada una responde a una pregunta distinta:

FormaQué cuenta
COUNT(*)Todas las filas, tengan lo que tengan
COUNT(columna)Las filas donde esa columna no es NULL
COUNT(DISTINCT columna)Los valores distintos de esa columna (sin contar NULL)

Con las tablas del curso, COUNT(DISTINCT) responde a preguntas como “¿en cuántas ciudades tenemos clientes?” o “¿cuántos clientes distintos han comprado?”:

SELECT COUNT(DISTINCT ciudad) AS ciudades FROM clientes;
ciudades
3
SELECT COUNT(*) AS pedidos,
       COUNT(DISTINCT cliente_id) AS clientes_que_compran
FROM pedidos;
pedidosclientes_que_compran
54

Hay 5 pedidos, pero Ana ha hecho dos, así que solo 4 clientes distintos han comprado.

Las tres formas con una tabla con NULL

Las tablas clientes y pedidos no tienen huecos. Para ver la diferencia entre COUNT(*) y COUNT(columna) crearemos una tabla de valoraciones de productos, donde algunos clientes han escrito una opinión pero no han puesto nota:

CREATE TABLE valoraciones (
  id INT PRIMARY KEY,
  cliente_id INT REFERENCES clientes(id),
  producto VARCHAR(100) NOT NULL,
  puntuacion INT
);

INSERT INTO valoraciones VALUES
  (1, 1, 'Pies de gato', 5),
  (2, 2, 'Pies de gato', 4),
  (3, 1, 'Arnés', 4),
  (4, 3, 'Mochila 30 L', NULL),
  (5, 5, 'Arnés', 2),
  (6, 3, 'Pies de gato', NULL);
SELECT COUNT(*)                   AS filas,
       COUNT(puntuacion)          AS puntuadas,
       COUNT(DISTINCT producto)   AS productos,
       COUNT(DISTINCT puntuacion) AS notas_distintas
FROM valoraciones;
filaspuntuadasproductosnotas_distintas
6433
  • COUNT(*): 6 valoraciones.
  • COUNT(puntuacion): solo 4 tienen nota; las dos con NULL no cuentan.
  • COUNT(DISTINCT producto): pies de gato, arnés y mochila.
  • COUNT(DISTINCT puntuacion): los valores 5, 4 y 2. El 4 repetido cuenta una vez y el NULL no cuenta.

Combinado con WHERE, cuenta solo las filas que cumplen la condición. El WHERE se aplica antes de contar:

SELECT COUNT(*) AS clientes_santander
FROM clientes
WHERE ciudad = 'Santander';   -- 2

Consejo: para saber si hay filas, COUNT(*) es lo más claro. No uses COUNT(1) ni COUNT(id) pensando que son más rápidos: en los sistemas actuales dan el mismo rendimiento.

SUM: sumar

SELECT SUM(importe) AS total_ventas FROM pedidos;
total_ventas
271.74

(45.00 + 120.50 + 12.99 + 60.00 + 33.25)

SUM solo tiene sentido con números. Dentro puedes poner una expresión, que se calcula fila a fila antes de sumar. Por ejemplo, el total de ventas con un 21 % de IVA:

SELECT ROUND(SUM(importe * 1.21), 2) AS total_con_iva
FROM pedidos;
total_con_iva
328.81

Cuando no hay filas que sumar

Si el WHERE no deja pasar ninguna fila, SUM no devuelve 0 sino NULL: no hay nada que sumar. Por ejemplo, las ventas del 2 de septiembre, un día sin pedidos:

SELECT SUM(importe) AS total,
       COALESCE(SUM(importe), 0) AS total_seguro
FROM pedidos
WHERE fecha = '2026-09-02';
totaltotal_seguro
NULL0

En un informe, un NULL en lugar de un 0 descuadra los cálculos posteriores. COALESCE lo arregla. En cambio, COUNT sí devuelve 0 cuando no hay filas.

AVG: media

SELECT AVG(edad) AS edad_media FROM clientes;
edad_media
29.4

(34 + 22 + 45 + 17 + 29 = 147, entre 5)

Aunque edad sea un entero, AVG devuelve decimales en todos los sistemas; no pasa como con la división entera de /. Eso sí, cada sistema lo muestra a su manera: MySQL escribe 29.4000 y PostgreSQL 29.4000000000000000.

Redondear la media con ROUND

Una media casi nunca sale redonda, y con dinero quieres dos decimales:

SELECT AVG(importe) AS media,
       ROUND(AVG(importe), 2) AS ticket_medio
FROM pedidos;
mediaticket_medio
54.34854.35

Fíjate en el orden: primero se calcula la media y después se redondea el resultado, ROUND(AVG(...), 2). Si escribieras AVG(ROUND(importe, 2)) redondearías cada importe antes de hacer la media, que no es lo mismo.

MIN y MAX

SELECT MIN(importe) AS menor, MAX(importe) AS mayor
FROM pedidos;
menormayor
12.99120.50

Como devuelven un valor, puedes operar con ellos. El rango de importes:

SELECT MAX(importe) - MIN(importe) AS diferencia FROM pedidos;   -- 107.51

Con fechas

Con fechas, MIN da la más antigua y MAX la más reciente. Es la forma natural de saber cuándo fue el primer y el último pedido:

SELECT MIN(fecha) AS primer_pedido, MAX(fecha) AS ultimo_pedido
FROM pedidos;
primer_pedidoultimo_pedido
2026-09-012026-09-15

Con textos

Con textos, comparan por orden alfabético: MIN es el primero y MAX el último.

SELECT MIN(nombre) AS primero, MAX(nombre) AS ultimo, MIN(ciudad) AS ciudad_min
FROM clientes;
primeroultimociudad_min
Ana GarcíaPablo DíazBilbao

Cuidado: si guardas números en una columna de texto (VARCHAR), se comparan letra a letra: entre '9', '10' y '100', el MAX es '9', porque el carácter 9 va después del 1. Es otra razón para usar el tipo de dato correcto. Además, cómo se ordenan mayúsculas, minúsculas y tildes depende de la collation (intercalación) de la columna.

Varias a la vez

Puedes calcular todas las que quieras en la misma consulta. Todas se aplican sobre las mismas filas, las que deja pasar el WHERE:

SELECT
  COUNT(*)               AS pedidos,
  SUM(importe)           AS total,
  ROUND(AVG(importe), 2) AS ticket_medio
FROM pedidos
WHERE fecha >= '2026-09-10';
pedidostotalticket_medio
3106.2435.41

(Pedidos 3, 4 y 5: 12.99 + 60.00 + 33.25 = 106.24, entre 3)

Y si necesitas contar o sumar solo una parte de las filas sin filtrar las demás, recuerda el truco de la lección de CASE: SUM(CASE WHEN ... THEN 1 ELSE 0 END).

Los agregados y NULL

Todas las funciones de agregación ignoran los NULL, salvo COUNT(*), que cuenta filas. Con la tabla de valoraciones:

SELECT COUNT(*)          AS filas,
       COUNT(puntuacion) AS puntuadas,
       SUM(puntuacion)   AS suma,
       AVG(puntuacion)   AS media,
       ROUND(AVG(COALESCE(puntuacion, 0)), 2) AS media_con_ceros,
       MIN(puntuacion)   AS peor
FROM valoraciones;
filaspuntuadassumamediamedia_con_cerospeor
64153.752.52
  • AVG(puntuacion) es 15 / 4 = 3.75: la media de las valoraciones con nota.
  • Con COALESCE(puntuacion, 0), los NULL cuentan como ceros: 15 / 6 = 2.5.
  • MIN da 2, no NULL: los huecos no participan.

¿Qué media es la buena? Aquí, 3.75: una valoración sin nota no es un suspenso. Pero es una decisión tuya, no de SQL. En la lección de valores NULL tienes más ejemplos.

Otro caso: si ninguna fila pasa el filtro, COUNT devuelve 0 y SUM, AVG, MIN y MAX devuelven NULL.

El error de mezclar columnas sueltas

No puedes mezclar una función de agregación con una columna normal sin más:

SELECT ciudad, COUNT(*) FROM clientes;   -- ✗ ¿de qué ciudad?

La función reduce 5 filas a 1, pero ciudad tiene 5 valores. ¿Cuál pone la base de datos junto al total? No tiene respuesta, y cada sistema reacciona de una forma:

SistemaQué pasa
MySQL 5.7+ (con ONLY_FULL_GROUP_BY, el modo por defecto)Error: In aggregated query without GROUP BY, expression #1 … contains nonaggregated column
PostgreSQLError: column “clientes.ciudad” must appear in the GROUP BY clause or be used in an aggregate function
SQLiteNo da error: coge el valor de una fila cualquiera

Que SQLite no se queje no significa que la consulta tenga sentido. Si quieres un recuento por ciudad, necesitas GROUP BY, que es la siguiente lección.

“¿Quién hizo el pedido más caro?”

Un caso muy frecuente de este error es intentar sacar la fila del máximo así:

SELECT cliente_id, MAX(importe) FROM pedidos;   -- ✗

MAX te da el importe mayor, no la fila a la que pertenece. Para obtener la fila completa, ordena y quédate con la primera, como aprendiste en ORDER BY y LIMIT:

SELECT id, cliente_id, importe
FROM pedidos
ORDER BY importe DESC
LIMIT 1;   -- pedido 2, cliente 3, 120.50

Agregados en el WHERE

Tampoco puedes usar una función de agregación dentro del WHERE:

SELECT id, importe FROM pedidos
WHERE importe > AVG(importe);   -- ✗ misuse of aggregate function

El WHERE decide fila a fila, y en ese momento la media todavía no existe. La solución es calcular la media aparte con una subconsulta:

SELECT id, importe FROM pedidos
WHERE importe > (SELECT AVG(importe) FROM pedidos);
idimporte
2120.50
460.00

Para filtrar por el resultado de un agregado por grupos existe HAVING, que verás con GROUP BY.

Errores frecuentes

  • Confundir COUNT(*) con COUNT(columna): el segundo no cuenta los NULL.
  • Esperar un 0 de SUM sin filas: devuelve NULL. Usa COALESCE(SUM(x), 0).
  • Redondear en el sitio equivocado: ROUND(AVG(x), 2), no AVG(ROUND(x, 2)).
  • Hacer MAX sobre números guardados como texto: '9' gana a '10'.
  • Mezclar columnas sueltas con agregados sin GROUP BY.
  • Usar MAX para obtener la fila del máximo: usa ORDER BY ... LIMIT 1 o una subconsulta.
  • Poner un agregado en el WHERE: usa una subconsulta o HAVING.

Resumen

FunciónDevuelveCon NULLSin filas
COUNT(*)Número de filasLas cuenta0
COUNT(col)Filas con valor en colLas ignora0
COUNT(DISTINCT col)Valores distintosLas ignora0
SUM(col)SumaLas ignoraNULL
AVG(col)MediaLas ignoraNULL
MIN(col) / MAX(col)Menor / mayor (números, fechas o textos)Las ignoraNULL
  • Una consulta con agregados y sin GROUP BY devuelve una sola fila.
  • El WHERE filtra antes de agregar.
  • Para resultados por categoría, sigue con GROUP BY y HAVING.

Pon a prueba lo que has aprendido

[SQL] ¿Qué devuelve esta consulta?
SELECT COUNT(*) FROM pedidos;

[SQL] Con la tabla pedidos del curso (5 pedidos, Ana tiene dos), ¿qué devuelve esta consulta?
SELECT COUNT(cliente_id), COUNT(DISTINCT cliente_id)
FROM pedidos;

[SQL] Ningún pedido supera los 1000 €. ¿Qué devuelve esta consulta?
SELECT COUNT(*), SUM(importe)
FROM pedidos
WHERE importe > 1000;

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.