944

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)?

styvane
  • 59,869
  • 19
  • 150
  • 156
emson
  • 10,005
  • 4
  • 24
  • 27

15 Answers15

1750

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}}}
);
JohnnyHK
  • 305,182
  • 66
  • 621
  • 471
613

Update:

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


  1. 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.

  1. 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).

Ahmed Ashour
  • 5,179
  • 10
  • 35
  • 56
Andrew Orsich
  • 52,935
  • 16
  • 139
  • 134
  • 6
    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; } "} ); – emson Oct 18 '11 at 17:51
  • you are welcome, yes you can use any javascript in `$where`, it is very flexible. – Andrew Orsich Oct 18 '11 at 17:58
  • 8
    @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. – nairbv Dec 13 '12 at 19:40
  • 1
    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 – pferrel Mar 20 '14 at 15:37
  • for the second answer, how to update the array length when I need to remove multiple elements? – Freedom May 31 '16 at 08:42
  • More efficient than `db.accommodations.find( { $where: "this.name && this.name.length > 1" } )`? – Cees Timmerman Jul 25 '16 at 08:54
  • 3
    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? – WarLord Dec 05 '16 at 13:44
177

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" ] }
>
Tobia
  • 17,856
  • 6
  • 74
  • 93
  • 13
    @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 Mar 14 '16 at 09:20
  • @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. – viren Mar 16 '16 at 04:33
  • 1
    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. – Dan Dascalescu Jul 20 '18 at 10:12
  • 2
    This may be fast but falls apart if you're looking for lists > N, where N isn't small. – Brandon Hill Jan 21 '19 at 20:14
  • 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. – Kyrstellaine Jan 28 '22 at 18:27
92

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

arun
  • 10,685
  • 6
  • 59
  • 81
  • This solution is the most general, along with @JohnnyHK's since it can be used for any array size. – arun Sep 23 '15 at 19:01
  • 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] ?? – Sudhanshu Gaur Jul 12 '16 at 20:27
88

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]}})
s7vr
  • 73,656
  • 11
  • 106
  • 127
  • 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"`. – Dan Dascalescu Jul 20 '18 at 10:23
  • 4
    @DanDascalescu It looks like stamps is not present in all documents. You can use [ifNull](https://docs.mongodb.com/manual/reference/operator/aggregation/ifNull/) to output empty array when the stamps is not present. Something like `db.col.find({$expr:{$gt:[{$size:{$ifNull:["$passport.stamps", []]}}, 1]}})` – s7vr Jul 20 '18 at 13:29
  • Thanks, I was looking to size an array key and this works perfectly! **db.getCollection("companies").find({ $expr: {$gt: [{$size:"$keywords"}, 0]} })** Also, you may change $gt to $eq to query those docs with no data to size: **db.getCollection("companies").find({ $expr: {$eq: [{$size:"$keywords"}, 0]} })** – Jcc.Sanabria Nov 13 '22 at 15:46
61

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.

Aman Goel
  • 3,351
  • 1
  • 21
  • 17
53

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]}});
Dhaval Chaudhary
  • 5,428
  • 2
  • 24
  • 39
Daniele Tassone
  • 2,104
  • 2
  • 17
  • 25
  • 2
    While correct, this is a duplicate answer. See https://stackoverflow.com/a/48410837/2424641 by @user2683814 – SteveB Aug 12 '19 at 15:39
46

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'} )
lesolorzanov
  • 3,536
  • 8
  • 35
  • 53
  • 2
    javascript executes more slowly than the native operators provided by mongodb, but it's very flexible. see:http://stackoverflow.com/a/7811259/2893073, So the final solution is : http://stackoverflow.com/a/15224544/2893073 – Eddy Sep 22 '16 at 02:06
32

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}}})
uhfocuz
  • 3,178
  • 2
  • 13
  • 27
Steffan Perry
  • 2,112
  • 1
  • 21
  • 21
28
db.accommodations.find({"name":{"$exists":true, "$ne":[], "$not":{"$size":1}}})
Yadvendar
  • 955
  • 9
  • 14
14

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 that's true for all the documents. Without it, I would get an error exception

"errmsg" : "exception: The argument to $size must be an Array, but was of type: EOO"
Barrard
  • 1,783
  • 1
  • 18
  • 25
11

You can MongoDB aggregation to do the task:

db.collection.aggregate([
  {
    $addFields: {
      arrayLength: {$size: '$array'}
    },
  },
  {
    $match: {
      arrayLength: {$gt: 1}
    },
  },
])
Nagabhushan Baddi
  • 1,164
  • 10
  • 18
3

this will work for you

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

This will work in Compass also. This is the fastest of all i have tried without indexing.

 {$expr: {
            $gt: [
              {
                $size: { "$ifNull": [ "$name", [] ] }
              },
              1
            ]
          }}
Hiren Namera
  • 390
  • 1
  • 10
1

you can use $expr to cover this

// $expr: Allows the use of aggregation expressions within the query language.
// syntax: {$expr: {<expression>}}

db.getCollection("person_service").find(
    { 
        "$expr" : { 
            // services is Array, find services length gt 3
            "$gt" : [
                { 
                    "$size" : "$services"
                }, 
                3.0
            ]
        }
    }
)
Wollens
  • 81
  • 5