Study guide
Postgres, the 80/20 for developers.
Relational databases from the ground up, Postgres-focused: schemas, connecting, roles and passwords, access security, indexes, CTEs, full-text search, backups and caching — with a Docker lab you can run today.
O que é um banco relacional e um schemaWhat a relational database and a schema are
Um banco relacional guarda dados em tabelas: linhas (registros) e colunas (campos), com relações entre tabelas por chaves. É "relacional" no sentido matemático de relação (um conjunto de tuplas), não porque as tabelas se relacionam entre si — embora também façam isso. O gerenciador que guarda, organiza e responde consultas sobre esses dados é um SGBD (sistema de gerenciamento de banco de dados; em inglês, RDBMS).A relational database stores data in tables: rows (records) and columns (fields), with relations between tables through keys. It is "relational" in the mathematical sense of a relation (a set of tuples), not because tables relate to one another — though they also do that. The manager that stores, organizes and answers queries over that data is a DBMS (database management system).
O que é um schema, de fatoWhat a schema actually is
Um schema é um namespace dentro de um database: uma forma de organizar tabelas (por exemplo, vendas.pedidos e rh.funcionarios no mesmo database) sem precisar de um database separado para cada área. É diferente do uso da palavra "schema" em outros bancos (no MySQL, "schema" costuma ser sinônimo de "database" inteiro).A schema is a namespace inside a database: a way to organize tables (for example, vendas.pedidos and rh.funcionarios in the same database) without needing a separate database for each area. This differs from how the word "schema" is used in other databases (in MySQL, "schema" is usually a synonym for a whole "database").
CREATE SCHEMA vendas;
CREATE TABLE vendas.pedidos (id bigint, total numeric(10,2));
SET search_path TO vendas, public; -- define a ordem de busca; agora "pedidos" sozinho já encontra vendas.pedidos
SELECT * FROM pedidos; -- funciona sem o prefixo "vendas."
Gerenciadores de banco: as opções mais comunsDatabase managers: the common options
| SGBDDBMS | TipoType | Quando escolherWhen to choose |
|---|---|---|
| PostgreSQL | relacional, objeto-relacionalrelational, object-relational | o padrão para a maioria dos projetos novos: tipos avançados (JSON, arrays, UUID), extensível, forte em correção e concorrênciathe default for most new projects: advanced types (JSON, arrays, UUID), extensible, strong on correctness and concurrency |
| MySQL / MariaDB | relacionalrelational | onipresente em hospedagem compartilhada e WordPress; engines de storage plugáveis (InnoDB)ubiquitous in shared hosting and WordPress; pluggable storage engines (InnoDB) |
| SQLite | relacional, embutido (sem servidor)relational, embedded (serverless) | um arquivo só, zero configuração: apps mobile/desktop, testes, protótiposa single file, zero config: mobile/desktop apps, tests, prototypes |
| SQL Server | relacionalrelational | ecossistema Microsoft/.NETMicrosoft/.NET ecosystem |
| Oracle Database | relacionalrelational | grandes empresas com contratos legadoslarge enterprises with legacy contracts |
| Gerenciados (RDS, Cloud SQL, Neon, Supabase)Managed (RDS, Cloud SQL, Neon, Supabase) | o mesmo Postgres/MySQL, hospedadothe same Postgres/MySQL, hosted | backups, patches e failover geridos pelo provedor (veja a nota de AWS, seção RDS)backups, patches and failover managed by the provider (see the AWS note, RDS section) |
Esta nota usa Postgres porque é hoje a escolha padrão para projetos novos: licença permissiva, extensões (PostGIS, pg_trgm, pgvector), tipos de dado ricos e um dos motores de consulta mais corretos que existem.This note uses Postgres because it is today's default choice for new projects: a permissive license, extensions (PostGIS, pg_trgm, pgvector), rich data types, and one of the most correct query engines around.
Ferramentas para abrir um bancoTools to open a database
| FerramentaTool | TipoType |
|---|---|
psql | o cliente de linha de comando oficial, sempre disponívelthe official command-line client, always available |
| pgAdmin | painel web oficial do projeto Postgresthe Postgres project's own web panel |
| DBeaver / TablePlus / DataGrip | clientes gráficos multiplataforma, funcionam com vários SGBDscross-platform graphical clients, work with several DBMSs |
| VS Code (extensão SQLTools/PostgreSQL) | abrir e consultar sem sair do editoropening and querying without leaving the editor |
Como conectarHow to connect
Conectar ao Postgres sempre passa pelos mesmos seis dados: quem você é, com qual senha, em qual máquina, em qual porta, e em qual database. Toda ferramenta — psql, uma lib de app, um painel gráfico — pede exatamente isso, às vezes como uma connection string só.Connecting to Postgres always goes through the same six pieces of data: who you are, with which password, on which machine, on which port, and which database. Every tool — psql, an app library, a graphical panel — asks for exactly that, sometimes as a single connection string.
DATABASE_URL=postgresql://ana:s3nha@db.exemplo.com:5432/loja?sslmode=requireThe anatomy of a connection string. DATABASE_URL=postgresql://ana:s3nha@db.exemplo.com:5432/loja?sslmode=requirePelo psqlThrough psql
psql -h db.exemplo.com -p 5432 -U ana -d loja # pede a senha interativamente
psql "postgresql://ana:s3nha@db.exemplo.com:5432/loja" # tudo numa string só
psql -U postgres # local, usuário "postgres", pede/usa auth local
PGPASSWORD=s3nha psql -h localhost -U ana loja # senha por variável (cuidado: aparece em `ps`!)
Dentro do psql, estes comandos resolvem 80% do dia a dia:Inside psql, these commands cover 80% of daily use:
| ComandoCommand | FazDoes |
|---|---|
\l | lista os databaseslists the databases |
\c loja | troca de databaseswitches database |
\dn | lista os schemaslists the schemas |
\dt · \dt vendas.* | lista tabelas (de um schema)lists tables (of a schema) |
\d pedidos | descreve uma tabela: colunas, tipos, índices, chaves estrangeirasdescribes a table: columns, types, indexes, foreign keys |
\du | lista roles (usuários)lists roles (users) |
\x | alterna exibição "expandida" (uma coluna por linha, ótimo para tabelas largas)toggles "expanded" display (one column per line, great for wide tables) |
\timing | mostra quanto tempo cada consulta levashows how long each query takes |
\q | saiquits |
peer por padrão em conexões locais (via socket Unix), que confia no seu usuário do sistema operacional. Isso é exatamente o que a próxima seção desembaraça.Locally, a lot of people never type a user/password at all: Postgres uses the peer method by default on local connections (via Unix socket), trusting your operating-system user. That is exactly what the next section untangles.Usuários e senhas: as três camadasUsers and passwords: the three layers
Esta é a maior fonte de confusão para quem está começando, porque a palavra "usuário" significa três coisas diferentes que não têm relação automática entre si. Desembaraçar isso resolve a maioria dos "funciona na minha máquina, não funciona no servidor".This is the biggest source of confusion for beginners, because the word "user" means three different things with no automatic relation between them. Untangling this solves most "works on my machine, not on the server" problems.
Como as camadas se conectam na práticaHow the layers connect in practice
| CenárioScenario | O que aconteceWhat happens |
|---|---|
Você digita psql sem argumentosYou type psql with no arguments | tenta conectar como uma role do Postgres com o MESMO NOME do seu usuário do SO, no database de mesmo nome — coincidência de nome, não de identidadetries to connect as a Postgres role with the SAME NAME as your OS user, to a database of the same name — a name coincidence, not an identity one |
| Instalação padrão cria o usuário do SO "postgres" e a role "postgres"A default install creates the OS user "postgres" and the role "postgres" | sudo -u postgres psql funciona porque o SO deixa o usuário "postgres" rodar o psql, que conecta à role "postgres" (super-usuária DO BANCO, não do SO)sudo -u postgres psql works because the OS lets the "postgres" OS user run psql, which connects as the "postgres" role (a DATABASE superuser, not an OS one) |
Sua aplicação conecta com DATABASE_URLYour app connects with DATABASE_URL | usa uma role do Postgres (ex.: app_user) e SUA senha; não tem nenhuma relação com o usuário do SO onde o app rodauses a Postgres role (e.g. app_user) with ITS password; has no relation to the OS user the app runs as |
| Um cliente faz login no seu siteA customer logs into your site | sua aplicação confere a senha na SUA tabela de usuários; o Postgres só vê a role da aplicação executando a consultayour application checks the password against YOUR users table; Postgres only sees the application's role running the query |
Exercício: criar uma role, um database e testarExercise: create a role, a database, and test it
sudo -u postgres psql # entra como a role "postgres" (super-usuária do banco)
CREATE ROLE ana LOGIN PASSWORD 's3nha-forte-de-verdade';
CREATE DATABASE loja OWNER ana;
GRANT ALL PRIVILEGES ON DATABASE loja TO ana;
\q
psql -h localhost -U ana -d loja # agora conecte como ANA, não como postgres
# Password for user ana: ****
Resultado esperado: o segundo psql pede senha (porque agora você está entrando como a role ana, via rede/senha, não por confiança do sistema operacional) e o prompt muda para loja=>. Se ana tentar CREATE DATABASE sem CREATEDB, recebe um erro de permissão — ela só é dona do banco loja, não super-usuária.Expected result: the second psql asks for a password (because you are now logging in as the role ana, over the network/password, not by operating-system trust) and the prompt changes to loja=>. If ana tries CREATE DATABASE without CREATEDB, she gets a permission error — she only owns the loja database, she is not a superuser.
Atributos de role mais usadosMost-used role attributes
| AtributoAttribute | SignificaMeans |
|---|---|
LOGIN | pode se conectar (sem isso, a role só serve para agrupar permissões)can connect (without it, the role only groups permissions) |
SUPERUSER | ignora toda checagem de permissão — use só para administração, nunca para appsskips every permission check — use only for administration, never for apps |
CREATEDB | pode criar databasescan create databases |
PASSWORD | define a senha (hash scram-sha-256 por padrão desde o Postgres 14)sets the password (scram-sha-256 hash by default since Postgres 14) |
VALID UNTIL | data de expiração da senhathe password's expiry date |
IN ROLE outra_role | herda as permissões de outra role (ótimo para grupos como app_readonly)inherits another role's permissions (great for groups like app_readonly) |
Segurança de acesso: pg_hba, TLS e privilégio mínimoAccess security: pg_hba, TLS and least privilege
Quem pode se conectar, de onde, e como ela precisa provar quem é — tudo isso é decidido por um arquivo: pg_hba.conf (host-based authentication). É o "firewall de identidade" do Postgres, separado do firewall de rede (nftables/ufw) e separado das permissões SQL (GRANT).Who may connect, from where, and how they must prove who they are — all of that is decided by one file: pg_hba.conf (host-based authentication). It is Postgres's "identity firewall", separate from the network firewall (nftables/ufw) and separate from SQL permissions (GRANT).
Lendo o pg_hba.confReading pg_hba.conf
# TYPE DATABASE USER ADDRESS METHOD
local all postgres peer
local all all scram-sha-256
host loja ana 127.0.0.1/32 scram-sha-256
host loja app 10.0.0.0/8 scram-sha-256
hostssl all all 0.0.0.0/0 scram-sha-256
host all all 0.0.0.0/0 reject
| ColunaColumn | SignificaMeans |
|---|---|
| TYPE | local = socket Unix (mesma máquina); host = TCP (com ou sem TLS); hostssl = TCP, exige TLSlocal = Unix socket (same machine); host = TCP (with or without TLS); hostssl = TCP, requires TLS |
| METHOD | peer/ident = confia no usuário do SO; scram-sha-256 = senha (o padrão recomendado); trust = sem senha nenhuma; reject = negapeer/ident = trusts the OS user; scram-sha-256 = password (the recommended default); trust = no password at all; reject = deny |
trust (sem senha) só faz sentido para local, numa máquina de estudo só sua — nunca em algo acessível pela rede. Depois de editar: sudo systemctl reload postgresql (recarrega sem derrubar conexões).Rules are read top to bottom and the first match wins. trust (no password) only makes sense for local, on a study machine that is only yours — never on anything network-reachable. After editing: sudo systemctl reload postgresql (reloads without dropping connections).TLS e o que "sslmode" realmente garanteTLS and what "sslmode" actually guarantees
sslmodesslmode | GaranteGuarantees |
|---|---|
disable | nada: conexão em texto puro — ok só em localhostnothing: plain-text connection — fine only on localhost |
require | cifra o tráfego, mas NÃO confere a identidade do servidor (vulnerável a um impostor na rede)encrypts traffic, but does NOT verify the server's identity (open to a network impostor) |
verify-full | cifra e confere o certificado do servidor contra uma CA confiável — o equivalente real de "HTTPS seguro"encrypts and verifies the server's certificate against a trusted CA — the real equivalent of "secure HTTPS" |
Privilégio mínimoLeast privilege
A aplicação nunca deveria conectar como super-usuária. O padrão sensato: uma role por aplicação, com só as permissões que ela usa.The application should never connect as a superuser. The sensible default: one role per application, with only the permissions it uses.
CREATE ROLE app_readonly NOLOGIN;
GRANT CONNECT ON DATABASE loja TO app_readonly;
GRANT USAGE ON SCHEMA public TO app_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO app_readonly; -- vale para tabelas FUTURAS também
CREATE ROLE app_web LOGIN PASSWORD 'outra-senha-forte' IN ROLE app_readonly;
GRANT INSERT, UPDATE, DELETE ON pedidos, itens_pedido TO app_web; -- só nessas tabelas
Pooling de conexõesConnection pooling
Cada conexão Postgres custa memória (um processo por conexão). Para apps com muitas conexões curtas (serverless, muitas réplicas), um pooler como PgBouncer ou Supavisor fica entre a aplicação e o Postgres, reaproveitando um número menor de conexões reais — o lado de segurança é que o pooler também costuma ser o lugar certo para autenticar e limitar por aplicação.Every Postgres connection costs memory (one process per connection). For apps with many short-lived connections (serverless, many replicas), a pooler like PgBouncer or Supavisor sits between the application and Postgres, reusing a smaller number of real connections — the security angle is that the pooler is also usually the right place to authenticate and limit per application.
Comandos do dia a diaEveryday commands
Um resumo do SQL que cobre a maior parte do trabalho de um desenvolvedor: criar estrutura (DDL) e ler/escrever dados (DML).A summary of the SQL that covers most of a developer's work: creating structure (DDL) and reading/writing data (DML).
DDL: estruturaDDL: structure
CREATE TABLE pedidos (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
cliente_id bigint NOT NULL REFERENCES clientes(id),
total numeric(10,2) NOT NULL DEFAULT 0,
status text NOT NULL DEFAULT 'pendente',
criado_em timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE pedidos ADD COLUMN observacao text;
ALTER TABLE pedidos ALTER COLUMN total SET NOT NULL;
DROP TABLE IF EXISTS tabela_antiga;
CREATE INDEX idx_pedidos_cliente ON pedidos (cliente_id);
DML: dadosDML: data
SELECT id, total FROM pedidos WHERE status = 'pendente' ORDER BY criado_em DESC LIMIT 20;
INSERT INTO pedidos (cliente_id, total) VALUES (42, 199.90) RETURNING id;
UPDATE pedidos SET status = 'pago' WHERE id = 7;
DELETE FROM pedidos WHERE status = 'cancelado' AND criado_em < now() - interval '1 year';
SELECT c.nome, count(p.id) AS total_pedidos
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.id
GROUP BY c.nome
HAVING count(p.id) > 0
ORDER BY total_pedidos DESC;
| Tipo de JOINJOIN type | TrazBrings |
|---|---|
INNER JOIN | só linhas com correspondência nas duas tabelasonly rows matching in both tables |
LEFT JOIN | todas as linhas da esquerda, com NULL onde não houver correspondência à direitaall rows from the left, with NULL where there is no match on the right |
FULL JOIN | todas as linhas dos dois ladosall rows from both sides |
BEGIN; ... ; COMMIT; (ou ROLLBACK; para desfazer). O Postgres já roda cada comando isolado em sua própria transação implícita por padrão.The transaction is the "all or nothing" unit: BEGIN; ... ; COMMIT; (or ROLLBACK; to undo). Postgres already runs each standalone statement in its own implicit transaction by default.CTEs e window functions (o "pivô")CTEs and window functions (the "pivot")
CTEs (Common Table Expressions, o bloco WITH) dão nome a uma subconsulta para usá-la como se fosse uma tabela, tornando consultas grandes legíveis. Window functions (funções de janela) fazem cálculos "através" de um grupo de linhas sem colapsá-las numa só — é a ferramenta certa para o que costuma ser pedido como "pivô" (comparar cada linha com um agregado do seu grupo).CTEs (Common Table Expressions, the WITH block) name a subquery so it can be used as if it were a table, making large queries readable. Window functions compute "across" a group of rows without collapsing them into one — the right tool for what is usually asked for as a "pivot" (comparing each row against an aggregate of its group).
CTE simplesA simple CTE
WITH pedidos_pagos AS (
SELECT cliente_id, total FROM pedidos WHERE status = 'pago'
)
SELECT c.nome, sum(p.total) AS total_gasto
FROM clientes c
JOIN pedidos_pagos p ON p.cliente_id = c.id
GROUP BY c.nome
ORDER BY total_gasto DESC;
CTE recursiva: percorrer uma árvoreRecursive CTE: walking a tree
WITH RECURSIVE subordinados AS (
SELECT id, nome, gerente_id FROM funcionarios WHERE id = 1 -- âncora: o chefe
UNION ALL
SELECT f.id, f.nome, f.gerente_id
FROM funcionarios f
JOIN subordinados s ON f.gerente_id = s.id -- passo recursivo
)
SELECT * FROM subordinados;
Resultado esperado: todos os subordinados diretos e indiretos do funcionário 1, por quantos níveis existirem — sem precisar saber a profundidade do organograma de antemão.Expected result: every direct and indirect subordinate of employee 1, through however many levels exist — with no need to know the org chart's depth beforehand.
Window functions: o "pivô" do PostgresWindow functions: Postgres's "pivot"
Bancos como o SQL Server têm um comando PIVOT; o Postgres resolve o mesmo problema com FILTER (agregação condicional) ou funções de janela com OVER.Databases like SQL Server have a PIVOT command; Postgres solves the same problem with FILTER (conditional aggregation) or window functions with OVER.
-- "pivô" clássico: uma coluna por status, por mês
SELECT
date_trunc('month', criado_em) AS mes,
count(*) FILTER (WHERE status = 'pago') AS pagos,
count(*) FILTER (WHERE status = 'cancelado') AS cancelados
FROM pedidos
GROUP BY mes
ORDER BY mes;
-- window function: o total do cliente ao lado de CADA pedido, sem agrupar
SELECT
id, cliente_id, total,
sum(total) OVER (PARTITION BY cliente_id) AS total_do_cliente,
rank() OVER (PARTITION BY cliente_id ORDER BY total DESC) AS posicao
FROM pedidos;
Resultado esperado: a segunda consulta mantém uma linha por pedido (diferente de um GROUP BY, que colapsaria), com total_do_cliente repetindo a soma do grupo em cada linha e posicao ranqueando os pedidos de cada cliente do maior para o menor.Expected result: the second query keeps one row per order (unlike a GROUP BY, which would collapse them), with total_do_cliente repeating the group's sum on every row and posicao ranking each customer's orders from largest to smallest.
Index: ler sem escanear tudoIndexes: reading without scanning everything
Um índice é como o sumário de um livro: uma estrutura à parte que aponta direto para onde cada valor está, em vez de o Postgres ler a tabela inteira (sequential scan) para achar o que você pediu. Custa espaço em disco e um pouco de velocidade a mais em cada escrita; em compensação, consultas que filtram ou ordenam por aquela coluna ficam ordens de magnitude mais rápidas.An index is like a book's index: a separate structure that points straight to where each value is, instead of Postgres reading the whole table (a sequential scan) to find what you asked for. It costs disk space and a little extra time on every write; in exchange, queries that filter or sort by that column get orders of magnitude faster.
| TipoType | Bom paraGood for | ExemploExample |
|---|---|---|
| B-tree (padrão) | igualdade e faixas: =, <, >, BETWEEN, ORDER BYequality and ranges: =, <, >, BETWEEN, ORDER BY | CREATE INDEX ON pedidos (cliente_id); |
| GIN | valores "compostos": JSONB, arrays, full-text searchcomposite values: JSONB, arrays, full-text search | CREATE INDEX ON produtos USING gin (busca); |
| GiST | dados geométricos/geográficos, faixas que se sobrepõemgeometric/geographic data, overlapping ranges | PostGIS, tsrangePostGIS, tsrange |
| BRIN | tabelas enormes, ordenadas fisicamente (ex.: logs por data)huge tables, physically ordered (e.g. logs by date) | bem menor que um B-tree, bom o bastante quando os dados já chegam em ordemmuch smaller than a B-tree, good enough when data already arrives in order |
Único (UNIQUE) | garante que não haja duplicata, além de acelerarguarantees no duplicates, as well as speeding things up | CREATE UNIQUE INDEX ON usuarios (email); |
Exercício: ver o índice funcionandoExercise: see the index at work
EXPLAIN ANALYZE SELECT * FROM pedidos WHERE cliente_id = 42;
-- antes de criar o índice: "Seq Scan on pedidos ... rows=1 ... Execution Time: 8.421 ms"
CREATE INDEX idx_pedidos_cliente ON pedidos (cliente_id);
EXPLAIN ANALYZE SELECT * FROM pedidos WHERE cliente_id = 42;
-- depois: "Index Scan using idx_pedidos_cliente ... Execution Time: 0.045 ms"
Resultado esperado: o plano muda de Seq Scan (lê a tabela inteira) para Index Scan ou Bitmap Index Scan, e o Execution Time cai — em tabelas pequenas a diferença é pequena, mas cresce junto com a tabela. EXPLAIN ANALYZE executa a consulta de verdade; para só ver o plano sem rodar, use EXPLAIN sozinho.Expected result: the plan changes from Seq Scan (reads the whole table) to Index Scan or Bitmap Index Scan, and Execution Time drops — on small tables the difference is small, but it grows with the table. EXPLAIN ANALYZE actually runs the query; to see just the plan without running it, use EXPLAIN alone.
INSERT/UPDATE precisa atualizar todos os índices da tabela também. Não crie um índice "só por garantia" em toda coluna — crie para as consultas que você de fato tem (olhe o WHERE, o JOIN e o ORDER BY das consultas lentas).Indexes are not free: every INSERT/UPDATE has to update all of the table's indexes too. Do not create an index "just in case" on every column — create them for the queries you actually have (look at the WHERE, the JOIN and the ORDER BY of your slow queries).UUID ou serial?UUID or serial?
A chave primária de uma tabela costuma ser um número sequencial (serial/GENERATED ALWAYS AS IDENTITY) ou um UUID (um número aleatório gigante, praticamente único no universo). Cada um resolve um problema diferente.A table's primary key is usually either a sequential number (serial/GENERATED ALWAYS AS IDENTITY) or a UUID (a huge random number, practically unique in the universe). Each solves a different problem.
| serial / IDENTITYserial / IDENTITY | UUIDUUID | |
|---|---|---|
| TamanhoSize | 4–8 bytes | 16 bytes |
| OrdemOrder | crescente e previsívelincreasing and predictable | aleatório (v4) — ruim para o índice; ordenável no tempo (v7) — bomrandom (v4) — bad for the index; time-ordered (v7) — good |
| Adivinhável?Guessable? | sim: o pedido 1001 existe se o 1000 existiryes: order 1001 exists if 1000 does | não, na prática impossível de adivinharno, practically impossible to guess |
| Gerar sem ir ao bancoGenerate without a round trip | não (o banco decide o próximo número)no (the database decides the next number) | sim: o cliente pode gerar o ID antes de inseriryes: the client can generate the ID before inserting |
| Bom paraGood for | a maioria dos casos internos; IDs curtos em URLsmost internal cases; short IDs in URLs | IDs expostos publicamente, sistemas distribuídos, merges entre bancospublicly exposed IDs, distributed systems, merging data across databases |
-- serial moderno (desde o Postgres 10, prefira a IDENTITY ao velho "serial")
CREATE TABLE pedidos (id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, ...);
-- UUID
CREATE EXTENSION IF NOT EXISTS pgcrypto; -- só em Postgres < 13
CREATE TABLE sessoes (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- nativo desde o Postgres 13
usuario_id bigint NOT NULL,
criada_em timestamptz NOT NULL DEFAULT now()
);
ULID é uma alternativa fora do padrão UUID com a mesma ideia. Prefira v7/ULID quando precisar das vantagens do UUID sem perder a localidade de um ID crescente.UUID v4 (fully random) spreads writes across the index in a way that is bad for caching and disk. UUID v7 (time-ordered, like a "disguised serial") exists to fix that — and ULID is a non-UUID-standard alternative with the same idea. Prefer v7/ULID when you need UUID's advantages without losing an increasing ID's locality.Full-text search no lugar de LIKEFull-text search instead of LIKE
LIKE '%palavra%' funciona, mas não usa índice de forma eficiente (o % no início impede o B-tree de "pular" direto ao lugar certo) e não entende idioma nenhum — busca "corre" não acha "correu". Para busca de texto de verdade, o Postgres tem busca textual embutida.LIKE '%word%' works, but does not use an index efficiently (the leading % stops a B-tree from "jumping" straight to the right place) and understands no language at all — searching "run" will not find "ran". For real text search, Postgres has built-in full-text search.
tsvector e tsquerytsvector and tsquery
ALTER TABLE produtos ADD COLUMN busca tsvector
GENERATED ALWAYS AS (to_tsvector('portuguese', nome || ' ' || descricao)) STORED;
CREATE INDEX idx_produtos_busca ON produtos USING gin (busca);
SELECT nome FROM produtos
WHERE busca @@ to_tsquery('portuguese', 'camisa & azul') -- E
OR busca @@ to_tsquery('portuguese', 'camisa | blusa'); -- OU
SELECT nome, ts_rank(busca, to_tsquery('portuguese', 'camisa')) AS relevancia
FROM produtos
WHERE busca @@ to_tsquery('portuguese', 'camisa')
ORDER BY relevancia DESC;
Resultado esperado: buscar "camisa" encontra também "camisas" (o to_tsvector reduz palavras à raiz — stemming) e o ts_rank ordena pela relevância, algo que LIKE nunca faz.Expected result: searching "shirt" also finds "shirts" (to_tsvector reduces words to their root — stemming) and ts_rank sorts by relevance, something LIKE never does.
Quando o full-text search é demais: pg_trgmWhen full-text search is overkill: pg_trgm
Para "busca parecida" (tolerar erros de digitação, autocompletar) sem a complexidade de idioma do full-text, a extensão pg_trgm indexa trigramas (grupos de 3 letras) e acelera ILIKE/similarity().For "fuzzy search" (tolerating typos, autocomplete) without full-text's language complexity, the pg_trgm extension indexes trigrams (groups of 3 letters) and speeds up ILIKE/similarity().
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX idx_produtos_nome_trgm ON produtos USING gin (nome gin_trgm_ops);
SELECT nome FROM produtos WHERE nome ILIKE '%camisa%'; -- agora usa o índice
SELECT nome, similarity(nome, 'camiza azul') AS parecido
FROM produtos ORDER BY parecido DESC LIMIT 5; -- tolera o erro de digitação
| FerramentaTool | Use quandoUse when |
|---|---|
LIKE/ILIKE sem índice | tabela pequena, ou um prefixo fixo (LIKE 'joão%', que ainda usa B-tree)small table, or a fixed prefix (LIKE 'joão%', which still uses a B-tree) |
Full-text search (tsvector) | busca de verdade em texto corrido: artigos, descrições, documentosreal search over running text: articles, descriptions, documents |
pg_trgm | autocompletar, tolerar erro de digitação, busca parecida em nomes/códigosautocomplete, typo tolerance, fuzzy search over names/codes |
| Elasticsearch/Meilisearch (fora do Postgres)Elasticsearch/Meilisearch (outside Postgres) | busca como funcionalidade central do produto, facetas, escala muito grandesearch as the product's core feature, facets, very large scale |
Quem tem acesso ao quêWho has access to what
"Quem tem acesso a quê" no Postgres se decide em camadas: a role precisa existir, precisa de permissão no database, no schema, e por fim na tabela (ou até na linha). GRANT concede, REVOKE retira.Who has access to what, in Postgres, is decided in layers: the role must exist, and needs permission on the database, on the schema, and finally on the table (or even on the row). GRANT gives, REVOKE takes away.
GRANT CONNECT ON DATABASE loja TO ana;
GRANT USAGE ON SCHEMA vendas TO ana;
GRANT SELECT, INSERT ON vendas.pedidos TO ana;
GRANT SELECT (nome, email) ON clientes TO suporte; -- só essas colunas
REVOKE INSERT ON vendas.pedidos FROM ana;
-- o que uma role pode fazer:
SELECT grantee, table_name, privilege_type
FROM information_schema.role_table_grants
WHERE grantee = 'ana';
Row-Level Security (RLS): permissão por linhaRow-Level Security (RLS): permission per row
Quando até colunas não bastam — por exemplo, cada cliente só pode ver seus próprios pedidos — o RLS aplica um filtro automático em toda consulta daquela role, sem precisar confiar que todo WHERE da aplicação está correto.When even columns are not enough — for example, each customer may only see their own orders — RLS applies an automatic filter to every query from that role, without having to trust that every WHERE in the application is correct.
ALTER TABLE pedidos ENABLE ROW LEVEL SECURITY;
CREATE POLICY pedidos_do_proprio_cliente ON pedidos
USING (cliente_id = current_setting('app.cliente_id')::bigint);
-- a aplicação define quem está logado antes de consultar:
SET app.cliente_id = '42';
SELECT * FROM pedidos; -- só retorna pedidos do cliente 42, mesmo sem WHERE
WHERE é esquecido em algum lugar do código.RLS is a second layer of defence, not an excuse to skip the check in the application — but it is exactly the kind of thing that prevents a leak when a WHERE is forgotten somewhere in the code.Backups e a regra 3-2-1Backups and the 3-2-1 rule
Um backup só vale alguma coisa se você já tiver restaurado ele com sucesso pelo menos uma vez. O Postgres tem três ferramentas principais, e por cima delas vale uma regra que não é específica de banco de dados: a 3-2-1.A backup is only worth something once you have restored it successfully at least once. Postgres has three main tools, and on top of them sits a rule that is not database-specific at all: the 3-2-1 rule.
As três ferramentasThe three tools
| FerramentaTool | O que fazWhat it does | Use paraUse for |
|---|---|---|
pg_dump | exporta um database (ou parte dele) para um arquivo lógico (SQL ou formato próprio)exports a database (or part of it) to a logical file (SQL or custom format) | backups do dia a dia, mover dados entre versões/servidoresday-to-day backups, moving data between versions/servers |
pg_dumpall | como o pg_dump, mas inclui roles e configurações de todo o clusterlike pg_dump, but includes roles and settings for the whole cluster | backup completo de um servidor com vários databasesa full backup of a server with several databases |
pg_basebackup + WAL | cópia física dos arquivos de dados + o log de escrita contínuo (WAL)a physical copy of the data files + the continuous write-ahead log (WAL) | bancos grandes, e recuperação para QUALQUER instante (PITR)large databases, and recovery to ANY point in time (PITR) |
pg_dump -U postgres -h localhost -Fc loja > loja_$(date +%F).dump # formato comprimido, -Fc
pg_dump -U postgres loja > loja.sql # texto puro, legível, dá pra ler/editar
pg_restore -U postgres -d loja_nova loja_$(date +%F).dump # restaura o formato -Fc
psql -U postgres -d loja_nova < loja.sql # restaura o SQL puro
Resultado esperado: depois do pg_restore num database novo e vazio, \dt mostra as mesmas tabelas do original, e um SELECT count(*) bate com a contagem de antes do dump.Expected result: after pg_restore into a new, empty database, \dt shows the same tables as the original, and a SELECT count(*) matches the count from before the dump.
Point-in-time recovery (PITR), em uma frasePoint-in-time recovery (PITR), in one sentence
Com pg_basebackup (uma foto dos arquivos) mais o WAL (write-ahead log: todo write é gravado ali antes de ir para a tabela) arquivado continuamente, dá para reconstruir o banco exatamente como estava um segundo antes de alguém rodar um DELETE sem WHERE — não só "como estava no backup de ontem à noite".With pg_basebackup (a snapshot of the files) plus the continuously archived WAL (write-ahead log: every write is recorded there before it reaches the table), you can rebuild the database exactly as it was one second before someone ran a DELETE with no WHERE — not just "as it was in last night's backup".
A regra 3-2-1The 3-2-1 rule
| CamadaLayer | Exemplo no mundo PostgresExample in the Postgres world |
|---|---|
| Cópia 1Copy 1 | o banco em produçãothe production database |
| Cópia 2Copy 2 | pg_dump diário num disco/volume separado da mesma máquinaa daily pg_dump on a disk/volume separate from the same machine |
| Cópia 3 (fora do local)Copy 3 (off-site) | o dump enviado para outro provedor/região (S3 em outra região, outra nuvem)the dump shipped to another provider/region (S3 in another region, another cloud) |
| Em serviços gerenciadosOn managed services | RDS já faz backups automáticos com retenção e cópia entre zonas; confirme se ela cobre outra REGIÃO tambémRDS already does automatic backups with retention and cross-zone copies; confirm it also covers another REGION |
Cache: o 80/20 para quem programaCaching: the 80/20 for developers
Cache é "guardar uma resposta pronta para não refazer o trabalho". É provavelmente a técnica isolada que mais acelera sistemas reais — e também a que mais causa bugs sutis, porque introduz uma segunda fonte de verdade que pode ficar desatualizada.A cache is "keeping a ready-made answer so you do not redo the work". It is probably the single technique that speeds up real systems the most — and also the one that causes the most subtle bugs, because it introduces a second source of truth that can go stale.
Onde o Postgres já faz cache por conta própriaWhere Postgres already caches on its own
| CamadaLayer | O que guardaWhat it keeps |
|---|---|
shared_buffers | páginas de dados recentemente usadas, na memória do próprio Postgresrecently used data pages, in Postgres's own memory |
| cache de página do SOOS page cache | o Linux também guarda os mesmos arquivos em memória livre — é por isso que a segunda leitura de algo é quase sempre mais rápidaLinux also keeps those same files in free memory — which is why a second read of something is almost always faster |
| plano de consultaquery plan | consultas preparadas reaproveitam o plano já calculadoprepared statements reuse an already-computed plan |
O 80/20 de cache de aplicaçãoThe 80/20 of application caching
| PadrãoPattern | Como funcionaHow it works | Risco principalMain risk |
|---|---|---|
| Cache-aside (lazy loading)Cache-aside (lazy loading) | a aplicação lê o cache; se faltar, lê o banco e GRAVA no cachethe application reads the cache; on a miss, it reads the database and WRITES to the cache | dado desatualizado até expirar ou ser invalidadostale data until it expires or is invalidated |
| Write-throughWrite-through | toda escrita vai para o banco E o cache, juntasevery write goes to the database AND the cache, together | escrita fica mais lenta (duas gravações)writes get slower (two writes) |
| TTL (time to live)TTL (time to live) | cada entrada expira sozinha depois de N segundoseach entry expires on its own after N seconds | escolher N é um equilíbrio entre "atual" e "rápido"choosing N is a trade-off between "fresh" and "fast" |
| Invalidação explícitaExplicit invalidation | ao escrever, apaga/atualiza a entrada do cache na horaon write, delete/update the cache entry right away | é fácil esquecer um caminho de escrita e deixar cache "furado"easy to forget a write path and leave the cache "stale" |
Onde colocar o cacheWhere to put the cache
O problema N+1: o "cache que você nem precisava"The N+1 problem: the "cache you did not even need"
-- N+1: 1 consulta para os pedidos + N consultas, uma por pedido, para o cliente
SELECT * FROM pedidos LIMIT 20; -- 1
SELECT * FROM clientes WHERE id = 1; -- repetida 20x por um ORM "ingênuo"
-- resolvido com UM join, sem precisar de cache nenhum
SELECT p.*, c.nome FROM pedidos p JOIN clientes c ON c.id = p.cliente_id LIMIT 20;
Boa parte do "preciso de cache" na prática é, na verdade, uma consulta ruim. Antes de adicionar uma camada de cache, rode EXPLAIN ANALYZE e confira se um índice ou um JOIN já resolvem.A good part of "I need a cache" in practice is actually a bad query. Before adding a cache layer, run EXPLAIN ANALYZE and check whether an index or a JOIN already fixes it.
Laboratório com DockerLab with Docker
Um Postgres local em segundos, para treinar tudo desta nota sem instalar nada no sistema.A local Postgres in seconds, to practise everything in this note without installing anything on your system.
mkdir lab-postgres && cd lab-postgres
cat > docker-compose.yml <<'EOF'
services:
db:
image: postgres:16
environment:
POSTGRES_USER: ana
POSTGRES_PASSWORD: s3nha-forte-de-verdade
POSTGRES_DB: loja
ports:
- "127.0.0.1:5432:5432"
volumes:
- dados:/var/lib/postgresql/data
volumes:
dados:
EOF
docker compose up -d
docker compose exec db psql -U ana -d loja -c "SELECT version();"
Resultado esperado: a última linha imprime a versão do Postgres — se aparecer, o container subiu, aceitou a senha definida e criou o database loja.Expected result: the last line prints the Postgres version — if it shows up, the container came up, accepted the configured password, and created the loja database.
psql "postgresql://ana:s3nha-forte-de-verdade@localhost:5432/loja" # do seu HOST, direto
docker compose exec db pg_dump -U ana loja > backup.sql # pratique o backup da seção anterior
docker compose down # para os containers, mantém o volume
docker compose down -v # remove TAMBÉM o volume (apaga os dados)
docker compose down, mas some com -v, com uma reinstalação do Docker, ou se você trocar de máquina; um serviço gerenciado como o RDS tem o disco (EBS) e os backups automáticos independentes do processo. A rede também é a mesma do Docker explicada na nota de segurança: 127.0.0.1:5432:5432 deixa a porta só no seu host; sem o 127.0.0.1, ficaria acessível por qualquer coisa que alcance a sua máquina. E não há TLS nem pg_hba.conf customizado aqui — a imagem oficial já libera senha de qualquer host dentro do container por padrão, o que é aceitável só porque é um laboratório local.Differences between this lab and a real database (managed or in production): here the data lives in a Docker volume on your disk — it survives a docker compose down, but disappears with -v, with a Docker reinstall, or if you switch machines; a managed service like RDS has the disk (EBS) and automated backups independent of the process. The network is also the same Docker setup explained in the security note: 127.0.0.1:5432:5432 keeps the port local to your host only; without the 127.0.0.1, it would be reachable by anything that can reach your machine. And there is no TLS or custom pg_hba.conf here — the official image already allows password auth from any host inside the container by default, which is acceptable only because this is a local lab.Para algo mais perto de produção, veja a nota de AWS (RDS) ou suba um Postgres numa VM de verdade e aplique o pg_hba.conf e o TLS desta nota de propósito.For something closer to production, see the AWS note (RDS) or spin up Postgres on a real VM and apply this note's pg_hba.conf and TLS on purpose.
Cola e revisãoCheat sheet and review
ColaCheat sheet
| Quero…I want… | ComandoCommand |
|---|---|
| listar databases/schemas/tabelaslist databases/schemas/tables | \l · \dn · \dt |
| ver a estrutura de uma tabelasee a table's structure | \d nome_tabela |
| criar usuário e dar acesso só leituracreate a user and grant read-only access | CREATE ROLE ... LOGIN; GRANT SELECT ON ... |
| ver se uma consulta usa índicesee whether a query uses an index | EXPLAIN ANALYZE SELECT ... |
| backup e restauraçãobackup and restore | pg_dump -Fc db > f.dump · pg_restore -d db f.dump |
| buscar texto de verdadesearch text for real | to_tsvector / to_tsquery |
Perguntas de revisãoReview questions
| PerguntaQuestion | RespostaAnswer |
|---|---|
| Qual a diferença entre um database e um schema?What is the difference between a database and a schema? | Database é isolado (não dá para fazer JOIN entre dois); schema é um namespace DENTRO de um database.A database is isolated (you cannot JOIN across two); a schema is a namespace INSIDE one database. |
| Por que o Postgres pede senha às vezes e não pede outras?Why does Postgres sometimes ask for a password and sometimes not? | Depende do método no pg_hba.conf: peer/ident confia no usuário do SO; scram-sha-256 pede senha.It depends on the pg_hba.conf method: peer/ident trusts the OS user; scram-sha-256 asks for a password. |
| A role "ana" do Postgres precisa existir como usuário do Linux?Does the Postgres role "ana" need to exist as a Linux user? | Não — são sistemas de identidade completamente separados.No — they are completely separate identity systems. |
| Quando um índice B-tree não ajuda num LIKE?When does a B-tree index not help with a LIKE? | Quando o padrão começa com %, como '%palavra%'; aí nem o índice nem um prefixo fixo ajudam.When the pattern starts with %, like '%word%'; then neither the index nor a fixed prefix helps. |
| O que muda entre EXPLAIN e EXPLAIN ANALYZE?What changes between EXPLAIN and EXPLAIN ANALYZE? | EXPLAIN só mostra o plano estimado; EXPLAIN ANALYZE executa de verdade e mostra tempos reais.EXPLAIN only shows the estimated plan; EXPLAIN ANALYZE actually runs it and shows real timings. |
| Por que UUID v4 pode ser ruim para performance de índice?Why can UUID v4 be bad for index performance? | É totalmente aleatório, então as escritas se espalham pelo índice inteiro em vez de ficarem no "final".It is fully random, so writes spread across the whole index instead of staying at the "end". |
| O que a regra 3-2-1 exige além de "ter backup"?What does the 3-2-1 rule require beyond "having a backup"? | 3 cópias, em 2 mídias diferentes, com 1 fora do local — e testar a restauração.3 copies, on 2 different media, with 1 kept off-site — and testing the restore. |
- PostgreSQL Global Development Group. PostgreSQL Documentation (official, free).
- Martin Kleppmann. Designing Data-Intensive Applications. O'Reilly, 2017.
- Dimitri Fontaine. The Art of PostgreSQL.
- Markus Winand. Use The Index, Luke! (free online, SQL indexing).
- PostgreSQL Documentation. The pg_hba.conf file.
- PostgreSQL Documentation. Full Text Search.
- PostgreSQL Documentation. Backup and Restore.
- PostgreSQL Documentation. Privileges and Row Security Policies.
- Redis documentation. Caching architecture patterns (cache-aside, write-through).