Query Language

SQL

SQL é a linguagem declarativa para fazer perguntas a dados relacionais. Você descreve o resultado desejado e o banco de dados decide como computá-lo.

beginner14 min readUpdated 16 de set. de 2026
report.sql
sql
-- report.sql
SELECT a.name          AS author,
       COUNT(p.id)     AS posts,
       MAX(p.created_at) AS latest
FROM authors a
LEFT JOIN posts p ON p.author_id = a.id
GROUP BY a.id, a.name
HAVING COUNT(p.id) > 0
ORDER BY posts DESC
LIMIT 10;
Primeiro padrão
1986
Paradigma
Declarativo
Funciona com
Bancos de dados relacionais
Verbo principal
SELECT
Case
Palavras-chave case-insensitive
Usado por
PostgreSQL, MySQL, SQLite, SQL Server

Por que importa

Por que o SQL sobreviveu a todas as tendências

Declarativo por design

Você escreve como o resultado deve ser, não como iterar sobre as linhas. O planejador escolhe os índices e a ordem dos joins para você.

Pensamento baseado em conjuntos

Cada instrução opera sobre conjuntos inteiros de linhas. Pensar em conjuntos, não em loops, é a mudança mental que faz o SQL fazer sentido.

Constraints como garantias

Primary keys, foreign keys e checks são impostos pelo banco de dados, portanto, estados inválidos sequer podem ser gravados.

O panorama completo

As três ideias por trás do SQL

Descreva o resultado em vez dos passos, pense em conjuntos inteiros de linhas e deixe que as constraints mantenham a integridade dos dados.

SELECT

Descrever

Nomeie as colunas e tabelas que você deseja; o banco de dados retorna um conjunto de resultados e decide o plano de execução.

Joins

Combinar

Relações são costuradas através de chaves correspondentes, com inner e outer joins decidindo o que acontece com as linhas não correspondentes.

Transactions

Proteger

BEGIN, COMMIT e ROLLBACK tornam um grupo de instruções atômico, para que gravações parciais nunca sobrevivam a uma falha.

HTML5 de uma olhada

As cláusulas que você usará diariamente

SELECT

Escolha colunas e tabelas para construir um conjunto de resultados.

WHERE

Mantenha apenas as linhas que correspondem a uma condição.

JOIN

Combine linhas de duas tabelas através de uma chave relacionada.

GROUP BY

Agrupe linhas em conjuntos e agregue-as.

WITH

Nomeie uma subquery para que uma consulta longa seja lida de cima para baixo.

COMMIT

Torne as alterações de uma transação permanentes, ou use ROLLBACK para desfazer.

Modelo de dados

Uma linha por post

Uma linha representa um único post de blog escrito por exatamente um autor.

A tabela de postsTabela relacional
  • idbigintPrimary key que identifica unicamente o post
  • author_idbigintForeign key para authors; quem o escreveu
  • titletextTítulo exibido nas listagens
  • bodytextO conteúdo do post em si
  • published_attimestamptzNULL enquanto o post ainda for um rascunho
  • created_attimestamptzQuando a linha foi inserida pela primeira vez

Uma linha representa um único post de blog escrito por exatamente um autor.

O guia completo

SQL: Tudo que voce precisa saber

O que é SQL?

SQL, pronunciado como “sequel” ou “S-Q-L”, é a Structured Query Language (Linguagem de Consulta Estruturada) usada para ler e escrever dados em bancos de dados relacionais. Foi padronizada em 1986, o que a torna mais antiga que a web, e continua sendo a forma como quase toda aplicação se comunica com seus dados.

O SQL divide-se em duas partes. DDL (data definition language) cria e altera a estrutura: CREATE TABLE, ALTER TABLE, DROP INDEX. DML (data manipulation language) trabalha com os dados em si: SELECT, INSERT, UPDATE, DELETE. A maior parte do seu tempo será gasta em DML, sendo SELECT, de longe, o comando mais comum.

O ponto importante a entender logo no início é que SQL não é uma linguagem de programação no sentido usual. Não existem loops na linguagem core nem variáveis em uma query simples. Você descreve o resultado desejado, e o banco de dados descobre como produzi-lo.

Declarativo e baseado em conjuntos

Em uma linguagem como JavaScript ou Python, você computaria um relatório através de iterações:

const result = [];
for (const author of authors) {
  let count = 0;
  for (const post of posts) {
    if (post.author_id === author.id) count++;
  }
  if (count > 0) result.push({ author, count });
}

Em SQL, você expressa a mesma intenção em uma única expressão:

SELECT a.name, COUNT(p.id) AS posts
FROM authors a
LEFT JOIN posts p ON p.author_id = a.id
GROUP BY a.id, a.name
HAVING COUNT(p.id) > 0;

Não existe loop. Você declarou que deseja cada autor com a sua respectiva contagem de posts. O banco de dados pode resolver isso com um index scan, um hash join ou algo completamente diferente — e se você adicionar um índice amanhã, a mesma query pode se tornar mais rápida sem alterar um único caractere.

Este é o mindset baseado em conjuntos (set-based): as instruções operam sobre conjuntos inteiros de linhas de uma só vez. Quando você compreende esse conceito, o SQL torna-se conciso de uma forma que loops raramente conseguem ser.

SELECT: escolhendo colunas

Toda leitura começa com SELECT, que lista as colunas que você deseja.

SELECT id, title, published_at
FROM posts;

SELECT * retorna todas as colunas e é útil durante a exploração, mas especifique o nome das colunas em queries de aplicações. Listas explícitas são estáveis quando o schema muda, evitam a transferência de colunas grandes e não utilizadas, e tornam a intenção óbvia. Você pode computar novas colunas e renomeá-las:

SELECT title,
       LENGTH(body) AS body_length,
       COALESCE(published_at, created_at) AS visible_at
FROM posts;

AS atribui um alias a uma coluna. Use-o para tornar os resultados legíveis e para dar nome a colunas computadas, o que é importante quando uma biblioteca cliente mapeia linhas para objetos.

Filtrando com WHERE

WHERE mantém apenas as linhas que satisfazem uma condição. Ele suporta as comparações usuais além de AND, OR, NOT, IN, BETWEEN e LIKE.

SELECT id, title
FROM posts
WHERE author_id = 7
  AND published_at IS NOT NULL
  AND title ILIKE '%sql%';

Dois detalhes valem a pena ser memorizados. Primeiro, LIKE diferencia maiúsculas de minúsculas (case-sensitive) em muitos bancos de dados, enquanto ILIKE (Postgres) não diferencia (case-insensitive); o LIKE do MySQL não diferencia com as collations usuais. Segundo, um caractere curinga no início, como '%sql', impede que um índice comum ajude, pois não há um prefixo para a busca. Para buscas reais de texto, utilize um índice de full-text em vez disso.

Ordenação e paginação

ORDER BY ordena o resultado, e LIMIT com OFFSET o fatia.

SELECT id, title
FROM posts
WHERE published_at IS NOT NULL
ORDER BY published_at DESC
LIMIT 20 OFFSET 40;

Sem ORDER BY, o banco de dados não garante a ordem das linhas. Uma query que “por acaso” retorna ordenada hoje pode mudar quando o planner escolhe um plano diferente, portanto, sempre ordene explicitamente quando a ordem for importante.

OFFSET pula linhas, o que se torna caro em tabelas grandes (deep paging) porque o banco de dados ainda precisa percorrer esses registros. A paginação por keyset evita esse custo ao lembrar a última linha visualizada:

SELECT id, title
FROM posts
WHERE published_at < :last_published_at
ORDER BY published_at DESC
LIMIT 20;

NULL e a lógica trivalente

NULL não significa zero ou uma string vazia. Significa desconhecido, e o SQL utiliza a lógica trivalente: cada condição é avaliada como verdadeira, falsa ou desconhecida. As linhas só passam por uma cláusula WHERE quando a condição é verdadeira, portanto, o “desconhecido” é tratado como falso para fins de filtragem.

SELECT id FROM posts WHERE published_at = NULL;     -- never matches
SELECT id FROM posts WHERE published_at IS NULL;    -- correct
SELECT id FROM posts WHERE published_at IS NOT NULL;

Consequências para ficar atento:

  • NULL = NULL é desconhecido, não verdadeiro. Use IS NULL ou IS NOT DISTINCT FROM.
  • NULL + 1 é NULL; use COALESCE(x, 0) para substituir por um valor padrão.
  • NOT IN (1, 2, NULL) nunca é verdadeiro, pois comparar com um elemento desconhecido resulta em desconhecido. Prefira NOT EXISTS quando a lista puder conter NULL.
  • Agregados ignoram NULL: COUNT(column) conta valores não nulos, enquanto COUNT(*) conta as linhas.

A nulidade é uma decisão de design. Declare as colunas como NOT NULL, a menos que valores ausentes sejam genuinamente significativos.

JOINs, com um diagrama

Um join combina linhas de duas tabelas com base em uma coluna relacionada. Suponha que authors tenha Ada, Grace e Linus, e posts tenha duas linhas da Ada e uma da Grace. Os tipos de join produzem resultados diferentes.

authors            posts
-------            ----------------------------
id name            id author_id title
1  Ada             10 1         Joins
2  Grace           11 1         Indexes
3  Linus           12 2         NULLs

INNER JOIN  ->  posts 10, 11, 12        (only matches)
LEFT JOIN   ->  posts 10, 11, 12, Linus (all authors, NULL post)
RIGHT JOIN  ->  posts 10, 11, 12        (all posts)
FULL JOIN   ->  posts 10, 11, 12, Linus (both sides)
-- Every author, including those with no posts.
SELECT a.name, p.title
FROM authors a
LEFT JOIN posts p ON p.author_id = a.id
ORDER BY a.name;

A condição do join fica no ON. Para outer joins, adicionar um filtro da tabela da direita ao WHERE transforma silenciosamente o outer join em um inner join, porque as linhas NULL falham no filtro. Coloque tais condições na cláusula ON quando quiser que as linhas não correspondentes sobrevivam.

-- Keeps authors with no published posts.
SELECT a.name, p.title
FROM authors a
LEFT JOIN posts p
  ON p.author_id = a.id
 AND p.published_at IS NOT NULL;

Um self join utiliza a mesma tabela duas vezes com aliases diferentes, sendo útil para hierarquias. Um cross join emparelha cada linha com todas as outras e raramente é o que você deseja por acidente.

GROUP BY e HAVING

Agregadores colapsam várias linhas em uma só: COUNT, SUM, AVG, MIN, MAX. GROUP BY define os grupos.

SELECT a.name          AS author,
       COUNT(p.id)     AS post_count,
       MAX(p.created_at) AS latest
FROM authors a
LEFT JOIN posts p ON p.author_id = a.id
GROUP BY a.id, a.name
HAVING COUNT(p.id) > 0
ORDER BY post_count DESC;

A regra é que toda coluna selecionada deve estar no GROUP BY ou envolta em um agregador. Bancos de dados que impõem isso (Postgres e MySQL 8 por padrão) estão protegendo você de resultados arbitrários.

WHERE filtra linhas antes do agrupamento, e HAVING filtra grupos depois. Portanto, uma condição sobre um agregador pertence ao HAVING, e uma condição sobre uma coluna simples geralmente pertence ao WHERE por questões de eficiência.

SELECT author_id, COUNT(*) AS posts
FROM posts
WHERE published_at IS NOT NULL   -- filter rows
GROUP BY author_id
HAVING COUNT(*) > 5;             -- filter groups

Subqueries e CTEs

Uma subquery é uma consulta aninhada dentro de outra. Ela pode aparecer em SELECT, FROM ou WHERE.

SELECT title
FROM posts
WHERE author_id IN (
  SELECT id FROM authors WHERE name = 'Ada'
);

Uma common table expression (CTE) nomeia uma subquery com WITH, o que geralmente torna a leitura melhor do que o aninhamento e permite que você reutilize o resultado.

WITH published AS (
  SELECT id, author_id, title
  FROM posts
  WHERE published_at IS NOT NULL
)
SELECT a.name, COUNT(p.id) AS posts
FROM authors a
LEFT JOIN published p ON p.author_id = a.id
GROUP BY a.id, a.name;

CTEs podem ser encadeadas, e uma recursive CTE referencia a si mesma para percorrer uma árvore, como categorias ou threads de comentários. Opte por uma CTE sempre que uma consulta começar a ter mais de um nível de aninhamento; a legibilidade também é um recurso de performance.

Escrevendo linhas

INSERT adiciona linhas, UPDATE as altera e DELETE as remove.

INSERT INTO posts (author_id, title, body)
VALUES (7, 'Learning SQL', 'SQL is declarative.');

UPDATE posts
SET published_at = now()
WHERE id = 42;

DELETE FROM posts
WHERE id = 42;

Sempre forneça uma cláusula WHERE para UPDATE e DELETE. Sem ela, todas as linhas serão afetadas. Um bom hábito é executar o SELECT correspondente primeiro para confirmar a contagem. Um UPDATE também pode referenciar valores existentes:

UPDATE posts
SET title = title || ' (updated)'
WHERE author_id = 7;

INSERT pode adicionar várias linhas de uma vez e pode ser alimentado por uma query:

INSERT INTO posts (author_id, title)
SELECT id, 'Welcome' FROM authors;

Transações

Uma transação agrupa instruções para que todas tenham sucesso ou todas falhem. É isso que mantém a consistência em alterações de múltiplas etapas.

BEGIN;

UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

COMMIT;

Se uma instrução falhar, ou se você decidir abortar, ROLLBACK desfaz cada alteração feita desde BEGIN. Sem uma transação, uma falha entre as duas atualizações deixaria o dinheiro desaparecido. Agrupe escritas relacionadas, mantenha a transação curta e nunca aguarde a resposta de um usuário ou uma chamada de rede enquanto mantiver uma transação aberta.

Chaves e constraints

Constraints são regras que o banco de dados aplica por você:

  • PRIMARY KEY — identifica unicamente cada linha; também cria um índice e implica NOT NULL.
  • FOREIGN KEY — exige que o valor exista em outra tabela, preservando a integridade referencial.
  • UNIQUE — proíbe duplicatas em uma coluna ou combinação de colunas.
  • NOT NULL — exige um valor.
  • CHECK — exige que uma condição seja atendida, como price >= 0.
  • DEFAULT — fornece um valor quando nenhum é informado.
CREATE TABLE posts (
  id           bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  author_id    bigint NOT NULL REFERENCES authors (id) ON DELETE CASCADE,
  title        text NOT NULL,
  body         text NOT NULL,
  published_at timestamptz,
  created_at   timestamptz NOT NULL DEFAULT now()
);

Uma primary key substituta (surrogate key), como uma coluna de identidade, é estável e compacta. Uma natural key, como um endereço de e-mail, é significativa, mas pode mudar; portanto, prefira uma surrogate key com uma constraint UNIQUE na chave natural.

Índices em nível conceitual

Um índice é uma estrutura separada e ordenada que mapeia valores de colunas para a localização das linhas. Sem um índice, encontrar linhas significa escanear a tabela; com um, o banco de dados pode saltar diretamente para as correspondências.

CREATE INDEX posts_author_published_idx
  ON posts (author_id, published_at DESC);

Conceitualmente, pense em uma agenda telefônica ordenada por sobrenome. Buscar um nome é rápido porque a agenda está ordenada; filtrar por uma coluna que não seja a chave de ordenação significa ler todas as páginas. Os índices trocam velocidade de escrita e armazenamento por velocidade de leitura, pois cada insert e update deve mantê-los atualizados.

Crie índices nas colunas que você utiliza para filtrar e fazer joins, prefira índices compostos que correspondam aos formatos reais das suas queries e verifique com EXPLAIN se o planner realmente os está utilizando. Ter mais índices não é melhor; ter os índices certos é que é.

Window functions de uma vez só

Uma window function realiza cálculos em um conjunto de linhas relacionadas à linha atual, sem colapsá-las da maneira que o GROUP BY faz. Isso torna simples a criação de totais acumulados, rankings e comparações por grupo.

SELECT author_id,
       title,
       published_at,
       ROW_NUMBER() OVER (
         PARTITION BY author_id
         ORDER BY published_at DESC
       ) AS rank_in_author
FROM posts
WHERE published_at IS NOT NULL;

A cláusula OVER define a janela: PARTITION BY divide as linhas em grupos e ORDER BY as ordena dentro de cada grupo. Funções comuns incluem ROW_NUMBER, RANK, LAG, LEAD e SUM(...) OVER (...). A principal diferença em relação à agregação é que cada linha original é preservada.

Normalização: 1NF, 2NF, 3NF

A normalização é o processo de remover redundâncias para que cada fato seja armazenado apenas uma vez. As três primeiras formas normais são as que você mais utilizará:

Primeira forma normal (1NF) — sem grupos repetidos ou colunas com múltiplos valores. Uma coluna tags que armazena "sql,indexes" viola esta regra; uma tabela post_tags separada resolve o problema.

Segunda forma normal (2NF) — 1NF mais a ausência de dependência parcial de parte de uma chave composta. Se um item de pedido é identificado por (order_id, product_id) e armazena product_name, esse nome depende apenas de product_id, portanto, ele pertence a products.

Terceira forma normal (3NF) — 2NF mais a ausência de dependência transitiva entre colunas que não são chaves. Se posts armazenasse tanto author_id quanto author_email, o e-mail dependeria do autor, e não do post, portanto, ele pertence a authors.

Considere uma tabela que repete o e-mail do autor em cada post. Se você alterar o e-mail, precisará atualizar diversas linhas; se esquecer de uma, os dados se tornarão contraditórios. Divida-a em authors e posts e o fato passará a existir em um único lugar. Desnormalize posteriormente, de forma deliberada, quando um problema de performance mensurável justificar isso.

Boas práticas

  • Nomeie as colunas explicitamente em vez de usar SELECT * nas queries da aplicação.
  • Sempre ORDER BY quando a ordem dos resultados for importante.
  • Declare NOT NULL por padrão e trate valores ausentes com COALESCE.
  • Use IS NULL em vez de = NULL, e prefira NOT EXISTS a NOT IN com listas anuláveis.
  • Filtre linhas no WHERE e grupos no HAVING.
  • Use CTEs para manter queries complexas legíveis.
  • Adicione uma cláusula WHERE em UPDATE e DELETE e confirme as linhas de destino primeiro.
  • Envolva escritas relacionadas em uma transação e mantenha-a curta.
  • Adicione índices para padrões de query reais e verifique-os com EXPLAIN.
  • Armazene cada fato apenas uma vez e busque a terceira forma normal antes de desnormalizar.

Erros comuns

  • Escrever WHERE column = NULL e receber zero linhas.
  • Usar NOT IN em uma lista que contém NULL e não retornar nada silenciosamente.
  • Esquecer o WHERE em um UPDATE ou DELETE e modificar todas as linhas.
  • Selecionar colunas não agrupadas e receber valores arbitrários.
  • Assumir que as linhas retornam em uma ordem útil sem o ORDER BY.
  • Usar LIMIT ... OFFSET para paginação profunda e arcar com um custo crescente.
  • Filtrar a tabela correta em WHERE e acidentalmente transformar um LEFT JOIN em um inner join.
  • Indexar todas as colunas e deixar as escritas mais lentas sem nenhum benefício de leitura.
  • Concatenar entradas do usuário em strings SQL em vez de usar parâmetros, o que abre brechas para injection.
  • Armazenar fatos repetidos em uma única tabela larga em vez de normalizá-los.

Próximos passos

SQL é a base de todo banco de dados relacional, portanto, o próximo passo é escolher um engine e aprender seu dialeto e particularidades. Comece com PostgreSQL por sua conformidade com os padrões e tipos ricos, ou MySQL por sua onipresença e facilidade de replicação. A partir daí, veja como os resultados das queries se tornam respostas HTTP no guia de REST, e como um serviço Node.js envia essas queries utilizando uma pooled connection em Node.js basics.

Na pratica

Ler, juntar, agregar, escrever

Quatro instruções que cobrem a maioria do SQL do dia a dia.

list-posts.sql
SELECT id, title, published_at
FROM posts
WHERE published_at IS NOT NULL
  AND title ILIKE '%sql%'
ORDER BY published_at DESC
LIMIT 20 OFFSET 0;

Filtrando grupos

O WHERE executa antes do agrupamento e o HAVING depois, portanto, uma condição de agregação deve usar HAVING.

Preferível
SELECT author_id, COUNT(*) AS posts
FROM posts
GROUP BY author_id
HAVING COUNT(*) > 5;
Evite
SELECT author_id, COUNT(*) AS posts
FROM posts
WHERE COUNT(*) > 5   -- aggregates are not
GROUP BY author_id;  -- allowed in WHERE

Testando valores ausentes

NULL significa desconhecido, então a igualdade contra ele nunca corresponde. Use IS NULL e IS NOT NULL.

Preferível
SELECT id, title
FROM posts
WHERE published_at IS NULL;
Evite
SELECT id, title
FROM posts
WHERE published_at = NULL;
-- always returns zero rows

Trade-offs

Onde o SQL se encaixa e onde ele falha

SQL é a ferramenta certa para perguntas relacionais. Conhecer seus limites evita que você lute contra a linguagem.

Strengths

  • Uma linguagem, muitos bancos de dados

    O núcleo do SQL é padronizado, então as habilidades migram entre PostgreSQL, MySQL, SQLite, SQL Server e outros.

  • O otimizador faz a parte difícil

    Você descreve o resultado e o planejador escolhe índices e estratégias de join, muitas vezes superando loops escritos à mão.

  • A integridade vive no schema

    Chaves, constraints e transações tornam a correção uma propriedade do banco de dados, em vez de uma promessa no código da aplicação.

Trade-offs

  • Dialetos divergem

    O SQL padrão é uma base, não uma garantia. Paginação, upserts, funções de data e operadores JSON diferem conforme o fornecedor.

  • NULL é sutil

    A lógica de três valores confunde iniciantes e especialistas, e um único NULL mal manipulado pode descartar linhas silenciosamente.

  • Performance não é automática

    O otimizador precisa de bons índices e estatísticas atualizadas. Uma query que funciona bem com mil linhas pode colapsar com um milhão.

Perguntas frequentes

Perguntas frequentes

Keep learning

Related topics from the roadmap.

$ comecar a aprender

Pronto para aprender SQL Fundamentals?

Nosso tutorial interativo te guia por SQL Fundamentals passo a passo — com quizzes e codigo real que voce pode executar no navegador.