Partilhar: WhatsApp
aulify · Sebenta
UC · Unidade de Competência · UC00664

Sebenta · NoSQL (UC00664)

MongoDB, Redis, Elasticsearch e modelagem moderna
25h · 2.25 pontos crédito Curso: T. Desenv. Software, T. Sist. Comp. Redes, T. Multimédia ↗ Referencial oficial SNQ
Índice

Introdução

Bases de dados SQL (PostgreSQL, MySQL) dominaram 40 anos. Servem tudo bem na maioria dos casos. Mas a partir do início dos anos 2000, surgiram necessidades novas: web em escala (Google, Facebook), real-time (chats), big data (logs, IoT), latência mínima (cache). SQL tradicional teve dificuldades.

NoSQL ("Not Only SQL") emergiu como família de soluções para esses casos. Não é "melhor" que SQL — é diferente, com trade-offs próprios. Saber quando usar SQL, quando usar NoSQL, e como modelar bem em cada um, é skill essencial em backend moderno.

Esta sebenta cobre os principais sistemas NoSQL: MongoDB (document store, mais usado), Redis (key-value, cache), Elasticsearch (search). Foca em uso prático: setup, CRUD, modelagem, integração com Python.

1. SQL vs NoSQL — quando usar cada um

SQL (relacional) — pontos fortes

NoSQL — pontos fortes

Stack moderno típico

Empresas raramente usam só um. Stack comum:

Polyglot persistence: ferramenta certa para cada job.

ACID vs BASE

ACID (SQL): - Atomicity: tudo ou nada. - Consistency: BD sempre válida. - Isolation: transactions concorrentes não interferem. - Durability: gravado = ficou gravado.

BASE (NoSQL): - Basically Available: sistema disponível mesmo com falhas. - Soft state: estado pode mudar mesmo sem inputs (replicação). - Eventually consistent: vai ficar consistente eventualmente.

CAP theorem: num sistema distribuído, escolhes 2 de 3: Consistency, Availability, Partition tolerance. Maior parte dos NoSQL sacrifica C para ter A+P.

2. Tipos de NoSQL

Document stores

Cada record = documento JSON-like (flexível, aninhado).

Casos: CMS, user-generated content, catálogos, prototipagem.

Key-value

Mapeamento chave → valor (simples ou estruturado).

Casos: cache, sessions, rate limiting, leaderboards.

Wide-column

Tabelas com famílias de colunas, esparsas. Escala para PB.

Casos: time-series, IoT, fraud detection, logging.

Graph

Nodes + relationships first-class.

Casos: redes sociais, recomendações, knowledge graphs, fraud detection.

Optimizado para full-text + analytics.

Casos: search em sites, logging, observability.

Time-series

Especializadas para data temporal.

Casos: métricas, IoT, monitoring.

3. MongoDB — em profundidade

Setup

Docker (mais rápido):

docker run -d -p 27017:27017 --name mongo -v mongo-data:/data/db mongo:7

MongoDB Atlas (cloud, free tier): 1. atlas.mongodb.com → sign up. 2. New cluster → free tier (M0). 3. Add database user. 4. Network access → allow your IP. 5. Connection string: mongodb+srv://....

Clientes: - MongoDB Compass (GUI desktop). - mongosh (CLI). - Drivers: pymongo (Python), mongoose (Node), reactive (Java).

Hierarquia

mongod (server)
└── Database (ex: "loja")
    └── Collection (ex: "products")
        └── Document (BSON)

CRUD básico (mongosh)

// Conectar
use loja

// CREATE
db.products.insertOne({
  name: "Caneta",
  price: 1.50,
  stock: 100,
  category: "Escolar"
});

db.products.insertMany([
  {name: "Caderno", price: 5.00, stock: 50, category: "Escolar"},
  {name: "Mochila", price: 35.00, stock: 20, category: "Acessórios"},
  {name: "Estojo", price: 8.00, stock: 30, category: "Acessórios"},
]);

// READ
db.products.find();
db.products.findOne({name: "Caneta"});

// UPDATE
db.products.updateOne(
  {name: "Caneta"},
  {$set: {price: 1.80}}
);

db.products.updateMany(
  {category: "Acessórios"},
  {$inc: {price: 1.00}}    // adiciona 1€
);

// DELETE
db.products.deleteOne({name: "Estojo"});
db.products.deleteMany({stock: 0});

Query operators

// Comparação
db.products.find({price: {$gt: 10}});           // >
db.products.find({price: {$gte: 10}});          // >=
db.products.find({price: {$lt: 100}});          // <
db.products.find({price: {$ne: 5}});            // !=
db.products.find({category: {$in: ["A","B"]}});  // IN
db.products.find({stock: {$nin: [0, 1]}});      // NOT IN

// Existência
db.products.find({description: {$exists: true}});
db.products.find({deleted: {$exists: false}});

// Type
db.products.find({stock: {$type: "int"}});

// Regex
db.products.find({name: /caneta/i});           // case insensitive

// Logical
db.products.find({
  $and: [{price: {$gt: 5}}, {stock: {$gt: 0}}]
});
db.products.find({
  $or: [{price: {$lt: 5}}, {category: "Escolar"}]
});
db.products.find({$not: {price: {$gt: 100}}});

// Array
db.products.find({tags: "novo"});               // contém
db.products.find({tags: {$all: ["a", "b"]}});   // contém ambos
db.products.find({tags: {$size: 3}});           // length = 3

Update operators

// $set — define
db.products.updateOne({_id: ID}, {$set: {price: 10}});

// $unset — remove field
db.products.updateOne({_id: ID}, {$unset: {oldField: ""}});

// $inc — incrementa
db.products.updateOne({_id: ID}, {$inc: {stock: -1, views: 1}});

// $mul — multiplica
db.products.updateMany({}, {$mul: {price: 1.10}});   // sobe 10%

// $push — adiciona a array
db.products.updateOne({_id: ID}, {$push: {tags: "promocao"}});

// $pull — remove de array
db.products.updateOne({_id: ID}, {$pull: {tags: "antigo"}});

// $addToSet — push sem duplicar
db.products.updateOne({_id: ID}, {$addToSet: {tags: "novo"}});

// $pop — remove primeiro/último
db.products.updateOne({_id: ID}, {$pop: {tags: 1}});  // último

Cursor methods

db.products
  .find({category: "Escolar"})
  .sort({price: -1})           // desc
  .skip(10)
  .limit(5);

// Projection (campos específicos)
db.products.find(
  {category: "Escolar"},
  {name: 1, price: 1, _id: 0}   // só name e price
);

// Count
db.products.countDocuments({stock: {$gt: 0}});

// Distinct values
db.products.distinct("category");

Aggregation pipeline

Sistema poderoso de transformações encadeadas.

// Total vendas por mês
db.orders.aggregate([
  // Stage 1: filtrar
  {$match: {status: "completed"}},

  // Stage 2: agrupar
  {$group: {
    _id: {
      year: {$year: "$date"},
      month: {$month: "$date"}
    },
    total: {$sum: "$amount"},
    count: {$sum: 1},
    avg: {$avg: "$amount"}
  }},

  // Stage 3: ordenar
  {$sort: {"_id.year": -1, "_id.month": -1}},

  // Stage 4: limitar
  {$limit: 12}
]);

Stages comuns: - $match — filtro (como WHERE). - $group — agregação (como GROUP BY). - $sort — ordenação. - $project — projecção (escolher fields). - $lookup — join com outra collection. - $unwind — explode array em multiple docs. - $limit, $skip. - $addFields, $set — adicionar fields computed.

Aggregation é onde MongoDB compete seriamente com SQL.

Indexes

// Single field
db.products.createIndex({name: 1});      // ascending
db.products.createIndex({price: -1});    // descending

// Composto
db.products.createIndex({category: 1, price: -1});

// Unique
db.users.createIndex({email: 1}, {unique: true});

// Text (full-text search básico)
db.products.createIndex({name: "text", description: "text"});
db.products.find({$text: {$search: "caneta azul"}});

// TTL — auto-delete após X segundos
db.sessions.createIndex({created: 1}, {expireAfterSeconds: 3600});

// Verificar uso
db.products.find({name: "X"}).explain("executionStats");

Regra: indexes em campos usados frequentemente em queries / sorts. Cada index tem custo (write slower, espaço).

Python — pymongo

pip install pymongo
from pymongo import MongoClient, ASCENDING, DESCENDING

client = MongoClient("mongodb://localhost:27017/")
# Ou: MongoClient("mongodb+srv://user:pass@cluster.mongodb.net/")

db = client.loja
products = db.products

# Insert
products.insert_one({
    "name": "Caneta",
    "price": 1.50,
    "stock": 100
})

# Multiple
products.insert_many([
    {"name": "Caderno", "price": 5.00},
    {"name": "Lápis", "price": 0.50},
])

# Find
for p in products.find({"price": {"$gt": 1}}):
    print(p["name"], p["price"])

# Update
products.update_one(
    {"name": "Caneta"},
    {"$set": {"price": 2.00}}
)

# Aggregate
results = products.aggregate([
    {"$group": {"_id": None, "total": {"$sum": "$price"}}}
])
for r in results:
    print(r)

# Index
products.create_index([("name", ASCENDING)])

Validação de schema (opcional)

db.createCollection("products", {
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["name", "price"],
      properties: {
        name: {bsonType: "string"},
        price: {bsonType: "number", minimum: 0},
        stock: {bsonType: "int", minimum: 0}
      }
    }
  }
});

Embora MongoDB seja schemaless, podes impor schema quando útil.

Transactions (multi-document)

MongoDB 4.0+ suporta ACID em transactions multi-doc:

with client.start_session() as session:
    with session.start_transaction():
        accounts.update_one(
            {"name": "Ana"},
            {"$inc": {"balance": -100}},
            session=session
        )
        accounts.update_one(
            {"name": "Bruno"},
            {"$inc": {"balance": 100}},
            session=session
        )
        # commit automático se chega aqui

Funciona só em replica sets (não standalone).

4. Redis — em profundidade

Para que serve

Redis é uma structured in-memory data store. Usado para:

In-memory = microssegundos de latência. 100k+ ops/seg single thread.

Setup

docker run -d -p 6379:6379 --name redis redis:7

# CLI
docker exec -it redis redis-cli
# ou se tens redis local: redis-cli

Tipos de dados

String (valores até 512MB):

SET nome "Ana"
GET nome
SETEX session 3600 "{user:...}"   # com TTL 1h
INCR contador                     # +1
INCRBY contador 5
EXPIRE nome 60                    # TTL em segundos
TTL nome                          # quanto resta
DEL nome

Hash (objeto):

HSET user:1 name "Ana" age 25 email "ana@x.pt"
HGET user:1 name
HGETALL user:1
HINCRBY user:1 age 1
HDEL user:1 email

List (deque):

LPUSH queue "task1"             # adiciona ao início
RPUSH queue "task2"             # ao fim
LPOP queue                      # retira do início
RPOP queue                      # do fim
LRANGE queue 0 -1               # all
LLEN queue                      # length

Set (únicos, sem ordem):

SADD tags "python" "web" "ai"
SMEMBERS tags
SISMEMBER tags "python"         # 0 ou 1
SCARD tags                      # count
SREM tags "ai"
SUNION tags1 tags2              # union
SINTER tags1 tags2              # intersect

Sorted set (score → member, ordenado):

ZADD leaderboard 100 "Ana" 200 "Bruno" 150 "Carla"
ZRANGE leaderboard 0 -1 WITHSCORES   # asc
ZREVRANGE leaderboard 0 -1 WITHSCORES # desc
ZRANK leaderboard "Ana"               # posição (0-based)
ZINCRBY leaderboard 50 "Ana"          # incrementa score

Python — redis-py

pip install redis
import redis
import json

r = redis.Redis(host="localhost", port=6379, decode_responses=True)

# Strings
r.set("name", "Ana")
r.get("name")  # "Ana"

r.setex("session", 3600, "data")  # TTL 1h
r.incr("counter")

# Hash
r.hset("user:1", mapping={"name": "Ana", "age": "25"})
r.hgetall("user:1")  # {"name": "Ana", "age": "25"}

# List (queue)
r.lpush("queue", "task1", "task2")
task = r.rpop("queue")

# Sorted set (leaderboard)
r.zadd("leaderboard", {"Ana": 100, "Bruno": 200})
top10 = r.zrevrange("leaderboard", 0, 9, withscores=True)

Cache pattern

def get_user(user_id):
    key = f"user:{user_id}"

    # 1. Try cache
    cached = r.get(key)
    if cached:
        return json.loads(cached)

    # 2. Cache miss → fetch
    user = fetch_from_db(user_id)

    # 3. Store in cache (1h TTL)
    r.setex(key, 3600, json.dumps(user))

    return user

def update_user(user_id, data):
    update_in_db(user_id, data)
    # Invalidate cache
    r.delete(f"user:{user_id}")

Rate limiting

def is_rate_limited(user_id, limit=100, window=60):
    key = f"rate:{user_id}"
    count = r.incr(key)
    if count == 1:
        r.expire(key, window)
    return count > limit

Pub/Sub

# Publisher
r.publish("channel-1", "Hello!")

# Subscriber (em outro processo)
ps = r.pubsub()
ps.subscribe("channel-1")
for msg in ps.listen():
    if msg["type"] == "message":
        print(msg["data"])

Setup

docker run -d -p 9200:9200 \
  -e "discovery.type=single-node" \
  -e "xpack.security.enabled=false" \
  -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
  --name es elasticsearch:8.12.0

# Verificar
curl http://localhost:9200

Conceitos

CRUD

# Index document (auto-cria index)
curl -X POST "localhost:9200/products/_doc" \
  -H 'Content-Type: application/json' \
  -d '{"name": "Caneta azul Bic", "price": 1.50, "category": "Escolar"}'

# Get by ID
curl "localhost:9200/products/_doc/ID"

# Search
curl -X GET "localhost:9200/products/_search" \
  -H 'Content-Type: application/json' \
  -d '{"query": {"match_all": {}}}'

# Full-text search
curl -X GET "localhost:9200/products/_search" \
  -H 'Content-Type: application/json' \
  -d '{"query": {"match": {"name": "caneta azul"}}}'

Queries

// Match (full-text com analysis)
{"query": {"match": {"name": "caneta azul"}}}

// Term (exact match, sem analysis)
{"query": {"term": {"category.keyword": "Escolar"}}}

// Range
{"query": {"range": {"price": {"gte": 5, "lte": 50}}}}

// Bool (combinar)
{
  "query": {
    "bool": {
      "must": [{"match": {"name": "caneta"}}],
      "filter": [{"term": {"category.keyword": "Escolar"}}],
      "must_not": [{"range": {"price": {"gt": 100}}}]
    }
  }
}

// Multi-match
{"query": {"multi_match": {
  "query": "caneta azul",
  "fields": ["name", "description"]
}}}

Aggregations

Equivalentes a GROUP BY:

{
  "size": 0,
  "aggs": {
    "by_category": {
      "terms": {"field": "category.keyword"},
      "aggs": {
        "avg_price": {"avg": {"field": "price"}}
      }
    }
  }
}

Python client

pip install elasticsearch
from elasticsearch import Elasticsearch

es = Elasticsearch("http://localhost:9200")

# Index
es.index(index="products", document={
    "name": "Caneta",
    "price": 1.50
})

# Search
results = es.search(
    index="products",
    query={"match": {"name": "caneta"}}
)
for hit in results["hits"]["hits"]:
    print(hit["_source"], hit["_score"])

ELK stack

Elasticsearch + Logstash + Kibana = stack popular para: - Centralizar logs de apps. - Análise + dashboards (Kibana). - Alertas.

Alternativas: Splunk (pago), Grafana Loki, OpenSearch (fork AWS).

6. Modelagem em document DBs

Princípio: design para queries

Em SQL normalizas (forma normal). Em document DBs, modelas para as queries que vais fazer.

Embed vs Reference

Embed (dados aninhados):

{
  "_id": "post123",
  "title": "Hello",
  "author": {"name": "Ana", "avatar": "..."},
  "comments": [
    {"text": "Nice!", "by": "Bruno"},
    {"text": "Cool", "by": "Carla"}
  ]
}

✅ 1 query, atómico. ❌ Doc cresce (limite 16MB). Duplicação se autor mudar.

Reference (links por ID):

// posts
{"_id": "post123", "title": "Hello", "authorId": "user1"}

// users
{"_id": "user1", "name": "Ana"}

// comments
{"_id": "c1", "postId": "post123", "text": "Nice"}

✅ Sem duplicação, docs pequenos. ❌ 2+ queries (ou $lookup).

Regras de polegar

Exemplo: blog

Posts (embed comments até 100)
  - _id
  - title, body, author (embed simples user)
  - comments: [...]   se < 100

Users (separado)
  - _id, name, email, avatar

Posts (reference comments se grande)
  - _id
  - commentIds: [...]

Comments
  - _id, postId, text, author

Anti-patterns

7. Performance e escala

Indexes

Críticos para performance. Cada query frequente precisa de index apropriado.

// Single
db.products.createIndex({sku: 1});

// Compound (order matters!)
db.orders.createIndex({userId: 1, date: -1});

// Análise
db.products.find({sku: "X"}).explain("executionStats");
// procurar "stage": "IXSCAN" (bom) vs "COLLSCAN" (mau)

Trade-offs: - Indexes aceleram reads. - Mas slow downs writes (precisa actualizar index). - Custam espaço.

Replica sets

3+ nodes: - Primary: aceita writes. - Secondaries: replicam primary. - Failover automático se primary cair.

High availability + read scalability.

Sharding

Para datasets enormes (TB+): - Shard key divide data entre nodes. - Queries com shard key → 1 node. - Sem shard key → scatter-gather (lento).

Setup complexo — normalmente cloud-managed (Atlas, ElastiCache).

Connection pooling

Drivers (pymongo, redis-py) fazem auto. Não criar nova connection por request:

# ✅
client = MongoClient(...)  # módulo-level, reusa pool

def get_user(id):
    return client.db.users.find_one({"_id": id})

8. Backup e segurança

Backup MongoDB

# Dump
mongodump --uri="mongodb://localhost:27017" --out=/backup/$(date +%F)

# Restore
mongorestore --uri="mongodb://localhost:27017" /backup/2026-05-26

Atlas: backups automáticos integrados, point-in-time recovery.

Backup Redis

# RDB snapshot (default)
SAVE     # síncrono
BGSAVE   # background

# AOF (append-only log) — mais resiliente
# config em redis.conf: appendonly yes

Segurança

MongoDB: - Auth obrigatório em produção (--auth). - Users com roles (read, readWrite, dbAdmin). - TLS para connections. - Network: nunca expor 27017 públicamente.

use admin
db.createUser({
  user: "admin",
  pwd: "strong-pass",
  roles: [{role: "root", db: "admin"}]
});

Redis: - AUTH password. - TLS (Redis 6+). - Bind localhost ou VPC.

# redis.conf
requirepass strong-pass
bind 127.0.0.1

GDPR

9. Quando NÃO usar NoSQL

Para a maioria dos apps web médios: PostgreSQL chega. NoSQL é addon especializado, não default.

10. Boas práticas

MongoDB

Redis

Elasticsearch

11. Próximos passos

Glossário

Recursos

NoSQL não é "anti-SQL". É adição ao toolkit. Sabe SQL bem primeiro (UC0XXXX), depois aprende quando NoSQL faz sentido. Em backend moderno, vais usar ambos — e ferramentas como Redis para cache quase sempre. MongoDB para CMS, logs ou data flexível. Elasticsearch quando busca importa. Cada uma tem o seu lugar.