SQL para análise de dados · Guias · 30 de outubro de 2026 · 11 min de leitura

Comandos SQL básicos: SELECT, WHERE, ORDER BY, GROUP BY e JOIN para iniciantes

Uma pessoa numa mesa monta uma consulta com blocos que filtram uma tabela grande até sobrar uma tabela pequena e ordenada

Uma consulta SQL básica tem sempre a mesma forma: SELECT diz quais colunas você quer ver, FROM de qual tabela elas vêm, WHERE filtra as linhas, GROUP BY agrupa as linhas para calcular totais, ORDER BY ordena o resultado e JOIN combina duas tabelas por uma coluna em comum. Com esses seis comandos você resolve a maior parte do trabalho diário de um analista. Este guia passa por cada um deles com consultas que funcionam de verdade: todas rodam no banco de dados de uma companhia aérea usado no curso de SQL da Nomto, e cada resultado mostrado abaixo é a saída real.

O banco de dados dos exemplos

Vamos trabalhar com cinco tabelas de uma companhia aérea (os textos dos dados estão em inglês, os valores em dólares americanos e os horários no horário-base da companhia, Moscou, UTC+3):

Tabela Uma linha é Colunas que vamos usar
airports um aeroporto (104) airport_code, airport_name, city, timezone
aircrafts um modelo de avião (9) aircraft_code, model, range (alcance em km)
flights uma partida de um voo num dia (33.121) flight_no, scheduled_departure, departure_airport, arrival_airport, status, aircraft_code, actual_departure
bookings uma reserva book_ref, book_date, total_amount
ticket_flights um trecho de voo num bilhete flight_id, fare_conditions (classe), amount (preço)

Antes de escrever qualquer consulta, vale saber o que representa uma linha de cada tabela. Quase todos os erros de iniciante em SQL vêm de contar linhas que não são o que a gente imagina.

SELECT e FROM: escolher colunas

A consulta mais simples pede colunas de uma tabela:

SELECT model, range
FROM aircrafts;

Algumas regras que vale fixar desde o começo:

O alcance está em quilômetros. Se quisermos também em milhas:

SELECT model,
       range,
       ROUND(range / 1.609) AS range_miles
FROM aircrafts
ORDER BY range DESC
LIMIT 5;
model range range_miles
Boeing 777-300 11100 6899
Boeing 767-300 7900 4910
Airbus A319-100 6700 4164
Airbus A320-200 5700 3543
Airbus A321-200 5600 3480

Sem ROUND, a coluna sairia cheia de casas decimais (6.898,69484151647). ORDER BY e LIMIT aparecem mais abaixo.

WHERE: filtrar linhas

O WHERE deixa passar só as linhas que atendem a uma condição. Os aeroportos de Moscou:

SELECT airport_code, airport_name, city
FROM airports
WHERE city = 'Moscow'
ORDER BY airport_code;
airport_code airport_name city
DME Domodedovo Moscow
SVO Sheremetyevo Moscow
VKO Vnukovo Moscow

Repare em dois detalhes: o texto vai entre aspas simples (no PostgreSQL as aspas duplas são para nomes de colunas) e a comparação diferencia maiúsculas de minúsculas: 'moscow' não devolve nada.

Combinar condições: AND, OR, IN, BETWEEN

As partidas de Sheremetyevo em 1º de agosto:

SELECT flight_no, scheduled_departure, arrival_airport, status
FROM flights
WHERE departure_airport = 'SVO'
  AND scheduled_departure >= '2026-08-01'
  AND scheduled_departure <  '2026-08-02'
ORDER BY scheduled_departure, flight_no
LIMIT 5;
flight_no scheduled_departure arrival_airport status
PG0120 2026-08-01 09:00:00 MJZ Arrived
PG0591 2026-08-01 09:00:00 TOF Arrived
PG0241 2026-08-01 09:40:00 CSY Arrived
PG0677 2026-08-01 09:40:00 MCX Arrived
PG0041 2026-08-01 09:50:00 NFG Arrived

Nesse dia saíram 50 voos de SVO. O filtro de datas segue o padrão “maior ou igual ao início do dia e menor que o início do dia seguinte”. É mais seguro que BETWEEN '2026-08-01' AND '2026-08-01', que em colunas de data e hora só pega o instante exato da meia-noite.

O IN substitui vários OR sobre a mesma coluna. Os voos que já decolaram (no ar ou já pousados):

SELECT COUNT(*)
FROM flights
WHERE status IN ('Departed', 'Arrived');

O resultado é 16.765. Quando misturar AND e OR, use parênteses: o AND é avaliado primeiro, e a OR b AND c significa a OR (b AND c).

LIKE: buscar por padrão

% substitui qualquer quantidade de caracteres e _ substitui exatamente um. As cidades que começam com “Novo”:

SELECT airport_code, airport_name, city
FROM airports
WHERE city LIKE 'Novo%'
ORDER BY city;
airport_code airport_name city
NOZ Spichenkovo Novokuznetsk
OVB Tolmachevo Novosibirsk

No PostgreSQL, o ILIKE faz o mesmo sem diferenciar maiúsculas de minúsculas.

IS NULL: valores vazios

NULL significa “sem valor”. Um voo que ainda não saiu tem actual_departure vazio. Aqui está a armadilha mais famosa do SQL:

SELECT COUNT(*) FROM flights WHERE actual_departure = NULL;   -- 0
SELECT COUNT(*) FROM flights WHERE actual_departure IS NULL;  -- 16348

A primeira devolve 0, embora existam milhares de voos que ainda não saíram: qualquer comparação com NULL dá “desconhecido”, e o WHERE só deixa passar o que é verdadeiro. Para valores vazios, use sempre IS NULL ou IS NOT NULL.

ORDER BY e LIMIT: ordenar e ficar com as primeiras linhas

Sem ORDER BY, o banco de dados devolve as linhas na ordem que for mais conveniente para ele, e essa ordem pode mudar de uma execução para outra. As cinco reservas mais caras:

SELECT book_ref, book_date, total_amount
FROM bookings
ORDER BY total_amount DESC
LIMIT 5;
book_ref book_date total_amount
3B54BB 2026-07-01 17:08:00 12045.00
3AC131 2026-07-27 01:06:00 10871.00
65A6EA 2026-06-29 06:28:00 10656.00
D7E9AA 2026-08-04 05:29:00 10628.00
EF479E 2026-07-29 15:58:00 10351.00

Funções de agregação: COUNT, SUM, AVG, MIN, MAX

As funções de agregação transformam muitas linhas num único número. Uma diferença que vale entender logo cedo:

SELECT COUNT(*)                          AS total,
       COUNT(actual_departure)           AS departed,
       COUNT(*) - COUNT(actual_departure) AS not_yet
FROM flights;
total departed not_yet
33121 16773 16348

COUNT(*) conta linhas; COUNT(coluna) conta só os valores não vazios. COUNT(DISTINCT coluna) conta valores distintos: SELECT COUNT(DISTINCT timezone) FROM airports devolve 17 fusos horários.

GROUP BY: um total por grupo

O GROUP BY divide a tabela em grupos e calcula a agregação dentro de cada um. Quantos voos existem em cada status:

SELECT status, COUNT(*) AS flights
FROM flights
GROUP BY status
ORDER BY flights DESC;
status flights
Arrived 16707
Scheduled 15383
On Time 518
Cancelled 414
Departed 58
Delayed 41

Várias agregações de uma vez, por classe tarifária:

SELECT fare_conditions,
       COUNT(*)              AS segments,
       ROUND(AVG(amount), 2) AS avg_price,
       MIN(amount)           AS min_price,
       MAX(amount)           AS max_price
FROM ticket_flights
GROUP BY fare_conditions
ORDER BY avg_price DESC;
fare_conditions segments avg_price min_price max_price
Business 107642 511.43 91.00 2033.00
Comfort 17291 327.41 199.00 476.00
Economy 920793 159.6 30.00 745.00

Em média, um trecho na executiva custa $511,43, e na econômica, $159,60. A regra de ouro: toda coluna do SELECT precisa estar no GROUP BY ou dentro de uma função de agregação. Se você escrever SELECT status, flight_no, COUNT(*) ... GROUP BY status, o PostgreSQL dá erro, porque num grupo há centenas de números de voo e ele não sabe qual mostrar.

HAVING: filtrar grupos

O WHERE filtra linhas antes de agrupar; o HAVING filtra grupos depois. As cidades com mais de um aeroporto:

SELECT city, COUNT(*) AS airports
FROM airports
GROUP BY city
HAVING COUNT(*) > 1
ORDER BY airports DESC;
city airports
Moscow 3
Ulyanovsk 2

Não dá para escrever WHERE COUNT(*) > 1: quando o WHERE é avaliado, os grupos ainda não existem. A diferença entre os dois está detalhada no guia de GROUP BY e HAVING.

JOIN: combinar tabelas

Na tabela flights, o avião aparece só como código (321, SU9). O nome do modelo está em aircrafts. O JOIN une as duas tabelas pela coluna em comum:

SELECT a.model, COUNT(*) AS flights
FROM flights f
JOIN aircrafts a ON a.aircraft_code = f.aircraft_code
GROUP BY a.model
ORDER BY flights DESC;
model flights
Cessna 208 Caravan 9273
Bombardier CRJ-200 9048
Sukhoi Superjet-100 8504
Airbus A321-200 1952
Boeing 737-300 1274
Airbus A319-100 1239
Boeing 767-300 1221
Boeing 777-300 610

f e a são aliases: nomes curtos para não repetir o nome inteiro da tabela. O ON diz quais linhas andam juntas.

INNER JOIN e LEFT JOIN

A tabela acima tem 8 modelos, mas em aircrafts há 9. O JOIN (que é o INNER JOIN) fica só com as linhas que têm par nas duas tabelas, então o modelo sem voos some. O LEFT JOIN mantém todas as linhas da tabela da esquerda e preenche com NULL o que não encontra. É assim que se procuram “linhas sem par”:

SELECT a.aircraft_code, a.model
FROM aircrafts a
LEFT JOIN flights f ON f.aircraft_code = a.aircraft_code
WHERE f.flight_id IS NULL;
aircraft_code model
320 Airbus A320-200

Se for contar com LEFT JOIN, use COUNT(f.flight_id) e não COUNT(*): assim o A320 aparece com 0, e não com 1.

Um aviso para quando você avançar: se você junta uma tabela com outra que tem várias linhas para cada linha da primeira (uma reserva com vários bilhetes), as linhas se multiplicam e as somas ficam infladas. Explicamos isso com números no guia de JOIN no SQL e nas perguntas de entrevista SQL.

Tudo junto: uma consulta completa

Os comandos se combinam numa ordem fixa: SELECT … FROM … JOIN … WHERE … GROUP BY … HAVING … ORDER BY … LIMIT. Cidades com pelo menos 300 voos já pousados e o atraso médio de partida, em minutos:

SELECT ap.city,
       COUNT(*) AS flights,
       ROUND(AVG(EXTRACT(EPOCH FROM f.actual_departure - f.scheduled_departure) / 60), 1) AS avg_delay_min
FROM flights f
JOIN airports ap ON ap.airport_code = f.departure_airport
WHERE f.status = 'Arrived'
GROUP BY ap.city
HAVING COUNT(*) >= 300
ORDER BY avg_delay_min DESC;
city flights avg_delay_min
Yekaterinburg 348 14.7
Bryansk 306 13.2
Krasnoyarsk 358 13.1
St. Petersburg 956 12.6
Moscow 3990 12.4
Novosibirsk 536 11.8
Perm 311 10.5
Rostov-on-Don 310 9.2

A subtração de duas datas dá um intervalo; EXTRACT(EPOCH FROM …) converte esse intervalo em segundos, e dividimos por 60.

Em que ordem uma consulta é executada

A gente escreve o SELECT primeiro, mas o banco de dados o processa quase no fim:

  1. FROM e JOIN: monta a tabela de trabalho.
  2. WHERE: descarta linhas.
  3. GROUP BY: forma os grupos.
  4. HAVING: descarta grupos.
  5. SELECT: calcula as colunas e os aliases.
  6. ORDER BY: ordena (aqui você já pode usar os aliases do SELECT).
  7. LIMIT: corta.

Essa ordem explica por que um alias do SELECT funciona no ORDER BY, mas não no WHERE, e por que as agregações vão no HAVING.

Erros comuns de iniciante

Como praticar

Ler consultas ajuda, mas se aprende escrevendo e errando. Todos os exemplos deste guia podem ser executados e modificados na sala de SQL da Nomto, com o mesmo banco de dados: troque a cidade, o status ou a ordenação e veja o que acontece. O primeiro módulo do curso começa justamente por SELECT e aliases, e cada aula termina com três exercícios corrigidos automaticamente. Quando dominar o básico, siga para os exercícios de SQL com respostas e para as funções de janela.

Perguntas frequentes

Quanto tempo leva para aprender os comandos SQL básicos?

SELECT, WHERE, ORDER BY, GROUP BY e JOIN podem ser aprendidos em duas ou três semanas praticando uma hora por dia. O que leva mais tempo é ler bem os dados: saber o que é uma linha e quando um JOIN multiplica linhas.

Qual a diferença entre WHERE e HAVING?

O WHERE filtra linhas antes de agrupar e não aceita agregações. O HAVING filtra os grupos já formados e é usado com COUNT, SUM, AVG e companhia.

O SQL deste guia serve para MySQL ou SQL Server?

Quase tudo, sim. SELECT, WHERE, GROUP BY, HAVING, JOIN e ORDER BY são iguais em todos os sistemas. Mudam detalhes: no SQL Server, LIMIT vira TOP; ILIKE não existe no MySQL nem no SQL Server; e as funções de data têm nomes diferentes.

Preciso instalar um banco de dados para praticar?

Não. Na sala de SQL da Nomto você escreve a consulta no navegador e a executa no banco de dados da companhia aérea, sem instalar nada.

Outros idiomas: English, Tiếng Việt, 简体中文, Español