Explore o novo chatbot do Developer Center! O MongoDB AI chatbot pode ser acessado na parte superior da sua navegação para responder a todas as suas perguntas sobre o MongoDB .

Junte-se a nós no Amazon Web Services re:Invent 2024! Saiba como usar o MongoDB para casos de uso de AI .
Desenvolvedor do MongoDB
Central de desenvolvedor do MongoDBchevron-right
Produtoschevron-right
MongoDBchevron-right

Otimizando o desempenho de $lookup usando o poder da indexação

Shani Roffe7 min read • Published Aug 30, 2024 • Updated Aug 30, 2024
MongoDB
Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Você já precisou unir dados de duas collections e queria saber como melhorar o desempenho das queries? Vamos mostrar como a indexação pode ajudar a alimentar resultados mais rápidos do Atlas Search ao usar $lookup.

O que é $lookup?

$lookup é um estágio em um pipeline de agregação que executa uma união externa esquerda em uma coleção para filtrar documentos da coleção "unida" para processamento. $lookup cria uma nova array no pipeline, onde cada campo é um documento retornado de $lookup.
Os índices de um banco de dados são extremamente poderosos, pois podem reduzir significativamente o uso de recursos e a duração da query. Sem eles, as queries teriam que verificar todos os documentos de uma collection para retornar o resultado da query, o que consome muito tempo e recursos. Portanto, é muito importante ter índices em vigor ao usar $lookup para coleções "join ".
Para que o desempenho de $lookup seja otimizado, é uma boa prática ter índices em ambas as collections, a collection de origem e a collection de união. Dessa forma, a query principal na collection de origem se beneficiará de um índice, assim como a query na collection unida.

Caminho para melhorar o desempenho por meio do uso de índices

Para orientá-lo sobre como podemos otimizar as queries $lookup, usaremos dados de filme como exemplo. Nossa query utiliza dados de duas collections, genres e movies. Queremos encontrar todos os filmes que se enquadram nos gêneros de comedia e teatro.
insira a descrição da imagem aqui
Documentos de origem e união de collections
Collection: gêneros
1db.genres.insertMany([
2 { "_id": 0, "genre": "Comedy" },
3 { "_id": 1, "genre": "Drama" },
4 { "_id": 2, "genre": "Action" },
5 { "_id": 3, "genre": "Romance" },
6 { "_id": 4, "genre": "Adventure" },
7 { "_id": 5, "genre": "Family" },
8 { "_id": 6, "genre": "Fantasy" },
9 { "_id": 7, "genre": "Thriller" }
10])
Collection: filmes
1 db.movies.insertMany([
2 { "_id": 0, "title": "Billy Madison", "genre_codes": [0], "year": 1995, "directors": ["Tamra Davis"] },
3 { "_id": 1, "title": "Happy Gilmore", "genre_codes": [0, 5], "year": 1996, "directors": ["Dennis Dugan"] },
4 { "_id": 2, "title": "The Waterboy", "genre_codes": [0, 5], "year": 1998, "directors": ["Frank Coraci"] },
5 { "_id": 3, "title": "Big Daddy", "genre_codes": [0, 5], "year": 1999, "directors": ["Dennis Dugan"] },
6 { "_id": 4, "title": "Little Nicky", "genre_codes": [0], "year": 2000, "directors": ["Steven Brill"] },
7 { "_id": 5, "title": "Mr. Deeds", "genre_codes": [0], "year": 2002, "directors": ["Dennis Dugan"] },
8 { "_id": 6, "title": "50 First Dates", "genre_codes": [0, 3], "year": 2004, "directors": ["Peter Segal"] },
9 { "_id": 7, "title": "The Longest Yard", "genre_codes": [0, 2], "year": 2005, "directors": ["Peter Segal"] },
10 { "_id": 8, "title": "Grown Ups", "genre_codes": [0, 5], "year": 2010, "directors": ["Dennis Dugan"] },
11 { "_id": 9, "title": "Just Go with It", "genre_codes": [0, 3], "year": 2011, "directors": ["Dennis Dugan"] },
12 { "_id": 10, "title": "Blended", "genre_codes": [0, 5], "year": 2014, "directors": ["Frank Coraci"] },
13 { "_id": 11, "title": "The Do-Over", "genre_codes": [0, 2], "year": 2016, "directors": ["Steven Brill"] },
14 { "_id": 12, "title": "Murder Mystery", "genre_codes": [0, 7], "year": 2019, "directors": ["Kyle Newacheck"] },
15 { "_id": 13, "title": "You Are So Not Invited to My Bat Mitzvah", "genre_codes": [0], "year": 2023, "directors": ["Sammi Cohen"] },
16 { "_id": 14, "title": "Punch-Drunk Love", "genre_codes": [1], "year": 2002, "directors": ["Paul Thomas Anderson"] },
17 { "_id": 15, "title": "Reign Over Me", "genre_codes": [1], "year": 2007, "directors": ["Mike Binder"] },
18 { "_id": 16, "title": "Funny People", "genre_codes": [1], "year": 2009, "directors": ["Judd Apatow"] },
19 { "_id": 17, "title": "Uncut Gems", "genre_codes": [1, 7], "year": 2019, "directors": ["Josh Safdie", "Benny Safdie"] },
20 { "_id": 18, "title": "Bedtime Stories", "genre_codes": [5, 6], "year": 2008, "directors": ["Adam Shankman"] },
21 { "_id": 19, "title": "Hotel Transylvania", "genre_codes": [5, 6], "year": 2012, "directors": ["Genndy Tartakovsky"] },
22 { "_id": 20, "title": "Pixels", "genre_codes": [0, 2], "year": 2015, "directors": ["Chris Columbus"] }
23])
Saída desejada:
1{
2 "_id": 1,
3 "genre": "Drama",
4 "movies": [
5 { "title": "Funny People", "year": 2009 },
6 { "title": "Punch-Drunk Love", "year": 2002 },
7 { "title": "Reign Over Me", "year": 2007 },
8 { "title": "Uncut Gems", "year": 2019 }
9 ]
10}
11
12{
13 "_id": 0,
14 "genre": "Comedy",
15 "movies": [
16 { "title": "50 First Dates", "year": 2004 },
17 { "title": "Big Daddy", "year": 1999 },
18 { "title": "Billy Madison", "year": 1995 },
19 { "title": "Blended", "year": 2014 },
20 { "title": "Grown Ups", "year": 2010 },
21 { "title": "Happy Gilmore", "year": 1996 },
22 { "title": "Just Go with It", "year": 2011 },
23 { "title": "Little Nicky", "year": 2000 },
24 { "title": "Mr. Deeds", "year": 2002 },
25 { "title": "Murder Mystery", "year": 2019 },
26 { "title": "Pixels", "year": 2015 },
27 { "title": "The Do-Over", "year": 2016 },
28 { "title": "The Longest Yard", "year": 2005 },
29 { "title": "The Waterboy", "year": 1998 },
30 { "title": "You Are So Not Invited to My Bat Mitzvah", "year": 2023 }
31 ]
32}
Podemos realizar nossa tarefa de encontrar todos os filmes de comedia e teatro criando um aggregation pipeline na collectiongenres e unindo dados da collectionmovies. É possível criar a aggregation na collectionmovies, mas, para fins de representação, vamos presumir que estamos unindo de genres a movies.
  1. Queremos garantir que só retornaremos os documentos que estão sob o gênero de comedia e dramma usando o estgio $match.
  2. No estágio $lookup, realizaremos uma junção à esquerda na collectionmovies para recuperar os títulos e anos dos filmes:
  • O campofrom é a nossa collection de união. Neste caso, é a coleçãomovies.
  • No let, estamos atribuindo o campo "_id " na coleçãogenres à variável "genre_id ", que usaremos no pipeline.
  • No pipeline, usaremos $match para levar os documentos da coleçãomovies em que o genre_id (da coleçãogenres) corresponde à array "genre_codes " (da coleção de filmes) . Em seguida, usaremos $project para retornar apenas o título e o ano de cada filme.
Vamos executar a agregação e ver seu desempenho:
1 db.genres.aggregate([
2 { "$match": { "genre": { "$in": [ "Comedy", "Drama" ] } } },
3 {
4 "$lookup": {
5 "from": "movies",
6 "let": { "genre_id": "$_id" },
7 "pipeline": [
8 { "$match": { "$expr": { "$in": [ "$$genre_id", "$genre_codes" ] } } },
9 { "$project": { _id: 0, "title": 1, "year":1 } }
10 ],
11 "as": "movies"
12 }
13 }
14]).explain("allPlansExecution")
Resultado:
1executionStats: {
2 nReturned: 2,
3 totalKeysExamined: 0,
4 totalDocsExamined: 8,
5 executionStages: {
6 stage: 'COLLSCAN',
7 filter: { genre: { '$in': [ 'Comedy', 'Drama' ] } },
8 nReturned: 2,
9 ...
10"totalDocsExamined": 42,
11"totalKeysExamined": 0,
12"collectionScans": 2,
13"indexesUsed": [],
14"nReturned": 2
Agora que sabemos que usar índices melhora o desempenho do programa, vamos criá-los em suas respectivas collections:
1db.genres.createIndex({ "genre": 1 })
2db.movies.createIndex({ "genre_codes": 1 })
insira a descrição da imagem aqui
Vamos executar a agregação e ver como o desempenho melhorou com a adição de índices:
1db.genres.aggregate([
2 { "$match": { "genre": { "$in": [ "Comedy", "Drama" ] } } },
3 {
4 "$lookup": {
5 "from": "movies",
6 "let": { "genre_id": "$_id" },
7 "pipeline": [
8 { "$match": { "$expr": { "$in": [ "$$genre_id", "$genre_codes" ] } } },
9 { "$project": { _id: 0, "title": 1, "year":1 } }
10 ],
11 "as": "movies"
12 }
13 }
14]).explain("allPlansExecution")
Resultado:
1"executionStats": {
2 "nReturned": 2,
3 "totalKeysExamined": 3,
4 "totalDocsExamined": 2,
5 "stage": "IXSCAN",
6 "nReturned": 2,
7 "indexName": "genre_1",
8 ...
9"totalDocsExamined": 42,
10"totalKeysExamined": 0,
11"collectionScans": 2,
12"indexesUsed": [],
13"nReturned": 2,
Uh oh! Podemos ver pela saídaexecutionStats que essa aggregation não usou o índice no movies que criamos. O totalDocsExamined é 42, enquanto isso há apenas oito documentos na coleçãogenres e 21 na coleçãomovies! A razão pela qual o número de documentos examinados pode ficar tão alto é porque, para cada um dos dois genres, ele verifica toda a coleçãomovies. Isso leva a duas verificações de collection, o que é abaixo do ideal e pode levar a problemas significativos de desempenho. Quando ocorre um $lookup, cada documento da coleção de origem verificará toda a coleção de união, fazendo com que a quantidade de documentos digitalizados seja significativamente alta em comparação com a quantidade de documentos retornados.
A razão pela qual esse método não usou um índice é que $expr não pode usar um índice quando um dos operandos é uma array, como o campo$genre_codes neste exemplo.

Usando um índice na coleção de união

Para evitar o uso de um operando de array com $expr para avaliar se os códigos de gênero estão presentes, podemos usar localField e foreignField, que fornecerão a mesma funcionalidade:
  • localField é o campo dentro genres, a collection de origem com a qual queremos realizar uma correspondência de igualdade com foreignField, neste caso _id.
  • foreignField é o campo dentro movies, a collection de união onde queremos realizar uma correspondência de igualdade com localField, neste caso genre_codes.
  • Como já estabelecemos quais variáveis usaremos em pipeline por meio de localField e foreignField, podemos deixar let vazio e remover o estágio $matchdepipeline.
Agregado:
1db.genres.aggregate([
2 { $match: { genre: { $in: ["Comedy", "Drama"] } } },
3 {
4 $lookup: {
5 from: "movies",
6 localField: "_id",
7 foreignField: "genre_codes",
8 let: {},
9 pipeline: [{ $project: { _id: 0, title: 1, year: 1 } }],
10 as: "movies"
11 }
12 }
13]).explain("allPlansExecution")
Ao utilizar localField e foreignField, podemos realizar uma correspondência de igualdade com campos de diferentes collections sem usar $expr e $in.
1"executionStats": {
2 "nReturned": 2,
3 "totalKeysExamined": 3,
4 "totalDocsExamined": 2,
5 "stage": "IXSCAN",
6 "nReturned": 2,
7 "indexName": "genre_1",
8 ...
9"totalDocsExamined": 19,
10"totalKeysExamined": 19,
11"collectionScans": 0,
12"indexesUsed": ["genre_codes_1"],
13"nReturned": 2
Podemos ver pela saída de .explain() que houve uma melhoria 65% no desempenho. Usamos com sucesso o índice genre_codes_1 na coleçãomovies. Isso melhora o desempenho, pois vemos que apenas 19 documentos foram examinados. No entanto, podemos melhorar o desempenho ainda mais incluindo os campos de projeção no índice.

Executando uma query coberta na collection de união

Agora que vemos que a implementação localField e foreignField usará os índices que criamos, vamos otimizar a consulta incluindo os campos de projeção como parte do índice para executar uma consulta coberta.
1db.movies.createIndex({ "genre_codes":1, "title":1, "year":1})
2
3"totalDocsExamined": 0,
4"totalKeysExamined": 19,
5"collectionScans": 0,
6"indexesUsed": [ "genre_codes_1_title_1_year_1" ],
7"nReturned": 2,
Sim! Nosso índice foi usado e ele executa uma query coberta (uma query que usa apenas o índice para recuperar os resultados)! :) A saída de explicação mostra que totalDocsExamined está 0, o que significa que não foi necessário buscar nenhum documento, seja para avaliação ou projeção. Já estava tudo no índice, pronto para uso.
Esperemos que esta tenha sido uma exemplo completo de como você pode otimizar o desempenho de $lookup quando um dos operandos é um array. Se você quiser testar isso em seu ambiente local, encontre instruções sobre como criar as coleçõesmovies e genres abaixo.
Obrigado por ler!

Para mais informações sobre índices e $lookup

Principais comentários nos fóruns
Ainda não há comentários sobre este artigo.
Iniciar a conversa

Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Relacionado
Tutorial

Análise de moeda com coleções de séries temporais #2 — Cálculo da média móvel simples e da média móvel exponencial


May 16, 2022 | 7 min read
Artigo

Arrays massivas


Oct 01, 2024 | 4 min read
Tutorial

Adição de notificações em tempo real ao Ghost CMS usando MongoDB e eventos enviados pelo servidor


Aug 14, 2023 | 7 min read
Tutorial

Começando com Kotlin e MongoDB no lado do servidor


Oct 08, 2024 | 6 min read
Sumário