ChatGPT解决这个技术问题 Extra ChatGPT

Query for documents where array size is greater than 1

I have a MongoDB collection with documents in the following format:

{
  "_id" : ObjectId("4e8ae86d08101908e1000001"),
  "name" : ["Name"],
  "zipcode" : ["2223"]
}
{
  "_id" : ObjectId("4e8ae86d08101908e1000002"),
  "name" : ["Another ", "Name"],
  "zipcode" : ["2224"]
}

I can currently get documents that match a specific array size:

db.accommodations.find({ name : { $size : 2 }})

This correctly returns the documents with 2 elements in the name array. However, I can't do a $gt command to return all documents where the name field has an array size of greater than 2:

db.accommodations.find({ name : { $size: { $gt : 1 } }})

How can I select all documents with a name array of a size greater than one (preferably without having to modify the current data structure)?

The newer versions of MongoDB have the $size operator; you should check out @tobia's answer
Actual solution: FooArray:{$gt:{$size:'length'}} --> lenght can be any number
@SergiNadal: I don't think this FooArray:{$gt:{$size:'length'}} is working! Well at least on nested object which is an array person:{ids:[123,456]}
Arrays should have a plural name so your array field name should be named names.

J
JohnnyHK

There's a more efficient way to do this in MongoDB 2.2+ now that you can use numeric array indexes (0 based) in query object keys.

// Find all docs that have at least two name array elements.
db.accommodations.find({'name.1': {$exists: true}})

You can support this query with an index that uses a partial filter expression (requires 3.2+):

// index for at least two name array elements
db.accommodations.createIndex(
    {'name.1': 1},
    {partialFilterExpression: {'name.1': {$exists: true}}}
);

Could somebody please explain how to index this.
I'm really impressed with how effective this is and also how 'out of the box' you were thinking to find this solution. This works on 2.6, as well.
Works on 3.0 aswell. Thank you so much for finding this.
@JoseRicardoBustosM. That would find the docs where name contains at least 1 element, but the OP was looking for greater than 1.
It'd be helpful to mention in the answer that indexation is 0-based here.
A
Ahmed Ashour

Update:

For mongodb versions 2.2+ more efficient way to do this described by @JohnnyHK in another answer.

Using $where db.accommodations.find( { $where: "this.name.length > 1" } );

But...

Javascript executes more slowly than the native operators listed on this page, but is very flexible. See the server-side processing page for more information.

Create extra field NamesArrayLength, update it with names array length and then use in queries: db.accommodations.find({"NamesArrayLength": {$gt: 1} });

It will be better solution, and will work much faster (you can create index on it).


Great, that was perfect thank you. Although I actually have some documents that don't have a name so had to modify the query to be: db.accommodations.find( { $where: "if (this.name && this.name.length > 1) {return this; } "} );
you are welcome, yes you can use any javascript in $where, it is very flexible.
@emson I would think it would be quicker to do something like { "name": {$exists:1}, $where: "this.name.lenght > 1"} ... minimizing the part in the slower javascript query. I assume that works and that the $exists would have higher precedence.
I had no idea you could embed javascript in the query, json can be cumbersome. Many of these queries are one time only entered by hand so optimization is not required. I'll use this trick often +1
After adding/removing elements from the Array, we need to update the count of "NamesArrayLength". Can this done in a single query? Or it requires 2 queries, one for updating the array and another for updating the count?
T
Tobia

I believe this is the fastest query that answers your question, because it doesn't use an interpreted $where clause:

{$nor: [
    {name: {$exists: false}},
    {name: {$size: 0}},
    {name: {$size: 1}}
]}

It means "all documents except those without a name (either non existant or empty array) or with just one name."

Test:

> db.test.save({})
> db.test.save({name: []})
> db.test.save({name: ['George']})
> db.test.save({name: ['George', 'Raymond']})
> db.test.save({name: ['George', 'Raymond', 'Richard']})
> db.test.save({name: ['George', 'Raymond', 'Richard', 'Martin']})
> db.test.find({$nor: [{name: {$exists: false}}, {name: {$size: 0}}, {name: {$size: 1}}]})
{ "_id" : ObjectId("511907e3fb13145a3d2e225b"), "name" : [ "George", "Raymond" ] }
{ "_id" : ObjectId("511907e3fb13145a3d2e225c"), "name" : [ "George", "Raymond", "Richard" ] }
{ "_id" : ObjectId("511907e3fb13145a3d2e225d"), "name" : [ "George", "Raymond", "Richard", "Martin" ] }
>

@viren I don't know. This was certainly better than Javascript solutions, but for newer MongoDB you should probably use {'name.1': {$exists: true}}
@Tobia my first use was $exists only but it actually use whole table scan so very slow. db.test.find({"name":"abc","d.5":{$exists:true},"d.6":{$exists:true}}) "nReturned" : 46525, "executionTimeMillis" : 167289, "totalKeysExamined" : 10990840, "totalDocsExamined" : 10990840, "inputStage" : { "stage" : "IXSCAN", "keyPattern" : { "name" : 1, "d" : 1 }, "indexName" : "name_1_d_1", "direction" : "forward", "indexBounds" : { "name" : [ "[\"abc\", \"abc\"]" ], "d" : [ "[MinKey, MaxKey]" ] } } If you see it scanned whole table.
Would be nice to update the answer to recommend other alternatives (like 'name.1': {$exists: true}}, and also because this is hardcoded for "1" and doesn't scale to an arbitrary or parametric minimum array length.
This may be fast but falls apart if you're looking for lists > N, where N isn't small.
This doesn't work if you're looking for a nested array where the inside array has length of at least 2, but {'foo.bar.details.2': {$exists: true}} will find those.
a
arun

You can use aggregate, too:

db.accommodations.aggregate(
[
     {$project: {_id:1, name:1, zipcode:1, 
                 size_of_name: {$size: "$name"}
                }
     },
     {$match: {"size_of_name": {$gt: 1}}}
])

// you add "size_of_name" to transit document and use it to filter the size of the name


This solution is the most general, along with @JohnnyHK's since it can be used for any array size.
if i want to use "size_of_name" inside projection then how can i do that ?? Actually i want to use $slice inside projection where its value is equal to $slice : [0, "size_of_name" - skip] ??
s
s7vr

You can use $expr ( 3.6 mongo version operator ) to use aggregation functions in regular query.

Compare query operators vs aggregation comparison operators.

db.accommodations.find({$expr:{$gt:[{$size:"$name"}, 1]}})

How would you pass instead of $name an array that is a subdocument, for example in a "person" record, passport.stamps? I tried various quoting combinations but I get "The argument to $size must be an array, but was of type: string/missing".
@DanDascalescu It looks like stamps is not present in all documents. You can use ifNull to output empty array when the stamps is not present. Something like db.col.find({$expr:{$gt:[{$size:{$ifNull:["$passport.stamps", []]}}, 1]}})
A
Aman Goel

Try to do something like this:

db.getCollection('collectionName').find({'ArrayName.1': {$exists: true}})

1 is number, if you want to fetch record greater than 50 then do ArrayName.50 Thanks.


The same answer was given three years earlier.
can we put some dynamic number like "ArrayName." inside the query?
Yes you can use any number. If you want to fetch record greater than N then pass n.
D
Dhaval Chaudhary

MongoDB 3.6 include $expr https://docs.mongodb.com/manual/reference/operator/query/expr/

You can use $expr in order to evaluate an expression inside a $match, or find.

{ $match: {
           $expr: {$gt: [{$size: "$yourArrayField"}, 0]}
         }
}

or find

collection.find({$expr: {$gte: [{$size: "$yourArrayField"}, 0]}});

While correct, this is a duplicate answer. See stackoverflow.com/a/48410837/2424641 by @user2683814
l
lesolorzanov

None of the above worked for me. This one did so I'm sharing it:

db.collection.find( {arrayName : {$exists:true}, $where:'this.arrayName.length>1'} )

javascript executes more slowly than the native operators provided by mongodb, but it's very flexible. see:stackoverflow.com/a/7811259/2893073, So the final solution is : stackoverflow.com/a/15224544/2893073
Y
Yadvendar
db.accommodations.find({"name":{"$exists":true, "$ne":[], "$not":{"$size":1}}})

This doesn't scale well to other minimum sizes (say, 10).
same as first answer
u
uhfocuz

Although the above answers all work, What you originally tried to do was the correct way, however you just have the syntax backwards (switch "$size" and "$gt")..

Correct:

db.collection.find({items: {$gt: {$size: 1}}})

I don't see why so many downvotes - this works perfectly for me!
Works perfectly fine, v 4.2.5
always post version when posting solutions like that. not working on 4.2
As of 4.4, the "correct" is not working. Just because $gt parameter must be a number. Either $size parameter must be a number.
Works fine on "4.4.8"
B
Barrard

I found this solution, to find items with an array field greater than certain length

db.allusers.aggregate([
  {$match:{username:{$exists:true}}},
  {$project: { count: { $size:"$locations.lat" }}},
  {$match:{count:{$gt:20}}}
])

The first $match aggregate uses an argument thats true for all the documents. If blank, i would get

"errmsg" : "exception: The argument to $size must be an Array, but was of type: EOO"

This is essentially the same answer as this one, provided 2 years earlier.
N
Nagabhushan Baddi

You can MongoDB aggregation to do the task:

db.collection.aggregate([
  {
    $addFields: {
      arrayLength: {$size: '$array'}
    },
  },
  {
    $match: {
      arrayLength: {$gt: 1}
    },
  },
])