Encuentra registros MongoDB donde el campo de matriz no está vacío

503

Todos mis registros tienen un campo llamado "imágenes". Este campo es una matriz de cadenas.

Ahora quiero los 10 registros más nuevos donde esta matriz NO ESTÁ vacía.

He buscado en Google, pero curiosamente no he encontrado mucho en esto. Leí en la opción $ where, pero me preguntaba qué tan lento es para las funciones nativas, y si hay una mejor solución.

E incluso entonces, eso no funciona:

ME.find({$where: 'this.pictures.length > 0'}).sort('-created').limit(10).execFind()

No devuelve nada Salir this.picturessin el bit de longitud funciona, pero también devuelve registros vacíos, por supuesto.

skerit
fuente

Respuestas:

831

Si también tiene documentos que no tienen la clave, puede usar:

ME.find({ pictures: { $exists: true, $not: {$size: 0} } })

MongoDB no usa índices si $ size está involucrado, así que aquí hay una mejor solución:

ME.find({ pictures: { $exists: true, $ne: [] } })

Desde el lanzamiento de MongoDB 2.6, puede comparar con el operador, $gtpero podría generar resultados inesperados (puede encontrar una explicación detallada en esta respuesta ):

ME.find({ pictures: { $gt: [] } })
Chris '
fuente
66
Para mí, ese es el enfoque correcto, ya que se asegura de que la matriz exista y no esté vacía.
LeandroCR
¿Cómo puedo lograr la misma funcionalidad usando?mongoengine
Rohit Khatri
54
CUIDADO, ME.find({ pictures: { $gt: [] } })ES PELIGROSO, incluso en las versiones más nuevas de MongoDB. Si tiene un índice en su campo de lista y ese índice se utiliza durante la consulta, obtendrá resultados inesperados. Por ejemplo: db.doc.find({'nums': { $gt: [] }}).hint({ _id: 1 }).count()devuelve el número correcto, mientras que db.doc.find({'nums': { $gt: [] }}).hint({ nums: 1 }).count()devuelve 0.
wojcikstefan
1
Vea mi respuesta detallada a continuación para saber por qué esto podría no funcionar para usted: stackoverflow.com/a/42601244/1579058
wojcikstefan
66
El comentario de @ wojcikstefan debe ser votado para evitar que la gente use la última sugerencia que, de hecho, en ciertas circunstancias no devuelve documentos coincidentes.
Thomas Jung el
181

Después de mirar un poco más, especialmente en los documentos mongodb, y trozos desconcertantes juntos, esta fue la respuesta:

ME.find({pictures: {$exists: true, $not: {$size: 0}}})
skerit
fuente
27
Esto no funciona No sé si esto funcionó anteriormente, pero también devolverá objetos que no tienen la clave 'imágenes'.
rdsoze
17
Increíble cómo esta respuesta tiene 63 votos a favor, cuando de hecho lo que @rdsoze dijo es cierto: la consulta también devolverá registros que no tienen el picturescampo.
Dan Dascalescu
55
Tenga cuidado, mongoDB no usará índices si $ size está involucrado en el enlace . Sería mejor incluir {$ ne: []} y posiblemente {$ ne: null}.
Levente Dobson
17
@rdsoze la primera línea de la pregunta dice "Todos mis registros tienen un campo llamado" imágenes ". Este campo es una matriz" . Además, este es un escenario perfectamente realista y común. Esta respuesta no es incorrecta, funciona para la pregunta exactamente como está escrita, y criticarla o rechazarla por el hecho de que no resuelve un problema diferente es una tontería.
Mark Amery
1
@Cec Toda la documentación dice que si usa $ size en la consulta, no usará ningún índice para obtener resultados más rápidos. Entonces, si tiene un índice en ese campo y desea usarlo, siga otros enfoques como {$ ne: []}, si eso funciona para usted, usará su índice.
Levente Dobson
108

Esto también podría funcionar para usted:

ME.find({'pictures.0': {$exists: true}});
tenbatsu
fuente
2
¡Agradable! Esto también le permite verificar un tamaño mínimo. ¿Sabes si las matrices siempre se indexan secuencialmente? ¿Habría algún caso donde pictures.2exista pero pictures.1no exista ?
anushr
2
El $existsoperador es un booleano, no un desplazamiento. @tenbatsu debería estar usando en truelugar de 1.
ekillaby
2
@anushr Would there ever be a case where pictures.2 exists but pictures.1 does not? Sí, ese caso podría suceder.
The Bndr
@TheBndr Eso solo podría suceder si pictureses un sub-documento, no una matriz. por ejemplopictures: {'2': 123}
JohnnyHK el
44
Esto es agradable e intuitivo, pero tenga cuidado si el rendimiento es importante: realizará un análisis completo de la colección incluso si tiene un índice activado pictures.
wojcikstefan
35

Al realizar consultas, le interesan dos cosas: precisión y rendimiento. Con eso en mente, probé algunos enfoques diferentes en MongoDB v3.0.14.

TL; DR db.doc.find({ nums: { $gt: -Infinity }})es el más rápido y confiable (al menos en la versión de MongoDB que probé).

EDITAR: ¡Esto ya no funciona en MongoDB v3.6! Vea los comentarios en esta publicación para una posible solución.

Preparar

Inserté 1k documentos sin un campo de lista, 1k documentos con una lista vacía y 5 documentos con una lista no vacía.

for (var i = 0; i < 1000; i++) { db.doc.insert({}); }
for (var i = 0; i < 1000; i++) { db.doc.insert({ nums: [] }); }
for (var i = 0; i < 5; i++) { db.doc.insert({ nums: [1, 2, 3] }); }
db.doc.createIndex({ nums: 1 });

Reconozco que esto no es una escala suficiente para tomar el rendimiento tan en serio como lo estoy en las pruebas a continuación, pero es suficiente para presentar la exactitud de varias consultas y el comportamiento de los planes de consulta elegidos.

Pruebas

db.doc.find({'nums': {'$exists': true}}) devuelve resultados incorrectos (para lo que estamos tratando de lograr).

MacBook-Pro(mongod-3.0.14) test> db.doc.find({'nums': {'$exists': true}}).count()
1005

-

db.doc.find({'nums.0': {'$exists': true}})devuelve resultados correctos, pero también es lento con un análisis de recopilación completo ( COLLSCANetapa de aviso en la explicación).

MacBook-Pro(mongod-3.0.14) test> db.doc.find({'nums.0': {'$exists': true}}).count()
5
MacBook-Pro(mongod-3.0.14) test> db.doc.find({'nums.0': {'$exists': true}}).explain()
{
  "queryPlanner": {
    "plannerVersion": 1,
    "namespace": "test.doc",
    "indexFilterSet": false,
    "parsedQuery": {
      "nums.0": {
        "$exists": true
      }
    },
    "winningPlan": {
      "stage": "COLLSCAN",
      "filter": {
        "nums.0": {
          "$exists": true
        }
      },
      "direction": "forward"
    },
    "rejectedPlans": [ ]
  },
  "serverInfo": {
    "host": "MacBook-Pro",
    "port": 27017,
    "version": "3.0.14",
    "gitVersion": "08352afcca24bfc145240a0fac9d28b978ab77f3"
  },
  "ok": 1
}

-

db.doc.find({'nums': { $exists: true, $gt: { '$size': 0 }}})devuelve resultados incorrectos. Eso se debe a un escaneo de índice no válido que no avanza documentos. Es probable que sea precisa pero lenta sin el índice.

MacBook-Pro(mongod-3.0.14) test> db.doc.find({'nums': { $exists: true, $gt: { '$size': 0 }}}).count()
0
MacBook-Pro(mongod-3.0.14) test> db.doc.find({'nums': { $exists: true, $gt: { '$size': 0 }}}).explain('executionStats').executionStats.executionStages
{
  "stage": "KEEP_MUTATIONS",
  "nReturned": 0,
  "executionTimeMillisEstimate": 0,
  "works": 2,
  "advanced": 0,
  "needTime": 0,
  "needFetch": 0,
  "saveState": 0,
  "restoreState": 0,
  "isEOF": 1,
  "invalidates": 0,
  "inputStage": {
    "stage": "FETCH",
    "filter": {
      "$and": [
        {
          "nums": {
            "$gt": {
              "$size": 0
            }
          }
        },
        {
          "nums": {
            "$exists": true
          }
        }
      ]
    },
    "nReturned": 0,
    "executionTimeMillisEstimate": 0,
    "works": 1,
    "advanced": 0,
    "needTime": 0,
    "needFetch": 0,
    "saveState": 0,
    "restoreState": 0,
    "isEOF": 1,
    "invalidates": 0,
    "docsExamined": 0,
    "alreadyHasObj": 0,
    "inputStage": {
      "stage": "IXSCAN",
      "nReturned": 0,
      "executionTimeMillisEstimate": 0,
      "works": 1,
      "advanced": 0,
      "needTime": 0,
      "needFetch": 0,
      "saveState": 0,
      "restoreState": 0,
      "isEOF": 1,
      "invalidates": 0,
      "keyPattern": {
        "nums": 1
      },
      "indexName": "nums_1",
      "isMultiKey": true,
      "direction": "forward",
      "indexBounds": {
        "nums": [
          "({ $size: 0.0 }, [])"
        ]
      },
      "keysExamined": 0,
      "dupsTested": 0,
      "dupsDropped": 0,
      "seenInvalidated": 0,
      "matchTested": 0
    }
  }
}

-

db.doc.find({'nums': { $exists: true, $not: { '$size': 0 }}})devuelve resultados correctos, pero el rendimiento es malo. Técnicamente realiza un escaneo de índice, pero luego avanza todos los documentos y luego tiene que filtrarlos).

MacBook-Pro(mongod-3.0.14) test> db.doc.find({'nums': { $exists: true, $not: { '$size': 0 }}}).count()
5
MacBook-Pro(mongod-3.0.14) test> db.doc.find({'nums': { $exists: true, $not: { '$size': 0 }}}).explain('executionStats').executionStats.executionStages
{
  "stage": "KEEP_MUTATIONS",
  "nReturned": 5,
  "executionTimeMillisEstimate": 0,
  "works": 2016,
  "advanced": 5,
  "needTime": 2010,
  "needFetch": 0,
  "saveState": 15,
  "restoreState": 15,
  "isEOF": 1,
  "invalidates": 0,
  "inputStage": {
    "stage": "FETCH",
    "filter": {
      "$and": [
        {
          "nums": {
            "$exists": true
          }
        },
        {
          "$not": {
            "nums": {
              "$size": 0
            }
          }
        }
      ]
    },
    "nReturned": 5,
    "executionTimeMillisEstimate": 0,
    "works": 2016,
    "advanced": 5,
    "needTime": 2010,
    "needFetch": 0,
    "saveState": 15,
    "restoreState": 15,
    "isEOF": 1,
    "invalidates": 0,
    "docsExamined": 2005,
    "alreadyHasObj": 0,
    "inputStage": {
      "stage": "IXSCAN",
      "nReturned": 2005,
      "executionTimeMillisEstimate": 0,
      "works": 2015,
      "advanced": 2005,
      "needTime": 10,
      "needFetch": 0,
      "saveState": 15,
      "restoreState": 15,
      "isEOF": 1,
      "invalidates": 0,
      "keyPattern": {
        "nums": 1
      },
      "indexName": "nums_1",
      "isMultiKey": true,
      "direction": "forward",
      "indexBounds": {
        "nums": [
          "[MinKey, MaxKey]"
        ]
      },
      "keysExamined": 2015,
      "dupsTested": 2015,
      "dupsDropped": 10,
      "seenInvalidated": 0,
      "matchTested": 0
    }
  }
}

-

db.doc.find({'nums': { $exists: true, $ne: [] }})devuelve resultados correctos y es un poco más rápido, pero el rendimiento aún no es el ideal. Utiliza IXSCAN que solo avanza documentos con un campo de lista existente, pero luego tiene que filtrar las listas vacías una por una.

MacBook-Pro(mongod-3.0.14) test> db.doc.find({'nums': { $exists: true, $ne: [] }}).count()
5
MacBook-Pro(mongod-3.0.14) test> db.doc.find({'nums': { $exists: true, $ne: [] }}).explain('executionStats').executionStats.executionStages
{
  "stage": "KEEP_MUTATIONS",
  "nReturned": 5,
  "executionTimeMillisEstimate": 0,
  "works": 1018,
  "advanced": 5,
  "needTime": 1011,
  "needFetch": 0,
  "saveState": 15,
  "restoreState": 15,
  "isEOF": 1,
  "invalidates": 0,
  "inputStage": {
    "stage": "FETCH",
    "filter": {
      "$and": [
        {
          "$not": {
            "nums": {
              "$eq": [ ]
            }
          }
        },
        {
          "nums": {
            "$exists": true
          }
        }
      ]
    },
    "nReturned": 5,
    "executionTimeMillisEstimate": 0,
    "works": 1017,
    "advanced": 5,
    "needTime": 1011,
    "needFetch": 0,
    "saveState": 15,
    "restoreState": 15,
    "isEOF": 1,
    "invalidates": 0,
    "docsExamined": 1005,
    "alreadyHasObj": 0,
    "inputStage": {
      "stage": "IXSCAN",
      "nReturned": 1005,
      "executionTimeMillisEstimate": 0,
      "works": 1016,
      "advanced": 1005,
      "needTime": 11,
      "needFetch": 0,
      "saveState": 15,
      "restoreState": 15,
      "isEOF": 1,
      "invalidates": 0,
      "keyPattern": {
        "nums": 1
      },
      "indexName": "nums_1",
      "isMultiKey": true,
      "direction": "forward",
      "indexBounds": {
        "nums": [
          "[MinKey, undefined)",
          "(undefined, [])",
          "([], MaxKey]"
        ]
      },
      "keysExamined": 1016,
      "dupsTested": 1015,
      "dupsDropped": 10,
      "seenInvalidated": 0,
      "matchTested": 0
    }
  }
}

-

db.doc.find({'nums': { $gt: [] }})ES PELIGROSO PORQUE DEPENDER DEL ÍNDICE UTILIZADO PUEDE DAR RESULTADOS INESPERADOS. Eso se debe a un escaneo de índice no válido que no avanza documentos.

MacBook-Pro(mongod-3.0.14) test> db.doc.find({'nums': { $gt: [] }}).count()
0
MacBook-Pro(mongod-3.0.14) test> db.doc.find({'nums': { $gt: [] }}).hint({ nums: 1 }).count()
0
MacBook-Pro(mongod-3.0.14) test> db.doc.find({'nums': { $gt: [] }}).hint({ _id: 1 }).count()
5

MacBook-Pro(mongod-3.0.14) test> db.doc.find({'nums': { $gt: [] }}).explain('executionStats').executionStats.executionStages
{
  "stage": "KEEP_MUTATIONS",
  "nReturned": 0,
  "executionTimeMillisEstimate": 0,
  "works": 1,
  "advanced": 0,
  "needTime": 0,
  "needFetch": 0,
  "saveState": 0,
  "restoreState": 0,
  "isEOF": 1,
  "invalidates": 0,
  "inputStage": {
    "stage": "FETCH",
    "filter": {
      "nums": {
        "$gt": [ ]
      }
    },
    "nReturned": 0,
    "executionTimeMillisEstimate": 0,
    "works": 1,
    "advanced": 0,
    "needTime": 0,
    "needFetch": 0,
    "saveState": 0,
    "restoreState": 0,
    "isEOF": 1,
    "invalidates": 0,
    "docsExamined": 0,
    "alreadyHasObj": 0,
    "inputStage": {
      "stage": "IXSCAN",
      "nReturned": 0,
      "executionTimeMillisEstimate": 0,
      "works": 1,
      "advanced": 0,
      "needTime": 0,
      "needFetch": 0,
      "saveState": 0,
      "restoreState": 0,
      "isEOF": 1,
      "invalidates": 0,
      "keyPattern": {
        "nums": 1
      },
      "indexName": "nums_1",
      "isMultiKey": true,
      "direction": "forward",
      "indexBounds": {
        "nums": [
          "([], BinData(0, ))"
        ]
      },
      "keysExamined": 0,
      "dupsTested": 0,
      "dupsDropped": 0,
      "seenInvalidated": 0,
      "matchTested": 0
    }
  }
}

-

db.doc.find({'nums.0’: { $gt: -Infinity }}) devuelve resultados correctos, pero tiene un mal rendimiento (utiliza una exploración de recopilación completa).

MacBook-Pro(mongod-3.0.14) test> db.doc.find({'nums.0': { $gt: -Infinity }}).count()
5
MacBook-Pro(mongod-3.0.14) test> db.doc.find({'nums.0': { $gt: -Infinity }}).explain('executionStats').executionStats.executionStages
{
  "stage": "COLLSCAN",
  "filter": {
    "nums.0": {
      "$gt": -Infinity
    }
  },
  "nReturned": 5,
  "executionTimeMillisEstimate": 0,
  "works": 2007,
  "advanced": 5,
  "needTime": 2001,
  "needFetch": 0,
  "saveState": 15,
  "restoreState": 15,
  "isEOF": 1,
  "invalidates": 0,
  "direction": "forward",
  "docsExamined": 2005
}

-

db.doc.find({'nums': { $gt: -Infinity }})¡Sorprendentemente, esto funciona muy bien! Da los resultados correctos y es rápido, avanzando 5 documentos desde la fase de exploración del índice.

MacBook-Pro(mongod-3.0.14) test> db.doc.find({'nums': { $gt: -Infinity }}).explain('executionStats').executionStats.executionStages
{
  "stage": "FETCH",
  "nReturned": 5,
  "executionTimeMillisEstimate": 0,
  "works": 16,
  "advanced": 5,
  "needTime": 10,
  "needFetch": 0,
  "saveState": 0,
  "restoreState": 0,
  "isEOF": 1,
  "invalidates": 0,
  "docsExamined": 5,
  "alreadyHasObj": 0,
  "inputStage": {
    "stage": "IXSCAN",
    "nReturned": 5,
    "executionTimeMillisEstimate": 0,
    "works": 15,
    "advanced": 5,
    "needTime": 10,
    "needFetch": 0,
    "saveState": 0,
    "restoreState": 0,
    "isEOF": 1,
    "invalidates": 0,
    "keyPattern": {
      "nums": 1
    },
    "indexName": "nums_1",
    "isMultiKey": true,
    "direction": "forward",
    "indexBounds": {
      "nums": [
        "(-inf.0, inf.0]"
      ]
    },
    "keysExamined": 15,
    "dupsTested": 15,
    "dupsDropped": 10,
    "seenInvalidated": 0,
    "matchTested": 0
  }
}
wojcikstefan
fuente
Gracias por tu respuesta muy detallada @wojcikstefan. Desafortunadamente, su solución sugerida no parece funcionar en mi caso. Tengo una colección MongoDB 3.6.4 con 2 millones de documentos, la mayoría de ellos con una seen_eventsmatriz de cadenas, que también está indexada. { $gt: -Infinity }Al buscar con , inmediatamente recibo 0 documentos. Con el uso { $exists: true, $ne: [] }obtengo los documentos de 1,2 m más probables, y se pierde mucho tiempo en la etapa FETCH: gist.github.com/N-Coder/b9e89a925e895c605d84bfeed648d82c
NCode
Parece que tienes razón @Ncode: esto ya no funciona en MongoDB v3.6 :( Jugué con él durante unos minutos y esto es lo que encontré: 1. db.test_collection.find({"seen_events.0": {$exists: true}})es malo porque usa un escaneo de colección. 2. db.test_collection.find({seen_events: {$exists: true, $ne: []}})es . mala porque su IXSCAN coincide con todos los documentos y luego el filtrado se realiza en la fase lenta FETCH 3. lo mismo sucede con db.test_collection.find({seen_events: {$exists: true, $not: {$size: 0}}})4. todos los demás consultas devuelven resultados no válidos..
wojcikstefan
1
¡@NCode encontró una solución! Si está seguro de que todo el que no esté vacía seen_eventscontienen cadenas, puede utilizar la siguiente: db.test_collection.find({seen_events: {$gt: ''}}).count(). Para confirmar que funciona bien, echa un vistazo db.test_collection.find({seen_events: {$gt: ''}}).explain(true).executionStats. Probablemente pueda exigir que los eventos vistos sean cadenas a través de la validación de esquema: docs.mongodb.com/manual/core/schema-validation
wojcikstefan
¡Gracias! Todos los valores existentes son cadenas, así que lo probaré. También hay un error que discute este problema en el rastreador de
NCode
30

Comenzando con la versión 2.6, otra forma de hacerlo es comparar el campo con una matriz vacía:

ME.find({pictures: {$gt: []}})

Probándolo en el shell:

> db.ME.insert([
{pictures: [1,2,3]},
{pictures: []},
{pictures: ['']},
{pictures: [0]},
{pictures: 1},
{foobar: 1}
])

> db.ME.find({pictures: {$gt: []}})
{ "_id": ObjectId("54d4d9ff96340090b6c1c4a7"), "pictures": [ 1, 2, 3 ] }
{ "_id": ObjectId("54d4d9ff96340090b6c1c4a9"), "pictures": [ "" ] }
{ "_id": ObjectId("54d4d9ff96340090b6c1c4aa"), "pictures": [ 0 ] }

Por lo tanto, incluye correctamente los documentos donde picturestiene al menos un elemento de matriz, y excluye los documentos donde pictureses una matriz vacía, no una matriz, o falta.

JohnnyHK
fuente
77
CUIDADOSO, esta respuesta puede darte problemas si intentas usar índices. Hacer db.ME.createIndex({ pictures: 1 })y luego db.ME.find({pictures: {$gt: []}})devolverá cero resultados, al menos en MongoDB v3.0.14
wojcikstefan
@wojcikstefan Buena captura. Necesito echar un vistazo a esto.
JohnnyHK
5

Puede usar cualquiera de los siguientes para lograr esto.
Ambos también se encargan de no devolver un resultado para los objetos que no tienen la clave solicitada:

db.video.find({pictures: {$exists: true, $gt: {$size: 0}}})
db.video.find({comments: {$exists: true, $not: {$size: 0}}})
Paul Imisi
fuente
4

Recupere todos y solo los documentos donde 'imágenes' es una matriz y no está vacía

ME.find({pictures: {$type: 'array', $ne: []}})

Si usa una versión de MongoDb anterior a 3.2 , use en $type: 4lugar de $type: 'array'. Tenga en cuenta que esta solución ni siquiera usa $ size , por lo que no hay problemas con los índices ("Las consultas no pueden usar índices para la porción $ size de una consulta")

Otras soluciones, incluidas estas (respuesta aceptada):

ME.find ({imágenes: {$ existe: verdadero, $ no: {$ tamaño: 0}}}); ME.find ({imágenes: {$ existe: verdadero, $ ne: []}})

están mal , ya que regresan documentos, incluso si, por ejemplo, 'imágenes' es null, undefined, 0, etc.

SC1000
fuente
2

Utilice el $elemMatchoperador: de acuerdo con la documentación

El operador $ elemMatch coincide con los documentos que contienen un campo de matriz con al menos un elemento que coincide con todos los criterios de consulta especificados.

$elemMatchesse asegura de que el valor sea una matriz y que no esté vacío. Entonces la consulta sería algo así como

ME.find({ pictures: { $elemMatch: {$exists: true }}})

PD Una variante de este código se encuentra en el curso M121 de la Universidad MongoDB.

Andres Moreno
fuente
0

También puede usar el método auxiliar Existe sobre el operador Mongo $ existe

ME.find()
    .exists('pictures')
    .where('pictures').ne([])
    .sort('-created')
    .limit(10)
    .exec(function(err, results){
        ...
    });
Comer en joes
fuente
0
{ $where: "this.pictures.length > 1" }

use $ where y pase this.field_name.length que devuelve el tamaño del campo de matriz y verifíquelo comparando con el número. si alguna matriz tiene un valor que el tamaño de la matriz debe ser al menos 1. entonces todos los campos de la matriz tienen una longitud de más de uno, significa que tiene algunos datos en esa matriz

Prabhat Yadav
fuente
-8
ME.find({pictures: {$exists: true}}) 

Tan simple como eso, esto funcionó para mí.

Luis Fletes
fuente