PostgreSQL: referência¶
Comandos e padrões úteis para PostgreSQL.
Script para criar tabelas usadas nos exemplos
-- Departamentos
CREATE TABLE departamentos (
id SERIAL PRIMARY KEY,
nome VARCHAR(100) NOT NULL
);
INSERT INTO departamentos (nome) VALUES
('Engenharia'), ('Vendas'), ('RH'), ('Financeiro');
-- Funcionários
CREATE TABLE funcionarios (
id SERIAL PRIMARY KEY,
nome VARCHAR(100) NOT NULL,
email VARCHAR(100),
departamento_id INT REFERENCES departamentos(id),
gerente_id INT REFERENCES funcionarios(id),
salario NUMERIC(10,2),
data_contratacao DATE
);
INSERT INTO funcionarios (nome, email, departamento_id, gerente_id, salario, data_contratacao) VALUES
('Ana Silva', '[email protected]', 1, NULL, 15000.00, '2020-01-15'),
('Bruno Costa', '[email protected]', 1, 1, 8000.00, '2021-03-10'),
('Carla Santos', '[email protected]', 1, 1, 9500.00, '2020-06-20'),
('Daniel Oliveira', '[email protected]', 2, NULL, 12000.00, '2019-11-01'),
('Eva Lima', '[email protected]', 2, 4, 7500.00, '2022-02-14'),
('Fernando Dias', '[email protected]', 3, NULL, 10000.00, '2021-08-30'),
('Gisele Ramos', '[email protected]', 4, NULL, 11000.00, '2020-04-25');
-- Clientes
CREATE TABLE clientes (
id SERIAL PRIMARY KEY,
nome VARCHAR(100) NOT NULL,
email VARCHAR(100),
cidade VARCHAR(50),
metadata JSONB DEFAULT '{}'
);
INSERT INTO clientes (nome, email, cidade, metadata) VALUES
('Tech Solutions', '[email protected]', 'São Paulo', '{"plano": "enterprise", "ativo": true}'),
('Data Corp', '[email protected]', 'Rio de Janeiro', '{"plano": "pro", "ativo": true}'),
('Cloud Services', '[email protected]', 'Belo Horizonte', '{"plano": "starter", "ativo": false}'),
('Digital Labs', '[email protected]', 'São Paulo', '{"plano": "pro", "ativo": true}');
-- Produtos
CREATE TABLE produtos (
id SERIAL PRIMARY KEY,
nome VARCHAR(100) NOT NULL,
preco NUMERIC(10,2),
estoque INT DEFAULT 0,
tags TEXT[]
);
INSERT INTO produtos (nome, preco, estoque, tags) VALUES
('Licença Software A', 500.00, 100, ARRAY['software', 'licença']),
('Licença Software B', 1200.00, 50, ARRAY['software', 'licença', 'premium']),
('Consultoria (hora)', 250.00, 999, ARRAY['serviço', 'consultoria']),
('Suporte Anual', 3000.00, 999, ARRAY['serviço', 'suporte']);
-- Pedidos
CREATE TABLE pedidos (
id SERIAL PRIMARY KEY,
cliente_id INT REFERENCES clientes(id),
data DATE NOT NULL,
status VARCHAR(20) DEFAULT 'pendente'
);
INSERT INTO pedidos (cliente_id, data, status) VALUES
(1, '2025-01-05', 'concluido'),
(1, '2025-01-10', 'concluido'),
(2, '2025-01-12', 'pendente'),
(3, '2025-01-15', 'pendente'),
(4, '2025-01-16', 'cancelado');
-- Itens do pedido
CREATE TABLE itens_pedido (
id SERIAL PRIMARY KEY,
pedido_id INT REFERENCES pedidos(id),
produto_id INT REFERENCES produtos(id),
quantidade INT,
valor_unitario NUMERIC(10,2)
);
INSERT INTO itens_pedido (pedido_id, produto_id, quantidade, valor_unitario) VALUES
(1, 1, 10, 500.00),
(1, 4, 1, 3000.00),
(2, 2, 5, 1200.00),
(2, 3, 8, 250.00),
(3, 1, 20, 500.00),
(4, 2, 3, 1200.00),
(5, 3, 4, 250.00);
-- Transações
CREATE TABLE transacoes (
id SERIAL PRIMARY KEY,
data DATE,
valor NUMERIC(10,2)
);
INSERT INTO transacoes (data, valor) VALUES
('2025-01-01', 1000.00),
('2025-01-02', -200.00),
('2025-01-03', 500.00),
('2025-01-04', -100.00),
('2025-01-05', 800.00);
-- Contas
CREATE TABLE contas (
id INT PRIMARY KEY,
titular VARCHAR(100),
saldo NUMERIC(10,2)
);
INSERT INTO contas (id, titular, saldo) VALUES
(1, 'Conta A', 5000.00),
(2, 'Conta B', 3000.00);
-- Logs (para particionamento)
CREATE TABLE logs (
id SERIAL,
created_at TIMESTAMP NOT NULL,
nivel VARCHAR(10),
mensagem TEXT
) PARTITION BY RANGE (created_at);
CREATE TABLE logs_2025_01 PARTITION OF logs
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
Tipos de dados¶
-- Strings
CHAR(10) -- fixo
VARCHAR(100) -- variável
TEXT -- ilimitado
-- Números
INT / INTEGER -- -2bi a 2bi
BIGINT -- muito grande
NUMERIC(10,2) -- precisão exata (dinheiro)
REAL / FLOAT -- aproximado
-- Data/hora
DATE -- só data
TIME -- só hora
TIMESTAMP -- data + hora
TIMESTAMPTZ -- com timezone (preferir)
INTERVAL -- duração
-- Outros
BOOLEAN -- true/false
UUID -- identificador único
JSONB -- JSON binário (preferir sobre JSON)
TEXT[] -- array de texto
INT[] -- array de inteiros
CTEs¶
WITH vendas_mes AS (
SELECT
cliente_id,
SUM(i.quantidade * i.valor_unitario) AS total
FROM pedidos p
JOIN itens_pedido i ON i.pedido_id = p.id
WHERE p.data >= CURRENT_DATE - INTERVAL '1 month'
GROUP BY cliente_id
)
SELECT * FROM vendas_mes WHERE total > 1000;
CTE recursiva¶
WITH RECURSIVE hierarquia AS (
-- Âncora
SELECT id, nome, gerente_id, 0 AS nivel
FROM funcionarios
WHERE gerente_id IS NULL
UNION ALL
-- Recursão
SELECT f.id, f.nome, f.gerente_id, h.nivel + 1
FROM funcionarios f
INNER JOIN hierarquia h ON f.gerente_id = h.id
)
SELECT * FROM hierarquia;
Window functions¶
SELECT
nome,
departamento_id,
salario,
ROW_NUMBER() OVER (ORDER BY salario DESC) AS ranking,
RANK() OVER (ORDER BY salario DESC) AS rank_com_gaps,
DENSE_RANK() OVER (ORDER BY salario DESC) AS rank_sem_gaps,
SUM(salario) OVER (PARTITION BY departamento_id) AS total_dept,
AVG(salario) OVER () AS media_geral,
LAG(salario) OVER (ORDER BY data_contratacao) AS salario_anterior,
LEAD(salario) OVER (ORDER BY data_contratacao) AS salario_proximo
FROM funcionarios;
Running total¶
SELECT
data,
valor,
SUM(valor) OVER (ORDER BY data ROWS UNBOUNDED PRECEDING) AS acumulado
FROM transacoes;
UPSERT (INSERT ON CONFLICT)¶
INSERT INTO clientes (id, nome, email)
VALUES (1, 'Tech Solutions Ltda', '[email protected]')
ON CONFLICT (id) DO UPDATE SET
nome = EXCLUDED.nome,
email = EXCLUDED.email;
-- Ignorar conflito
INSERT INTO clientes (id, nome, email)
VALUES (1, 'Tech Solutions', '[email protected]')
ON CONFLICT DO NOTHING;
RETURNING¶
-- Retorna dados após INSERT/UPDATE/DELETE
INSERT INTO clientes (nome, email)
VALUES ('Nova Empresa', '[email protected]')
RETURNING id, nome;
UPDATE produtos
SET preco = preco * 1.1
WHERE estoque < 10
RETURNING id, nome, preco;
DELETE FROM pedidos
WHERE status = 'cancelado'
RETURNING *;
JSONB¶
-- Criar
SELECT '{"nome": "João", "idade": 30}'::JSONB;
-- Acessar campo
SELECT metadata->>'plano' FROM clientes; -- como texto
SELECT metadata->'plano' FROM clientes; -- como jsonb
-- Acessar nested
SELECT metadata->'endereco'->>'cidade' FROM clientes;
-- Filtrar
SELECT * FROM clientes
WHERE metadata->>'plano' = 'enterprise';
SELECT * FROM clientes
WHERE metadata @> '{"ativo": true}'; -- contém
-- Atualizar campo
UPDATE clientes
SET metadata = jsonb_set(metadata, '{plano}', '"pro"')
WHERE id = 1;
-- Adicionar campo
UPDATE clientes
SET metadata = metadata || '{"desconto": 10}'
WHERE id = 1;
-- Remover campo
UPDATE clientes
SET metadata = metadata - 'desconto'
WHERE id = 1;
-- Agregar em JSON
SELECT jsonb_agg(nome) FROM clientes;
SELECT jsonb_object_agg(id, nome) FROM clientes;
Arrays¶
-- Criar
SELECT ARRAY['a', 'b', 'c'];
SELECT '{a,b,c}'::TEXT[];
-- Acessar (1-indexed)
SELECT tags[1] FROM produtos;
-- Contém
SELECT * FROM produtos WHERE 'premium' = ANY(tags);
SELECT * FROM produtos WHERE tags @> ARRAY['software'];
-- Adicionar
UPDATE produtos
SET tags = array_append(tags, 'novo')
WHERE id = 1;
-- Remover
UPDATE produtos
SET tags = array_remove(tags, 'novo')
WHERE id = 1;
-- Expandir (unnest)
SELECT id, unnest(tags) AS tag FROM produtos;
-- Agregar
SELECT array_agg(nome) FROM clientes;
STRING_AGG¶
SELECT
departamento_id,
STRING_AGG(nome, ', ' ORDER BY nome) AS funcionarios
FROM funcionarios
GROUP BY departamento_id;
Funções de data¶
NOW() -- timestamp atual
CURRENT_DATE -- só data
CURRENT_TIMESTAMP -- com timezone
-- Aritmética
CURRENT_DATE + INTERVAL '7 days'
CURRENT_DATE - INTERVAL '1 month'
-- Diferença
AGE(data_fim, data_inicio) -- retorna interval
DATE_PART('day', AGE(data_fim, data_inicio)) -- em dias
-- Extrair
EXTRACT(YEAR FROM data)
EXTRACT(MONTH FROM data)
DATE_TRUNC('month', data) -- truncar para início do mês
-- Construir
MAKE_DATE(2025, 1, 15)
TO_DATE('15/01/2025', 'DD/MM/YYYY')
TO_TIMESTAMP('2025-01-15 10:30', 'YYYY-MM-DD HH24:MI')
-- Formatar
TO_CHAR(NOW(), 'DD/MM/YYYY HH24:MI')
COALESCE / NULLIF / CASE¶
COALESCE(a, b, c) -- primeiro não-nulo
NULLIF(a, b) -- se a = b, retorna null
CASE
WHEN nota >= 9 THEN 'A'
WHEN nota >= 7 THEN 'B'
WHEN nota >= 5 THEN 'C'
ELSE 'F'
END
LIMIT / OFFSET (paginação)¶
LATERAL JOIN¶
-- Subquery correlacionada como JOIN
SELECT c.nome, ultimos.*
FROM clientes c
CROSS JOIN LATERAL (
SELECT p.data, p.status
FROM pedidos p
WHERE p.cliente_id = c.id
ORDER BY p.data DESC
LIMIT 3
) ultimos;
FILTER¶
SELECT
COUNT(*) AS total,
COUNT(*) FILTER (WHERE status = 'concluido') AS concluidos,
COUNT(*) FILTER (WHERE status = 'pendente') AS pendentes,
SUM(valor) FILTER (WHERE valor > 0) AS entradas
FROM pedidos p
JOIN itens_pedido i ON i.pedido_id = p.id;
DISTINCT ON¶
-- Primeiro registro de cada grupo
SELECT DISTINCT ON (cliente_id)
cliente_id, data, status
FROM pedidos
ORDER BY cliente_id, data DESC;
Transações¶
BEGIN;
UPDATE contas SET saldo = saldo - 100 WHERE id = 1;
UPDATE contas SET saldo = saldo + 100 WHERE id = 2;
-- Se tudo ok
COMMIT;
-- Se erro
ROLLBACK;
Savepoints¶
BEGIN;
UPDATE contas SET saldo = saldo - 100 WHERE id = 1;
SAVEPOINT antes_transferencia;
UPDATE contas SET saldo = saldo + 100 WHERE id = 2;
-- Desfaz só até o savepoint
ROLLBACK TO antes_transferencia;
COMMIT;
Índices¶
-- B-tree (padrão)
CREATE INDEX idx_pedidos_cliente ON pedidos(cliente_id);
-- Composto
CREATE INDEX idx_pedidos_data_cliente ON pedidos(data, cliente_id);
-- Parcial
CREATE INDEX idx_pedidos_pendentes ON pedidos(data)
WHERE status = 'pendente';
-- JSONB
CREATE INDEX idx_clientes_metadata ON clientes USING GIN(metadata);
-- Array
CREATE INDEX idx_produtos_tags ON produtos USING GIN(tags);
-- Texto (busca)
CREATE INDEX idx_clientes_nome ON clientes USING GIN(nome gin_trgm_ops);
-- Unique
CREATE UNIQUE INDEX idx_clientes_email ON clientes(email);
-- Concurrent (não bloqueia)
CREATE INDEX CONCURRENTLY idx_nome ON tabela(coluna);
EXPLAIN¶
-- Plano estimado
EXPLAIN SELECT * FROM pedidos WHERE cliente_id = 1;
-- Plano real com execução
EXPLAIN ANALYZE SELECT * FROM pedidos WHERE cliente_id = 1;
-- Com buffers e timing
EXPLAIN (ANALYZE, BUFFERS, TIMING)
SELECT * FROM pedidos WHERE cliente_id = 1;
Queries úteis¶
Tabelas e tamanhos¶
SELECT
relname AS tabela,
n_live_tup AS linhas,
pg_size_pretty(pg_total_relation_size(relid)) AS tamanho
FROM pg_stat_user_tables
ORDER BY pg_total_relation_size(relid) DESC;
Índices não utilizados¶
SELECT
indexrelname AS indice,
relname AS tabela,
idx_scan AS scans,
pg_size_pretty(pg_relation_size(indexrelid)) AS tamanho
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;
Queries lentas (requer pg_stat_statements)¶
SELECT
calls,
mean_exec_time::INT AS avg_ms,
SUBSTRING(query, 1, 100) AS query
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
Locks ativos¶
SELECT
pid,
mode,
relation::regclass,
query
FROM pg_locks l
JOIN pg_stat_activity a ON l.pid = a.pid
WHERE relation IS NOT NULL;
Conexões ativas¶
SELECT
state,
COUNT(*) AS conexoes,
MAX(NOW() - state_change) AS mais_antiga
FROM pg_stat_activity
WHERE datname = current_database()
GROUP BY state;