De SQL Server para PostgreSQL: Instância, database e schema. Onde cada coisa mora.

SQL Server e PostgreSQL organizam instância, database e schema de formas estruturalmente diferentes — e essa diferença muda completamente como uma query cruza dados entre bancos em cada um.
Francisco Porfírio · Time PostgreSQL da Power Tuning
Na primeira postagem desta série, falamos sobre o primeiro choque de quem sai do wizard do SQL Server no Windows para o apt install/initdb do Linux. Resolvida a instalação, o segundo estranhamento normalmente aparece já no primeiro dia de uso, na primeira vez que alguém tenta fazer um JOIN entre duas bases diferentes no mesmo servidor.
No SQL Server, a resposta usa nomeação de três partes, banco.schema.objeto, dentro da mesma instância. No Postgres, a resposta segue outro caminho, porque a arquitetura é diferente: cada database é uma unidade isolada, com catálogo e visibilidade de transação próprios.
Este post organiza a hierarquia instância → database → schema nos dois bancos, mostra onde cada coisa mora fisicamente em disco, e explica por que uma cross-database query segue caminhos diferentes em cada um — e o que isso muda na prática, na hora de cruzar dados entre databases.
|
SQL Server (Instância) 1 instância = 1 processo/serviço
↔ Referência direta entre bancos BancoA.dbo.Clientes c JOIN BancoB.dbo.Pedidos p Catálogo compartilhado 1 conexãoenxerga BancoA e BancoB
|
PostgreSQL (Cluster) 1 cluster = 1 processo/serviço
× Isto não roda no Postgres: bancoa.public.clientes c JOIN bancob.public.pedidos p Só é possível via postgres_fdw
|
No SQL Server, o catálogo é compartilhado entre databases da instância, permitindo referência direta entre eles.
No Postgres, um JOIN direto entre bancoa e bancob não existe: é preciso importar a tabela via postgres_fdw antes.
Como o SQL Server te acostumou a pensar
Uma instância do SQL Server é um único processo (o serviço MSSQLSERVER ou uma instância nomeada) que hospeda vários databases ao mesmo tempo, todos visíveis pela mesma conexão. Dentro de cada database existem um ou mais schemas (o padrão sendo dbo), e dentro de cada schema, os objetos: tabelas, views, procedures.
A nomeação de três partes deixa isso natural no dia a dia:
|
1 2 3 4 5 |
<span style="color: #555555;">-- Uma única conexão referenciando dois databases diferentes</span> SELECT c.nome, p.valor FROM BancoA.dbo.Clientes c JOIN BancoB.dbo.Pedidos p ON p.cliente_id = c.id WHERE c.ativo = 1; |
Isso funciona porque, dentro de uma instância, o SQL Server mantém um catálogo de metadados e um gerenciador de locks/transações compartilhado entre todos os databases. O USE apenas troca o database “padrão” de contexto para nomes sem prefixo — mas qualquer objeto de qualquer outro database da mesma instância continua acessível via nome completo, no mesmo plano de execução, na mesma transação.
A filosofia do Postgres: cada database é um cofre isolado
Lembra do “cluster” que criamos no post anterior com o initdb? Esse é o equivalente mais próximo de uma instância: um único processo postgres, ouvindo em uma porta, capaz de hospedar múltiplos databases. Até aqui, parecido com o SQL Server.
A diferença aparece na camada de baixo: no Postgres, cada database tem seu próprio catálogo de sistema (as tabelas internas que descrevem tabelas, colunas, índices, permissões etc.) e sua própria visibilidade de transações (MVCC). Fisicamente, cada database vive em um diretório separado dentro do cluster:
|
1 2 3 |
<span style="color: #555555;"># Cada subdiretório numérico é um database, identificado pelo OID</span> /var/lib/postgresql/18/main/base/16384/ <span style="color: #555555;"># bancoa</span> /var/lib/postgresql/18/main/base/16391/ <span style="color: #555555;"># bancob</span> |
E, consequência direta disso: uma conexão no Postgres se autentica em um único database por vez. Não existe um USE outrobanco; que troque o contexto dentro da mesma sessão — trocar de database significa abrir uma nova conexão. No psql, o comando mais próximo disso é \c — mas por baixo dos panos ele fecha a conexão atual e abre outra.
|
1 2 3 |
postgres=# \c bancoa <span style="color: #555555;">You are now connected to database "bancoa" as user "postgres".</span> <span style="color: #555555;">-- \c não "troca de contexto": é uma reconexão de verdade</span> |
E se eu tentar mesmo assim? O Postgres nem deixa passar pelo parser. Tentar referenciar um database no nome do objeto retorna um erro bem direto ao ponto: ERROR: cross-database references are not implemented. Não é uma permissão faltando, é a arquitetura dizendo que aquele caminho não existe.
Onde entra o schema
Dentro de cada database, o Postgres também organiza objetos em schemas — o conceito em si é idêntico ao do SQL Server. A diferença é só o nome do padrão: em vez de dbo, todo database novo já nasce com um schema chamado public.
A nomeação de objetos no Postgres é, portanto, de duas partes, não três: schema.objeto. O database nunca entra no nome, porque ele já está implícito na conexão atual:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
<span style="color: #555555;">-- Funciona: schema.objeto, dentro do database já conectado</span> SELECT * FROM public.clientes; <span style="color: #555555;">-- Também funciona, dois schemas do MESMO database</span> SELECT c.nome, v.total FROM vendas.clientes c JOIN financeiro.vendas v ON v.cliente_id = c.id; <span style="color: #555555;">-- NÃO funciona: bancoa e bancob são databases diferentes</span> SELECT * FROM bancoa.public.clientes c JOIN bancob.public.pedidos p ON p.cliente_id = c.id; <span style="color: #555555;">-- ERROR: cross-database references are not implemented</span> |
Repare que cruzar schemas diferentes do mesmo database funciona sem nenhuma restrição especial — o limite é sempre o database, nunca o schema.
Passo a passo: criando database e schema no Postgres
|
1 2 3 4 5 |
<span style="color: #555555;"># Cria um database novo (equivalente a CREATE DATABASE no SSMS)</span> sudo -u postgres createdb bancoa <span style="color: #555555;"># Conecta nele</span> sudo -u postgres psql -d bancoa |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<span style="color: #555555;">-- Dentro do psql, já conectado em "bancoa"</span> <span style="color: #555555;">-- Cria um schema adicional (equivalente a CREATE SCHEMA no SQL Server)</span> CREATE SCHEMA vendas; <span style="color: #555555;">-- Define em que ordem os schemas são pesquisados quando o nome vem sem prefixo</span> SET search_path TO vendas, public; <span style="color: #555555;">-- Comandos de exploração no psql</span> \l <span style="color: #555555;">-- lista os databases do cluster</span> \c bancoa <span style="color: #555555;">-- conecta em outro database (na prática, reconecta)</span> \dn <span style="color: #555555;">-- lista os schemas do database atual</span> \dt vendas.*<span style="color: #555555;"> -- lista as tabelas de um schema específico</span> |
O search_path é o equivalente funcional do schema padrão do usuário no SQL Server: define em qual schema o Postgres procura um objeto quando você não escreve o prefixo. Sem configurar nada, o padrão de fábrica já inclui public.
De onde vem um database novo? Assim como o SQL Server usa o database model como molde para todo database criado, o Postgres clona o database template1 por padrão em todo CREATE DATABASE/createdb. Alterar algo em template1 (uma extensão, uma função utilitária) propaga para todo database novo criado depois. Já template0 é um molde “de fábrica”, intocado, usado para restaurar dumps com encoding/collation diferentes do padrão do cluster.
Por que cross-database query segue caminhos diferentes em cada banco
É uma decisão de arquitetura presente desde as origens do Postgres: cada database é uma unidade de isolamento completa, com catálogo próprio, controle de acesso próprio e visibilidade de transação própria. Não existe join de execução única entre databases dentro do motor — qualquer cruzamento passa por uma camada explícita.
Quando você realmente precisa cruzar dados entre databases no mesmo cluster (ou até em servidores diferentes), as saídas comuns são:
- postgres_fdw — a opção mais robusta e recomendada hoje em dia. Cria “tabelas estrangeiras” que apontam para tabelas em outro database (mesmo servidor ou remoto), permitindo usá-las quase como locais, inclusive com algum push-down de filtros para performance. É o equivalente conceitual do Linked Server, explicado no quadro abaixo.
- dblink — extensão mais antiga, baseada em funções que executam queries em outro database e retornam o resultado. Mais simples de usar pontualmente, porém sem otimizações que o
postgres_fdwoferece. - Join em nível de aplicação — abrir duas conexões, buscar os dados de cada lado, e cruzar em memória na aplicação. Mais trabalho, mas evita acoplar bancos logicamente separados.
- Repensar a separação em databases — se dois “bancos” precisam de JOINs frequentes entre si, muitas vezes o desenho correto no Postgres é um único database com múltiplos schemas, já que cruzar schemas do mesmo database não tem nenhuma restrição.
Pensando como DBA SQL Server: a analogia mais próxima de um postgres_fdw é o Linked Server. Os dois resolvem o mesmo problema — enxergar objetos de outro banco como se fossem locais — e os dois têm o mesmo tipo de pegadinha: parecem transparentes na sintaxe, mas por baixo geram uma conexão separada, com custo de rede/serialização e comportamento transacional próprio (nada de BEGIN TRAN cobrindo os dois lados de graça). A diferença prática está no grau de exposição: um Linked Server, uma vez criado, expõe o catálogo inteiro do servidor remoto automaticamente; o postgres_fdw é mais granular — você importa explicitamente, tabela por tabela ou schema por schema, via IMPORT FOREIGN SCHEMA/CREATE FOREIGN TABLE, o que tende a deixar a superfície de acesso mais controlada.
Quer custo zero? Use schemas, não databases. No Postgres, um JOIN entre schemas do mesmo database é nativo, sem nenhuma camada extra — o planejador enxerga as duas tabelas normalmente e otimiza como qualquer outro JOIN. Já um JOIN entre databases diferentes sempre passa por postgres_fdw/dblink, mesmo que os dois databases estejam no mesmo cluster, na mesma máquina física — e essa camada tem custo real: serialização dos dados, tráfego de rede/loopback e, em geral, menos otimizações de plano do que um JOIN nativo. Se o objetivo é evitar esse custo, a resposta arquitetural correta é databases diferentes viram schemas diferentes dentro de um único database.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<span style="color: #555555;">-- Exemplo mínimo de postgres_fdw, executado dentro de "bancoa"</span> <span style="color: #555555;">-- para enxergar uma tabela que vive em "bancob"</span> CREATE EXTENSION IF NOT EXISTS postgres_fdw; CREATE SERVER bancob_srv FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host 'localhost', dbname 'bancob', port '5432'); CREATE USER MAPPING FOR CURRENT_USER SERVER bancob_srv OPTIONS (user 'postgres', password 'senha_segura'); IMPORT FOREIGN SCHEMA public LIMIT TO (pedidos) FROM SERVER bancob_srv INTO public; <span style="color: #555555;">-- Agora "pedidos" existe como tabela estrangeira em bancoa, e o JOIN funciona</span> SELECT c.nome, p.valor FROM clientes c JOIN pedidos p ON p.cliente_id = c.id; |
Comparativo lado a lado
| Conceito | SQL Server | PostgreSQL |
| Nível mais alto | Instância | Cluster (criado pelo initdb) |
| Schema padrão | dbo |
public |
| Database molde para novos databases | model |
template1 |
| Nomeação completa de objeto | 3 partes: banco.schema.objeto |
2 partes: schema.objeto |
| Trocar de database na mesma sessão | USE outrobanco; |
Não existe — requer nova conexão (\c no psql) |
| JOIN entre databases da mesma instância | Referência direta via catálogo compartilhado | Via camada explícita: postgres_fdw/dblink |
| JOIN entre schemas do mesmo database | Referência direta | Referência direta |
| Catálogo de metadados | Compartilhado entre databases da instância | Isolado por database |
Erros comuns de quem vem do SQL Server
- Escrever nome de três partes por hábito.
bancoa.public.clientessó funciona sebancoafor o próprio database da conexão atual — o Postgres aceita o prefixo, mas ele precisa “bater” com o database conectado, não pode apontar para outro. - Achar que
\cé igual a umUSE. É uma reconexão de verdade: fecha a sessão TCP atual e abre outra, o que também derruba qualquer estado de sessão (variáveis temporárias, tabelas temp, transação em aberto). - Modelar um database por cliente/tenant achando que vai poder cruzar dados facilmente depois. Se o plano é multi-tenant com necessidade de relatórios cruzando tenants, schemas separados dentro de um único database costumam ser um desenho mais simples de manter no Postgres do que databases separados.
- Esperar que uma transação cubra os dois lados de um
dblink/postgres_fdw. São conexões distintas por baixo dos panos; garantias de atomicidade entre os dois databases exigem cuidado extra (two-phase commit manual, ou aceitar consistência eventual), além de compreender a adição do custo de rede a operação.
Resumo
No SQL Server, a instância compartilha um único catálogo entre todos os databases que hospeda, permitindo referência direta entre eles. No Postgres, o cluster hospeda vários databases, mas cada um mantém catálogo e visibilidade de transação próprios, e cada conexão fica associada a um único database. Schemas continuam sendo o nível de organização dentro do database, sem nenhuma restrição extra entre si. Quando o cruzamento entre databases é realmente necessário, postgres_fdw é a ponte mais robusta disponível hoje — e muitas vezes a pergunta certa é se aqueles “dois bancos” não deveriam, desde o início, ser dois schemas de um único database.
Tem PostgreSQL em produção na sua empresa?
Faça o Power PostgreSQL Check-up: uma avaliação gratuita de performance, backup, segurança e alta disponibilidade, feita pelos nossos especialistas. Sem custo e sem compromisso.

