SQL · Guías · 30 de octubre de 2026 · 10 min de lectura
Funciones de agregación en SQL: COUNT, SUM, AVG, MIN y MAX con ejemplos

Una función de agregación recibe muchas filas y devuelve un solo valor. Las cinco que usarás a diario son COUNT (cuántas), SUM (total), AVG (media), MIN y MAX (el menor y el mayor). Sin GROUP BY resumen toda la tabla en una fila; con GROUP BY devuelven una fila por grupo. Dos reglas explican casi todas las sorpresas: todas salvo COUNT(*) ignoran los NULL, y COUNT(*), COUNT(columna) y COUNT(DISTINCT columna) cuentan tres cosas distintas.
Si vienes de Excel, la equivalencia es directa: COUNT es CONTAR (o CONTARA), SUM es SUMA, AVG es PROMEDIO, MIN y MAX se llaman igual, y COUNT(*) FILTER (WHERE ...) hace el trabajo de CONTAR.SI. La diferencia es que en SQL no arrastras fórmulas: escribes la agregación una vez y se calcula para todos los grupos.
Todos los ejemplos usan la base de datos de una aerolínea del curso de SQL de Nomto, con sintaxis de PostgreSQL sobre DuckDB (el motor de la sala de SQL de Nomto). Los valores están en inglés, las tarifas en dólares estadounidenses y cada tabla de resultados es la salida real de la consulta.
Las cinco funciones de agregación en una consulta
ticket_flights tiene una fila por pasajero y tramo de vuelo, con la tarifa en amount:
SELECT COUNT(*) AS segments,
SUM(amount) AS revenue,
ROUND(AVG(amount), 2) AS avg_fare,
MIN(amount) AS min_fare,
MAX(amount) AS max_fare
FROM ticket_flights;
| segments | revenue | avg_fare | min_fare | max_fare |
|---|---|---|---|---|
| 1045726 | 207669809.00 | 198.59 | 30.00 | 2033.00 |
| función | devuelve | funciona con |
|---|---|---|
COUNT(*) |
número de filas | cualquier tabla |
COUNT(col) |
número de valores no NULL | cualquier tipo |
SUM(col) |
total | números |
AVG(col) |
media aritmética | números |
MIN(col), MAX(col) |
el menor, el mayor | números, texto, fechas |
COUNT(*) vs COUNT(columna) vs COUNT(DISTINCT columna)
Esta es la diferencia que más importa en la práctica:
SELECT COUNT(*) AS flights,
COUNT(actual_departure) AS departed,
COUNT(DISTINCT arrival_airport) AS destinations,
COUNT(DISTINCT aircraft_code) AS models_used
FROM flights;
| flights | departed | destinations | models_used |
|---|---|---|---|
| 33121 | 16773 | 104 | 8 |
COUNT(*)cuenta filas, tengan lo que tengan: 33.121 vuelos.COUNT(actual_departure)cuenta las filas en las que esa columna no esNULL: solo 16.773 vuelos han salido de verdad; el resto aún no ha llegado su fecha o se canceló.COUNT(DISTINCT arrival_airport)cuenta valores diferentes: 104 destinos. LosNULLno cuentan como valor.
COUNT(1) da el mismo resultado que COUNT(*) y no es más rápido en ninguna base de datos moderna. Elige uno y sé coherente.
COUNT(DISTINCT ...) es la forma de responder “cuántos X únicos hay”. La tabla tickets tiene una fila por billete de pasajero, y varios billetes pueden compartir una reserva:
SELECT COUNT(*) AS tickets,
COUNT(contact_data->>'email') AS with_email,
COUNT(DISTINCT book_ref) AS bookings
FROM tickets;
| tickets | with_email | bookings |
|---|---|---|
| 366733 | 199083 | 262788 |
Para contar combinaciones distintas de dos columnas, PostgreSQL y DuckDB no aceptan COUNT(DISTINCT a, b) (MySQL sí). Cuenta en su lugar las filas de un SELECT DISTINCT:
SELECT COUNT(*) AS routes
FROM (SELECT DISTINCT departure_airport, arrival_airport FROM flights) r;
| routes |
|---|
| 618 |
SUM y AVG
SUM suma; AVG divide esa suma entre el número de valores no NULL. Las dos solo aceptan números. Por clase de tarifa, ordenado por tarifa media:
SELECT fare_conditions,
COUNT(*) AS segments,
ROUND(SUM(amount)) AS revenue,
ROUND(AVG(amount), 2) AS avg_fare,
MIN(amount) AS min_fare,
MAX(amount) AS max_fare
FROM ticket_flights
GROUP BY fare_conditions
ORDER BY avg_fare DESC;
| fare_conditions | segments | revenue | avg_fare | min_fare | max_fare |
|---|---|---|---|---|---|
| Business | 107642 | 55051796 | 511.43 | 91.00 | 2033.00 |
| Comfort | 17291 | 5661169 | 327.41 | 199.00 | 476.00 |
| Economy | 920793 | 146956844 | 159.6 | 30.00 | 745.00 |
La trampa de la media de medias
¿Cuál es la tarifa media? Promediar las medias de las tres clases da una respuesta muy distinta a promediar todos los tramos:
SELECT ROUND(AVG(avg_fare), 2) AS avg_of_averages,
(SELECT ROUND(AVG(amount), 2) FROM ticket_flights) AS true_average
FROM (
SELECT fare_conditions, AVG(amount) AS avg_fare
FROM ticket_flights
GROUP BY fare_conditions
) t;
| avg_of_averages | true_average |
|---|---|
| 332.81 | 198.59 |
La media de medias da a los 17.291 tramos de Comfort el mismo peso que a los 920.793 de Economy. Promedia siempre las filas originales, o calcula SUM(amount) / SUM(segments) a partir de totales agrupados. Es el mismo error que promediar en Excel una columna de porcentajes por región: el resultado parece razonable y está mal.
División entera
En PostgreSQL, SQL Server y SQLite, dividir un entero entre otro descarta el resto: los ratios del estilo COUNT(*) / COUNT(*) se convierten sin avisar en 0 o 1. En DuckDB, / devuelve un decimal (7 / 2 es 3,5; 7 // 2 es 3), y en MySQL también. Multiplicar primero por 100.0, como en los ejemplos de abajo, da el resultado correcto en todas.
MIN y MAX
MIN y MAX funcionan con cualquier tipo que se pueda ordenar: números, fechas y texto.
SELECT MIN(book_date) AS first_booking,
MAX(book_date) AS last_booking,
MIN(total_amount) AS smallest,
MAX(total_amount) AS largest
FROM bookings;
| first_booking | last_booking | smallest | largest |
|---|---|---|---|
| 2026-06-17 14:05:00 | 2026-08-11 18:00:00 | 34.00 | 12045.00 |
Con texto, MIN(city) devuelve el primer valor por orden alfabético (Abakan) y MAX(city), el último (Yuzhno-Sakhalinsk). Si tus datos tienen tildes o ñ, ese “orden alfabético” depende de la intercalación, como explica la guía de ORDER BY en SQL. MAX devuelve el valor, no la fila: para ver qué reserva tuvo el importe más alto, usa ORDER BY total_amount DESC LIMIT 1, o una función de ventana si lo necesitas por grupo (mira las funciones de ventana).
Funciones de agregación y NULL
SUM,AVG,MIN,MAXyCOUNT(col)se saltan losNULL.AVGde 1,NULLy 3 es 2, no 4/3.COUNT(*)cuenta todas las filas,NULLincluidos.- Sobre cero filas,
COUNTdevuelve 0, peroSUM,AVG,MINyMAXdevuelvenNULL. Envuélvelos enCOALESCE(SUM(x), 0)cuando un informe deba mostrar 0. - Sustituir
NULLpor 0 antes de promediar (AVG(COALESCE(x, 0))) cambia el resultado; hazlo solo cuando un valor ausente signifique de verdad cero.
La guía de NULL en SQL muestra cada uno de estos casos con datos reales, incluido cuánto se mueve una media cuando los valores desconocidos se convierten en ceros.
Agregados con GROUP BY y HAVING
GROUP BY reparte las filas en grupos, y cada agregado se calcula una vez por grupo. Después, HAVING filtra grupos según un agregado:
SELECT departure_airport, COUNT(*) AS departures
FROM flights
GROUP BY departure_airport
HAVING COUNT(*) >= 1500
ORDER BY departures DESC;
| departure_airport | departures |
|---|---|
| DME | 3217 |
| SVO | 2981 |
| LED | 1900 |
| VKO | 1719 |
Del orden en que SQL ejecuta una consulta salen dos reglas:
- No se permiten agregados en
WHERE, que se ejecuta antes de agrupar. DuckDB respondeWHERE clause cannot contain aggregates!; filtra por agregados conHAVING. - Toda columna del
SELECTtiene que estar enGROUP BYo dentro de un agregado. Si no, la base de datos no sabe cuál de los valores del grupo mostrar.
Los detalles, con los errores típicos, están en GROUP BY y HAVING.
Agregados condicionales: FILTER y CASE
A menudo quieres varios recuentos con condiciones distintas en una sola fila, como harías con varios CONTAR.SI. La cláusula FILTER aplica una condición a un único agregado:
SELECT COUNT(*) FILTER (WHERE status = 'Cancelled') AS cancelled,
COUNT(*) FILTER (WHERE status = 'Delayed') AS delayed,
SUM(CASE WHEN status = 'Cancelled' THEN 1 ELSE 0 END) AS cancelled_case,
ROUND(100.0 * COUNT(*) FILTER (WHERE status = 'Cancelled') / COUNT(*), 2) AS cancelled_pct
FROM flights;
| cancelled | delayed | cancelled_case | cancelled_pct |
|---|---|---|---|
| 414 | 41 | 414 | 1.25 |
FILTER funciona en PostgreSQL (9.4+), DuckDB y SQLite (3.30+). MySQL y SQL Server no lo admiten; ahí usa SUM(CASE WHEN ... THEN 1 ELSE 0 END) o COUNT(CASE WHEN ... THEN 1 END), que devuelven el mismo número, como muestra la columna cancelled_case. Como MySQL y SQL Server son muy habituales en empresas de España y Latinoamérica, conviene que la versión con CASE te salga de memoria.
El mismo patrón da cuotas sobre un total en una sola pasada:
SELECT ROUND(SUM(amount)) AS revenue,
ROUND(SUM(amount) FILTER (WHERE fare_conditions = 'Business')) AS business_revenue,
ROUND(100.0 * SUM(amount) FILTER (WHERE fare_conditions = 'Business') / SUM(amount), 1) AS business_pct
FROM ticket_flights;
| revenue | business_revenue | business_pct |
|---|---|---|
| 207669809 | 55051796 | 26.5 |
La clase Business es más o menos el 10 % de los tramos, pero el 26,5 % de los ingresos. Más lógica condicional en la guía de CASE WHEN en SQL.
Totales inflados después de un JOIN
Los agregados cuentan las filas que les llegan. Un join que empareja una fila con varias la repite, y SUM la suma varias veces. Importes de reserva sumados después de unir con los billetes:
SELECT SUM(b.total_amount) AS wrong_revenue,
(SELECT SUM(total_amount) FROM bookings) AS right_revenue
FROM bookings b
JOIN tickets t ON t.book_ref = b.book_ref;
| wrong_revenue | right_revenue |
|---|---|
| 343206192.00 | 207669809.00 |
Una reserva con tres pasajeros tiene tres billetes, así que su importe se sumó tres veces. La consulta corre sin errores y el número parece verosímil. Agrega primero cada tabla a su propio nivel y une después los resultados, o usa COUNT(DISTINCT ...) si solo necesitas recuentos. La guía de JOIN en SQL explica esta multiplicación de filas en detalle.
Agregados anidados
No puedes meter un agregado directamente dentro de otro: SELECT MAX(COUNT(*)) FROM flights GROUP BY departure_airport falla con “aggregate function calls cannot be nested”. Agrega en una subconsulta o CTE y luego agrega el resultado:
SELECT AVG(n) AS avg_tickets_per_booking, MAX(n) AS max_tickets
FROM (SELECT book_ref, COUNT(*) AS n FROM tickets GROUP BY book_ref) t;
| avg_tickets_per_booking | max_tickets |
|---|---|
| 1.3955469808362635 | 5 |
De media, una reserva tiene 1,4 billetes, y la más grande tiene cinco. Para escribir agregaciones en varios pasos de forma legible, mira la guía de CTE en SQL.
Otras funciones de agregación útiles
SELECT split_part(model, ' ', 1) AS maker,
COUNT(*) AS models,
MAX(range) AS max_range,
STRING_AGG(model, ', ' ORDER BY range DESC) AS line_up
FROM aircrafts
GROUP BY maker
ORDER BY max_range DESC;
| maker | models | max_range | line_up |
|---|---|---|---|
| Boeing | 3 | 11100 | Boeing 777-300, Boeing 767-300, Boeing 737-300 |
| Airbus | 3 | 6700 | Airbus A319-100, Airbus A320-200, Airbus A321-200 |
| Sukhoi | 1 | 3000 | Sukhoi Superjet-100 |
| Bombardier | 1 | 2700 | Bombardier CRJ-200 |
| Cessna | 1 | 1200 | Cessna 208 Caravan |
STRING_AGG(col, ', ')une valores en una sola cadena (en MySQL,GROUP_CONCAT; en SQL Server existeSTRING_AGGdesde la versión 2017, con el orden enWITHIN GROUP (ORDER BY ...)).percentile_cont(0.5) WITHIN GROUP (ORDER BY amount)da la mediana: aquí 134,00, muy por debajo de la media de 198,59, porque unas pocas tarifas Business caras tiran de la media hacia arriba. DuckDB tiene ademásmedian(amount). MySQL no trae ningún agregado de percentiles.BOOL_AND,BOOL_ORcomprueban si una condición se cumple en todas o en alguna fila del grupo (PostgreSQL, DuckDB).ARRAY_AGGjunta valores en un array.
Cualquier agregado se puede usar también como función de ventana con OVER (...), que conserva las filas individuales en lugar de resumirlas: así se calculan los acumulados y las cuotas dentro de un grupo. Y si lo que necesitas es convertir los grupos en columnas (meses en columnas, por ejemplo), eso es una tabla dinámica: lo cuenta la guía de PIVOT en SQL.
Preguntas frecuentes
¿Cuáles son las funciones de agregación en SQL?
Las cinco estándar son COUNT, SUM, AVG, MIN y MAX. La mayoría de bases de datos añade más, como STRING_AGG, ARRAY_AGG, STDDEV, VARIANCE y funciones de percentiles. Todas convierten un conjunto de filas en un único valor, por grupo si hay GROUP BY.
¿Qué diferencia hay entre COUNT(*) y COUNT(columna)?
COUNT(*) cuenta filas. COUNT(columna) cuenta las filas en las que la columna no es NULL. En la tabla de vuelos dan 33.121 y 16.773 para actual_departure, porque los vuelos que aún no han salido no tienen hora de salida.
¿Las funciones de agregación ignoran NULL?
Sí, todas salvo COUNT(*). AVG divide entre el número de valores no NULL, y SUM de una columna que es entera NULL (o de cero filas) devuelve NULL, no 0.
¿Puedo usar una función de agregación en WHERE?
No. WHERE filtra filas antes de agruparlas, así que los agregados todavía no existen. Usa HAVING para filtrar por un agregado, o calcúlalo en una subconsulta y filtra en la consulta exterior.
¿Puedo usar funciones de agregación sin GROUP BY?
Sí. Sin GROUP BY toda la tabla (después del WHERE) es un único grupo y obtienes una sola fila. Lo que no puedes es mezclar esos agregados con columnas sueltas en el mismo SELECT.
¿Cómo cuento valores distintos en SQL?
Con COUNT(DISTINCT columna). Para combinaciones de varias columnas, cuenta las filas de SELECT DISTINCT a, b en una subconsulta, ya que COUNT(DISTINCT a, b) solo funciona en MySQL.
También en English, Português (Brasil), Tiếng Việt, 简体中文