Meu Banco de Dados Estava "Correto." Também Estava 296x Lento Demais.
Descobri que meu banco de dados Postgres tinha 89 chaves estrangeiras mas zero índices nelas — transformando queries de milissegundos em pesadelos de 843ms e quase destruindo meu lançamento do alpha.
25 de outubro de 2025. Minha aplicação SaaS finalmente estava com todas as funcionalidades completas. Nove grandes funcionalidades, arquitetura multi-tenant, aprendizado progressivo — tudo funcionava. Mas estava lenta. Muito lenta.
Queries de dashboard rastejando. Listas que deveriam carregar instantaneamente levando vários segundos. Testadores alpha começavam a perguntar "O site está quebrado?"
Não estava apenas preocupado com performance. Estava preocupado com sobrevivência.
Passei dois dias otimizando queries, reescrevendo políticas RLS, adicionando caching. Nada fazia diferença. A frustração estava aumentando — 5 dias antes do lançamento planejado do alpha e não conseguia descobrir por que um banco de dados tecnicamente correto estava performando tão mal.
Então executei uma query diagnóstica que fez meu estômago afundar.
89 chaves estrangeiras. Zero índices.
Esta é a história de um "recurso" do Postgres que ninguém te conta — as 2 semanas que quase destruíram meu lançamento do alpha — e o fix de 4 minutos que salvou tudo.
As Stakes: Quando Lento Significa Morto
Pesquisas mostram que cada 100ms de latência custa 1% das conversões. Com tempos de carregamento de 2-3 segundos, não estava apenas entregando uma experiência ruim — estava destruindo as primeiras impressões que fariam ou quebrariam meu alpha.
As stakes imediatas:
- Testadores alpha questionando se a plataforma era estável o suficiente para recomendar
- Credibilidade perdida com usuários iniciais que haviam me dado sua confiança
- Se persistisse no lançamento completo: pesquisas do setor sugerem 40% de taxa de rejeição em tempos de carregamento de 3 segundos
- A diferença entre "isso é promissor" e "isso parece quebrado"
- CPU do banco de dados em 80-90% (caro e piorando)
- 5 dias do lançamento planejado, completamente bloqueado
O que essas 2 semanas realmente custaram:
- 40+ horas depurando queries lentas, perfilando código, culpando políticas RLS
- 1 semana de atraso no lançamento (perdi meu prazo interno)
- Oportunidade perdida de construir funcionalidades
- Dano à credibilidade com testadores iniciais
A dívida técnica estava se tornando dívida de negócios. E eu não sabia por quê.
---
A Investigação: Seguindo as Queries Lentas
No final de outubro, minha plataforma tinha complexidade real: 30+ tabelas, 80+ políticas RLS, centenas de relacionamentos de chave estrangeira. Operações simples que deveriam ser instantâneas eram dolorosamente lentas — carregar listas, buscar dados do dashboard, filtrar por org_id.
Comecei com pg_stat_statements para encontrar os culpados:
```sql
SELECT query, mean_exec_time, calls
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
```
Toda query lenta tinha o mesmo padrão — filtrar por chaves estrangeiras:
```sql
SELECT * FROM user_data
WHERE org_id = '...' AND resource_id = '...';
```
Deveria ser instantâneo. Eu tinha essas colunas. Tinha políticas RLS. Por que estava lento?
Verifiquei o plano de execução:
```sql
EXPLAIN ANALYZE
SELECT * FROM user_data
WHERE org_id = 'abc-123' AND resource_id = 'xyz-789';
```
O resultado fez meu estômago afundar:
```
Seq Scan on user_data (cost=0.00..2847.23 rows=15 width=1024)
Filter: ((org_id = 'abc-123') AND (resource_id = 'xyz-789'))
Rows Removed by Filter: 12,834
Execution Time: 843.271 ms
```
Sequential scan. O Postgres estava lendo cada linha da tabela, depois filtrando. Em uma query que deveria levar milissegundos.
Mas eu tinha chaves estrangeiras em org_id e resource_id. Não deveriam ser indexadas?
---
A Revelação: Chaves Estrangeiras Não Criam Índices Automaticamente
Aqui está o que aprendi naquele dia, do jeito difícil:
O Postgres cria automaticamente índices para CHAVEs PRIMÁRIAS e restrições UNIQUE.
O Postgres NÃO cria automaticamente índices para chaves estrangeiras.
Deixa eu repetir, porque esse único equívoco custou 2 semanas da minha vida:
CHAVES ESTRANGEIRAS ≠ ÍNDICES
Quando você escreve isso:
```sql
CREATE TABLE user_data (
id UUID PRIMARY KEY, -- ✅ Indexado automaticamente
org_id UUID REFERENCES organizations(id), -- ❌ NÃO indexado!
resource_id UUID REFERENCES resources(id), -- ❌ NÃO indexado!
created_at TIMESTAMPTZ
);
```
O Postgres cria a restrição de chave estrangeira (integridade referencial), mas **não** cria um índice em org_id ou resource_id.
Por quê? Porque o Postgres não pode assumir como você vai consultar os dados. Talvez você nunca filtre por chaves estrangeiras. Talvez você sempre faça join em uma direção específica. Então deixa a decisão para você.
O problema? Não sabia que precisava tomar essa decisão. Assumi que "chave estrangeira" significava "indexada para queries." Não significa.
---
O Diagnóstico: Quão Ruim Era?
Escrevi uma query para encontrar cada chave estrangeira sem um índice:
```sql
SELECT
c.conrelid::regclass AS table_name,
a.attname AS column_name
FROM pg_constraint c
JOIN pg_attribute a ON a.attnum = ANY(c.conkey) AND a.attrelid = c.conrelid
WHERE c.contype = 'f'
AND NOT EXISTS (
SELECT 1 FROM pg_index i
WHERE i.indrelid = c.conrelid AND a.attnum = ANY(i.indkey)
);
```
O resultado me pegou como uma pedrada:
89 linhas.
89 chaves estrangeiras em 32 tabelas. Zero índices.
Cada join, cada filtro, cada verificação de política RLS estava fazendo full table scans. Não é de admirar que tudo estava lento.
---
O Fix: O Apocalipse de Índices
25 de outubro de 2025. 7h51. Sabia o que precisava fazer.
Uma migração. 89 índices. Chamei de "O Apocalipse de Índices."
```sql
-- A Migração do Apocalipse de Índices
-- Propósito: Corrigir performance indexando todas as colunas de chave estrangeira
-- Tabelas com escopo de organização
CREATE INDEX idx_users_org_id ON users(org_id);
CREATE INDEX idx_resources_org_id ON resources(org_id);
CREATE INDEX idx_documents_org_id ON documents(org_id);
-- Tabelas com escopo de recurso
CREATE INDEX idx_user_data_resource_id ON user_data(resource_id);
CREATE INDEX idx_tasks_resource_id ON tasks(resource_id);
-- Índices multi-coluna para padrões de query comuns
CREATE INDEX idx_user_data_org_resource ON user_data(org_id, resource_id);
-- Queries de soft-delete
CREATE INDEX idx_resources_org_archived ON resources(org_id, archived_at);
-- Total: 89 índices em 32 tabelas
```
Executei a migração. Minha mão literalmente ficou parada sobre a tecla Enter por um momento, me perguntando se estava prestes a piorar as coisas. T.T
Tempo da migração: 4 minutos.
---
Os Resultados: De Desastre a Pronto para o Lançamento
Antes (Sem Índices)
```sql
Seq Scan on user_data
Execution Time: 843.271 ms
Rows Removed by Filter: 12,834
```
Depois (Com Índices)
```sql
Index Scan using idx_user_data_org_resource
Execution Time: 2.847 ms
```
843ms → 2,8ms
Isso é 296x mais rápido para uma única query.
Impacto no mundo real:
- Carregamento do dashboard: 2-3 segundos → 120ms
- Listas de recursos: 1+ segundo → 45ms
- Queries de dados: 850ms → 12ms
Melhoria média: 20-40x mais rápido. Alguns joins complexos com múltiplas chaves estrangeiras? 100x mais rápido.
A diferença foi noite e dia. A plataforma foi de "Isso está quebrado?" para "Uau, isso é rápido." :D
---
Por Que Isso Importa para SaaS Multi-Tenant
Se você está construindo SaaS multi-tenant com Row-Level Security, isso é absolutamente crítico.
As políticas RLS rodam em cada query:
```sql
CREATE POLICY resources_org_isolation ON resources
USING (org_id = get_user_org_id());
```
Sem um índice em org_id, essa política força um sequential scan em cada query. Até declarações SELECT simples ficam dolorosamente lentas.
O custo: Isolamento multi-tenant sem índices = infraestrutura cara + experiência lenta do usuário = churn.
A lição de negócios: Funcionalidades de segurança sem otimização de performance não são realmente seguras — porque os usuários vão embora antes de experimentar sua segurança.
---
O Padrão: O Que Indexar
Depois dessa lição cara, aqui está meu checklist para cada nova tabela:
Sempre indexe esses:
1. Colunas de chave estrangeira — Cada uma
2. Colunas em políticas RLS — Especialmente org_id em apps multi-tenant
3. Colunas em cláusulas WHERE — Se você filtra por ela com frequência, indexe
4. Colunas em ORDER BY — Ordenar sem índices mata a performance
Considere índices multi-coluna:
```sql
-- Para: WHERE org_id = X AND resource_id = Y
CREATE INDEX idx_table_org_resource ON table(org_id, resource_id);
```
Não exagere nos índices: Cada índice custa armazenamento, performance de escrita e manutenção. Regra geral: se aparece em WHERE/JOIN/ORDER BY com frequência, indexe. Caso contrário, não.
---
Como Verificar Seu Banco de Dados Agora Mesmo
Execute esta query diagnóstica:
```sql
SELECT
c.conrelid::regclass AS table_name,
a.attname AS column_name
FROM pg_constraint c
JOIN pg_attribute a ON a.attnum = ANY(c.conkey) AND a.attrelid = c.conrelid
WHERE c.contype = 'f'
AND NOT EXISTS (
SELECT 1 FROM pg_index i
WHERE i.indrelid = c.conrelid AND a.attnum = ANY(i.indkey)
);
```
Se você obtiver resultados, tem índices faltando.
Depois encontre suas queries lentas:
```sql
SELECT query, mean_exec_time, calls
FROM pg_stat_statements
WHERE mean_exec_time > 100
ORDER BY mean_exec_time DESC;
```
Use EXPLAIN ANALYZE nos lentos para verificar se está vendo sequential scans.
---
Lições Aprendidas
1. Suposições do Postgres são perigosas
Assumi que chaves estrangeiras eram indexadas. Não são. Sempre verifique.
2. RLS precisa de índices
Row-Level Security é inútil — na verdade pior que inútil — sem índices adequados. Adiciona overhead a cada query.
3. Multi-tenant = indexe org_id em todo lugar
Em arquitetura multi-tenant, org_id aparece em quase cada query. Indexe em cada tabela. Sem exceções.
4. Performance é um problema de negócios
Usuários não ligam que seu SQL está tecnicamente correto se a página leva 3 segundos para carregar. Queries rápidas = melhor conversão = receita.
5. Adicione índices cedo
Adicionar índices a tabelas vazias é instantâneo. Adicioná-los a tabelas com milhões de linhas leva horas e trava a tabela. Faça isso durante o desenvolvimento inicial.
6. Meça tudo
Use EXPLAIN ANALYZE antes e depois. Prove a melhoria com dados.
---
Seu Checklist
Para cada nova tabela que você criar:
```sql
CREATE TABLE new_table (
id UUID PRIMARY KEY,
org_id UUID REFERENCES organizations(id),
resource_id UUID REFERENCES resources(id),
created_at TIMESTAMPTZ,
archived_at TIMESTAMPTZ
);
-- ✅ Indexe chaves estrangeiras
CREATE INDEX idx_new_table_org_id ON new_table(org_id);
CREATE INDEX idx_new_table_resource_id ON new_table(resource_id);
-- ✅ Indexe colunas RLS + filtros comuns
CREATE INDEX idx_new_table_org_archived ON new_table(org_id, archived_at);
-- ✅ Indexe colunas de ordenação
CREATE INDEX idx_new_table_created ON new_table(created_at DESC);
-- ✅ Crie políticas RLS
CREATE POLICY new_table_org_isolation ON new_table
FOR ALL TO authenticated
USING (org_id = get_user_org_id());
```
---
Pensamentos Finais
89 índices faltando. 2 semanas de debugging. Uma query diagnóstica para encontrá-los todos. 4 minutos para corrigir.
A ironia? O Postgres te dá todas as ferramentas para diagnosticar isso. Eu só não sabia onde procurar. Agora cada tabela que crio recebe indexação imediata — chaves estrangeiras, colunas RLS, campos de ordenação, tudo.
A migração de 4 minutos que adicionou 89 índices poupou semanas de trabalho de otimização futuro e evitou um desastre no lançamento.
Para fundadores: Performance não é apenas um problema de engenharia. É um problema de conversão, um problema de retenção e um problema de credibilidade. Aquele banco de dados "tecnicamente correto" que está lento demais para usar? Está te custando clientes. Invista em performance cedo.
Para engenheiros: O Postgres não vai indexar automaticamente suas chaves estrangeiras. Mas você deveria. Seus usuários (e a CPU do seu banco de dados) vão agradecer.
Você já teve um momento de "tecnicamente correto mas dolorosamente lento" com seu banco de dados? Qual foi o fix — e quanto tempo levou para encontrar?
Abraços,
Chandler
A série de arquitetura do STRAŦUM: Essa crise de performance foi a última peça de um quebra-cabeça de multi-tenancy que começou com construir multi-tenancy no Dia 2, continuou com uma reconstrução completa do schema no Dia 67 e incluiu a correção de 31 telas em branco por contexto de navegação perdido.
---
Construindo SaaS multi-tenant com Postgres? Aprendi essa lição do jeito difícil para que você não precise. Verifique seu banco de dados agora antes que seus usuários notem.
Solicite acesso alpha em https://stratum.chandlernguyen.com/request-invitation
---
Ainda aprendendo que "funciona" e "funciona rápido" são objetivos diferentes — e só um deles lança.
---





