MongoDB
 sql >> Base de Dados >  >> NoSQL >> MongoDB

Encontre a contagem de todos os intervalos sobrepostos


Como você mencionou corretamente, existem diferentes abordagens com complexidade variável inerente à sua execução. Isso basicamente cobre como eles são feitos e qual você implementa na verdade depende de quais seus dados e caso de uso são mais adequados.

Correspondência de intervalo atual

MongoDB 3.6 $lookup


A abordagem mais simples pode ser empregada usando a nova sintaxe do $pesquisa operador com MongoDB 3.6 que permite um pipeline a ser dada como a expressão para "auto-juntar" à mesma coleção. Isso pode basicamente consultar a coleção novamente para quaisquer itens em que o starttime "ou" horário de fim do documento atual está entre os mesmos valores de qualquer outro documento, não incluindo o original é claro:
db.getCollection('collection').aggregate([
  { "$lookup": {
    "from": "collection",
    "let": {
      "_id": "$_id",
      "starttime": "$starttime",
      "endtime": "$endtime"
    },
    "pipeline": [
      { "$match": {
        "$expr": {
          "$and": [
            { "$ne": [ "$$_id", "$_id" },
            { "$or": [
              { "$and": [
                { "$gte": [ "$$starttime", "$starttime" ] },
                { "$lte": [ "$$starttime", "$endtime" ] }
              ]},
              { "$and": [
                { "$gte": [ "$$endtime", "$starttime" ] },
                { "$lte": [ "$$endtime", "$endtime" ] }
              ]}
            ]},
          ]
        },
        "as": "overlaps"
      }},
      { "$count": "count" },
    ]
  }},
  { "$match": { "overlaps.0": { "$exists": true }  } }
])

O único $lookup executa o "join" na mesma coleção permitindo que você mantenha os valores do "documento atual" para o "_id" , "hora de início" e "horário final" valores respectivamente através do "let" opção do estágio de pipeline. Elas estarão disponíveis como "variáveis ​​locais" usando o $$ prefixo no "pipeline" subsequente da expressão.

Dentro deste "sub-pipeline" você usa o $match estágio de pipeline e o $expr operador de consulta, que permite avaliar expressões lógicas da estrutura de agregação como parte da condição de consulta. Isso permite a comparação entre os valores, pois seleciona novos documentos que correspondam às condições.

As condições simplesmente procuram os "documentos processados" onde o "_id" campo não é igual ao "documento atual", $and onde o "starttime" $or "horário final" os valores do "documento atual" ficam entre as mesmas propriedades do "documento processado". Observando aqui que estes, bem como os respectivos $gte e $lte os operadores são os "operadores de comparação de agregação" e não o "operador de consulta" formulário, como o resultado retornado avaliado por $expr deve ser booleano no contexto. Isso é o que os operadores de comparação de agregação realmente fazem e também é a única maneira de passar valores para comparação.

Como queremos apenas a "contagem" das correspondências, o $conta O estágio de pipeline é usado para fazer isso. O resultado geral do $lookup será um array de "elemento único" onde houve uma contagem ou um "array vazio" onde não houve correspondência com as condições.

Um caso alternativo seria "omitir" o $count estágio e simplesmente permitir que os documentos correspondentes retornem. Isso permite uma identificação fácil, mas como um "array embutido no documento" você precisa estar atento ao número de "sobreposições" que serão retornadas como documentos inteiros e que isso não cause uma violação do limite BSON de 16 MB. Na maioria dos casos, isso deve ser bom, mas para casos em que você espera um grande número de sobreposições para um determinado documento, isso pode ser um caso real. Então é realmente algo mais para estar ciente.

O $lookup O estágio de pipeline neste contexto "sempre" retornará uma matriz no resultado, mesmo que esteja vazia. O nome da propriedade de saída "merge" no documento existente será "overlaps" conforme especificado em "como" propriedade para o $lookup palco.

Seguindo o $lookup , podemos fazer um simples $match com uma expressão de consulta regular empregando o $exists teste para o 0 valor de índice da matriz de saída. Onde realmente há algum conteúdo na matriz e, portanto, "sobrepõe", a condição será verdadeira e o documento será retornado, mostrando a contagem ou os documentos "sobrepostos" conforme sua seleção.

Outras versões - Consultas para "entrar"


O caso alternativo em que seu MongoDB não possui esse suporte é "ingressar" manualmente emitindo as mesmas condições de consulta descritas acima para cada documento examinado:
db.getCollection('collection').find().map( d => {
  var overlaps = db.getCollection('collection').find({
    "_id": { "$ne": d._id },
    "$or": [
      { "starttime": { "$gte": d.starttime, "$lte": d.endtime } },
      { "endtime": { "$gte": d.starttime, "$lte": d.endtime } }
    ]
  }).toArray();

  return ( overlaps.length !== 0 ) 
    ? Object.assign(
        d,
        {
          "overlaps": {
            "count": overlaps.length,
            "documents": overlaps
          }
        }
      )
    : null;
}).filter(e => e != null);

Esta é essencialmente a mesma lógica, exceto que, na verdade, precisamos "voltar ao banco de dados" para emitir a consulta para corresponder aos documentos sobrepostos. Desta vez, são os "operadores de consulta" usados ​​para descobrir onde os valores do documento atual estão entre os do documento processado.

Como os resultados já são retornados do servidor, não há restrição de limite BSON na adição de conteúdo à saída. Você pode ter restrições de memória, mas isso é outro problema. Simplificando, retornamos o array em vez do cursor via .toArray() então temos os documentos correspondentes e podemos simplesmente acessar o tamanho do array para obter uma contagem. Se você realmente não precisar dos documentos, use .count() em vez de .find() é muito mais eficiente, pois não há sobrecarga de busca de documentos.

A saída é então simplesmente mesclada com o documento existente, onde a outra distinção importante é que, como as teses são "múltiplas consultas", não há como fornecer a condição de que elas devem "corresponder" a algo. Portanto, isso nos deixa considerando que haverá resultados em que a contagem (ou comprimento da matriz) é 0 e tudo o que podemos fazer neste momento é retornar um null valor que podemos mais tarde .filter() da matriz de resultados. Outros métodos de iteração do cursor empregam o mesmo princípio básico de "descartar" resultados onde não os queremos. Mas nada impede que a consulta seja executada no servidor e essa filtragem é "pós-processamento" de uma forma ou de outra.

Reduzindo a complexidade


Portanto, as abordagens acima funcionam com a estrutura descrita, mas é claro que a complexidade geral exige que, para cada documento, você essencialmente examine todos os outros documentos da coleção para procurar sobreposições. Portanto, ao usar $lookup permite alguma "eficiência" na redução da sobrecarga de transporte e resposta, ele ainda sofre o mesmo problema que você ainda está comparando essencialmente cada documento com tudo.

Uma solução melhor "onde você pode ajustar" é armazenar um "valor fixo"* representativo do intervalo em cada documento. Por exemplo, poderíamos "presumir" que existem períodos de "reserva" sólidos de uma hora dentro de um dia para um total de 24 períodos de reserva. Este "poderia" ser representado por algo como:
{ "_id": "A", "booking": [ 10, 11, 12 ] }
{ "_id": "B", "booking": [ 12, 13, 14 ] }
{ "_id": "C", "booking": [ 7, 8 ] }
{ "_id": "D", "booking": [ 9, 10, 11 ] }

Com os dados organizados assim, onde havia um indicador definido para o intervalo, a complexidade é bastante reduzida, pois é apenas uma questão de "agrupar" o valor do intervalo do array dentro do "booking" propriedade:
db.booking.aggregate([
  { "$unwind": "$booking" },
  { "$group": { "_id": "$booking", "docs": { "$push": "$_id" } } },
  { "$match": { "docs.1": { "$exists": true } } }
])

E a saída:
{ "_id" : 10, "docs" : [ "A", "D" ] }
{ "_id" : 11, "docs" : [ "A", "D" ] }
{ "_id" : 12, "docs" : [ "A", "B" ] }

Isso identifica corretamente isso para o 10 e 11 intervalos ambos "A" e "D" conter a sobreposição, enquanto "B" e "A" sobreposição em 12 . Outros intervalos e correspondências de documentos são excluídos pelo mesmo $exists teste, exceto desta vez no 1 index (ou segundo elemento da matriz presente) para ver que havia "mais de um" documento no agrupamento, indicando uma sobreposição.

Isso simplesmente emprega o $unwind estágio de pipeline de agregação para "desconstruir/desnormalizar" o conteúdo da matriz para que possamos acessar os valores internos para agrupamento. Isso é exatamente o que acontece no $group estágio em que a "chave" fornecida é o ID do intervalo de reserva e o $push operador é usado para "coletar" dados sobre o documento atual que foi encontrado naquele grupo. O $match é como explicado anteriormente.

Isso pode até ser expandido para apresentação alternativa:
db.booking.aggregate([
  { "$unwind": "$booking" },
  { "$group": { "_id": "$booking", "docs": { "$push": "$_id" } } },
  { "$match": { "docs.1": { "$exists": true } } },
  { "$unwind": "$docs" },
  { "$group": {
    "_id": "$docs",
    "intervals": { "$push": "$_id" }  
  }}
])

Com saída:
{ "_id" : "B", "intervals" : [ 12 ] }
{ "_id" : "D", "intervals" : [ 10, 11 ] }
{ "_id" : "A", "intervals" : [ 10, 11, 12 ] }

É uma demonstração simplificada, mas onde os dados que você tem permitiriam o tipo de análise necessário, essa é a abordagem muito mais eficiente. Portanto, se você puder manter a "granularidade" a ser fixada em intervalos "definidos" que podem ser comumente registrados em cada documento, a análise e o relatório podem usar a última abordagem para identificar de forma rápida e eficiente tais sobreposições.

Essencialmente, é assim que você implementaria o que você basicamente mencionou como uma abordagem "melhor" de qualquer maneira, e a primeira sendo uma "leve" melhoria em relação ao que você originalmente teorizou. Veja qual realmente se adapta à sua situação, mas isso deve explicar a implementação e as diferenças.