Working with arrays in MongoDB

28-Dec-2020 Like this? Dislike this? Let me know

A collection (pun!) of snippets that I find useful. Some of these are "easier" to deal with if you simply $unwind the arrays but unwinding big arrays in big objects can become very expensive. In general, be kind: don't unwind. Use $map,$reduce, and $filter whenever you can!

Concatenating two or more arrays to produce one larger array

Very often you find yourself with an array of arrays:
{"items": [ [0, 1, 2], [0], [2,5] ]}
Here is a means to concatenate them
db.foo.aggregate([
// This is unfortunately not as straightforward as simply doing
//  {newList: {$concatArrays: "$items"}}
// because $items is a single array of arrays, NOT a vararg list of
// arrays that is expected in $concatArrays.  In other words,
//   {$concatArrays: "$items"}}
// is not the same thing as
//   {$concatArrays: [ arr1, arr2, arr3 ]}
// from a call signature perspective.
// 
// Solution:  We will "walk" $items with $reduce and call $concatArrays over and
// over to build the list.  We use $items as the input and overwrite it for output.
// This is a clever use of $reduce by treating an array as the output value instead of a simple scalar:
{$project: {items: {$reduce: {
		     input: "$items",
		     initialValue:[],
                     in:{$concatArrays: [ "$$value", "$$this"]}
                    }}
            }}
]);

{ "_id" : 0, "items" : [ 0, 1, 2, 0, 2, 5 ] }

Getting a set of unique entries in one or more arrays

Essentially, the same as above but taking the resulting array and passing it to $setUnion:
db.foo.aggregate([
    {$project: { items: {"$setUnion": [
		     {$reduce: {
			 input: "$items",
			 initialValue:[],
			 in:{$concatArrays: [ "$$value", "$$this"]}
		     }} ] }
		}}

]);

{ "_id" : 0, "items" : [ 0, 1, 2, 5 ] }

Finding where in an array of objects a field has a target value

Given a doc like this:
{_id:0, items: [ {a:0}, {a:0}, {b:7}, {a:1},{b:0},{b:7} ] }
At what index offsets does b = 7 (for example?). Challenges:
  1. Can't use $filter. You will lose the positional info.
  2. Can't use the dot operator $items.b to turn the array of objects into an array of just element b because those objects that do not contain b are not pushed to the output array which again messes up proper positioning and indexing. Don't understand? Try this:
    db.foo.aggregate([{$project: {WW: "$items.b"}}]);
    and look at the output array:
    {_id: 0, "WW" : [ 7, 0, 7 ] }
The solution is effectively this:
    matches = [];
    for(i = 0; i < items.size(); i++) {
	   if(items[i]['b'] == 7) {
	      matches.push(i);
           }
    }		   
db.foo.aggregate([
    {$project: {XX: {$reduce: {
	input: {$range:[0,{$size:"$items"}]},
	initialValue: [],  // Clever use of array as value.

// Good use of $let here to extract the object from the
// $items array at offset $$this.  It would be nice if MongoDB could let us do
//   {$arrayElemAt:["$items","$$this"]}.b
// or some such but you cannot so we have to get to our target field b in TWO steps.
// Almost looks like perl!  :-)  If $ee.b does not exist, $eq will eval it
// to null, which is fine.  If you need more control, use the $ifNull function
// to set a default value, e.g. {$ifNull:["$$ee.b",-1]}
// If the condition is true, append the index to the growing
// result array.  Note we must wrap the integer index with [] to make it an
// array of size 1 because $concatArrays need an array as input, not int. If
// the condition is not true, append a blank list [] (clever!)
        in: {
        $let: {
	  vars: { ee: {$arrayElemAt:["$items","$$this"] } },
          in: {$concatArrays: [ "$$value",
          {$cond: [ {$eq:[7,"$$ee.b"]} , ["$$this"] , [] ] }
                              ]}
        }}
    }}
}}
]);

{ XX: [ 2, 5 ] }

Like this? Dislike this? Let me know


Site copyright © 2013-2021 Buzz Moschetti. All rights reserved