Trilha de Kotlin · parte 53 de 59
Índices e transações no MongoDB
Aula da fase 7 da trilha de Kotlin: Como usar `explain` para evitar varredura completa da coleção, e o que o replica set muda nas transações?
intermediárioPublicada em · 14 min de leitura
Pense numa biblioteca com um milhão de livros e um bibliotecário que, toda vez que alguém pede um assunto, percorre as estantes uma a uma. Funciona, mas a fila cresce com o acervo. Um índice é o catálogo de fichas que o bibliotecário consulta antes de ir às estantes: ele diz em que prateleira está cada livro. No MongoDB, o índice cumpre esse papel, e a ferramenta para ver se ele está sendo usado se chama explain. Sem índice, a consulta examina a coleção inteira, e o tempo cresce com o tamanho dela.
A segunda metade da aula trata de transações. Um documento é atômico por si só, mas uma operação que envolve vários documentos ou várias coleções precisa de uma transação, e o MongoDB só oferece transações em topologias com replica set ou cluster particionado. Um standalone, que é o servidor de um único nó usado para desenvolvimento, não as aceita. A pergunta central da aula é como usar o explain para evitar varredura completa, e o que o replica set muda nas transações.
o mapa da aula
A primeira parte mostra o plano de execução de uma consulta, com e sem índice, e explica como ler os estágios do explain. A segunda trata do índice composto e da regra ESR, que diz a ordem dos campos. A terceira parte é sobre transações: por que exigem replica set, como o Spring Data MongoDB as conduz, e o que acontece com o rollback. Por fim, as boas práticas e o que ficou sem verificação.
Versões desta aula: MongoDB 8.3.13 (imagem mongo:8), em dois containers, um standalone na porta 27020 e um replica set de um nó na porta 27019. O driver Java é o 5.8.1 e o Spring Data MongoDB é o 5.1.1, com o Kotlin 2.3.21 do BOM do Spring Boot 4.1.1. Os experimentos de índice rodaram no mongosh dentro do container; os de transação rodaram com o driver e o MongoTransactionManager, sem subir o Spring Boot.
1. o plano de execução e o que o explain mostra
Quando uma consulta é enviada ao MongoDB, o otimizador escolhe um plano: um caminho pela coleção ou por um índice. O explain mostra esse plano e, com o modo executionStats, executa a consulta e conta o trabalho feito. A documentação diz que o modo padrão é queryPlanner, e que executionStats “executes the winning plan to completion, and returns statistics describing the execution of the winning plan.”
O experimento criou uma coleção pedidos com 200 mil documentos. Cada documento tem um cliente (mil valores distintos), um status (quatro valores), um criadoEm e um total. A consulta buscada foi a de um cliente com status pago, ordenada pela data mais recente. Sem índice, o resultado real foi:
--- sem indice, filtro cliente+status, ordena por criadoEm
estagios: SORT > COLLSCAN
nReturned=50 totalKeysExamined=0 totalDocsExamined=200000
Os estágios se leem de cima para baixo como camadas. O COLLSCAN é a varredura da coleção, que examinou os 200 mil documentos para devolver 50. Em cima dele, o SORT ordenou os 50 resultados em memória. O número que importa é o totalDocsExamined: 200 mil documentos lidos para 50 úteis. A razão entre os dois cresce com o tamanho da coleção, e é isso que faz a consulta piorar com o tempo.
A aula não mediu tempo de execução, e não os registra aqui. O executionTimeMillis depende da máquina e da carga do momento. O que a aula usa são os contadores, que são estáveis para a mesma coleção e o mesmo índice.
2. índice composto e a regra ESR
Um índice composto cobre mais de um campo, e a ordem dos campos decide para quais consultas ele serve. A documentação é direta: “The order of fields in a compound index is very important.” e, para escolher essa ordem, ela recomenda a regra ESR: “To create efficient compound indexes, follow the ESR (Equality, Sort, Range) guideline.” Primeiro vêm os campos de igualdade, depois os de ordenação, e por último os de faixa.
O índice criado no experimento segue essa regra para a consulta de cliente e status, ordenada por data:
banco.pedidos.createIndex({ cliente: 1, status: 1, criadoEm: -1 }, { name: "cliente_status_criadoEm" });
Os campos cliente e status são de igualdade, e criadoEm é o de ordenação. Com esse índice, o plano mudou:
--- com indice ESR (igualdade cliente, igualdade status, ordenacao criadoEm)
estagios: FETCH > IXSCAN
nReturned=50 totalKeysExamined=50 totalDocsExamined=50
O IXSCAN percorreu 50 chaves do índice, o FETCH leu 50 documentos e não apareceu nenhum SORT. A ordenação já vinha do índice, porque ele está guardado na ordem de criadoEm dentro de cada par de cliente e status. Os 200 mil documentos examinados viraram 50. A figura compara os três cenários lado a lado.
A figura tem uma terceira faixa que mostra o que acontece quando o campo do meio é omitido da consulta. Com o mesmo índice, uma busca só por cliente e ordenada por data cai no plano FETCH > SORT > IXSCAN, e o resultado real foi:
--- com indice, mas sem o campo do meio (status)
estagios: FETCH > SORT > IXSCAN
nReturned=200 totalKeysExamined=200 totalDocsExamined=200
Essa é a regra do prefixo na prática. O índice serve para consultas sobre os campos da frente, na ordem em que estão. Pular o status quebra a sequência, e a ordenação por criadoEm precisa acontecer em memória. A documentação confirma a parte de prefixo: “Compound indexes support queries on all fields included in the index prefix.” O experimento mostra o efeito de pular um campo do meio, e não o limite de memória da ordenação, que a aula não testou.
Um último caso mostra por que o índice certo depende da consulta. Um índice simples só em status serve a uma consulta por status, mas o resultado real, com um quarto da coleção selecionado, foi:
--- com indice de status, filtro so por status (faixa de 1/4 da colecao)
estagios: FETCH > IXSCAN
nReturned=50000 totalKeysExamined=50000 totalDocsExamined=50000
O índice foi usado, e o trabalho ficou proporcional ao resultado. Isso não significa que o índice é sempre a melhor escolha: quando a consulta devolve quase toda a coleção, uma varredura direta pode ser mais barata. A aula não mediu esse ponto de corte.
3. transações e o replica set
Um documento é atômico por si só. A documentação do MongoDB diz: “In MongoDB, an operation on a single document is atomic.” Quando uma operação precisa mudar vários documentos, ou várias coleções, de forma que ou tudo aconteça ou nada aconteça, entra a transação. A documentação afirma que o MongoDB suporta transações “on replica sets and sharded clusters”, e a tabela de compatibilidade traz o featureCompatibilityVersion mínimo de 4.0 para replica set (e de 4.2 para sharded cluster).
O experimento confirmou essa exigência de forma direta. Um standalone recusa a transação com esta mensagem, que apareceu tanto no mongosh quanto no driver:
This MongoDB deployment does not support retryable writes. Please add retryWrites=false to your connection string.
A mensagem menciona retryWrites, o que pode levar a crer que a opção de conexão resolve o problema. O experimento testou essa hipótese: o código de transação para o standalone usa retryWrites=false na URI, e a transação falhou com a mesma mensagem, tanto no driver Java quanto no mongosh com a mesma opção. Ou seja, a opção não torna a transação possível, porque o standalone não as suporta. A correção é usar um replica set.
Para o replica set, o experimento usou um nó só, com rs.initiate e o membro em localhost:27019. O código abriu o cliente, criou o MongoTransactionManager sobre a mesma MongoDatabaseFactory que o template usava e executou duas transações. A primeira inseriu dois documentos e fez commit. A segunda inseriu um documento e lançou uma exceção. A contagem real foi:
replica set, apos commit: documentos=2
replica set, excecao capturada: erro depois do insert
replica set, apos rollback: documentos=2
O documento inserido antes da exceção não ficou no banco. A figura mostra os dois caminhos.
Um cuidado do Spring Data MongoDB aparece no código. Quando o template e o gerenciador de transações recebem fábricas separadas, o rollback não acontece. Num teste à parte, com duas fábricas sobre o mesmo cliente, a contagem depois do rollback ficou em 3, com o documento que deveria ter sumido:
excecao capturada: erro depois do insert
fabricas separadas, apos rollback: documentos=3
Com a mesma fábrica, o rollback funcionou. O motivo é que o template e o gerenciador precisam compartilhar a mesma MongoDatabaseFactory, porque é ela que liga a sessão da transação às operações. Com a mesma fábrica, o rollback funcionou. A documentação do Spring diz que o template “detects the session and operates on these resources”, e a fábrica é a peça que faz essa ligação.
No Spring Boot, a anotação @Transactional usa o mesmo mecanismo. A documentação lembra que, sem um MongoTransactionManager no contexto, o suporte a transações fica desativado: “Unless you specify a MongoTransactionManager within your application context, transaction support is DISABLED.” A aula não subiu um contexto com @Transactional para medir isso, e por isso o experimento usa TransactionTemplate, que é a forma programática da mesma operação.
Pergunta de entrevistaComo você ordenaria os campos num índice composto?
Sigo a regra ESR: primeiro os campos de igualdade, depois os de ordenação, e por último os de intervalo. Assim o índice atende o filtro e a ordenação sem varrer e reordenar em memória. Confirmo com explain, olhando se o plano usa o índice e se ainda há ordenação em memória.
4. erros comuns e boas práticas
O primeiro erro é criar índices por intuição, sem olhar o explain. Um índice a mais custa espaço e tempo de escrita, e um índice que nenhuma consulta usa é só custo. A prática é rodar o explain("executionStats") na consulta real, conferir se há COLLSCAN e comparar totalDocsExamined com nReturned.
O segundo erro é montar um índice composto na ordem em que os campos aparecem no código. A regra ESR existe para evitar isso: igualdade primeiro, ordenação depois, faixa por último. Um índice fora dessa ordem pode até ser usado, mas costuma deixar um SORT em memória que o plano precisaria evitar.
O terceiro erro é usar transação para tudo. A documentação de transações diz que, “for many scenarios, the denormalized data model (embedded documents and arrays) will continue to be optimal for your data and use cases”. Para muitos casos, o modelo desnormalizado da aula Spring Data MongoDB continua sendo a melhor opção, e a transação fica para a operação que precisa ser atômica entre vários documentos. Esta aula não mediu o custo da transação, então não afirma quanto ele é maior.
O quarto erro é levar um ambiente de desenvolvimento em standalone para produção com transações. O teste que passa no standalone pode nunca ter exercitado a transação, porque ela falha logo na primeira operação. Um teste de integração deve rodar contra um replica set, mesmo que seja de um nó só.
A boa prática, em resumo, é usar o explain para cada consulta importante, criar índices compostos pela regra ESR, conferir o plano depois de cada mudança de consulta, usar replica set em todos os ambientes que rodam transações e garantir que template e gerenciador compartilhem a mesma fábrica.
5. exercícios
- Crie um índice
{ status: 1, criadoEm: -1 }e rode a consulta de cliente e status. Diga qual estágio aparece no plano e explique por que o índice do exercício não cobre a consulta da mesma forma que o ESR. - Rode a consulta da seção 2 sem índice e com o índice ESR, e compare
totalDocsExaminednos dois casos. Repare que oexecutionTimeMillisvaria entre execuções, e explique por que a aula usou os contadores e não o tempo. - Escreva uma transação que atualiza dois pedidos de clientes diferentes e lança exceção no meio. Confirme que as duas atualizações foram desfeitas, e explique o que aconteceria sem a transação.
- Pesquise como iniciar um replica set com mais de um nó e descreva, sem executar, o que muda nas transações. Diga o que a documentação diz sobre a versão mínima para cada tipo de implantação.
6. glossário
Índice: estrutura que mantém os valores de um ou mais campos em ordem, para localizar documentos sem varredura da coleção.
Índice composto: índice sobre mais de um campo, com uma ordem que determina quais consultas ele atende.
ESR: regra de ordem para índices compostos: primeiro igualdade, depois ordenação e por último faixa.
explain: comando que mostra o plano de execução de uma consulta e, com executionStats, os contadores da execução.
COLLSCAN: estágio de varredura completa da coleção.
IXSCAN: estágio de leitura de chaves de um índice.
FETCH: estágio que busca os documentos completos a partir das chaves do índice.
Replica set: grupo de servidores MongoDB que mantêm os mesmos dados, com um nó primário. As transações de vários documentos exigem essa topologia.
Standalone: servidor MongoDB de um único nó, sem replicação, que não aceita transações de vários documentos.
fontes
Consultadas em 20/10/2026:
- MongoDB, transações, com a citação sobre atomicidade de um documento, o suporte a replica sets e sharded clusters, o featureCompatibilityVersion mínimo e a citação sobre o modelo desnormalizado: https://www.mongodb.com/docs/manual/core/transactions/
- MongoDB, índices compostos, com a citação sobre a ordem dos campos, a regra ESR e o uso de prefixos: https://www.mongodb.com/docs/manual/core/indexes/index-types/index-compound/
- MongoDB,
cursor.explain(), com os modos de verbosidadequeryPlannereexecutionStats: https://www.mongodb.com/docs/manual/reference/method/cursor.explain/ - MongoDB, limites do sistema, com o tamanho máximo do documento BSON: https://www.mongodb.com/docs/manual/reference/limits/
- Spring Data MongoDB, transações de sessão do cliente, com a exigência de
replicaSetna URI, a ligação do template à transação e a desativação semMongoTransactionManager: https://docs.spring.io/spring-data/mongodb/reference/mongodb/client-session-transactions.html
Consultadas em 20/10/2026. As citações foram lidas com WebFetch nas URLs acima.
Verificações locais, feitas em 20/10/2026:
- Os planos e os contadores das seções 1 e 2 vieram do
explain("executionStats")executado nomongoshdo containermongo:8, com a coleção de 200 mil documentos criada no próprio script. - A mensagem do standalone foi copiada do
mongoshe do driver. A tentativa comretryWrites=falsefoi repetida nomongoshe no driver Java, e as duas falharam com a mesma mensagem. - Os planos e contadores do
explainforam reproduzidos nesta revisão com o mesmo script, no containermongo:8standalone, com os mesmos números. - A separação de fábricas no template e no gerenciador de transações foi reproduzida no replica set.
- Os números de commit e rollback vieram da execução do código do replica set.
- O
rs.status()confirmou o nó primário do replica set antes do teste.
Código do experimento de transação (arquivo aula66/Main.kt):
package aula66
import com.mongodb.client.MongoClients
import org.springframework.data.annotation.Id
import org.springframework.data.mongodb.MongoTransactionManager
import org.springframework.data.mongodb.core.MongoTemplate
import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory
import org.springframework.data.mongodb.core.mapping.Document
import org.springframework.data.mongodb.core.query.Query
import org.springframework.transaction.support.TransactionTemplate
@Document("pedidos_tx")
data class PedidoTx(@Id val id: String, val total: Int)
// Mesma logica nos dois servidores: dois inserts com commit, depois um insert seguido de erro.
fun executar(uri: String, rotulo: String) {
val client = MongoClients.create(uri)
try {
// Template e gerenciador precisam compartilhar a MESMA MongoDatabaseFactory,
// senao o template nao enxerga a sessao aberta pela transacao.
val fabrica = SimpleMongoClientDatabaseFactory(client, "aula66")
val template = MongoTemplate(fabrica)
template.dropCollection(PedidoTx::class.java)
val tx = TransactionTemplate(MongoTransactionManager(fabrica))
tx.execute {
template.insert(PedidoTx("ok-1", 10))
template.insert(PedidoTx("ok-2", 20))
}
println("$rotulo, apos commit: documentos=${template.count(Query(), PedidoTx::class.java)}")
try {
tx.execute {
template.insert(PedidoTx("falha-1", 30))
throw IllegalStateException("erro depois do insert")
}
} catch (e: IllegalStateException) {
println("$rotulo, excecao capturada: ${e.message}")
}
println("$rotulo, apos rollback: documentos=${template.count(Query(), PedidoTx::class.java)}")
} catch (e: Exception) {
println("$rotulo, falha: ${e::class.simpleName}: ${e.message?.lineSequence()?.first()}")
} finally {
client.close()
}
}
fun main() {
executar("mongodb://localhost:27019/?replicaSet=rs0", "replica set")
executar("mongodb://localhost:27020/?retryWrites=false", "standalone")
}
Script de índices, executado no mongosh (arquivo scripts/indices.js):
const banco = db.getSiblingDB("aula66");
banco.pedidos.drop();
const statuses = ["pago", "pendente", "cancelado", "enviado"];
const docs = [];
for (let i = 0; i < 200000; i++) {
docs.push({
cliente: "c" + (i % 1000),
status: statuses[Math.floor(i / 1000) % 4],
criadoEm: new Date(Date.UTC(2026, 0, 1) + i * 60000),
total: (i % 500) / 10
});
}
const _ins = banco.pedidos.insertMany(docs);
print("documentos: " + banco.pedidos.countDocuments());
function estagios(p) {
return p ? [p.stage].concat(estagios(p.inputStage)) : [];
}
function resumo(rotulo, ex) {
const s = ex.executionStats;
print("--- " + rotulo);
print("estagios: " + estagios(ex.queryPlanner.winningPlan).join(" > "));
print("nReturned=" + s.nReturned + " totalKeysExamined=" + s.totalKeysExamined + " totalDocsExamined=" + s.totalDocsExamined);
}
const consulta = { cliente: "c42", status: "pago" };
resumo("sem indice, filtro cliente+status, ordena por criadoEm",
banco.pedidos.find(consulta).sort({ criadoEm: -1 }).explain("executionStats"));
banco.pedidos.createIndex({ cliente: 1, status: 1, criadoEm: -1 }, { name: "cliente_status_criadoEm" });
resumo("com indice ESR (igualdade cliente, igualdade status, ordenacao criadoEm)",
banco.pedidos.find(consulta).sort({ criadoEm: -1 }).explain("executionStats"));
resumo("com indice, mas sem o campo do meio (status)",
banco.pedidos.find({ cliente: "c42" }).sort({ criadoEm: -1 }).explain("executionStats"));
banco.pedidos.createIndex({ status: 1 }, { name: "status_simples" });
resumo("com indice de status, filtro so por status (faixa de 1/4 da colecao)",
banco.pedidos.find({ status: "pago" }).explain("executionStats"));
Não verificado nesta aula: o tempo de execução das consultas, o ponto de corte em que uma varredura vence um índice, o comportamento com replica set de vários nós, o @Transactional com contexto Spring, e o limite de memória do SORT. A regra ESR foi citada da documentação, mas o efeito de uma ordem diferente de campos no índice não foi medido com uma consulta alternativa.
relacionados
- Trilha de Kotlin
- Curriculo de um Desenvolvedor Kotlin
- Spring Data MongoDB: documentos e modelagem
- Migrations com Liquibase
- Trabalhando com JSON