PostgreSQL: Operações INSERT, UPDATE e DELETE no PostgreSQL
Última atualização: 2026-08-26
A modificação de dados (DML) é a operação diária mais frequente — além do SQL padrão, o PostgreSQL adiciona dois recursos distintos: RETURNING e UPSERT.
1. O Que Você Vai Aprender
- INSERT INTO: linha única / múltiplas linhas / inserir a partir de consulta
- Cláusula RETURNING (recurso PG: retorna as linhas afetadas)
- UPDATE / DELETE com RETURNING
- UPSERT: INSERT ON CONFLICT (recurso PG)
- TRUNCATE TABLE para esvaziar uma tabela
- DML dentro de uma transação (BEGIN / COMMIT / ROLLBACK)
2. A História Real de um Profissional de Operações
(1) A Dor: Dados Duplicados ao Importar Produtos em Lote
Charlie precisa importar em lote 5.000 registros de produtos para o banco de dados de e-commerce, mas alguns produtos já existem (identificados por name). Ele precisa:
- Para produtos existentes: atualizar preço e estoque (sem gerar erro)
- Para novos produtos: inserir normalmente
- Após a importação: saber imediatamente quais linhas foram inseridas e quais foram atualizadas
(2) A Solução: UPSERT + RETURNING
O INSERT ON CONFLICT (UPSERT) do PostgreSQL lida tanto com inserção quanto com atualização em uma única instrução SQL, e a cláusula RETURNING retorna as linhas afetadas:
-- Upsert: insere novos produtos, atualiza os existentes
INSERT INTO products (name, price, stock, category)
VALUES ('Running Shoes', 99.99, 200, 'Footwear')
ON CONFLICT (name)
DO UPDATE SET
price = EXCLUDED.price,
stock = products.stock + EXCLUDED.stock
RETURNING id, name, price, stock,
CASE WHEN xmax = 0 THEN 'inserted' ELSE 'updated' END AS operation;
(3) O Resultado
- Não é necessário SELECT primeiro para decidir entre INSERT ou UPDATE (uma consulta a menos)
- Sem condição de corrida de concorrência (operação atômica)
- RETURNING retorna o resultado imediatamente, sem necessidade de segunda consulta
- Eficiência da importação em lote melhorou mais de 10x
3. INSERT
(1) Sintaxe Básica
INSERT INTO table_name (column1, column2, ...)
VALUES (value1, value2, ...)
RETURNING * | column_list;
▶ Exemplo: Inserção de Linha Única
-- Insere uma única linha
INSERT INTO users (email, name, password_hash)
VALUES ('charlie@example.com', 'Charlie', '$2a$12$hash123');
-- Insere com RETURNING (obtém o id gerado automaticamente)
INSERT INTO users (email, name, password_hash)
VALUES ('diana@example.com', 'Diana', '$2a$12$hash456')
RETURNING id, email, created_at;
Output:
id | email | created_at
----+----------------------+-------------------------------
5 | diana@example.com | 2026-07-13 10:30:00+00
▶ Exemplo: Inserção de Múltiplas Linhas
-- Insere múltiplas linhas em uma instrução (mais rápido que vários INSERTs)
INSERT INTO products (name, price, stock, category) VALUES
('Wireless Mouse', 29.99, 500, 'Accessories'),
('USB-C Cable', 12.99, 1000, 'Accessories'),
('Mechanical Keyboard', 79.99, 200, 'Input Devices'),
('Monitor Stand', 49.99, 150, 'Accessories'),
('Webcam HD', 59.99, 300, 'Video');
Output:
INSERT 0 1
▶ Exemplo: Inserir a Partir de uma Consulta
-- Cria uma tabela de arquivo e copia dados da original
CREATE TABLE orders_archive (LIKE orders INCLUDING ALL);
-- Insere linhas a partir de uma consulta SELECT
INSERT INTO orders_archive
SELECT * FROM orders
WHERE status = 'cancelled'
AND created_at < NOW() - INTERVAL '90 days';
Output:
INSERT 0 1
4. Cláusula RETURNING (Recurso PG)
RETURNING é um recurso exclusivo do PostgreSQL — após INSERT / UPDATE / DELETE, ele retorna as linhas afetadas diretamente, sem necessidade de SELECT extra.
(1) RETURNING em Diferentes Operações
| Operação | Sintaxe | Retorna |
|---|---|---|
| INSERT | INSERT ... RETURNING * |
A linha recém-inserida |
| UPDATE | UPDATE ... RETURNING * |
A linha atualizada |
| DELETE | DELETE ... RETURNING * |
A linha excluída |
| Comparação | MySQL | PostgreSQL |
|---|---|---|
| Obter o ID após INSERT | SELECT LAST_INSERT_ID() |
INSERT ... RETURNING id |
| Obter as linhas excluídas | SELECT primeiro, depois DELETE | DELETE ... RETURNING * |
| Obter valores após UPDATE | UPDATE primeiro, depois SELECT | UPDATE ... RETURNING * |
▶ Exemplo: A Mágica do RETURNING
-- INSERT: obtém valores gerados automaticamente imediatamente
INSERT INTO users (email, name, password_hash)
VALUES ('eve@example.com', 'Eve', '$2a$12$hash789')
RETURNING id, email, created_at;
-- UPDATE: vê os valores antes e depois
UPDATE products
SET price = 69.99, stock = stock - 10
WHERE id = 1
RETURNING id, name,
price AS new_price,
stock AS new_stock;
-- DELETE: arquiva antes de excluir
WITH deleted AS (
DELETE FROM orders
WHERE status = 'cancelled'
AND created_at < NOW() - INTERVAL '1 year'
RETURNING *
)
INSERT INTO orders_archive SELECT * FROM deleted;
Output:
INSERT 0 1
5. UPDATE
(1) Sintaxe Básica
UPDATE table_name
SET column1 = value1, column2 = value2, ...
[WHERE condition]
[RETURNING * | column_list];
▶ Exemplo: Atualização Básica
-- Atualiza uma única linha
UPDATE users
SET name = 'Alice Smith', updated_at = NOW()
WHERE email = 'alice@example.com';
-- Atualiza com condição
UPDATE products
SET price = price * 0.9 -- 10% de desconto
WHERE category = 'Accessories' AND stock > 100;
-- Atualiza com RETURNING
UPDATE products
SET stock = stock - 1
WHERE id = 1 AND stock > 0
RETURNING id, name, stock;
Output:
-- Comando SQL executado com sucesso
▶ Exemplo: UPDATE Baseado em JOIN
-- Atualiza o total do pedido com base nos itens do pedido
UPDATE orders o
SET total_amount = (
SELECT SUM(quantity * unit_price)
FROM order_items oi
WHERE oi.order_id = o.id
)
WHERE o.status = 'pending';
-- Atualiza usando a cláusula FROM (extensão PG)
UPDATE orders o
SET total_amount = oi_sum.total
FROM (
SELECT order_id, SUM(quantity * unit_price) AS total
FROM order_items
GROUP BY order_id
) oi_sum
WHERE o.id = oi_sum.order_id
AND o.status = 'pending';
Output:
result
----------
42.50
(1 row)
6. DELETE
(1) Sintaxe Básica
DELETE FROM table_name
[WHERE condition]
[RETURNING * | column_list];
▶ Exemplo: Operações de Exclusão
-- Exclui linhas específicas
DELETE FROM products
WHERE stock = 0 AND is_available = false
RETURNING id, name;
-- Exclui com subconsulta
DELETE FROM orders
WHERE user_id IN (
SELECT id FROM users WHERE is_active = false
);
-- Exclui todas as linhas (use TRUNCATE para tabelas grandes!)
-- DELETE FROM logs; -- lento, gera WAL para cada linha
Output:
DELETE 2
(2) DELETE vs TRUNCATE
| Dimensão | DELETE | TRUNCATE |
|---|---|---|
| Velocidade | Exclui linha por linha, lento | Esvazia de uma vez, extremamente rápido |
| Transação | Pode ser revertido (dentro de uma transação) | Também pode ser revertido (dentro de uma transação) |
| Gatilhos | Dispara gatilhos no nível da linha | Não dispara gatilhos no nível da linha |
| Reinício de sequência | Não reinicia | Pode reiniciar (RESTART IDENTITY) |
| WHERE | Suportado | Não suportado (esvazia a tabela inteira) |
| Caso de uso | Excluir algumas linhas | Esvaziar a tabela inteira |
▶ Exemplo: TRUNCATE para Esvaziar uma Tabela
-- Trunca uma tabela (rápido, redefine o armazenamento)
TRUNCATE TABLE logs;
-- Trunca múltiplas tabelas de uma vez
TRUNCATE TABLE order_items, orders;
-- Cascade: também trunca tabelas com referências de chave estrangeira
TRUNCATE TABLE users CASCADE;
-- Reinicia as sequências de auto-incremento
TRUNCATE TABLE products RESTART IDENTITY;
Output:
-- Comando SQL executado com sucesso
7. UPSERT (INSERT ON CONFLICT)
O UPSERT é um dos recursos distintos mais práticos do PostgreSQL — quando os dados inseridos entram em conflito com uma linha existente, ele realiza automaticamente uma atualização em vez de gerar um erro.
(1) Fluxograma do UPSERT
graph TB
START[INSERT linha] --> CONFLICT{Restrição única<br/>em conflito?}
CONFLICT -->|Sem conflito| INSERT[Inserir nova linha<br/>RETURN inserida]
CONFLICT -->|Conflito!| ACTION{Ação ON CONFLICT<br/>?}
ACTION -->|DO NOTHING| SKIP[Ignorar esta linha<br/>Sem erro, sem atualização]
ACTION -->|DO UPDATE| UPDATE[Atualizar linha existente<br/>usando EXCLUDED]
UPDATE --> RET2[RETURN linha atualizada]
(2) Sintaxe
INSERT INTO table_name (column_list)
VALUES (value_list)
ON CONFLICT (column_name | constraint_name)
DO NOTHING | DO UPDATE SET column = EXCLUDED.column ...
[RETURNING *];
| Palavra-chave | Descrição |
|---|---|
ON CONFLICT (column) |
A coluna para detectar conflitos (deve ter uma restrição UNIQUE ou chave primária) |
ON CONFLICT ON CONSTRAINT name |
Especifica um nome de restrição específico |
DO NOTHING |
Ignora a linha em conflito (sem erro, sem atualização) |
DO UPDATE SET ... |
Realiza uma atualização em conflito |
EXCLUDED |
Tabela virtual contendo os valores que originalmente seriam inseridos |
▶ Exemplo: DO NOTHING (Ignorar Duplicatas)
-- Insere apenas se o email ainda não existir
INSERT INTO users (email, name, password_hash)
VALUES ('alice@example.com', 'Alice', '$2a$12$newhash')
ON CONFLICT (email) DO NOTHING;
-- Sem erro se o email já existir
Output:
INSERT 0 1
▶ Exemplo: DO UPDATE (Atualizar Linha Existente)
-- Upsert: insere novos produtos, atualiza preço/estoque dos existentes
INSERT INTO products (name, price, stock, category)
VALUES
('Running Shoes', 99.99, 200, 'Footwear'),
('USB-C Cable', 14.99, 800, 'Accessories'),
('New Product', 39.99, 50, 'Gadgets')
ON CONFLICT (name)
DO UPDATE SET
price = EXCLUDED.price,
stock = products.stock + EXCLUDED.stock,
category = EXCLUDED.category
RETURNING id, name, price, stock;
Output:
INSERT 0 1
▶ Exemplo: UPSERT Condicional (Atualizar Apenas em Casos Específicos)
-- Atualiza apenas se o novo preço for menor
INSERT INTO products (name, price, stock, category)
VALUES ('Running Shoes', 79.99, 100, 'Footwear')
ON CONFLICT (name)
DO UPDATE SET
price = EXCLUDED.price,
stock = EXCLUDED.stock
WHERE EXCLUDED.price < products.price;
-- Atualiza apenas se o novo preço for mais barato que o preço atual
Output:
INSERT 0 1
8. DML Dentro de uma Transação
(1) Operações Básicas de Transação
| Comando | Descrição |
|---|---|
BEGIN ou START TRANSACTION |
Inicia uma transação |
COMMIT |
Confirma a transação (persiste permanentemente) |
ROLLBACK |
Reverte a transação (desfaz todas as alterações) |
SAVEPOINT name |
Cria um ponto de salvamento |
ROLLBACK TO SAVEPOINT name |
Reverte para um ponto de salvamento |
▶ Exemplo: Transação Protegendo uma Operação em Lote
-- Inicia uma transação para importação em lote
BEGIN;
-- Insere pedido e itens como uma unidade
INSERT INTO orders (user_id, total_amount, status)
VALUES (1, 0, 'pending')
RETURNING id;
-- Assume que o comando acima retornou id = 10
INSERT INTO order_items (order_id, product_id, quantity, unit_price)
VALUES
(10, 1, 2, 89.99),
(10, 2, 5, 12.99);
-- Atualiza o total do pedido
UPDATE orders
SET total_amount = (2 * 89.99 + 5 * 12.99)
WHERE id = 10;
-- Verifica antes de confirmar
SELECT * FROM orders WHERE id = 10;
SELECT * FROM order_items WHERE order_id = 10;
-- Tudo certo? Confirma
COMMIT;
-- Algo errado? Reverte tudo
-- ROLLBACK;
Output:
result
----------
42.50
(1 row)
9. Exemplo Completo: Importação de Produtos em Lote
-- ============================================
-- Exemplo completo: importação de produtos em lote com UPSERT
-- Charlie importa 5000 produtos, alguns já existem
-- ============================================
-- Passo 1: Cria uma tabela temporária de importação
CREATE TEMP TABLE import_products (
name VARCHAR(200) NOT NULL,
price DECIMAL(10, 2) NOT NULL,
stock INTEGER DEFAULT 0,
category VARCHAR(50)
);
-- Passo 2: Carrega os dados de importação (simulado com linhas de exemplo)
INSERT INTO import_products (name, price, stock, category) VALUES
('Running Shoes', 99.99, 200, 'Footwear'), -- já existe
('Laptop Backpack', 59.99, 300, 'Accessories'), -- já existe, preço alterado
('Smart Water Bottle', 34.99, 500, 'Gadgets'), -- novo produto
('Wireless Earbuds', 79.99, 400, 'Audio'), -- novo produto
('Yoga Mat', 24.99, 600, 'Fitness'); -- novo produto
-- Passo 3: Upsert de todos os dados de importação em uma instrução
BEGIN;
INSERT INTO products (name, price, stock, category)
SELECT name, price, stock, category
FROM import_products
ON CONFLICT (name)
DO UPDATE SET
price = EXCLUDED.price,
stock = products.stock + EXCLUDED.stock
RETURNING id, name, price, stock;
-- Passo 4: Verifica os resultados
SELECT id, name, price, stock
FROM products
WHERE name IN ('Running Shoes', 'Laptop Backpack',
'Smart Water Bottle', 'Wireless Earbuds', 'Yoga Mat')
ORDER BY name;
-- Passo 5: Limpeza
DROP TABLE import_products;
COMMIT;
❓ Perguntas Frequentes
P: Qual a diferença entre INSERT ON CONFLICT e REPLACE INTO (MySQL)? R: O REPLACE INTO do MySQL é na verdade DELETE + INSERT — ele exclui a linha antiga e insere uma nova, o que altera o ID de auto-incremento, dispara gatilhos e quebra chaves estrangeiras. O ON CONFLICT DO UPDATE do PG é uma verdadeira atualização local: o ID permanece o mesmo e nenhum gatilho DELETE é disparado.
P: O que é EXCLUDED? R: EXCLUDED é uma tabela virtual que o PG fornece no UPSERT, contendo os valores que originalmente seriam inseridos mas falharam devido a um conflito. Use-a para referenciar os novos valores, distinguindo-os dos valores antigos existentes na tabela. Por exemplo,
EXCLUDED.priceé o novo preço eproducts.priceé o preço antigo.
P: Quantas linhas são ideais para uma inserção em lote? R: Para um único INSERT com múltiplos valores, recomenda-se de 100 a 1000 linhas. Acima de 1000 linhas você pode enfrentar problemas de desempenho de parsing SQL. Para lotes maiores, use o comando COPY (5 a 10 vezes mais rápido que INSERT, abordado posteriormente na lição de backup).
P: O que acontece se eu esquecer o WHERE em um UPDATE? R: Ele atualiza a tabela inteira! Este é um dos erros SQL mais perigosos. Medidas de proteção: 1) Sempre escreva WHERE antes de SET; 2) Opere dentro de uma transação — faça SELECT para confirmar o escopo primeiro, depois UPDATE; 3) Configure
sql_safe_updates = on(execute no psql).
P: O RETURNING pode ser usado dentro de um CTE? R: Sim! Este é um padrão PG muito poderoso — DELETE ... RETURNING combinado com INSERT ... SELECT implementa migração de dados:
WITH moved AS (DELETE FROM active WHERE ... RETURNING *) INSERT INTO archive SELECT * FROM moved.
P: O TRUNCATE pode ser revertido? R: Sim! Dentro de uma transação, TRUNCATE seguido de ROLLBACK restaura os dados. Isso difere do MySQL (o TRUNCATE do MySQL é DDL e não pode ser revertido). O TRUNCATE do PG é seguro dentro de uma transação.
📖 Resumo
- INSERT suporta linha única, múltiplas linhas e inserção a partir de consulta; inserções de múltiplas linhas são muito mais eficientes que inserções linha por linha
- A cláusula RETURNING (recurso PG) permite que INSERT/UPDATE/DELETE retornem as linhas afetadas diretamente, sem segunda consulta
- UPDATE suporta atualizações baseadas em JOIN (cláusula FROM), mais claro que uma subconsulta
- DELETE remove linhas uma a uma (lento); TRUNCATE esvazia de uma vez (rápido); TRUNCATE pode ser revertido dentro de uma transação
- UPSERT (INSERT ON CONFLICT) é um recurso central do PG: uma instrução SQL lida com "inserir se não existir, atualizar se existir"
- A tabela virtual EXCLUDED referencia os valores recém-inseridos, distintos dos valores existentes na tabela
- Transações (BEGIN/COMMIT/ROLLBACK) protegem operações em lote; em caso de erro, você pode reverter
📝 Exercícios
-
Básico (★): Crie uma tabela
tags(id SERIAL chave primária, name VARCHAR(50) UNIQUE), insira 5 tags e use RETURNING para retornar o id e o name de cada linha inserida. -
Intermediário (★★): Realize um UPSERT na tabela
productsdesta lição — insira 3 linhas de produtos, uma das quais tem um nome que já existe. Para o produto existente, atualize o preço; para novos produtos, insira normalmente. Use RETURNING para mostrar o resultado da operação. -
Desafio (★★★): Em uma única transação, complete o seguinte: 1) Crie uma tabela
products_backup(mesma estrutura deproducts); 2) Migre produtos com preço < 30 deproductsparaproducts_backupusando DELETE ... RETURNING + INSERT; 3) Após verificar o resultado da migração, COMMIT. Escreva o script SQL completo.