SQL e NoSQL
Os modelos de dados disponíveis e os critérios para escolher o banco de um sistema.
A mesma operação revela a diferença entre os modelos: criar um pedido com os seus itens. No relacional, são várias linhas em duas tabelas, gravadas numa transação. No modelo de documentos, é um único documento.
No PostgreSQL, com transação
Os exemplos usam o driver mais comum de cada linguagem, sem ORM, e as tabelas definidas no nível Conceito. Todos seguem o mesmo roteiro: abrir a transação, gravar o pedido, gravar os itens e confirmar, ou desfazer tudo se algo falhar.
import { Pool } from "pg";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
type Item = { produtoId: number; quantidade: number };
export async function criarPedido(clienteId: number, itens: Item[]) {
const conexao = await pool.connect();
try {
await conexao.query("BEGIN");
const { rows } = await conexao.query(
`INSERT INTO pedidos (cliente_id, status)
VALUES ($1, 'aberto') RETURNING id`,
[clienteId],
);
const pedidoId = rows[0].id;
for (const item of itens) {
await conexao.query(
`INSERT INTO itens_pedido (pedido_id, produto_id, quantidade)
VALUES ($1, $2, $3)`,
[pedidoId, item.produtoId, item.quantidade],
);
}
await conexao.query("COMMIT");
return pedidoId;
} catch (erro) {
await conexao.query("ROLLBACK");
throw erro;
} finally {
conexao.release();
}
}import psycopg
def criar_pedido(conexao: psycopg.Connection, cliente_id: int, itens: list[dict]) -> int:
# Confirma ao sair do bloco; desfaz se houver exceção
with conexao.transaction():
with conexao.cursor() as cursor:
cursor.execute(
"""INSERT INTO pedidos (cliente_id, status)
VALUES (%s, 'aberto') RETURNING id""",
(cliente_id,),
)
pedido_id = cursor.fetchone()[0]
cursor.executemany(
"""INSERT INTO itens_pedido (pedido_id, produto_id, quantidade)
VALUES (%s, %s, %s)""",
[(pedido_id, i["produto_id"], i["quantidade"]) for i in itens],
)
return pedido_idpackage pedidos
import (
"context"
"database/sql"
)
type Item struct {
ProdutoID int64
Quantidade int
}
func CriarPedido(ctx context.Context, db *sql.DB, clienteID int64, itens []Item) (int64, error) {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return 0, err
}
defer tx.Rollback() // não faz nada se o Commit já aconteceu
var pedidoID int64
err = tx.QueryRowContext(ctx,
`INSERT INTO pedidos (cliente_id, status)
VALUES ($1, 'aberto') RETURNING id`,
clienteID,
).Scan(&pedidoID)
if err != nil {
return 0, err
}
for _, item := range itens {
_, err := tx.ExecContext(ctx,
`INSERT INTO itens_pedido (pedido_id, produto_id, quantidade)
VALUES ($1, $2, $3)`,
pedidoID, item.ProdutoID, item.Quantidade,
)
if err != nil {
return 0, err
}
}
return pedidoID, tx.Commit()
}import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
public class Pedidos {
public record Item(long produtoId, int quantidade) {}
public long criarPedido(Connection conexao, long clienteId, List<Item> itens)
throws SQLException {
conexao.setAutoCommit(false);
try (var pedido = conexao.prepareStatement(
"INSERT INTO pedidos (cliente_id, status) VALUES (?, 'aberto') RETURNING id");
var item = conexao.prepareStatement(
"INSERT INTO itens_pedido (pedido_id, produto_id, quantidade) VALUES (?, ?, ?)")) {
pedido.setLong(1, clienteId);
long pedidoId;
try (var resultado = pedido.executeQuery()) {
resultado.next();
pedidoId = resultado.getLong(1);
}
for (Item i : itens) {
item.setLong(1, pedidoId);
item.setLong(2, i.produtoId());
item.setInt(3, i.quantidade());
item.addBatch();
}
item.executeBatch();
conexao.commit();
return pedidoId;
} catch (SQLException erro) {
conexao.rollback();
throw erro;
}
}
}using Npgsql;
public record Item(long ProdutoId, int Quantidade);
public class Pedidos(NpgsqlDataSource dados)
{
public async Task<long> CriarPedido(long clienteId, IEnumerable<Item> itens)
{
await using var conexao = await dados.OpenConnectionAsync();
// Sem CommitAsync, o descarte da transação desfaz tudo
await using var transacao = await conexao.BeginTransactionAsync();
await using var pedido = new NpgsqlCommand(
"INSERT INTO pedidos (cliente_id, status) VALUES ($1, 'aberto') RETURNING id",
conexao, transacao)
{
Parameters = { new() { Value = clienteId } },
};
var pedidoId = (long)(await pedido.ExecuteScalarAsync())!;
foreach (var item in itens)
{
await using var comando = new NpgsqlCommand(
"INSERT INTO itens_pedido (pedido_id, produto_id, quantidade) VALUES ($1, $2, $3)",
conexao, transacao)
{
Parameters =
{
new() { Value = pedidoId },
new() { Value = item.ProdutoId },
new() { Value = item.Quantidade },
},
};
await comando.ExecuteNonQueryAsync();
}
await transacao.CommitAsync();
return pedidoId;
}
}O que muda de uma linguagem para outra
| Driver | Marcador de parâmetro | Como a transação é desfeita | |
|---|---|---|---|
| Node | pg | $1, $2 | ROLLBACK explícito no catch |
| Python | psycopg 3 | %s | Automático, se o bloco with lançar exceção |
| Go | database/sql com driver do PostgreSQL | $1, $2 | defer tx.Rollback(), sem efeito depois do Commit |
| Java | JDBC | ? | rollback() explícito no catch |
| C# | Npgsql | $1, $2 | Automático, ao descartar a transação sem Commit |
Dois pontos que o código gerado erra com frequência:
- No Node, chamar
pool.queryem vez deconexao.querydentro da transação manda cada comando para uma conexão possivelmente diferente do pool. OBEGINvai para uma conexão, oINSERTpara outra, e a transação não existe de fato. - No Java, a conexão volta ao pool com
autoCommitdesligado se o código não restaurar o valor. Algumas bibliotecas de pool restauram; outras não.
Em todos os exemplos, os valores vão como parâmetros, nunca concatenados na string do SQL. É isso que impede a injeção de SQL.
No MongoDB, um documento
No modelo de documentos, o pedido e os itens formam um único documento, gravado de uma vez:
import { MongoClient } from "mongodb";
const cliente = new MongoClient(process.env.MONGODB_URL!);
const pedidos = cliente.db("loja").collection("pedidos");
export async function criarPedido(
clienteId: number,
nomeCliente: string,
itens: { produtoId: number; quantidade: number }[],
) {
const { insertedId } = await pedidos.insertOne({
cliente: { id: clienteId, nome: nomeCliente }, // cópia intencional
status: "aberto",
itens,
criadoEm: new Date(),
});
return insertedId;
}Não há transação no código, e ela não faz falta: no MongoDB, a gravação de um único documento já é atômica. O pedido nunca fica sem itens.
A transação volta a ser necessária quando a operação toca mais de um documento, como baixar o estoque de cada produto junto com a criação do pedido. O MongoDB aceita transações entre documentos, mas elas custam mais e exigem um conjunto de réplicas. Se o desenho exige transações o tempo todo, o modelo relacional provavelmente serve melhor a esses dados.
As consultas, lado a lado
| Pergunta | PostgreSQL | MongoDB |
|---|---|---|
| Um pedido com os itens | JOIN entre pedidos e itens_pedido | findOne({ _id }); os itens já estão no documento |
| Pedidos de um cliente | WHERE cliente_id = $1, com índice | find({ "cliente.id": id }), com índice |
| Produtos mais vendidos no mês | GROUP BY produto_id com SUM | Pipeline de agregação com $unwind e $group |
| O cliente mudou de nome | Um UPDATE em clientes | Decidir se os pedidos antigos também mudam; se sim, updateMany |
A última linha resume a troca: o documento facilita a leitura e transfere à aplicação a responsabilidade pelos dados repetidos.
Lista de revisão
Na escolha
- As principais consultas do sistema estão listadas, e o banco atende todas bem?
- O sistema precisa de transações entre várias entidades? Com que frequência?
- A equipe sabe operar, monitorar e fazer backup desse banco?
No relacional
- As regras importantes estão no banco: chaves estrangeiras,
NOT NULL,UNIQUE,CHECK? - Operações com várias gravações estão numa transação, na mesma conexão?
- Todo valor vai como parâmetro, nunca concatenado no SQL?
Em documentos
- Existe validação de esquema no banco ou uma camada obrigatória na aplicação?
- Nenhuma lista embutida cresce sem limite?
- Para cada dado copiado entre documentos, está decidido quem atualiza as cópias?
- As consultas frequentes têm índice?
Prompts para aprofundar
Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.
A mesma funcionalidade em SQL e em documentos
Quero implementar a mesma funcionalidade num banco relacional e num banco de documentos, para sentir as diferenças. Pergunte em qual linguagem eu trabalho. A funcionalidade é um carrinho de compras que vira pedido: adicionar itens, finalizar o pedido baixando o estoque e listar os pedidos de um cliente com os produtos. Vamos em etapas, primeiro com PostgreSQL e depois com MongoDB. Em cada etapa, deixe que eu tente e depois mostre a sua versão: 1. A modelagem: tabelas com restrições, e documentos com validação. 2. Finalizar o pedido com baixa de estoque, discutindo onde é preciso transação em cada banco. 3. A listagem de pedidos com os produtos. 4. Um relatório dos produtos mais vendidos no mês. No final, monte comigo uma tabela comparando as duas versões: linhas de código, onde ficaram as regras de integridade e o que foi mais trabalhoso em cada uma.
Usar JSONB no PostgreSQL
Quero aprender a usar colunas JSONB no PostgreSQL para dados com formato variável, sem abrir mão do modelo relacional. Pergunte em qual linguagem e driver eu trabalho. Explique, com exemplos de SQL e de código: 1. A diferença entre JSON e JSONB. 2. Como gravar, ler e alterar partes de um documento JSONB. 3. Os operadores de consulta mais usados e como criar índices GIN e índices sobre expressões. 4. Como validar o formato do JSONB com restrições CHECK ou na aplicação. 5. Quando um campo deve sair do JSONB e virar uma coluna própria. Use o exemplo de um catálogo em que cada categoria de produto tem atributos diferentes.
Revisar o acesso ao banco
Vou colar abaixo código que acessa um banco de dados, relacional ou de documentos, e talvez o esquema ou o desenho dos documentos. Revise com foco no uso correto do modelo de dados. Verifique, e para cada item diga se está correto, incorreto ou ausente, citando a linha: 1. Os valores vão como parâmetros, nunca concatenados na consulta? 2. Operações com várias gravações estão numa transação, na mesma conexão ou sessão? 3. As regras de integridade importantes estão no banco, ou ao menos numa camada obrigatória? 4. Há listas embutidas que podem crescer sem limite? 5. Para dados copiados, está claro quem atualiza as cópias? 6. As consultas frequentes têm índice? 7. O modelo escolhido combina com as consultas que o código faz? Para cada problema, explique a consequência em produção e mostre a correção. Não reescreva o que já está correto. Código e esquema: [cole aqui]