Uma novidade antes de começar
Se você acompanha o blog há algum tempo, já percebeu que praticamente tudo por aqui gira em torno de SQL Server. E faz sentido são mais de 15 anos vivendo esse banco em produção, entre troubleshooting, corrupções de banco, migrações, tuning e muita madrugada de plantão.
Mas o mercado de dados mudou, e o PostgreSQL deixou de ser “o outro banco” para se tornar presença obrigatória na vida de qualquer DBA, está nas nuvens (RDS, Azure Database, Cloud SQL), nas startups, nas migrações de redução de custo de licenciamento e cada vez mais, nas grandes empresas.
Há alguns meses venho estudando PostgreSQL a fundo, com foco de administração e performance montando laboratório, quebrando, corrigindo e documentando.
Então, a partir deste post, o blog também terá conteúdo de PostgreSQL. E vou fazer do jeito que gostaria que tivessem feito comigo quando comecei, explicando cada comando novo que aparecer e sempre que possível, comparando com o equivalente no SQL Server porque aprender um banco novo fica muito mais fácil quando você ancora no que já conhece. Então se você é DBA SQL Server curioso sobre o mundo Postgres, este blog agora também é pra você.
E nada melhor para estrear do que dar sequência a um assunto que já conhecemos bem por aqui.
O problema
No post anterior, vimos como no SQL Server uma tipagem errada o clássico VARCHAR vs NVARCHAR gera conversões implícitas silenciosas que transformam Index Seeks em Index Scans. A pergunta natural de quem trabalha com os dois bancos é: e no PostgreSQL, isso acontece igual?
A resposta curta e grossa (=D) não igual mas acontece, e em lugares diferentes dos que você espera.
O sistema de tipos do PostgreSQL é mais rígido que do SQL Server, onde o SQL Server tenta “adivinhar” e ajustar convertendo em segundo plano, o PostgresSQL muitas vezes simplesmente recusa a query com erro e isso é uma proteção. Mas quando a conversão implícita passa e cai na coluna e aí o índice B-Tree morre do mesmo jeito.
Neste post vou mostrar, com EXPLAIN ANALYZE real em um PostgreSQL 18, onde o índice sobrevive, onde ele morre, e um recurso do Postgres que o SQL Server não tem para resolver o problema.
Montando o ambiente de teste
Um detalhe didático importante, não adianta testar isso com meia dúzia de linhas. Com uma tabela de 4 registros, o planner escolhe Seq Scan sempre, porque a tabela inteira cabe em uma página e você vai concluir errado. Vamos popular com pelo menos 100 mil linhas com generate_series.
CREATE TABLE clientes (
id SERIAL PRIMARY KEY,
codigo_cliente VARCHAR(20) NOT NULL,
nome VARCHAR(100) NOT NULL,
criado_em TIMESTAMP NOT NULL DEFAULT now()
);
INSERT INTO clientes (codigo_cliente, nome, criado_em)
SELECT 'C' || LPAD(g::text, 6, '0'),
'Cliente ' || g,
now() - (g || ' minutes')::interval
FROM generate_series(1, 100000) g;
CREATE INDEX idx_clientes_codigo ON clientes(codigo_cliente);
CREATE INDEX idx_clientes_criado_em ON clientes(criado_em);
ANALYZE clientes;
Traduzindo para “SQL Serverês”:
SERIALé oIDENTITY(1,1)do Postgres (por baixo, ele cria uma sequence em versões novas, prefiraGENERATED ALWAYS AS IDENTITY, masSERIALainda funciona muito bem);generate_series(1, 100000)gera uma tabela virtual de números a famosa tally table que sempre improvisamos no SQL Server (que, aliás, ganhouGENERATE_SERIESnativo só no SQL Server 2022);::é o operador de cast do PostgresSQL,g::textequivale aCAST(g AS VARCHAR). Guarde esse operador ele é o protagonista deste post.
E o ANALYZE clientes? É o equivalente ao UPDATE STATISTICS do SQL Server coleta estatísticas da tabela (cardinalidade, valores mais comuns, histogramas) para o planner estimar corretamente os custos. Como acabamos de encher uma tabela que nasceu vazia, rodá-lo explicitamente garante que o seu plano saia igual ao do post, sem esperar o autovacuum disparar a coleta automática. Cuidado para não confundir o ANALYZE com o EXPLAIN ANALYZE das queries a seguir lá, ANALYZE significa “execute a query de verdade e mostre os tempos reais”.
Antes de começar: como ver os planos (psql e pgAdmin)
Todos os planos deste post foram gerados com EXPLAIN ANALYZE em modo texto que funciona igual no psql e no pgAdmin, escreva EXPLAIN ANALYZE antes da query e execute normalmente (F5 no pgAdmin). O plano volta como texto na coluna QUERY PLAN.
Mas o pgAdmin também tem um plano gráfico, no estilo do plano de execução visual do SSMS e aqui mora uma pegadinha que confunde muita gente vinda do SQL Server. (inclusive apanhei no começo :=))
- Plano em texto → query com
EXPLAIN ANALYZEescrito + execução normal (F5); - Plano gráfico → query sem
EXPLAINnenhum + os botões de Explain do Query Tool: F7 para o plano estimado (o Ctrl+L do SSMS) ou Shift+F7 para o plano real (o Ctrl+M do SSMS).
Os dois modos são mutuamente exclusivos: se você escrever EXPLAIN ANALYZE na query e apertar Shift+F7, o pgAdmin adiciona um segundo EXPLAIN por baixo dos panos (EXPLAIN (ANALYZE, FORMAT JSON) EXPLAIN ANALYZE SELECT...), o servidor devolve erro de sintaxe e nada aparece. É o mesmo motivo pelo qual, no SSMS, você não escreve SET SHOWPLAN_XML ON junto com o Ctrl+M botão já faz isso por você.
No modo gráfico, leia o diagrama da direita para a esquerda (como no SSMS) e explore as sub-abas: Graphical (setas mais grossas = mais linhas fluindo seta gorda saindo de um Seq Scan é alerta), Analysis (compare rows estimado vs real: divergência grande é sinal de estatística desatualizada lembre do ANALYZE) e Statistics. No dropdown do botão, habilite Buffers para ter o equivalente ao SET STATISTICS IO ON: páginas vindas do cache (shared hit) vs lidas do disco (read).
EXPLAIN ANALYZE
SELECT id, nome
FROM clientes
WHERE codigo_cliente = 'C001003';

Index Scan, 0.046 ms. Perfeito.
Cuidado para não confunfir o SCAN no PostgreSQL,
Index Scané a busca pontual pela árvore do índice ou seja, o que no SQL Server chamamos de Index Seek. O equivalente ao Index Scan do SQL Server (varrer o índice inteiro) existe, mas o PostgreSQL também pode aparecer comoBitmap Index Scanou, para varredura completa da tabela,Seq Scan(o nosso Table Scan). Todo DBA SQL Server estranha isso nos primeirosEXPLAIN.Mas repare em um detalhe que quase ninguém nota: o
Index Condmostra(codigo_cliente)::text. O cast paratextestá ali mesmo na query “ideal”.

Isso acontece porque VARCHAR no PostgreSQL não tem operadores de comparação próprios ele usa os operadores de text através de um cast binary-coercible (os dois tipos têm a mesma representação em disco). Esse cast é gratuito e não impede o uso do índice.
Cenário 2 – O mito: ::text na coluna NÃO quebra o índice
Aqui está uma pegadinha que circula por aí (inclusive em respostas geradas por IA), a ideia de que qualquer CAST na coluna do WHERE anula o índice. No PostgreSQL, isso não é verdade para casts binary-coercible:
EXPLAIN ANALYZE
SELECT id, nome
FROM clientes
WHERE codigo_cliente::text = 'C001003'::text;


Mesmo plano, mesmo índice, mesma performance. VARCHAR ↔ TEXT é o “casal feliz” do Postgres é o oposto do drama VARCHAR vs NVARCHAR do SQL Server.
A regra correta não é “cast na coluna quebra índice”. A regra é o índice quebra quando a expressão sobre a coluna produz algo que o índice não indexou. Vamos ver onde isso acontece de verdade.
Cenário 3 – Onde o índice morre de verdade: INTEGER vs NUMERIC
A coluna id é INTEGER e tem índice (PK). Agora imagine que a aplicação ou o ORM manda o parâmetro como decimal:
EXPLAIN ANALYZE
SELECT id, nome
FROM clientes
WHERE id = 1003.0;


Ali está a conversão implícita clássica, em pleno PostgreSQL: o literal 1003.0 é NUMERIC, não existe operador integer = numeric na família de operadores do índice B-Tree, então o planner converte a coluna ((id)::numeric) e o índice da PK vira paisagem. Seq Scan, 99.999 linhas descartadas, 244x mais lento (14,2 ms vs 0,058 ms). Em uma tabela de 100 milhões de linhas, isso é a diferença entre milissegundos e minutos.
E o famoso INT vs BIGINT que assusta muita gente? esse funciona bem, sem conversão de dados.
EXPLAIN ANALYZE;
SELECT id, nome
FROM clientes
WHERE id = 1003


Os tipos de dados inteiros (smallint, integer, bigint) compartilham a mesma família de operadores B-Tree (integer_ops), com operadores cross-type nativos. O problema é sempre quando entra NUMERIC, REAL ou DOUBLE PRECISION na comparação com coluna inteira.
Cenário 4 – A rigidez que protege
E se compararmos VARCHAR com número, o cenário que no SQL Server gera conversão silenciosa (e às vezes erro em runtime no meio da produção)?
SELECT id
FROM clientes
WHERE codigo_cliente = 1003;

O PostgreSQL nem executa. Enquanto o SQL Server converteria a coluna inteira silenciosamente, o Postgres devolve o problema para o desenvolvedor em tempo de desenvolvimento. É chato e mata o dev de raiva? Sim. Mas esse “chato” já salvou muita madrugada inclusive o DBA agradece!!!!
Cenário 5 – O assassino mais comum no dia a dia: função/cast que transforma o tipo
Agora o caso que eu mais encontro em consultoria, filtrar um TIMESTAMP por dia, usando um cast para date.
EXPLAIN ANALYZE
SELECT count(*)
FROM clientes
WHERE criado_em::date = '2026-06-15';


Seq Scan, mesmo com índice em criado_em. Diferente do varchar::text, o cast timestamp::date não é binary-coercible ele transforma o valor em outra coisa. O índice indexou timestamps, a query pergunta sobre date. O planner não tem como usar o índice.
A solução clássica (válida também no SQL Server) é reescrever como intervalo, um predicado SARGable:
WHERE criado_em >= '2026-06-15'
AND criado_em < '2026-06-16'

Mas o PostgreSQL tem uma segunda saída que o SQL Server não tem de forma tão direta.
PostgreSQL vs SQL Server: resumo das diferenças
| Comportamento | SQL Server | PostgreSQL |
|---|---|---|
VARCHAR vs NVARCHAR / TEXT | Converte implicitamente pela precedência (NVARCHAR ganha) → coluna convertida → Index Scan | VARCHAR e TEXT são binary-coercible → índice funciona normalmente |
| Coluna string vs literal numérico | Converte a coluna silenciosamente → índice morre + risco de erro em runtime | Erro imediato: operator does not exist |
Coluna INT vs parâmetro BIGINT | Converte a coluna (precedência de tipos) → pode degradar | Operadores cross-type nativos → índice funciona |
Coluna INT vs literal/parâmetro NUMERIC | Converte a coluna → degrada | Converte a coluna ((id)::numeric) → Seq Scan |
| Aviso no plano | CONVERT_IMPLICIT + warning no plano XML | Nenhum warning — você precisa ler o Filter no EXPLAIN |
| Indexar uma expressão | No SQL Server você não pode criar um índice diretamente sobre uma expressão. A alternativa é usar uma Computed Column. | Expression Index nativo: CREATE INDEX ON t ((expr)) |
Resumo
- A rigidez do Postgres protege, comparações de tipos incompatíveis viram erro em desenvolvimento, não Seq Scan silencioso em produção.
::textem colunavarcharnão quebra índice, casts binary-coercible são gratuitos.- O índice morre quando a expressão muda o tipo,
intvsnumeric,timestamp::date,lower(),json::jsonb. - No Postgres não há warning de conversão no plano, aprenda a ler o
Filter:doEXPLAIN ANALYZE. - Expression Index é o superpoder do PG, quando não dá para mudar a query, indexe a expressão.
Gostou desse conteudo?
Então continue nos acompanhando que sempre teremos novidades por aqui =D