Como as ideias centrais de Jeffrey Ullman impulsionam bancos de dados modernos: álgebra relacional, regras de otimização, joins e planejamento ao estilo de compilador que ajudam sistemas a escalar.

A maioria das pessoas que escreve SQL, monta dashboards ou afina uma consulta lenta já se beneficiou do trabalho de Jeffrey Ullman — mesmo que nunca tenha ouvido seu nome. Ullman é um cientista da computação e educador cuja pesquisa e livros ajudaram a definir como bancos de dados descrevem dados, raciocinam sobre consultas e as executam eficientemente.
Quando um motor de banco de dados transforma seu SQL em algo que ele pode executar rápido, está confiando em ideias que precisam ser ao mesmo tempo precisas e adaptáveis. Ullman ajudou a formalizar o significado das consultas (para que o sistema possa reescrevê-las com segurança) e a conectar o pensamento sobre bancos de dados com o pensamento de compiladores (para que uma consulta seja analisada, otimizada e traduzida em passos executáveis).
Essa influência é discreta porque não aparece como um botão na sua ferramenta de BI ou como uma opção visível no console da nuvem. Ela aparece como:
JOINEste post usa as ideias centrais de Ullman como um guia pelos internos de bancos de dados que mais importam na prática: como a álgebra relacional fica por baixo do SQL, como reescritas de consulta preservam significado, por que otimizadores baseados em custo tomam certas escolhas e como algoritmos de join frequentemente decidem se um trabalho termina em segundos ou horas.
Também vamos aproveitar alguns conceitos parecidos com compiladores — parsing, reescrita e planejamento — porque engines de banco de dados se comportam mais como compiladores sofisticados do que muita gente imagina.
Uma promessa rápida: manteremos a discussão precisa, mas evitaremos provas pesadas. O objetivo é dar modelos mentais que você possa aplicar no trabalho na próxima vez que desempenho, escalabilidade ou comportamento confuso de uma consulta aparecer.
Se você já escreveu uma consulta SQL e esperou que ela “signifique apenas uma coisa”, está confiando em ideias que Jeffrey Ullman ajudou a popularizar e formalizar: um modelo limpo para dados, mais formas precisas de descrever o que uma consulta pede.
No essencial, o modelo relacional trata dados como tabelas (relações). Cada tabela tem linhas (tuplas) e colunas (atributos). Isso soa óbvio hoje, mas a parte importante é a disciplina que isso cria:
Esse enquadramento torna possível raciocinar sobre correção e desempenho sem superficialidade. Quando você sabe o que uma tabela representa e como linhas são identificadas, consegue prever o que joins devem fazer, o que duplicatas significam e por que certos filtros mudam resultados.
O ensino de Ullman frequentemente usa a álgebra relacional como uma espécie de calculadora de consultas: um pequeno conjunto de operações (select, project, join, union, difference) que você pode combinar para expressar o que deseja.
Por que isso importa para quem trabalha com SQL: os bancos de dados traduzem SQL para uma forma algébrica e então reescrevem essa forma em outra equivalente. Duas consultas que parecem diferentes podem ser algebraicamente iguais — é assim que otimizadores podem reordenar joins, empurrar filtros para baixo ou remover trabalho redundante mantendo o significado intacto.
SQL é em grande parte “o quê”, mas engines frequentemente otimizam usando álgebra “como”.
Dialetos SQL variam (Postgres vs. Snowflake vs. MySQL), mas os fundamentos não. Entender chaves, relacionamentos e equivalência algébrica ajuda você a perceber quando uma consulta está logicamente errada, quando está apenas lenta e quais mudanças preservam significado entre plataformas.
Álgebra relacional é a “matemática por trás” do SQL: um pequeno conjunto de operadores que descrevem o resultado que você quer. O trabalho de Jeffrey Ullman ajudou a tornar essa visão por operadores nítida e ensinável — e é ainda o modelo mental que a maioria dos otimizadores usa.
Uma consulta de banco de dados pode ser expressa como um pipeline de poucos blocos de construção:
WHERE do SQL)SELECT col1, col2)JOIN ... ON ...)UNION)EXCEPT em vários dialectos)Por ser um conjunto pequeno, fica mais fácil raciocinar sobre correção: se duas expressões algébricas são equivalentes, elas retornam a mesma tabela para qualquer estado de banco de dados válido.
Considere uma consulta familiar:
SELECT c.name
FROM customers c
JOIN orders o ON o.customer_id = c.id
WHERE o.total > 100;
Conceitualmente, isso é:
comece com um join de customers e orders: customers ⋈ orders
select apenas orders acima de 100: σ(o.total > 100)(...)
project a coluna que você quer: π(c.name)(...)
Isso não é a notação interna exata usada por todo motor, mas é a ideia certa: SQL vira uma árvore de operadores.
Muitas árvores diferentes podem significar o mesmo resultado. Por exemplo, filtros podem frequentemente ser empurrados para mais cedo (aplicar σ antes de um grande join), e projeções podem eliminar colunas não usadas mais cedo (aplicar π antecipadamente).
Essas regras de equivalência é que permitem que um banco de dados reescreva sua consulta para um plano mais barato sem mudar o significado. Uma vez que você vê consultas como álgebra, “otimização” deixa de ser mágica e vira um remodelamento guiado por regras seguras.
Quando você escreve SQL, o banco de dados não executa “como está escrito”. Ele traduz sua instrução em um plano de consulta: uma representação estruturada do trabalho a ser feito.
Um bom modelo mental é uma árvore de operadores. Folhas leem tabelas ou índices; nós internos transformam e combinam linhas. Operadores comuns incluem scan, filter (seleção), project (escolha de colunas), join, group/aggregate e sort.
Bancos de dados normalmente separam o planejamento em duas camadas:
A influência de Ullman aparece na ênfase em transformações que preservam o significado: rearranje o plano lógico de várias maneiras sem mudar a resposta, então escolha uma estratégia física eficiente.
Antes de escolher a abordagem final de execução, otimizadores aplicam regras algébricas de “limpeza”. Essas reescritas não mudam os resultados; elas reduzem trabalho desnecessário.
Exemplos comuns:
Suponha que você queira pedidos de usuários de um país:
SELECT o.order_id, o.total
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.country = 'CA';
Uma interpretação ingênua poderia juntar todos os users com todos os orders e só depois filtrar para Canadá. Uma reescrita que preserva o significado empurra o filtro para baixo para que o join toque menos linhas:
country = 'CA'order_id e totalEm termos de plano, o otimizador tenta transformar:
Join(Users, Orders) → Filter(country='CA') → Project(order_id,total)
em algo mais próximo de:
Filter(country='CA') on Users → Join(with Orders) → Project(order_id,total)
Mesmo resultado. Menos trabalho.
Essas reescritas são fáceis de ignorar porque você nunca as digita — e ainda assim são uma das grandes razões de o mesmo SQL rodar rápido em um banco e lento em outro.
Quando você executa uma consulta SQL, o banco de dados considera múltiplas maneiras válidas de obter a mesma resposta e escolhe a que espera ser mais barata. Esse processo de decisão é chamado otimização baseada em custo — e é um dos pontos mais práticos onde a teoria no estilo de Ullman aparece no desempenho do dia a dia.
Um modelo de custo é um sistema de pontuação que o otimizador usa para comparar planos alternativos. A maioria dos motores estima custo usando alguns recursos centrais:
O modelo não precisa ser perfeito; precisa ser direcionalmente correto com frequência suficiente para escolher bons planos.
Antes de pontuar planos, o otimizador pergunta em cada passo: quantas linhas isso vai produzir? Isso é a estimativa de cardinalidade.
Se você filtra WHERE country = 'CA', o motor estima qual fração da tabela casa. Se você junta customers com orders, estima quantos pares vão bater na chave do join. Esses palpites de contagem de linhas determinam se ele prefere um index scan a uma varredura completa, um hash join a um nested loop ou se uma ordenação será pequena ou enorme.
Os palpites do otimizador são guiados por estatísticas: contagens, distribuições de valores, taxas de nulos e às vezes correlações entre colunas.
Quando estatísticas estão desatualizadas ou ausentes, o motor pode errar a estimativa por ordens de grandeza. Um plano que parecia barato no papel pode ficar caro na prática — sintomas clássicos incluem quedas de desempenho após aumento de dados, mudanças “aleatórias” de plano ou joins que inesperadamente derramam para disco.
Melhores estimativas frequentemente exigem mais trabalho: estatísticas mais detalhadas, amostragem ou explorar mais planos candidatos. Mas planejar também custa tempo, especialmente para consultas complexas.
Então os otimizadores equilibram dois objetivos:
Entender esse trade-off ajuda a interpretar EXPLAIN: o otimizador não tenta ser genial — tenta ser previsivelmente correto com informação limitada.
O trabalho de Ullman ajudou a popularizar uma ideia simples mas poderosa: SQL não é tanto “executado” quanto traduzido em um plano de execução. Em nenhum lugar isso é mais óbvio do que nos joins. Duas consultas que retornam as mesmas linhas podem ter tempos de execução muito diferentes dependendo de qual algoritmo de join o motor escolhe — e em que ordem ele junta as tabelas.
Nested loop join é conceitualmente simples: para cada linha à esquerda, encontre linhas que batem à direita. Pode ser rápido quando o lado esquerdo é pequeno e o lado direito tem um índice útil.
Hash join constrói uma tabela hash a partir de uma entrada (frequentemente a menor) e a consulta com a outra. Brilha com entradas grandes e não ordenadas em condições de igualdade (ex.: A.id = B.id), mas precisa de memória; derramar para disco pode eliminar a vantagem.
Merge join percorre dois inputs em ordem ordenada. É ótimo quando ambos os lados já estão ordenados (ou ordenáveis barato), por exemplo quando índices entregam linhas na ordem da chave de join.
Com três ou mais tabelas, o número de ordens de join possíveis explode. Juntar duas tabelas grandes primeiro pode criar um resultado intermediário enorme que atrasa tudo. Uma ordem melhor costuma começar pelo filtro mais seletivo (menos linhas) e expandir a partir daí, mantendo intermediários pequenos.
Índices não apenas aceleram buscas — eles tornam certas estratégias de join viáveis. Um índice na chave de join pode transformar um nested loop caro em um padrão de “seek por linha” rápido. Por outro lado, índices faltantes ou inúteis podem empurrar o motor para hash joins ou grandes ordenações para um merge join.
Bancos de dados não apenas “rodam SQL”. Eles o compilam. A influência de Ullman se estende tanto à teoria de banco de dados quanto ao pensamento de compiladores, e essa conexão explica por que engines de consulta se parecem com toolchains de linguagens: elas traduzem, reescrevem e otimizam antes de fazer qualquer trabalho.
Quando você envia uma consulta, o primeiro passo se parece com a front-end de um compilador. O motor tokeniza palavras-chave e identificadores, checa gramática e constrói uma árvore de parse (frequentemente simplificada em uma árvore de sintaxe abstrata). É aqui que erros básicos são capturados: vírgulas faltando, nomes de coluna ambíguos, regras de agrupamento inválidas.
Um modelo mental útil: SQL é uma linguagem de programação cujo “programa” descreve relacionamentos de dados em vez de loops.
Compiladores convertem sintaxe em uma representação intermediária (IR). Bancos fazem algo similar: traduzem a sintaxe SQL em operadores lógicos tais como:
GROUP BY)Essa forma lógica fica mais próxima da álgebra relacional do que do texto SQL, o que facilita raciocinar sobre significado e equivalência.
Otimizações de compilador mantêm o resultado do programa idêntico enquanto tornam a execução mais barata. Otimizadores de banco fazem o mesmo, usando sistemas de regra como:
Essa é a versão de banco de dados de “eliminação de código morto”: não são técnicas idênticas, mas a mesma filosofia — preservar semântica, reduzir custo.
Se sua consulta está lenta, não fique só no SQL. Olhe o plano de consulta como inspecionaria a saída de um compilador. Um plano mostra o que o motor escolheu de fato: ordem de join, uso de índice e onde o tempo é gasto.
Conclusão prática: aprenda a ler EXPLAIN como uma listagem de assembly de desempenho. Isso transforma tuning de adivinhação em depuração baseada em evidência. Para mais sobre transformar isso em hábito, veja /blog/practical-query-optimization-habits.
Bom desempenho muitas vezes começa antes de você escrever SQL. A teoria de design de esquema de Ullman (especialmente normalização) trata de estruturar dados para que o banco consiga mantê-los corretos, previsíveis e eficientes conforme crescem.
A normalização visa:
Essas vitórias de correção se traduzem em ganhos de desempenho depois: menos campos duplicados, índices menores e menos updates caros.
Você não precisa decorar provas para usar as ideias:
Denormalização pode ser uma escolha inteligente quando:
O importante é denormalizar deliberadamente, com um processo para manter duplicações sincronizadas.
O design do esquema molda o que o otimizador pode fazer. Chaves claras e foreign keys permitem melhores estratégias de join, reescritas mais seguras e estimativas de contagem de linhas mais acuradas. Em contrapartida, duplicação excessiva pode inflar índices e desacelerar escritas, e colunas multivalor bloqueiam predicados eficientes. Conforme o volume cresce, decisões de modelagem iniciais frequentemente contam mais do que micro‑otimizações de uma única consulta.
Quando um sistema “escala”, raramente é só adicionar máquinas maiores. Frequentemente o ponto difícil é que o mesmo significado de consulta precisa ser preservado enquanto o motor escolhe uma estratégia física bem diferente para manter tempos previsíveis. A ênfase de Ullman em equivalências formais é justamente o que permite essas trocas de estratégia sem mudar resultados.
Em tamanhos pequenos, muitos planos “funcionam”. Em escala, a diferença entre varrer uma tabela, usar um índice ou usar um resultado pré‑computado pode ser segundos vs. horas. O lado teórico importa porque o otimizador precisa de um conjunto seguro de regras de reescrita (ex.: empurrar filtros antes, reordenar joins) que não alterem a resposta — mesmo que mudem radicalmente o trabalho realizado.
Particionamento (por data, cliente, região etc.) transforma uma tabela lógica em muitos pedaços físicos. Isso afeta o planejamento:
O texto SQL pode permanecer inalterado, mas o melhor plano passa a depender de onde as linhas vivem.
Views materializadas são essencialmente “subexpressões salvas”. Se o motor puder provar que sua consulta coincide (ou pode ser reescrita para coincidir) com um resultado armazenado, ele pode substituir trabalho caro — como joins e agregações repetidas — por uma busca rápida. Isso é álgebra relacional em prática: reconhecer equivalência e então reutilizar.
Cache pode acelerar leituras repetidas, mas não salva uma consulta que precisa varrer muitos dados, embaralhar intermediários enormes ou computar um join gigantesco. Quando aparecem problemas de escala, a solução frequentemente é: reduzir a quantidade de dados tocados (layout/particionamento), reduzir computação repetida (views materializadas) ou mudar o plano — não apenas “adicionar cache”.
A influência de Ullman aparece em uma mentalidade simples: trate uma consulta lenta como uma declaração de intenção que o banco é livre para reescrever e, depois, verifique o que ele realmente decidiu fazer. Você não precisa ser teórico para se beneficiar — só precisa de uma rotina repetível.
Comece pelas partes que geralmente dominam o tempo de execução:
Se só fizer uma coisa, identifique o primeiro operador onde a contagem de linhas explode. Normalmente ali está a causa raiz.
São fáceis de escrever e surpreendentemente custosos:
WHERE LOWER(email) = ... pode impedir uso de índice (use uma coluna normalizada ou índice funcional se suportado).Álgebra relacional incentiva dois movimentos práticos:
WHERE antes de joins sempre que possível para reduzir inputs.Uma boa hipótese soa como: “Este join é caro porque estamos juntando muitas linhas; se filtrarmos orders dos últimos 30 dias primeiro, a entrada do join cai.”
Use uma regra de decisão simples:
EXPLAIN mostrar trabalho evitável (joins desnecessários, filtro aplicado tardiamente, predicados não sargáveis).O objetivo não é “SQL engenhoso”. É obter resultados intermediários previsíveis e menores — exatamente o tipo de melhoria que as ideias de Ullman tornam mais fáceis de identificar.
Esses conceitos não são só para DBAs. Se você entrega uma aplicação, está tomando decisões de banco de dados e planejamento de consultas mesmo sem perceber: forma do esquema, escolhas de chave, padrões de consulta e a camada de acesso a dados influenciam o que o otimizador pode fazer.
Se você usa um fluxo de trabalho de vibe‑coding (por exemplo, gerar um app React + Go + PostgreSQL a partir de uma interface de chat no Koder.ai), modelos mentais no estilo Ullman são uma rede de segurança prática: você pode revisar o esquema gerado para checar chaves e relacionamentos limpos, inspecionar as consultas que sua app usa e validar desempenho com EXPLAIN antes que problemas apareçam em produção. Quanto mais rápido você iterar em “intenção da consulta → plano → correção”, mais valor tira do desenvolvimento acelerado.
Você não precisa “estudar teoria” como hobby separado. A maneira mais rápida de se beneficiar das bases de Ullman é aprender só o suficiente para ler planos de consulta com confiança — e então praticar no seu próprio banco.
Procure por esses livros e tópicos de aula (sem afiliação — apenas pontos de partida amplamente citados):
Comece pequeno e mantenha cada passo vinculado a algo observável:
Escolha 2–3 consultas reais e itere:
IN por EXISTS, empurrar predicados, remover colunas desnecessárias e comparar resultados.Use linguagem clara, baseada em planos:
Esse é o ganho prático das fundações de Ullman: você obtém um vocabulário compartilhado para explicar desempenho — sem ficar adivinhando.
Jeffrey Ullman ajudou a formalizar como os bancos de dados representam o significado de uma consulta e como eles podem transformar consultas de forma segura para obter versões mais rápidas. Essa base aparece sempre que um motor reescreve uma consulta, reordena joins ou seleciona um plano de execução diferente, garantindo o mesmo conjunto de resultados.
Álgebra relacional é um pequeno conjunto de operadores (seleção, projeção, join, união, diferença) que descrevem com precisão os resultados de uma consulta. Os motores costumam traduzir SQL para uma árvore de operadores semelhante à álgebra para aplicar regras de equivalência (como empurrar filtros para mais cedo) antes de escolher uma estratégia de execução.
Porque a otimização depende de provar que uma consulta reescrita retorna os mesmos resultados. Regras de equivalência permitem que o otimizador, por exemplo:
WHERE para antes de um joinEssas mudanças podem reduzir muito o trabalho sem alterar o significado.
Um plano lógico descreve o que precisa ser calculado (filtros, joins, agregações) sem detalhes de armazenamento. Um plano físico escolhe como executar (varredura por índice vs. varredura completa, hash join vs. nested loop, paralelismo, estratégias de ordenação). A maioria das diferenças de desempenho vem das escolhas físicas, habilitadas por reescritas lógicas.
Otimização baseada em custo avalia múltiplos planos válidos e escolhe o que tem menor custo estimado. Os custos são normalmente guiados por fatores práticos como linhas processadas, I/O, CPU e memória (incluindo quando um hash ou ordenação derrama para disco).
Estimativa de cardinalidade é o palpite do otimizador sobre “quantas linhas esta etapa vai produzir?”. Essas estimativas determinam ordem de joins, tipo de join e se um acesso por índice vale a pena. Quando as estimativas estão erradas (frequentemente por estatísticas desatualizadas/ausentes), você pode ter quedas de desempenho súbitas, grandes derramamentos para disco ou mudanças inesperadas no plano.
Nested loop join: bom quando o lado esquerdo é pequeno e o lado direito pode ser sondado eficientemente (geralmente via índice).Hash join: excelente para joins de igualdade em entradas grandes e não ordenadas, mas precisa de memória suficiente para evitar derramamento.Merge join: ideal quando ambos os inputs já estão ordenados (ou podem ser ordenados barato), muitas vezes favorecido por índices que entregam ordem.Concentre-se em algumas pistas de alto sinal:
Trate o plano como saída compilada: ele mostra o que o motor realmente decidiu fazer.
A normalização reduz fatos duplicados e anomalias de atualização, o que costuma resultar em tabelas e índices menores e joins mais confiáveis. A denormalização pode ser apropriada para cargas analíticas ou padrões de leitura intensiva, mas deve ser feita de forma deliberada (regras claras de atualização, redundância conhecida) para não degradar a consistência ao longo do tempo.
Escalar frequentemente exige mudar a estratégia física mantendo o significado da consulta idêntico. Ferramentas comuns incluem:
Cache ajuda leituras repetidas, mas não resolve uma consulta que precisa tocar muitos dados ou gerar grandes joins intermediários.