Otimizando o desempenho de $lookup usando o poder da indexação
Avalie esse Tutorial
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.
$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.
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.Documentos de origem e união de collections
Collection: gêneros
1 db.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 collection
genres
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
.- Queremos garantir que só retornaremos os documentos que estão sob o gênero de comedia e dramma usando o estgio $match.
- No estágio $lookup, realizaremos uma junção à esquerda na collection
movies
para recuperar os títulos e anos dos filmes:
- O campo
from
é 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 ogenre_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:
1 executionStats: { 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:
1 db.genres.createIndex({ "genre": 1 }) 2 db.movies.createIndex({ "genre_codes": 1 })
Vamos executar a agregação e ver como o desempenho melhorou com a adição de índices:
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:
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ída
executionStats
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.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 dentrogenres
, a collection de origem com a qual queremos realizar uma correspondência de igualdade comforeignField
, neste caso_id
.foreignField
é o campo dentromovies
, a collection de união onde queremos realizar uma correspondência de igualdade comlocalField
, neste casogenre_codes
.- Como já estabelecemos quais variáveis usaremos em
pipeline
por meio delocalField
eforeignField
, podemos deixarlet
vazio e remover o estágio$match
depipeline
.
Agregado:
1 db.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.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.1 db.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ções
movies
e genres
abaixo.Obrigado por ler!
Principais comentários nos fóruns
Ainda não há comentários sobre este artigo.