Знайдіть записи MongoDB, де поле масиву не порожнє


502

У всіх моїх записах є поле під назвою "малюнки". Це поле є масивом рядків.

Тепер я хочу останні 10 записів, де цей масив НЕ порожній.

Я гуляв навколо, але, як не дивно, я мало що на цьому знайшов. Я читав варіант $ where, але мені було цікаво, наскільки повільно це відбувається до власних функцій, і чи є краще рішення.

І навіть тоді це не працює:

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

Повертає нічого. Залишившись this.picturesбез біт довжини, це спрацює, але потім він також повертає порожні записи, звичайно.

Відповіді:


827

Якщо у вас також є документи, у яких немає ключа, ви можете використовувати:

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

MongoDB не використовує індекси, якщо йдеться про розмір $, тому ось краще рішення:

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

З моменту випуску MongoDB 2.6 ви можете порівняти його з оператором, $gtале це може призвести до несподіваних результатів (детальну пояснення ви можете знайти у цій відповіді ):

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

6
Для мене це правильний підхід, оскільки він гарантує, що масив існує та не є порожнім.
ЛеандроКР

Як я можу досягти такої ж функціональності, використовуючиmongoengine
Рохіт Хатрі

54
ДЕРЖАВНО, ME.find({ pictures: { $gt: [] } })НЕБЕЗПЕЧНО, навіть у нових версіях MongoDB. Якщо у полі списку є індекс, який використовується під час запиту, ви отримаєте несподівані результати. Наприклад: db.doc.find({'nums': { $gt: [] }}).hint({ _id: 1 }).count()повертає потрібне число, а db.doc.find({'nums': { $gt: [] }}).hint({ nums: 1 }).count()повертає 0.
wojcikstefan

1
Дивіться мій розгорнуту відповідь нижче , щоб дізнатися , чому це не може працювати для вас: stackoverflow.com/a/42601244/1579058
wojcikstefan

6
@ wojcikstefan коментар потрібно заохочувати, щоб не дозволяти людям використовувати останню пропозицію, яка дійсно за певних обставин не повертає відповідні документи.
Томас Юнг

181

Після ще одного розгляду, особливо в документах mongodb, і загадкових шматочків разом, це була відповідь:

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

27
Це не працює. Я не знаю, чи працювало це раніше, але це також поверне об’єкти, у яких немає клавіші "малюнки".
rdsoze

17
Неймовірно , як ця відповідь має 63 upvotes, коли насправді , що @rdsoze сказав вірно - запит буде також повертати записи , які НЕ мають picturesполя.
Дан Даскалеску

5
Будьте уважні, mongoDB не буде використовувати індекси, якщо в розмірі $ розміщено посилання . Було б краще включити {$ ne: []} і, можливо, {$ ne: null}.
Левенте Добсон

17
@rdsoze У першому рядку запитання зазначено "У всіх моїх записах є поле під назвою" малюнки ". Це поле є масивом" . Більше того, це цілком реалістичний і поширений сценарій. Ця відповідь не є помилковою, вона працює на запитання точно так само, як написано, а критикувати чи спростовувати її за те, що вона не вирішує іншу проблему, нерозумно.
Марк Амері

1
@Cec Вся документація говорить про те, що якщо ви використовуєте $ розмір у запиті, він не використовуватиме жодного індексу, щоб отримати швидші результати. Тож якщо у вас є індекс у цьому полі і ви хочете його використовувати, дотримуйтесь інших підходів, таких як {$ ne: []}, якщо це працює для вас, він буде використовувати ваш індекс.
Левенте Добсон

108

Це також може працювати для вас:

ME.find({'pictures.0': {$exists: true}});

2
Приємно! Це також дозволяє перевірити наявність мінімального розміру. Чи знаєте ви, чи масиви завжди індексуються послідовно? Чи буде колись такий випадок, коли він pictures.2існує, але pictures.1ні?
anushr

2
$existsОператор є логічним, а не усунуте. @tenbatsu слід використовувати trueзамість 1.
ekillaby

2
@anushr Would there ever be a case where pictures.2 exists but pictures.1 does not? Так, так може статися.
The Bndr

@TheBndr Це може статися лише у тому випадку, якщо picturesце субдокумент , а не масив. наприкладpictures: {'2': 123}
JohnnyHK

4
Це приємно та інтуїтивно, але будьте обережні, якщо продуктивність важлива - вона виконає повне сканування колекції, навіть якщо у вас індекс pictures.
wojcikstefan

35

Ви дбаєте про дві речі при запиті - точність та ефективність. Зважаючи на це, я перевірив кілька різних підходів у MongoDB v3.0.14.

TL; DR db.doc.find({ nums: { $gt: -Infinity }})- найшвидший і надійний (принаймні, у тестованій мною версії MongoDB).

EDIT: Це більше не працює в MongoDB v3.6! Дивіться коментарі під цим повідомленням щодо можливого рішення.

Налаштування

Я вставив поле 1k без документа зі списком, 1 путь із порожнім списком та 5 документів із не порожнім списком.

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 });

Я усвідомлюю, що цього не вистачає на шкалі, щоб сприймати продуктивність так само серйозно, як і в тестах нижче, але досить представити правильність різних запитів та поведінку обраних планів запитів.

Тести

db.doc.find({'nums': {'$exists': true}}) повертає неправильні результати (для того, що ми намагаємось зробити).

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

-

db.doc.find({'nums.0': {'$exists': true}})повертає правильні результати, але це також повільно, використовуючи повне сканування колекції ( COLLSCANетап повідомлення в поясненні).

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 }}})повертає неправильні результати. Це через недійсне сканування індексу, в якому не просуваються документи. Це, швидше за все, буде точним, але повільним без індексу.

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 }}})повертає правильні результати, але продуктивність погана. Це технічно робить сканування індексів, але потім він все-таки просуває всі документи і потім повинен фільтрувати їх).

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: [] }})повертає правильні результати і трохи швидше, але продуктивність все ще не ідеальна. Він використовує IXSCAN, який просуває лише документи з існуючим полем списку, але потім повинен фільтрувати порожні списки один за одним.

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: [] }})НЕБЕЗПЕЧНО ЗАВИСИМО ВІД ВИКОРИСТАНОГО ІНДЕКСУ МОЖЛИВО ДОБАВАТИ НЕЧАКОВІ РЕЗУЛЬТАТИ Це через недійсне сканування індексів, яке не надає документи.

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 }}) повертає правильні результати, але має погані показники (використовує повне сканування колекції).

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 }})дивно, але це працює дуже добре! Це дає правильні результати, і це швидко, просуваючи 5 документів з фази сканування індексу.

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. На жаль, моє запропоноване рішення в моєму випадку не працює. У мене є збірка MongoDB 3.6.4 з документами на 2 м. Більшість з них має seen_eventsмасив String, який також індексується. Шукаючи { $gt: -Infinity }, я одразу отримую 0 документів. Використовуючи, { $exists: true, $ne: [] }я отримую більш імовірний 1,2-мільйонний документ, причому багато часу витрачається на етапі FETCH: gist.github.com/N-Coder/b9e89a925e895c605d84bfeed648d82c
NCode

Здається , що ви правильно @Ncode - це більше не працює в MongoDB v3.6 :( Я грав з ним в протягом декількох хвилин , і ось що я знайшов: 1. db.test_collection.find({"seen_events.0": {$exists: true}})це погано , тому що він використовує для збору сканування 2 .. db.test_collection.find({seen_events: {$exists: true, $ne: []}})Є погано, тому що його IXSCAN відповідає всім документам, а потім фільтрація проводиться у повільній фазі FETCH. 3. Те саме стосується db.test_collection.find({seen_events: {$exists: true, $not: {$size: 0}}})4. Усі інші запити повертають недійсні результати.
wojcikstefan

1
@NCode знайшов рішення! Якщо ви впевнені , що все непусті seen_eventsмістять рядки, ви можете використовувати це: db.test_collection.find({seen_events: {$gt: ''}}).count(). Щоб підтвердити, чи добре працює, перевірте db.test_collection.find({seen_events: {$gt: ''}}).explain(true).executionStats. Ви, ймовірно, можете примусити, що побачені події є рядками за допомогою перевірки схеми: docs.mongodb.com/manual/core/schema-validation
wojcikstefan

Дякую! Усі існуючі значення - це рядки, тому я спробую це випробувати. Проблема також обговорює цю проблему в програмі MongoDB: jira.mongodb.org/browse/SERVER-26655
NCode

30

Починаючи з випуску 2.6, ще один спосіб зробити це порівняти поле з порожнім масивом:

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

Тестування його в оболонці:

> 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 ] }

Таким чином, він належним чином включає документи, у яких picturesє щонайменше один елемент масиву, і виключає документи, де picturesабо порожній масив, не масив, або відсутній.


7
ДЕРЖАВНА ця відповідь може доставити вам проблеми, якщо ви спробуєте використовувати індекси. Виконання, db.ME.createIndex({ pictures: 1 })а потім db.ME.find({pictures: {$gt: []}})поверне нульові результати, принаймні в MongoDB v3.0.14
wojcikstefan

@wojcikstefan Хороший улов. Потрібно по-новому поглянути на це.
JohnnyHK

5

Для цього можна скористатися будь-яким із наведених нижче.
Вони також дбають про те, щоб не повертати результат для об'єктів, у яких немає запитуваного ключа:

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

4

Отримайте всі та лише документи, де "малюнки" є масивом, а вони не порожні

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

Якщо ви використовуєте версію MongoDb до 3.2 , використовуйте $type: 4замість $type: 'array'. Зауважте, що це рішення навіть не використовує $ size , тому немає проблем з індексами ("Запити не можуть використовувати індекси для частини розміру $ запиту")

Інші рішення, включаючи ці (прийнята відповідь):

ME.find ({малюнки: {$ існує: вірно, $ не: {$ розмір: 0}}}); ME.find ({малюнки: {$ існує: вірно, $ ne: []}})

є неправильно , тому що вони повертають документи , навіть якщо, наприклад, «картинка» є null, undefined, 0 і т.д.


2

Використовуйте $elemMatchоператор: згідно з документацією

Оператор $ elemMatch відповідає документам, що містять поле масиву, принаймні один елемент, який відповідає всім заданим критеріям запиту.

$elemMatchesперевіряє, що значення є масивом і що воно не порожнє. Тож запит був би чимось на кшталт

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

PS Варіант цього коду знайдено в курсі М121 університету МонгоДБ.


0

Ви також можете скористатися допоміжним методом Існує над оператором $ Mongo $ існує

ME.find()
    .exists('pictures')
    .where('pictures').ne([])
    .sort('-created')
    .limit(10)
    .exec(function(err, results){
        ...
    });

0
{ $where: "this.pictures.length > 1" }

використовуйте $ where і передайте this.field_name.length, який повертає розмір поля масиву, і перевіряйте його, порівнюючи з номером. якщо будь-який масив має будь-яке значення, ніж розмір масиву, повинен бути не менше 1. тому все поле масиву має довжину більше однієї, це означає, що в ньому є деякі дані в цьому масиві


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.