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


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:

(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:

SQL
-- 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


3. INSERT

(1) Sintaxe Básica

SQL
INSERT INTO table_name (column1, column2, ...)
VALUES (value1, value2, ...)
RETURNING * | column_list;

▶ Exemplo: Inserção de Linha Única

SQL
-- 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:

TEXT 📖 Somente leitura
 id |       email          |          created_at
----+----------------------+-------------------------------
  5 | diana@example.com    | 2026-07-13 10:30:00+00

▶ Exemplo: Inserção de Múltiplas Linhas

SQL
-- 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:

TEXT 📖 Somente leitura
INSERT 0 1

▶ Exemplo: Inserir a Partir de uma Consulta

SQL
-- 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:

TEXT 📖 Somente leitura
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

SQL
-- 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:

TEXT 📖 Somente leitura
INSERT 0 1

5. UPDATE

(1) Sintaxe Básica

SQL
UPDATE table_name
SET column1 = value1, column2 = value2, ...
[WHERE condition]
[RETURNING * | column_list];

▶ Exemplo: Atualização Básica

SQL
-- 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:

TEXT 📖 Somente leitura
-- Comando SQL executado com sucesso

▶ Exemplo: UPDATE Baseado em JOIN

SQL
-- 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:

TEXT 📖 Somente leitura
  result  
----------
   42.50
(1 row)
⚠️ Nota: Um UPDATE sem cláusula WHERE atualiza a tabela inteira! Esta é a operação perigosa mais comum. Sempre escreva o WHERE antes do SET, como hábito.


6. DELETE

(1) Sintaxe Básica

SQL
DELETE FROM table_name
[WHERE condition]
[RETURNING * | column_list];

▶ Exemplo: Operações de Exclusão

SQL
-- 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:

TEXT 📖 Somente leitura
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

SQL
-- 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:

TEXT 📖 Somente leitura
-- 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

100%
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

SQL
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)

SQL
-- 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:

TEXT 📖 Somente leitura
INSERT 0 1

▶ Exemplo: DO UPDATE (Atualizar Linha Existente)

SQL
-- 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:

TEXT 📖 Somente leitura
INSERT 0 1

▶ Exemplo: UPSERT Condicional (Atualizar Apenas em Casos Específicos)

SQL
-- 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:

TEXT 📖 Somente leitura
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

SQL
-- 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:

TEXT 📖 Somente leitura
  result  
----------
   42.50
(1 row)

9. Exemplo Completo: Importação de Produtos em Lote

SQL
-- ============================================
-- 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 e products.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


📝 Exercícios

  1. 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.

  2. Intermediário (★★): Realize um UPSERT na tabela products desta 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.

  3. Desafio (★★★): Em uma única transação, complete o seguinte: 1) Crie uma tabela products_backup (mesma estrutura de products); 2) Migre produtos com preço < 30 de products para products_backup usando DELETE ... RETURNING + INSERT; 3) Após verificar o resultado da migração, COMMIT. Escreva o script SQL completo.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%