COUNT, SUM, AVG, MIN y MAX en SQL
En esta lección
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:
| Forma | Qué 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;
| pedidos | clientes_que_compran |
|---|---|
| 5 | 4 |
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;
| filas | puntuadas | productos | notas_distintas |
|---|---|---|---|
| 6 | 4 | 3 | 3 |
COUNT(*): 6 valoraciones.COUNT(puntuacion): solo 4 tienen nota; las dos conNULLno 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 elNULLno 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 usesCOUNT(1)niCOUNT(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';
| total | total_seguro |
|---|---|
| NULL | 0 |
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;
| media | ticket_medio |
|---|---|
| 54.348 | 54.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;
| menor | mayor |
|---|---|
| 12.99 | 120.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_pedido | ultimo_pedido |
|---|---|
| 2026-09-01 | 2026-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;
| primero | ultimo | ciudad_min |
|---|---|---|
| Ana García | Pablo Díaz | Bilbao |
Cuidado: si guardas números en una columna de texto (
VARCHAR), se comparan letra a letra: entre'9','10'y'100', elMAXes'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';
| pedidos | total | ticket_medio |
|---|---|---|
| 3 | 106.24 | 35.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;
| filas | puntuadas | suma | media | media_con_ceros | peor |
|---|---|---|---|---|---|
| 6 | 4 | 15 | 3.75 | 2.5 | 2 |
AVG(puntuacion)es 15 / 4 = 3.75: la media de las valoraciones con nota.- Con
COALESCE(puntuacion, 0), losNULLcuentan como ceros: 15 / 6 = 2.5. MINda 2, noNULL: 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:
| Sistema | Qué 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 |
| PostgreSQL | Error: column “clientes.ciudad” must appear in the GROUP BY clause or be used in an aggregate function |
| SQLite | No 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);
| id | importe |
|---|---|
| 2 | 120.50 |
| 4 | 60.00 |
Para filtrar por el resultado de un agregado por grupos existe HAVING, que verás con GROUP BY.
Errores frecuentes
- Confundir
COUNT(*)conCOUNT(columna): el segundo no cuenta losNULL. - Esperar un 0 de
SUMsin filas: devuelveNULL. UsaCOALESCE(SUM(x), 0). - Redondear en el sitio equivocado:
ROUND(AVG(x), 2), noAVG(ROUND(x, 2)). - Hacer
MAXsobre números guardados como texto:'9'gana a'10'. - Mezclar columnas sueltas con agregados sin
GROUP BY. - Usar
MAXpara obtener la fila del máximo: usaORDER BY ... LIMIT 1o una subconsulta. - Poner un agregado en el
WHERE: usa una subconsulta oHAVING.
Resumen
| Función | Devuelve | Con NULL | Sin filas |
|---|---|---|---|
COUNT(*) | Número de filas | Las cuenta | 0 |
COUNT(col) | Filas con valor en col | Las ignora | 0 |
COUNT(DISTINCT col) | Valores distintos | Las ignora | 0 |
SUM(col) | Suma | Las ignora | NULL |
AVG(col) | Media | Las ignora | NULL |
MIN(col) / MAX(col) | Menor / mayor (números, fechas o textos) | Las ignora | NULL |
- Una consulta con agregados y sin
GROUP BYdevuelve una sola fila. - El
WHEREfiltra antes de agregar. - Para resultados por categoría, sigue con GROUP BY y HAVING.
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.