Investigando Slow Queries

Como encontrar Slow Queries no MongoDB

Seja qual for o SGBD com o qual trabalhemos, sabemos que ter queries com uma performance ruim (as slow queries) pode prejudicar e muito o ambiente como um todo. Essas queries agem como sanguessugas de recursos dos nosso servidores, drenando o que há de mais valioso neles: recursos computacionais, como CPU, memória e I/O. Como administradores destes ambientes, temos o dever de identificar essas queries e melhorar sua performance.

No Mongo não é diferente, e por se tratar de um banco NoSQL, onde a velocidade é um ponto chave, a diferença de míseros milissegundos no retorno de uma query pode gerar um grande impacto na ponta do processo.

Importante: neste post estou usando a versão 7.0.16 do Mongo.

Selecionando o que observar

Para identificar as slow queries ofensoras utilizamos o Database Profiler. Essa funcionalidade coleta dados de comandos executados contra o banco de dados e os escreve na collection system.profile que existe dentro de cada database da instância do Mongo. A informação de slow queries também é exibida no log da instância do Mongo.

Por padrão, o profiler fica desabilitado. Podemos verificar seu status com o comando db.getProfilingStatus(), enquanto conectado a um database. No meu caso vou conectar ao database spotify que tenho na minha instância e verificar o status do profiler:

rsMongoServer [direct: primary] admin> use spotify
switched to db spotify
rsMongoServer [direct: primary] spotify> db.getProfilingStatus()
{
  was: 0,
  slowms: 100,
  sampleRate: 1,
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1742344574, i: 1 }),
    signature: {
      hash: Binary.createFromBase64('nUhvCv9TXd6uCxLZ1dEGCTKUXlI=', 0),
      keyId: Long('7417017111232380934')
    }
  },
  operationTime: Timestamp({ t: 1742344574, i: 1 })
}

No documento retornado, o campo “was” mostra o nível do profiler. Existem três níveis para o profiler:

  • 0 (zero): o profiler não coleta nenhum dado. Este é o default;
  • 1: o profiler coleta dados de operações que levam mais tempo que o valor do campo “slowms” ou que satisfaçam um filtro configurado no profiler;
  • 2: o profiler coleta dados para todas as operações;

É importante ter em mente que um overhead na performance é adicionado a cada nível que subimos, uma vez que o banco precisa fazer um trabalho extra, logando as informações para cada nível habilitado.

Para alterar o nível do profiler, usamos o comando db.setProfilingLevel() passando o nível desejado por parâmetro, e opcionalmente parâmetros para controlar o que será logado:

rsMongoServer [direct: primary] spotify> db.setProfilingLevel(1)
{
  was: 0,
  slowms: 100,
  sampleRate: 1,
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1742358087, i: 1 }),
    signature: {
      hash: Binary.createFromBase64('OpoKZ46LUU9AvsuHfX5mUV+bASg=', 0),
      keyId: Long('7417017111232380934')
    }
  },
  operationTime: Timestamp({ t: 1742358087, i: 1 })

Nesse caso o documento retornado mostra o nível do profile anterior no campo “was”. Podemos ver o nível 1 habilitado com o comando db.getProfilingStatus():

{
  was: 1,
  slowms: 100,
  sampleRate: 1,
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1742358097, i: 1 }),
    signature: {
      hash: Binary.createFromBase64('pFqKwmFyXskiuIKR9JKjs7ICstQ=', 0),
      keyId: Long('7417017111232380934')
    }
  },
  operationTime: Timestamp({ t: 1742358097, i: 1 })
}

Por padrão, o valor do campo “slowms” é de 100ms, mas podemos alterá-lo passando o valor como parâmetro:

rsMongoServer [direct: primary] spotify> db.setProfilingLevel(1, {"slowms" : 200})
{
  was: 1,
  slowms: 100,
  sampleRate: 1,
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1742358277, i: 1 }),
    signature: {
      hash: Binary.createFromBase64('zwvuHbWmzYbuIMZaKRDaBhQAQqM=', 0),
      keyId: Long('7417017111232380934')
    }
  },
  operationTime: Timestamp({ t: 1742358277, i: 1 })
}
rsMongoServer [direct: primary] spotify> db.getProfilingStatus()
{
  was: 1,
  slowms: 200,
  sampleRate: 1,
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1742358277, i: 1 }),
    signature: {
      hash: Binary.createFromBase64('zwvuHbWmzYbuIMZaKRDaBhQAQqM=', 0),
      keyId: Long('7417017111232380934')
    }
  },
  operationTime: Timestamp({ t: 1742358277, i: 1 })
}

Uma estratégia interessante é iniciar com um valor de “slowms” mais alto e o diminuir de acordo com a necessidade, pois dessa forma o banco reduz o volume de operações logadas.

Outra forma de diminuir o que está sendo logado é através do parâmetro “sampleRate“, onde definimos a fração de operações lentas que desejamos logar. Por exemplo, caso desejássemos logar apenas 40% das operações lentas, usaríamos:

rsMongoServer [direct: primary] spotify> db.setProfilingLevel(1, {"sampleRate" : 0.40})
{
  was: 1,
  slowms: 200,
  sampleRate: 1,
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1742358617, i: 1 }),
    signature: {
      hash: Binary.createFromBase64('SM49mleSUVNE+H4/VyaP3eTr52g=', 0),
      keyId: Long('7417017111232380934')
    }
  },
  operationTime: Timestamp({ t: 1742358617, i: 1 })
}
rsMongoServer [direct: primary] spotify> db.getProfilingStatus()
{
  was: 1,
  slowms: 200,
  sampleRate: 0.4,
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1742358627, i: 1 }),
    signature: {
      hash: Binary.createFromBase64('5Ueyk4Plc+VQw7VvN/8NUQIaugg=', 0),
      keyId: Long('7417017111232380934')
    }
  },
  operationTime: Timestamp({ t: 1742358627, i: 1 })
}

Por fim, a última forma de controlar o que será logado, é através da implementação de um filtro. Em um cenário onde desejamos logar apenas queries que levem mais de 1 segundo para executarem, usaríamos:

rsMongoServer [direct: primary] spotify> db.setProfilingLevel( 1, { filter: { op: "query", millis: { $gt: 1000 } } } )
{
  was: 1,
  slowms: 200,
  sampleRate: 0.4,
  note: 'When a filter expression is set, slowms and sampleRate are not used for profiling and slow-query log lines.',
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1742358777, i: 1 }),
    signature: {
      hash: Binary.createFromBase64('1+HTMCeBluqsGLiXJVCd9geAdPw=', 0),
      keyId: Long('7417017111232380934')
    }
  },
  operationTime: Timestamp({ t: 1742358777, i: 1 })
}
rsMongoServer [direct: primary] spotify> db.getProfilingStatus()
{
  was: 1,
  slowms: 200,
  sampleRate: 0.4,
  filter: {
    '$and': [ { op: { '$eq': 'query' } }, { millis: { '$gt': 1000 } } ]
  },
  note: 'When a filter expression is set, slowms and sampleRate are not used for profiling and slow-query log lines.',
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1742358777, i: 1 }),
    signature: {
      hash: Binary.createFromBase64('1+HTMCeBluqsGLiXJVCd9geAdPw=', 0),
      keyId: Long('7417017111232380934')
    }
  },
  operationTime: Timestamp({ t: 1742358777, i: 1 })
}

Como a própria nota reportada no retorno do comando db.getProfilingStatus() cita, quando usamos um filtro, o Mongo desconsidera os valores configurados para “slowms” e “sampleRate“. Para desativar o filtro usamos o valor “unset“:

rsMongoServer [direct: primary] spotify> db.setProfilingLevel( 1, { filter: "unset" } )
{
  was: 1,
  slowms: 200,
  sampleRate: 0.4,
  filter: {
    '$and': [ { op: { '$eq': 'query' } }, { millis: { '$gt': 1000 } } ]
  },
  note: 'When a filter expression is set, slowms and sampleRate are not used for profiling and slow-query log lines.',
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1742358897, i: 1 }),
    signature: {
      hash: Binary.createFromBase64('MCkZmHmKMpWKOYHjKlCtHUZ3htE=', 0),
      keyId: Long('7417017111232380934')
    }
  },
  operationTime: Timestamp({ t: 1742358897, i: 1 })
}
rsMongoServer [direct: primary] spotify> db.getProfilingStatus()
{
  was: 1,
  slowms: 200,
  sampleRate: 0.4,
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1742358897, i: 1 }),
    signature: {
      hash: Binary.createFromBase64('MCkZmHmKMpWKOYHjKlCtHUZ3htE=', 0),
      keyId: Long('7417017111232380934')
    }
  },
  operationTime: Timestamp({ t: 1742358897, i: 1 })
}

Para os testes, vou manter os valores padrão para “slowms” e “sampleRate“:

rsMongoServer [direct: primary] spotify> db.setProfilingLevel(1, {"slowms" : 100,"sampleRate" : 1})
{
  was: 1,
  slowms: 200,
  sampleRate: 0.4,
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1742359037, i: 1 }),
    signature: {
      hash: Binary.createFromBase64('GXtp9wkoun0AmgVJ0tSKf/fLkLY=', 0),
      keyId: Long('7417017111232380934')
    }
  },
  operationTime: Timestamp({ t: 1742359037, i: 1 })
}

Analisando as informações coletadas

Para fins de teste, vou realizar uma consulta dentro da collection playlists, filtrando por um campo sem índice, para que a operação seja logada pelo profiler. Na sequência desabilitamos o profiler:

rsMongoServer [direct: primary] spotify> db.playlists.find({num_tracks : 75},{name:1, num_tracks:1,_id:0})
[
  { name: 'Classical', num_tracks: 75 },
  { name: 'Running', num_tracks: 75 },
  { name: 'Chillll', num_tracks: 75 },
  { name: 'Meow', num_tracks: 75 },
  { name: 'Running', num_tracks: 75 },
  { name: 'Sleepy', num_tracks: 75 },
  { name: 'Chill', num_tracks: 75 },
  { name: 'Summer drives', num_tracks: 75 },
  { name: "John's Playlist", num_tracks: 75 },
  { name: 'Run It', num_tracks: 75 },
  { name: 'disney channel', num_tracks: 75 },
  { name: 'summerrr', num_tracks: 75 },
  { name: '😍😍😍', num_tracks: 75 },
  { name: 'Car Beats', num_tracks: 75 },
  { name: 'Volleyball', num_tracks: 75 },
  { name: 'Oldies', num_tracks: 75 },
  { name: '/chill', num_tracks: 75 },
  { name: 'RAP', num_tracks: 75 },
  { name: 'Study Playlist', num_tracks: 75 },
  { name: 'September 2016', num_tracks: 75 }
]
Type "it" for more

rsMongoServer [direct: primary] spotify> db.setProfilingLevel(0)
{
  was: 1,
  slowms: 100,
  sampleRate: 1,
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1742360837, i: 1 }),
    signature: {
      hash: Binary.createFromBase64('d75/HdAzTwdxSvHPzbJatFImoYo=', 0),
      keyId: Long('7417017111232380934')
    }
  },
  operationTime: Timestamp({ t: 1742360837, i: 1 })
}

Verificando o último registro do log que contém a expressão “Slow”, temos o seguinte retorno:

$ cat /var/log/mongodb/mongod.log | grep "Slow" | tail -n 1
{"t":{"$date":"2025-03-19T04:45:58.834+00:00"},"s":"I",  "c":"COMMAND",  "id":51803,   "ctx":"conn43","msg":"Slow query",
"attr":{"type":"command","ns":"spotify.playlists","appName":"mongosh 2.3.1","command":{"find":"playlists",
"filter":{"num_tracks":75},"projection":{"name":1,"num_tracks":1,"_id":0},"lsid":{"id":{"$uuid":
"0f5d5b70-ef56-4f63-96ef-55449af4e1cb"}},"$clusterTime":{"clusterTime":{"$timestamp":{"t":1742359547,"i":1}},"signature":
{"hash":{"$binary":{"base64":"/IxouAZJuMlh2TDghDaul03Pu0g=","subType":"0"}},"keyId":7417017111232380934}},"$readPreference"
:{"mode":"primaryPreferred"},"$db":"spotify"},"planSummary":"COLLSCAN","planningTimeMicros":99,"cursorid":
6680968565243767053,"keysExamined":0,"docsExamined":18079,"nBatches":1,"numYields":45,"nreturned":101,"queryHash":
"4F5B942E","planCacheKey":"4F5B942E","queryFramework":"classic","reslen":4748,"locks":{"FeatureCompatibilityVersion":
{"acquireCount":{"r":46}},"Global":{"acquireCount":{"r":46}}},"readConcern":{"level":"local","provenance":"implicitDefault"},"storage":"data":"bytesRead":355737612,"timeReadingMicros":490735}},"cpuNanos":380919276,"remote":"127.0.0.1:48324","protocol":"op_msg","durationMillis":778}}

Se este é o primeiro trecho de log do Mongo que você está tendo contato, eu entendo o seu sentimento (que bagunça é essa?). O Mongo organiza o seu de uma forma com que cada registro dele seja um documento. Vamos analisar aqui alguns campos específicos:

  • t“: timestamp da mensagem;
  • s“: severidade, neste caso “I” de “Informational“;
  • c“: componente, neste caso “COMMAND” remete a um comando executado contra o banco;
  • msg“: mensagem gerada pelo server;
  • attr“: atributos com informações adicionais.

É no campo “attr” que podemos ver qual query foi executada, olhando para o atributo “command“:

"command":{"find":"playlists","filter":{"num_tracks":75},"projection":{"name":1,"num_tracks":1,"_id":0},"lsid":{"id":{"$uuid":"0f5d5b70-ef56-4f63-96ef-55449af4e1cb"}},"$clusterTime":{"clusterTime":{"$timestamp":{"t":1742359547,"i":1}},"signature":{"hash":{"$binary":{"base64":"/IxouAZJuMlh2TDghDaul03Pu0g=","subType":"0"}},"keyId":7417017111232380934}},"$readPreference":{"mode":"primaryPreferred"},"$db":"spotify"}
  • “find”:”playlists”: comando find, na collection playlists;
  • “filter”:{“num_tracks”:75}: filtro de documentos com o campo num_tracks = 75;
  • “projection”:{“name”:1,”num_tracks”:1,”_id”:0}: projeção, lista de campos retornados (1) e omitidos (0);
  • “$db”:”spotify”: database onde o comando foi executado;

Logo após o atributo “command“, podemos ver o “planSummary” mostrando um COLLSCAN, que significa que o banco está fazendo um scan em todos os documentos de uma collection (da mesma forma que um FULL TABLE SCAN ocorre em um banco relacional), indicando o problema de performance.

Mais informações? Sem problemas

Caso as informações do log não sejam suficientes para análise, podemos consultar a collection system.profile dentro do database no qual a query foi logada (requer a role de dbAdmin no database). Vamos ver a estrutura do documento retornado, com o commando findOne():

rsMongoServer [direct: primary] spotify> db.system.profile.findOne()
{
  op: 'query',
  ns: 'spotify.playlists',
  command: {
    find: 'playlists',
    filter: { num_tracks: 75 },
    projection: { name: 1, num_tracks: 1, _id: 0 },
    lsid: { id: UUID('8f620c8f-f493-4b2b-95fc-14ad3f535367') },
    '$clusterTime': {
      clusterTime: Timestamp({ t: 1742344654, i: 1 }),
      signature: {
        hash: Binary.createFromBase64('DXysXWuc0a+sonFyjpNF0xTejWk=', 0),
        keyId: Long('7417017111232380934')
      }
    },
    '$readPreference': { mode: 'primaryPreferred' },
    '$db': 'spotify'
  },
  cursorid: Long('9013903626341513349'),
  keysExamined: 0,
  docsExamined: 18079,
  nBatches: 1,
  numYield: 26,
  nreturned: 101,
  queryHash: '4F5B942E',
  planCacheKey: '4F5B942E',
  queryFramework: 'classic',
  locks: {
    FeatureCompatibilityVersion: { acquireCount: { r: Long('28') } },
    Global: { acquireCount: { r: Long('28') } }
  },
  flowControl: {},
  readConcern: { level: 'local', provenance: 'implicitDefault' },
  storage: {
    data: { bytesRead: Long('89659414'), timeReadingMicros: Long('138362') }
  },
  responseLength: 4748,
  protocol: 'op_msg',
  cpuNanos: 106646554,
  millis: 218,
  planSummary: 'COLLSCAN',
  planningTimeMicros: 98,
  execStats: {
    stage: 'PROJECTION_SIMPLE',
    nReturned: 101,
    executionTimeMillisEstimate: 207,
    works: 18079,
    advanced: 101,
    needTime: 17978,
    needYield: 0,
    saveState: 27,
    restoreState: 26,
    isEOF: 0,
    transformBy: {},
    inputStage: {
      stage: 'COLLSCAN',
      filter: { num_tracks: { '$eq': 75 } },
      nReturned: 101,
      executionTimeMillisEstimate: 207,
      works: 18079,
      advanced: 101,
      needTime: 17978,
      needYield: 0,
      saveState: 27,
      restoreState: 26,
      isEOF: 0,
      direction: 'forward',
      docsExamined: 18079
    }
  },
  ts: ISODate('2025-03-19T00:37:44.686Z'),
  client: '127.0.0.1',
  appName: 'mongosh 2.3.1',
  allUsers: [ { user: 'admin', db: 'admin' } ],
  user: 'admin@admin'
}

Conhecendo essa estrutura agora conseguimos montar queries buscando por campos específicos. Digamos que eu queira analisar o usuário e o comando executado para queries mais lentas que 200ms. Eu usaria:

rsMongoServer [direct: primary] spotify> db.system.profile.find({millis: {$gt:200}},{ user: 1, command: 1})
[
  {
    command: {
      find: 'playlists',
      filter: { num_tracks: 75 },
      projection: { name: 1, num_tracks: 1, _id: 0 },
      lsid: { id: UUID('8f620c8f-f493-4b2b-95fc-14ad3f535367') },
      '$clusterTime': {
        clusterTime: Timestamp({ t: 1742344654, i: 1 }),
        signature: {
          hash: Binary.createFromBase64('DXysXWuc0a+sonFyjpNF0xTejWk=', 0),
          keyId: Long('7417017111232380934')
        }
      },
      '$readPreference': { mode: 'primaryPreferred' },
      '$db': 'spotify'
    },
    user: 'admin@admin'
  },
  {
    command: {
      find: 'playlists',
      filter: { num_tracks: 75 },
      projection: { name: 1, num_tracks: 1, _id: 0 },
      lsid: { id: UUID('0f5d5b70-ef56-4f63-96ef-55449af4e1cb') },
      '$clusterTime': {
        clusterTime: Timestamp({ t: 1742359547, i: 1 }),
        signature: {
          hash: Binary.createFromBase64('/IxouAZJuMlh2TDghDaul03Pu0g=', 0),
          keyId: Long('7417017111232380934')
        }
      },
      '$readPreference': { mode: 'primaryPreferred' },
      '$db': 'spotify'
    },
    user: 'admin@admin'
  }
]

Conclusão

O profiler é uma ótima ferramenta para utilizar quando notamos um aumento de recursos no banco, pois geralmente esse aumento é ocasionado por alguma slow query rodando no ambiente. Quanto antes a identificarmos mais rápida será a tratativa.

Como leitura complementar a este artigo sugiro:

Grande abraço a todos!

Publicar comentário