sql >> Base de Datos >  >> NoSQL >> MongoDB

Encuentra el recuento de todos los intervalos superpuestos

Como mencionas correctamente, existen diferentes enfoques con una complejidad variable inherente a su ejecución. Esto básicamente cubre cómo se hacen y cuál implementar en realidad depende de a qué datos y caso de uso se adapten mejor.

Coincidencia de rango actual

MongoDB 3.6 $búsqueda

El enfoque más simple se puede emplear usando la nueva sintaxis del $lookup operador con MongoDB 3.6 que permite un pipeline para ser dado como la expresión para "auto unirse" a la misma colección. Básicamente, esto puede consultar la colección nuevamente para cualquier elemento donde el starttime "o" endtime del documento actual se encuentra entre los mismos valores de cualquier otro documento, sin incluir el original, por supuesto:

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 }  } }
])

El único $lookup realiza la "unión" en la misma colección, lo que le permite mantener los valores del "documento actual" para el "_id" , "starttime" y "endtime" valores respectivamente a través de "let" opción de la etapa de tubería. Estos estarán disponibles como "variables locales" usando el $$ prefijo en el subsiguiente "pipeline" de la expresión.

Dentro de este "canal secundario", utiliza el $match etapa de canalización y $expr operador de consulta, que le permite evaluar expresiones lógicas del marco de agregación como parte de la condición de consulta. Esto permite la comparación entre valores ya que selecciona nuevos documentos que cumplen las condiciones.

Las condiciones simplemente buscan los "documentos procesados" donde el "_id" el campo no es igual al "documento actual", $and donde el "starttime" $or "endtime" valores del "documento actual" se encuentran entre las mismas propiedades del "documento procesado". Señalando aquí que estos, así como los respectivos $gte y $lte Los operadores son los "operadores de comparación de agregación" y no el "operador de consulta" formulario, como el resultado devuelto evaluado por $expr debe ser boolean en contexto. Esto es lo que realmente hacen los operadores de comparación de agregación, y también es la única forma de pasar valores para la comparación.

Como solo queremos el "recuento" de las coincidencias, el $count etapa de canalización se utiliza para hacer esto. El resultado general de $lookup será una matriz de "elemento único" en la que hubo un recuento, o una "matriz vacía" en la que no coincidieron las condiciones.

Un caso alternativo sería "omitir" el $count etapa y simplemente permita que regresen los documentos coincidentes. Esto permite una fácil identificación, pero como una "matriz incrustada en el documento", debe tener en cuenta la cantidad de "superposiciones" que se devolverán como documentos completos y que esto no cause una violación del límite BSON de 16 MB. En la mayoría de los casos, esto debería estar bien, pero para los casos en los que espera una gran cantidad de superposiciones para un documento determinado, este puede ser un caso real. Así que es realmente algo más a tener en cuenta.

El $lookup etapa de canalización en este contexto "siempre" devolverá una matriz en el resultado, incluso si está vacío. El nombre de la propiedad de salida "que se fusiona" con el documento existente será "overlaps" como se especifica en el "as" propiedad al $lookup escenario.

Siguiendo el $lookup , podemos hacer un simple $match con una expresión de consulta regular que emplea $exists prueba para el 0 valor de índice de la matriz de salida. Donde realmente hay algo de contenido en la matriz y, por lo tanto, se "superpone", la condición será verdadera y se devolverá el documento, mostrando el recuento o los documentos "superpuestos" según su selección.

Otras versiones - Consultas para "unirse"

El caso alternativo en el que su MongoDB carece de este soporte es "unirse" manualmente emitiendo las mismas condiciones de consulta descritas anteriormente 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 es esencialmente la misma lógica, excepto que en realidad necesitamos "volver a la base de datos" para emitir la consulta para que coincida con los documentos superpuestos. Esta vez son los "operadores de consulta" que se usan para encontrar dónde se encuentran los valores del documento actual entre los del documento procesado.

Debido a que los resultados ya se devuelven desde el servidor, no existe una restricción de límite de BSON para agregar contenido a la salida. Es posible que tenga restricciones de memoria, pero ese es otro problema. En pocas palabras, devolvemos la matriz en lugar del cursor a través de .toArray() por lo que tenemos los documentos coincidentes y podemos simplemente acceder a la longitud de la matriz para obtener un recuento. Si en realidad no necesita los documentos, use .count() en lugar de .find() es mucho más eficiente ya que no hay sobrecarga de obtención de documentos.

Luego, la salida simplemente se fusiona con el documento existente, donde la otra distinción importante es que, dado que estas son "consultas múltiples", no hay forma de proporcionar la condición de que deben "coincidir" con algo. Entonces, esto nos deja considerando que habrá resultados en los que el conteo (o la longitud de la matriz) sea 0 y todo lo que podemos hacer en este momento es devolver un null valor que podemos luego .filter() de la matriz de resultados. Otros métodos de iteración del cursor emplean el mismo principio básico de "descartar" resultados donde no los queremos. Pero nada impide que la consulta se ejecute en el servidor y este filtrado es un "procesamiento posterior" de una forma u otra.

Reducción de la complejidad

Por lo tanto, los enfoques anteriores funcionan con la estructura descrita, pero, por supuesto, la complejidad general requiere que, para cada documento, debe examinar esencialmente todos los demás documentos de la colección para buscar superposiciones. Por lo tanto, al usar $lookup permite algo de "eficiencia" en la reducción de los gastos generales de transporte y respuesta, todavía sufre el mismo problema de que todavía está comparando esencialmente cada documento con todo.

Una solución mejor "donde usted puede hacer que encaje" es almacenar en su lugar un "valor duro"* representativo del intervalo en cada documento. Por ejemplo, podríamos "presumir" que hay períodos sólidos de "reserva" de una hora dentro de un día para un total de 24 períodos de reserva. Este "podría" representarse de la siguiente manera:

{ "_id": "A", "booking": [ 10, 11, 12 ] }
{ "_id": "B", "booking": [ 12, 13, 14 ] }
{ "_id": "C", "booking": [ 7, 8 ] }
{ "_id": "D", "booking": [ 9, 10, 11 ] }

Con datos organizados de esa manera donde había un indicador establecido para el intervalo, la complejidad se reduce considerablemente, ya que en realidad es solo una cuestión de "agrupar" el valor del intervalo de la matriz dentro de la "booking" propiedad:

db.booking.aggregate([
  { "$unwind": "$booking" },
  { "$group": { "_id": "$booking", "docs": { "$push": "$_id" } } },
  { "$match": { "docs.1": { "$exists": true } } }
])

Y la salida:

{ "_id" : 10, "docs" : [ "A", "D" ] }
{ "_id" : 11, "docs" : [ "A", "D" ] }
{ "_id" : 12, "docs" : [ "A", "B" ] }

Eso identifica correctamente eso para el 10 y 11 intervalos ambos "A" y "D" contener la superposición, mientras que "B" y "A" superposición en 12 . Otros intervalos y coincidencias de documentos se excluyen mediante el mismo $exists prueba excepto esta vez en el 1 índice (o el segundo elemento de la matriz presente) para ver que había "más de un" documento en la agrupación, lo que indica una superposición.

Esto simplemente emplea el $unwind etapa de canalización de agregación para "deconstruir/desnormalizar" el contenido de la matriz para que podamos acceder a los valores internos para agrupar. Esto es exactamente lo que sucede en el $group etapa donde la "clave" proporcionada es la identificación del intervalo de reserva y el $push El operador se utiliza para "recopilar" datos sobre el documento actual que se encontró en ese grupo. El $match es como se explicó anteriormente.

Esto incluso se puede ampliar para una presentación 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" }  
  }}
])

Con salida:

{ "_id" : "B", "intervals" : [ 12 ] }
{ "_id" : "D", "intervals" : [ 10, 11 ] }
{ "_id" : "A", "intervals" : [ 10, 11, 12 ] }

Es una demostración simplificada, pero donde los datos que tiene le permitirían el tipo de análisis requerido, entonces este es el enfoque mucho más eficiente. Por lo tanto, si puede mantener la "granularidad" fijada en intervalos "establecidos" que se pueden registrar comúnmente en cada documento, entonces el análisis y la generación de informes pueden utilizar este último enfoque para identificar rápida y eficientemente tales superposiciones.

Esencialmente, así es como implementaría lo que básicamente mencionó como un enfoque "mejor" de todos modos, y el primero es una mejora "ligera" sobre lo que originalmente teorizó. Vea cuál se adapta realmente a su situación, pero esto debería explicar la implementación y las diferencias.