SQL para análise de dados · Guias · 30 de outubro de 2026 · 10 min de leitura
Funções de agregação no SQL: COUNT, SUM, AVG, MIN e MAX com exemplos

Uma função de agregação recebe muitas linhas e devolve um único valor. As cinco que você vai usar todo dia são COUNT (quantos), SUM (soma), AVG (média), MIN e MAX (menor e maior). Sem GROUP BY, elas resumem a tabela inteira em uma linha; com GROUP BY, devolvem uma linha por grupo. Duas regras explicam a maioria das surpresas: todas, exceto COUNT(*), ignoram NULL, e COUNT(*), COUNT(coluna) e COUNT(DISTINCT coluna) contam três coisas diferentes.
Os exemplos rodam no banco de dados de uma companhia aérea usado no curso de SQL da Nomto, com sintaxe do PostgreSQL executada no DuckDB (o motor da sala de SQL da Nomto). Os valores estão em dólares americanos, os textos dos dados em inglês, e cada tabela de resultado saiu da execução real da consulta.
As cinco funções de agregação numa consulta
A tabela ticket_flights tem uma linha por passageiro por trecho de voo, com a tarifa em 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 |
| função | devolve | funciona com |
|---|---|---|
COUNT(*) |
número de linhas | qualquer tabela |
COUNT(col) |
número de valores não NULL | qualquer tipo |
SUM(col) |
soma | números |
AVG(col) |
média aritmética | números |
MIN(col), MAX(col) |
menor, maior | números, texto, datas |
COUNT(*) vs COUNT(coluna) vs COUNT(DISTINCT coluna)
Esta é a diferença que mais importa na prática:
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(*)conta linhas, seja o que for que tenham dentro: 33.121 voos.COUNT(actual_departure)conta as linhas em que essa coluna não éNULL: só 16.773 voos partiram de fato; os outros ainda estão no futuro ou foram cancelados.COUNT(DISTINCT arrival_airport)conta valores diferentes: 104 destinos.NULLnão é contado como valor.
COUNT(1) dá o mesmo resultado que COUNT(*) e não é mais rápido em nenhum banco moderno. Escolha um e seja consistente.
O COUNT(DISTINCT ...) responde “quantos X únicos”. A tabela tickets tem uma linha por bilhete de passageiro, e vários bilhetes podem pertencer a uma mesma 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 combinações distintas de duas colunas, PostgreSQL e DuckDB não aceitam COUNT(DISTINCT a, b) (o MySQL aceita). Conte as linhas de um SELECT DISTINCT:
SELECT COUNT(*) AS routes
FROM (SELECT DISTINCT departure_airport, arrival_airport FROM flights) r;
| routes |
|---|
| 618 |
SUM e AVG
O SUM soma, e o AVG divide essa soma pelo número de valores não NULL. Os dois só aceitam números. Por classe tarifária, ordenado pela tarifa média:
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 |
A armadilha da média das médias
Qual é a tarifa média? Tirar a média das três médias por classe dá uma resposta bem diferente de tirar a média de todos os trechos:
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 |
A média das médias dá aos 17.291 trechos da Comfort o mesmo peso que aos 920.793 da Economy. Calcule sempre a média sobre as linhas brutas ou faça SUM(amount) / SUM(segments) a partir dos totais agrupados. É o mesmo erro de tirar a média simples do ticket médio de cada loja numa rede de varejo: a loja pequena pesa tanto quanto a grande.
Divisão inteira
No PostgreSQL, no SQL Server e no SQLite, dividir um inteiro por outro descarta o resto: razões no estilo COUNT(*) / COUNT(*) viram 0 ou 1 sem aviso. No DuckDB, o / devolve uma fração (7 / 2 é 3,5; 7 // 2 é 3), e no MySQL o / também. Multiplicar primeiro por 100.0, como nos exemplos abaixo, dá a resposta certa em todo lugar.
MIN e MAX
MIN e MAX funcionam com qualquer tipo que possa ser ordenado: números, datas e 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 |
Em texto, MIN(city) devolve o primeiro valor em ordem alfabética (Abakan) e MAX(city) o último (Yuzhno-Sakhalinsk). O MAX devolve o valor, não a linha: para ver qual reserva teve o maior valor, use ORDER BY total_amount DESC LIMIT 1 (ORDER BY no SQL), ou uma função de janela quando precisar disso por grupo (veja funções de janela).
Funções de agregação e NULL
SUM,AVG,MIN,MAXeCOUNT(col)pulam osNULLs. OAVGde 1,NULL, 3 é 2, e não 4/3.COUNT(*)conta todas as linhas, inclusive as comNULL.- Sobre zero linhas, o
COUNTdevolve 0, masSUM,AVG,MINeMAXdevolvemNULL. UseCOALESCE(SUM(x), 0)quando um relatório deve mostrar 0. - Trocar
NULLpor 0 antes da média (AVG(COALESCE(x, 0))) muda a resposta; faça isso só quando um valor ausente realmente significa zero.
O guia de NULL no SQL mostra cada um desses pontos em dados reais, incluindo quanto uma média se desloca quando valores desconhecidos viram zero.
Agregações com GROUP BY e HAVING
O GROUP BY divide as linhas em grupos, e cada agregação é calculada uma vez por grupo. O HAVING depois filtra os grupos por uma agregação:
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 |
Duas regras decorrem da ordem em que o SQL executa uma consulta:
- Agregações não são permitidas no
WHERE, que roda antes do agrupamento. O DuckDB respondeWHERE clause cannot contain aggregates!; filtre por agregações comHAVING. - Toda coluna do
SELECTprecisa estar noGROUP BYou dentro de uma agregação. Senão o banco não tem como saber qual dos valores do grupo mostrar.
Os detalhes, com os erros comuns, estão em GROUP BY e HAVING.
Agregação condicional: FILTER e CASE
Muitas vezes você quer várias contagens com condições diferentes numa única linha. A cláusula FILTER aplica uma condição a uma só agregação:
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 |
O FILTER funciona no PostgreSQL (9.4+), no DuckDB e no SQLite (3.30+). MySQL e SQL Server não têm; neles, use SUM(CASE WHEN ... THEN 1 ELSE 0 END) ou COUNT(CASE WHEN ... THEN 1 END), que devolvem o mesmo número, como mostra a coluna cancelled_case.
O mesmo padrão dá participações no total numa única passada:
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 |
A classe Business é cerca de 10% dos trechos, mas 26,5% da receita. Mais lógica condicional no guia de CASE WHEN no SQL; colocar essas contagens lado a lado, uma coluna por valor, é exatamente o que se chama de pivot no SQL.
Totais errados depois de um JOIN
As agregações contam as linhas que recebem. Um join que liga uma linha a várias repete essa linha, e o SUM a soma várias vezes. Os totais das reservas somados depois de juntar com os bilhetes:
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 |
Uma reserva com três passageiros tem três bilhetes, então o total dela foi somado três vezes. A consulta roda sem erros e o número parece plausível. Agregue cada tabela no seu próprio nível primeiro e depois junte os resultados, ou use COUNT(DISTINCT ...) quando só precisar de contagens. O guia de JOIN no SQL explica essa multiplicação de linhas em detalhe.
Aninhar agregações
Você não pode colocar uma agregação diretamente dentro de outra: SELECT MAX(COUNT(*)) FROM flights GROUP BY departure_airport falha com “aggregate function calls cannot be nested”. Agregue numa subconsulta ou numa CTE e depois agregue o 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 |
Em média, uma reserva tem 1,4 bilhete, e a maior tem cinco. Veja o guia de CTE no SQL para escrever agregações em várias etapas de forma legível.
Outras funções de agregação úteis
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, ', ')junta valores numa única string (no MySQL:GROUP_CONCAT).percentile_cont(0.5) WITHIN GROUP (ORDER BY amount)dá a mediana: 134.00 aqui, bem abaixo da média de 198.59, porque algumas tarifas Business caras puxam a média para cima. O DuckDB também temmedian(amount). O MySQL não tem uma agregação de percentil embutida.BOOL_AND,BOOL_ORverificam se uma condição vale para todas ou para alguma linha do grupo (PostgreSQL, DuckDB).ARRAY_AGGjunta os valores num array.
Toda agregação também pode ser usada como função de janela com OVER (...), que mantém as linhas individuais em vez de resumi-las: é assim que se calculam totais acumulados e participações dentro de um grupo.
Se você vem do Excel, o mapa é direto: COUNT é o CONT.VALORES, SUM é a SOMA, AVG é a MÉDIA, COUNT(*) FILTER (WHERE ...) faz o papel do CONT.SE e SUM(...) FILTER (WHERE ...) o do SOMASE. Mais comparações no guia SQL vs Excel.
Perguntas frequentes
Quais são as funções de agregação do SQL?
As cinco padrão são COUNT, SUM, AVG, MIN e MAX. A maioria dos bancos acrescenta outras, como STRING_AGG, ARRAY_AGG, STDDEV, VARIANCE e funções de percentil. Todas transformam um conjunto de linhas num único valor, por grupo se houver GROUP BY.
Qual a diferença entre COUNT(*) e COUNT(coluna)?
COUNT(*) conta linhas. COUNT(coluna) conta as linhas em que a coluna não é NULL. Na tabela de voos, eles dão 33.121 e 16.773 para actual_departure, porque os voos que ainda não partiram não têm horário de partida.
As funções de agregação ignoram NULL?
Sim, todas exceto COUNT(*). O AVG divide pelo número de valores não NULL, e o SUM de uma coluna inteiramente NULL (ou de zero linhas) devolve NULL, e não 0.
Posso usar uma função de agregação no WHERE?
Não. O WHERE filtra as linhas antes de elas serem agrupadas, então as agregações ainda não existem. Use HAVING para filtrar por uma agregação, ou calcule-a numa subconsulta e filtre na consulta externa.
Posso usar funções de agregação sem GROUP BY?
Sim. Sem GROUP BY, a tabela inteira (depois do WHERE) é um único grupo, e você recebe uma linha só. Você não pode misturar essas agregações com colunas simples no mesmo SELECT.
Como contar valores distintos no SQL?
COUNT(DISTINCT coluna). Para combinações de várias colunas, conte as linhas de SELECT DISTINCT a, b numa subconsulta, já que COUNT(DISTINCT a, b) só funciona no MySQL.
Outros idiomas: English, Español, Tiếng Việt, 简体中文