Pular para o conteúdo

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

Tamanho da fonte:
Largura da coluna:
Tom:
Espaço entre linhas:
Fonte:

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.

Plano de execução com e sem índice. COLLSCAN → SORT (200 000 docs); SORT → resultado (50 docs); IXSCAN → FETCH (50 chaves); FETCH → resultado (50 docs)…
figura 1 · Plano de execução com e sem índice

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.

Transação no MongoDB: onde ela funciona. Aplicação → Driver e sessão (startTransaction); Driver e sessão → Servidor é replica set?…
figura 2 · Transação no MongoDB: onde ela funciona

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

  1. 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.
  2. Rode a consulta da seção 2 sem índice e com o índice ESR, e compare totalDocsExamined nos dois casos. Repare que o executionTimeMillis varia entre execuções, e explique por que a aula usou os contadores e não o tempo.
  3. 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.
  4. 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:

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 no mongosh do container mongo:8, com a coleção de 200 mil documentos criada no próprio script.
  • A mensagem do standalone foi copiada do mongosh e do driver. A tentativa com retryWrites=false foi repetida no mongosh e no driver Java, e as duas falharam com a mesma mensagem.
  • Os planos e contadores do explain foram reproduzidos nesta revisão com o mesmo script, no container mongo:8 standalone, 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

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?