From ac39cf3f3ded15ddec1a944f0942147891945b97 Mon Sep 17 00:00:00 2001 From: Andrew Aldridge Date: Wed, 3 Sep 2014 13:11:42 -0400 Subject: [PATCH] DOCS-2643: Clarify distinction between positional $ and projection $elemMatch. --- ...act-positional-projection-vs-elemmatch.rst | 10 + .../includes/ref-toc-operator-projection.yaml | 2 +- source/reference/method/cursor.sort.txt | 8 + .../operator/projection/elemMatch.txt | 308 +++++++++--------- .../operator/projection/positional.txt | 86 +++-- 5 files changed, 230 insertions(+), 184 deletions(-) create mode 100644 source/includes/fact-positional-projection-vs-elemmatch.rst diff --git a/source/includes/fact-positional-projection-vs-elemmatch.rst b/source/includes/fact-positional-projection-vs-elemmatch.rst new file mode 100644 index 00000000000..1412320ea08 --- /dev/null +++ b/source/includes/fact-positional-projection-vs-elemmatch.rst @@ -0,0 +1,10 @@ +Both the :projection:`$` operator and the :projection:`$elemMatch` operator project +a subset of elements from an array based on a condition. + +The :projection:`$` operator projects the array elements based on some condition +from the query statement. + +The :projection:`$elemMatch` projection operator takes an explicit condition +argument. This allows you to project based on a condition not in the query, or +if you need to project based on multiple fields in the array's subdocuments. +See :ref:`array-field-limitation` for an example. diff --git a/source/includes/ref-toc-operator-projection.yaml b/source/includes/ref-toc-operator-projection.yaml index 173fb9b43f4..bf2ae09d0ce 100644 --- a/source/includes/ref-toc-operator-projection.yaml +++ b/source/includes/ref-toc-operator-projection.yaml @@ -4,7 +4,7 @@ description: "Projects the first element in an array that matches the query cond --- name: ":projection:`$elemMatch`" file: /reference/operator/projection/elemMatch -description: "Projects only the first element from an array that matches the specified :projection:`$elemMatch` condition." +description: "Projects the first element in an array that matches the specified :projection:`$elemMatch` condition." --- name: ":projection:`$meta`" file: /reference/operator/projection/meta diff --git a/source/reference/method/cursor.sort.txt b/source/reference/method/cursor.sort.txt index 1fe2cf95368..b047e21d1b8 100644 --- a/source/reference/method/cursor.sort.txt +++ b/source/reference/method/cursor.sort.txt @@ -108,6 +108,14 @@ Or use :method:`~cursor.sort()` in conjunction with db.stocks.find().sort( { ticker: 1, date: -1 } ).limit(100) +.. _sort-with-projection: + +Interaction with :term:`Projection` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When a set of results are both sorted and projected, the MongoDB query engine +will always apply the sorting **first**. + Examples -------- diff --git a/source/reference/operator/projection/elemMatch.txt b/source/reference/operator/projection/elemMatch.txt index fa96e8d139b..196c8f48162 100644 --- a/source/reference/operator/projection/elemMatch.txt +++ b/source/reference/operator/projection/elemMatch.txt @@ -6,161 +6,171 @@ $elemMatch (projection) .. default-domain:: mongodb -.. projection:: $elemMatch - - .. versionadded:: 2.2 - - The :projection:`$elemMatch` projection operator limits the contents - of an array field that is included in the query results to contain - only the array element that matches the :projection:`$elemMatch` - condition. - - .. note:: - - - The elements of the array are documents. - - - If multiple elements match the :projection:`$elemMatch` - condition, the operator returns the **first** matching element - in the array. - - - The :projection:`$elemMatch` projection operator is similar to - the positional :projection:`$` projection operator. - - The examples on the :projection:`$elemMatch` projection operator - assumes a collection ``school`` with the following documents: - - .. code-block:: javascript - - { - _id: 1, - zipcode: "63109", - students: [ - { name: "john", school: 102, age: 10 }, - { name: "jess", school: 102, age: 11 }, - { name: "jeff", school: 108, age: 15 } - ] - } - { - _id: 2, - zipcode: "63110", - students: [ - { name: "ajax", school: 100, age: 7 }, - { name: "achilles", school: 100, age: 8 }, - ] - } - - { - _id: 3, - zipcode: "63109", - students: [ - { name: "ajax", school: 100, age: 7 }, - { name: "achilles", school: 100, age: 8 }, - ] - } - - { - _id: 4, - zipcode: "63109", - students: [ - { name: "barney", school: 102, age: 7 }, - ] - } - - .. example:: - - The following :method:`~db.collection.find()` operation - queries for all documents where the value of the ``zipcode`` - field is ``63109``. The :projection:`$elemMatch` projection - returns only the **first** matching element of the ``students`` - array where the ``school`` field has a value of ``102``: +Definition +---------- - .. code-block:: javascript - - db.schools.find( { zipcode: "63109" }, - { students: { $elemMatch: { school: 102 } } } ) - - The operation returns the following documents: - - .. code-block:: javascript - - { "_id" : 1, "students" : [ { "name" : "john", "school" : 102, "age" : 10 } ] } - { "_id" : 3 } - { "_id" : 4, "students" : [ { "name" : "barney", "school" : 102, "age" : 7 } ] } - - - For the document with ``_id`` equal to ``1``, the ``students`` - array contains multiple elements with the ``school`` field - equal to ``102``. However, the :projection:`$elemMatch` - projection returns only the first matching element from the - array. - - - The document with ``_id`` equal to ``3`` does not contain the - ``students`` field in the result since no element in its - ``students`` array matched the :projection:`$elemMatch` - condition. - - The :projection:`$elemMatch` projection can specify criteria on multiple - fields: - - .. example:: - - The following :method:`~db.collection.find()` operation - queries for all documents where the value of the ``zipcode`` - field is ``63109``. The projection includes the **first** - matching element of the ``students`` array where the ``school`` - field has a value of ``102`` **and** the ``age`` field is greater - than ``10``: - - .. code-block:: javascript - - db.schools.find( { zipcode: "63109" }, - { students: { $elemMatch: { school: 102, age: { $gt: 10} } } } ) - - The operation returns the three documents that have ``zipcode`` equal to ``63109``: - - .. code-block:: javascript - - { "_id" : 1, "students" : [ { "name" : "jess", "school" : 102, "age" : 11 } ] } - { "_id" : 3 } - { "_id" : 4 } - - Documents with ``_id`` equal to ``3`` and ``_id`` equal to ``4`` - do not contain the ``students`` field since no element matched - the :projection:`$elemMatch` criteria. - - When the :method:`~db.collection.find()` method includes a - :method:`~cursor.sort()`, the :method:`~db.collection.find()` method - applies the :method:`~cursor.sort()` to order the matching documents - **before** it applies the projection. - - If an array field contains multiple documents with the same field - name and the :method:`~db.collection.find()` method includes a - :method:`~cursor.sort()` on that repeating field, the returned - documents may not reflect the sort order because the - :method:`~cursor.sort()` was applied to the elements of the array - before the :projection:`$elemMatch` projection. - - .. example:: - - The following query includes a :method:`~cursor.sort()` to order - by descending ``students.age`` field: +.. projection:: $elemMatch - .. code-block:: javascript + .. versionadded:: 2.2 + + The :projection:`$elemMatch` operator limits the contents of an + ```` field from the query results to contain only the **first** + element matching the :projection:`$elemMatch` condition. + +Usage Considerations +-------------------- + +.. include:: /includes/fact-positional-projection-vs-elemmatch.rst + +Examples +-------- + +The examples on the :projection:`$elemMatch` projection operator +assumes a collection ``school`` with the following documents: + +.. code-block:: javascript + + { + _id: 1, + zipcode: "63109", + students: [ + { name: "john", school: 102, age: 10 }, + { name: "jess", school: 102, age: 11 }, + { name: "jeff", school: 108, age: 15 } + ] + } + { + _id: 2, + zipcode: "63110", + students: [ + { name: "ajax", school: 100, age: 7 }, + { name: "achilles", school: 100, age: 8 }, + ] + } + { + _id: 3, + zipcode: "63109", + students: [ + { name: "ajax", school: 100, age: 7 }, + { name: "achilles", school: 100, age: 8 }, + ] + } + { + _id: 4, + zipcode: "63109", + students: [ + { name: "barney", school: 102, age: 7 }, + { name: "ruth", school: 102, age: 16 }, + ] + } + +Zipcode Search +~~~~~~~~~~~~~~ + +The following :method:`~db.collection.find()` operation +queries for all documents where the value of the ``zipcode`` +field is ``63109``. The :projection:`$elemMatch` projection +returns only the **first** matching element of the ``students`` +array where the ``school`` field has a value of ``102``: + +.. code-block:: javascript + + db.schools.find( { zipcode: "63109" }, + { students: { $elemMatch: { school: 102 } } } ) + +The operation returns the following documents: + +.. code-block:: javascript + + { "_id" : 1, "students" : [ { "name" : "john", "school" : 102, "age" : 10 } ] } + { "_id" : 3 } + { "_id" : 4, "students" : [ { "name" : "barney", "school" : 102, "age" : 7 } ] } + +- For the document with ``_id`` equal to ``1``, the ``students`` + array contains multiple elements with the ``school`` field + equal to ``102``. However, the :projection:`$elemMatch` + projection returns only the first matching element from the + array. + +- The document with ``_id`` equal to ``3`` does not contain the + ``students`` field in the result since no element in its + ``students`` array matched the :projection:`$elemMatch` + condition. + +:projection:`$elemMatch` with Multiple Fields +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :projection:`$elemMatch` projection can specify criteria on multiple +fields: + +The following :method:`~db.collection.find()` operation +queries for all documents where the value of the ``zipcode`` +field is ``63109``. The projection includes the **first** +matching element of the ``students`` array where the ``school`` +field has a value of ``102`` **and** the ``age`` field is greater +than ``10``: + +.. code-block:: javascript + + db.schools.find( { zipcode: "63109" }, + { students: { $elemMatch: { school: 102, age: { $gt: 10} } } } ) + +The operation returns the three documents that have ``zipcode`` equal to ``63109``: + +.. code-block:: javascript + + { "_id" : 1, "students" : [ { "name" : "jess", "school" : 102, "age" : 11 } ] } + { "_id" : 3 } + { "_id" : 4, "students" : [ { "name" : "ruth", "school" : 102, "age" : 16 } ] } + +Documents with ``_id`` equal to ``3`` and ``_id`` equal to ``4`` +do not contain the ``students`` field since no array element matched +the :projection:`$elemMatch` criteria. + +:projection:`$elemMatch` with :method:`~cursor.sort()` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When the :method:`~db.collection.find()` method includes a +:method:`~cursor.sort()`, the :method:`~db.collection.find()` method +applies the :method:`~cursor.sort()` to order the matching documents +**before** it applies the projection. This is a general rule when sorting +and projecting, and is discussed in :ref:`sort-with-projection`. + +If an array field contains multiple documents with the same field +name and the :method:`~db.collection.find()` method includes a +:method:`~cursor.sort()` on that repeating field, the returned +documents may not reflect the sort order because the +:method:`~cursor.sort()` was applied to the elements of the array +before the :projection:`$elemMatch` projection. + +An array's sorting value is taken from either its "minimum" or "maximum" value, +depending on which way the sorting goes. The way that :method:`~cursor.sort()` +sorts documents containing arrays is described in :ref:`sort-asc-desc`. + +The following query includes a :method:`~cursor.sort()` to order +by descending ``students.age`` field: + +.. code-block:: javascript + + db.schools.find( + { zipcode: "63109" }, + { students: { $elemMatch: { school: 102 } } } + ).sort( { "students.age": -1 } ) - db.schools.find( - { zipcode: 63109 }, - { students: { $elemMatch: { school: 102 } } } - ).sort( { "students.age": -1 } ) +The operation applies the :method:`~cursor.sort()` to order the +documents that have the field ``zipcode`` equal to ``63109`` and +then applies the projection. The operation returns the three +documents in the following order: - The operation applies the :method:`~cursor.sort()` to order the - documents that have the field ``zipcode`` equal to ``63109`` and - then applies the projection. The operation returns the three - documents in the following order: +.. code-block:: javascript - .. code-block:: javascript + { "_id" : 4, "students" : [ { "name" : "barney", "school" : 102, "age" : 7 } ] } + { "_id" : 1, "students" : [ { "name" : "john", "school" : 102, "age" : 10 } ] } + { "_id" : 3 } - { "_id" : 1, "students" : [ { "name" : "john", "school" : 102, "age" : 10 } ] } - { "_id" : 3 } - { "_id" : 4, "students" : [ { "name" : "barney", "school" : 102, "age" : 7 } ] } +Even though the sort is descending, the younger student is listed first. This +is because the sort occured before the older students in Barney's document were +projected out. .. seealso:: diff --git a/source/reference/operator/projection/positional.txt b/source/reference/operator/projection/positional.txt index edfa160e9fd..a8d23562f0e 100644 --- a/source/reference/operator/projection/positional.txt +++ b/source/reference/operator/projection/positional.txt @@ -9,55 +9,70 @@ Definition .. projection:: $ - The positional :projection:`$` operator limits the contents of the - ```` field that is included in the query results to contain - the **first** matching element. To specify an array element to - update, see the :doc:`positional $ operator for updates + The positional :projection:`$` operator limits the contents of an + ```` from the query results to contain only the **first** element + matching the query document. To specify an array element to update, see the + :doc:`positional $ operator for updates `. - Used in the :term:`projection` document of the + Use :projection:`$` in the :term:`projection` document of the :method:`~db.collection.find()` method or the - :method:`~db.collection.findOne()` method: + :method:`~db.collection.findOne()` method when you only need one particular + array element in selected documents. - - The :projection:`$` projection operator limits the content of the - ```` field to the **first** element that matches the - :ref:`query document `. +Usage Considerations +-------------------- - - The ```` field **must** appear in the :ref:`query document - ` +.. include:: /includes/fact-positional-projection-vs-elemmatch.rst - .. code-block:: javascript +Behavior +-------- - db.collection.find( { : ... }, - { ".$": 1 } ) - db.collection.find( { : ...}, - { ".$": 1 } ) +Usage Requirements +~~~~~~~~~~~~~~~~~~ - The ```` can be documents that contains :ref:`query operator - expressions `. +Given the form: - - Only **one** positional :projection:`$` operator can appear in the - projection document. +.. code-block:: javascript - - Only **one** array field can appear in the :ref:`query document - `; i.e. the following query is - **incorrect**: + db.collection.find( { : ... }, + { ".$": 1 } ) + db.collection.find( { : ...}, + { ".$": 1 } ) - .. code-block:: javascript +The ```` field being limited **must** appear in the :ref:`query document +`, and the ```` can be documents +that contain :ref:`query operator expressions `. - db.collection.find( { : , : }, - { ".$": 1 } ) +.. _array-field-limitation: -Behavior --------- +Array Field Limitations +~~~~~~~~~~~~~~~~~~~~~~~ + +MongoDB requires the following when dealing with projection over arrays: -Array Field Limitation -~~~~~~~~~~~~~~~~~~~~~~ +- Only one positional :projection:`$` operator may appear in the projection + document. -Since only **one** array field can appear in the query document, -if the array contains documents, to specify criteria on multiple -fields of these documents, use the -:query:`$elemMatch` operator. For example: +- Only one array field may appear in the + :ref:`query document `. + +- The :ref:`query document ` should only + contain a single condition on the array field being projected. Multiple + conditions may override each other internally and lead to undefined + behavior. + +Under these requirements, the following query is **incorrect**: + +.. code-block:: javascript + + db.collection.find( { : , : }, + { ".$": 1 } ) + +To specify criteria on multiple fields of documents inside that array, use the +:query:`$elemMatch` query operator. The following query will return any +subdocuments inside a ``grades`` array that have a ``mean`` of greater than 70 +and a ``grade`` of greater than 90. .. code-block:: javascript @@ -67,6 +82,9 @@ fields of these documents, use the } } }, { "grades.$": 1 } ) +You must use the :projection:`$elemMatch` operator if you need separate conditions +for selecting documents and for choosing fields within those documents. + Sorts and the Positional Operator ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~