Jhonatan Pinheiro
Loading page...
Jhonatan Pinheiro
Loading page...
Jhonatan Pinheiro
Loading page...
SQL or NoSQL? The five NoSQL families, ACID vs BASE, the CAP theorem, the same system modeled both ways, and when each one is the right call.
"Should I use SQL or NoSQL?" is the wrong question. The right one is: what shape does my data have, and what guarantees do I need about it? Answer that and the choice of database shows up on its own.
This guide puts the two worlds side by side — same application, different modelling — with the commands for each.
A relational database keeps data in tables — rows and columns with defined types — and the relationships between them are declared and enforced by the database itself.
Three ideas hold everything up:
CREATE TABLE clientes (
id BIGSERIAL PRIMARY KEY,
nome VARCHAR(120) NOT NULL,
email VARCHAR(200) UNIQUE NOT NULL
);
CREATE TABLE pedidos (
id BIGSERIAL PRIMARY KEY,
cliente_id BIGINT NOT NULL REFERENCES clientes (id),
total DECIMAL(12,2) NOT NULL CHECK (total >= 0),
criado_em TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);Notice what that snippet already guarantees, without a single line of application code: the email cannot repeat, the total is never negative, and no order points at a customer who does not exist. That is the value proposition of the relational model: the rule lives in the data, not in whoever writes to it.
The main names: PostgreSQL, MySQL, SQL Server, Oracle, Firebird, SQLite, MariaDB, DB2.
"NoSQL" never meant "no SQL" — these days it reads as Not Only SQL. It is an umbrella for databases that gave up part of the relational model to gain something else: horizontal scale, schema flexibility, or performance on one specific access pattern.
What usually changes:
cliente_id exists — your code does.// MongoDB: the order carries what it needs along with it
db.pedidos.insertOne({
_id: ObjectId(),
cliente: { id: 42, nome: "Ana Souza", email: "ana@email.com" },
itens: [
{ produto: "Teclado", qtd: 1, preco: 199.9 },
{ produto: "Mouse", qtd: 2, preco: 89.9 }
],
total: 379.7,
criadoEm: new Date()
});One read brings back the whole order, ready for the screen. The price of that: if Ana changes her email, this document still holds the old one — and you have to decide whether that is a bug or exactly what you wanted (a historical snapshot of the order).
Treating "NoSQL" as a single category is the most common mistake. They are families with very different purposes.
| Family | How it stores | Good for | Examples |
|---|---|---|---|
| Document | Nested JSON/BSON | Catalogues, profiles, content with a variable shape | MongoDB, CouchDB, Firestore |
| Key-value | Key → opaque value | Cache, session, counter, queue | Redis, DynamoDB, Memcached |
| Columnar | Column families, distributed | Massive writes, series, telemetry | Cassandra, HBase, ScyllaDB |
| Graph | Nodes and edges | Deep relationships, recommendation, fraud | Neo4j, ArangoDB, Neptune |
| Vector / time series | Embeddings or points in time | Semantic search, metrics, IoT | Pinecone, Qdrant, InfluxDB, TimescaleDB |
db.produtos.find({ categoria: "games", preco: { $lt: 500 } })
.sort({ preco: -1 })
.limit(10);SET sessao:abc123 '{"userId":42}' EX 3600 # expires in 1 hour
GET sessao:abc123
INCR contador:visitas:2026-08-10-- Cassandra (CQL): the modelling comes from the query, not from the domain
CREATE TABLE eventos_por_usuario (
usuario_id UUID,
quando TIMESTAMP,
tipo TEXT,
PRIMARY KEY (usuario_id, quando)
) WITH CLUSTERING ORDER BY (quando DESC);// Neo4j (Cypher): "friends of friends who like what I like"
MATCH (eu:Pessoa {id: 42})-[:AMIGO*2]-(sugestao:Pessoa)
WHERE NOT (eu)-[:AMIGO]-(sugestao)
RETURN sugestao.nome, COUNT(*) AS forca
ORDER BY forca DESC LIMIT 10;This is the case where NoSQL wins beyond argument: the same question in SQL would need recursive JOINs and would get much slower as the depth grows.
-- pgvector: semantic search inside PostgreSQL itself
SELECT titulo, embedding <-> $1 AS distancia
FROM documentos ORDER BY distancia LIMIT 5;A blog with posts, authors and comments.
Relational — three tables, each fact once:
CREATE TABLE autores (
id BIGSERIAL PRIMARY KEY,
nome VARCHAR(120) NOT NULL,
email VARCHAR(200) UNIQUE NOT NULL
);
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
autor_id BIGINT NOT NULL REFERENCES autores (id),
titulo VARCHAR(200) NOT NULL,
corpo TEXT NOT NULL,
publicado_em TIMESTAMP
);
CREATE TABLE comentarios (
id BIGSERIAL PRIMARY KEY,
post_id BIGINT NOT NULL REFERENCES posts (id) ON DELETE CASCADE,
autor_nome VARCHAR(120) NOT NULL,
texto TEXT NOT NULL,
criado_em TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- The post screen: one JOIN
SELECT p.titulo, p.corpo, a.nome AS autor
FROM posts p JOIN autores a ON a.id = p.autor_id
WHERE p.id = 1;Document — the post carries what the screen needs:
db.posts.insertOne({
_id: 1,
titulo: "Bancos relacionais e não relacionais",
corpo: "...",
autor: { id: 7, nome: "Jhonatan Pinheiro" }, // denormalised
comentarios: [ // nested
{ autorNome: "Ana", texto: "Ótimo post!", criadoEm: new Date() }
],
publicadoEm: new Date()
});
// The post screen: one read, no JOIN
db.posts.findOne({ _id: 1 });The trade-off shows up when something changes: renaming the author is an UPDATE on one row in the relational model, and an updateMany across all of their documents in NoSQL. Reading the page, on the other hand, is a single lookup on the document — and a JOIN in the relational one.
Nesting comments inside the post works well right up until the viral post with 50 thousand comments. A document has a size limit (16 MB on MongoDB) and grows with every write. A separate collection becomes the answer again.
ACID is the contract of relational databases:
BEGIN;
UPDATE contas SET saldo = saldo - 100 WHERE id = 1;
UPDATE contas SET saldo = saldo + 100 WHERE id = 2;
COMMIT; -- or nothing happensBASE is the stance of much of distributed NoSQL: Basically Available, Soft state, Eventually consistent. The system accepts being temporarily inconsistent across replicas in exchange for availability and scale.
The CAP theorem explains why: a distributed system subject to a network partition has to choose between consistency and availability. It is not a philosophical choice — it is what happens when the cable between two data centres goes down.
| Profile | Choice | Behaviour during a partition | Examples |
|---|---|---|---|
| CP | Consistency | Refuses operations so as not to diverge | MongoDB (default), HBase, etcd |
| AP | Availability | Accepts and reconciles later | Cassandra, DynamoDB, Riak |
| CA | Only without a partition | Applies to a single node | PostgreSQL on a single server |
Two common myths are worth correcting: NoSQL is not a synonym for "no transactions" (MongoDB has had multi-document transactions since 4.0) and relational is not a synonym for "does not scale" (there is PostgreSQL running dozens of terabytes in production).
The same question — "the 10 most expensive games products under 500" — in each language:
-- SQL
SELECT nome, preco FROM produtos
WHERE categoria = 'games' AND preco < 500
ORDER BY preco DESC LIMIT 10;// MongoDB
db.produtos.find(
{ categoria: "games", preco: { $lt: 500 } },
{ nome: 1, preco: 1 }
).sort({ preco: -1 }).limit(10);-- Cassandra (CQL): it only filters by what is in the key; the rest needs another table
SELECT nome, preco FROM produtos_por_categoria
WHERE categoria = 'games' LIMIT 10;// Neo4j
MATCH (p:Produto {categoria: 'games'})
WHERE p.preco < 500
RETURN p.nome, p.preco ORDER BY p.preco DESC LIMIT 10;And the aggregation — revenue by month:
-- SQL
SELECT DATE_TRUNC('month', criado_em) AS mes, SUM(total) AS faturamento
FROM pedidos GROUP BY 1 ORDER BY 1;// MongoDB — aggregation pipeline
db.pedidos.aggregate([
{ $group: {
_id: { $dateTrunc: { date: "$criadoEm", unit: "month" } },
faturamento: { $sum: "$total" }
} },
{ $sort: { _id: 1 } }
]);The underlying difference: SQL is declarative and standardised — you learn it once and take it to any relational database. Each NoSQL has its own language, and switching databases usually means rewriting the data layer.
Relational databases scale reads naturally through replicas:
-- PostgreSQL: the read replica takes the reports
SELECT pg_is_in_recovery(); -- true = it is a replicaWrites are the hard part: only one primary accepts writes. The ways out are partitioning, sharding in the application, or extensions such as Citus.
-- Native partitioning: one partition per month
CREATE TABLE eventos (id BIGSERIAL, criado_em DATE NOT NULL, dados JSONB)
PARTITION BY RANGE (criado_em);
CREATE TABLE eventos_2026_08 PARTITION OF eventos
FOR VALUES FROM ('2026-08-01') TO ('2026-09-01');In distributed databases, sharding is a design premise:
// MongoDB: the shard key defines the distribution — and is nearly impossible to change later
sh.shardCollection("loja.pedidos", { clienteId: "hashed" });Choosing the shard key is the most expensive decision in a distributed database. A bad key creates a hot partition: one node with 90% of the traffic and the rest of the cluster idle.
Use relational when (and this is the default for most systems):
Use document when:
Use key-value when:
Use columnar when:
Use graph when:
Use vector when:
A practical summary:
| Need | Natural choice |
|---|---|
| Financial transaction | Relational |
| Cart and session | Key-value |
| Catalogue with variable attributes | Document |
| Sensor metrics | Columnar / time series |
| "Who knows whom" | Graph |
| Reporting with unpredictable filters | Relational |
| Semantic search over text | Vector |
On the NoSQL side:
On the relational side:
Mature systems rarely use a single database. A common e-commerce architecture:
| Component | Database | Why |
|---|---|---|
| Orders, payments, stock | PostgreSQL | Transactions and integrity are non-negotiable |
| Session, cart, rate limit | Redis | Extremely low latency and native expiry |
| Catalogue and search | Elasticsearch / OpenSearch | Text search with ranking and facets |
| Events and clickstream | Cassandra / Kafka + columnar | Massive continuous writes |
| Recommendation | Neo4j or pgvector | Relationships and similarity |
The cost of that design is real: more pieces to operate, monitor, version and keep in sync. Start with one well-modelled relational database and add a specialised piece when a concrete bottleneck appears — never in anticipation.
Relational and non-relational do not compete: they solve different problems.
The final question is never "which is the best database". It is "which guarantees do I need, and what price am I willing to pay for them".