Статьи

Совет по индексированию MongoDB № 3: как справиться со слишком многими полями

[Примечание редактора: это третья часть серии советов для MongoDB. Посмотрите совет № 1 и № 2. ]

Эта проблема

Существуют ситуации, когда у ваших документов много разных полей, и вы хотите иметь возможность эффективно запрашивать любое из них. Скажем, у вас есть документ, описывающий человека:

{
    _id: 123,
    firstName: "John",
    lastName: "Smith",
    age: 25,
    height: 6.0,
    dob: Date,
    eyes: "blue",
    sign: "Capricorn",
    ...
}

Тогда вы можете захотеть найти всех людей с голубыми глазами, определенной высоты, по фамилии и т. Д. Скажем, у вас есть десятки таких свойств, или вы можете даже не знать заранее, какими они будут, или они различаются в зависимости от документа. основа … Как вы можете использовать индексирование для быстрого разрешения этих запросов? Очевидно, что было бы дорого и непрактично создавать индекс для каждого из этих полей.

Решение № 1: Составной индекс по имени и значению

Давайте начнем с разработки схемы и используем возможности JSON, используя список для хранения всех свойств:

{
    _id: 123,
    props: [
    { n: "firstName", v: "John"},
    { n: "lastName", v: "Smith"},
    { n: "age", v: 25},
    ...
    ]
}

Индекс, который нужно создать здесь, является составной частью полей имени и значения в списке. Чтобы проиллюстрировать это, давайте создадим миллионы документов с фиктивными свойствами (от «prop0» до «prop9»), которые принимают случайное значение от 0 до 1000.

> for (var i = 0; i < 5000000; ++i) { var arr = []; for (var j = 0; j < 10; ++j) { arr.push({n: "prop" + j, v: Math.floor(Math.random() * 1000) }) }; db.generic.insert({props: arr}) }
> db.generic.findOne()
{
  "_id": ObjectId("515dd3b4f0bd676b816aa9b0"),
  "props": [
    {
      "n": "prop0",
      "v": 40
    },
    {
      "n": "prop1",
      "v": 198
    },
...
    {
      "n": "prop9",
      "v": 652
    }
  ]
}
> db.generic.ensureIndex({"props.n": 1, "props.v": 1})
> db.generic.stats()
{
  "ns": "test.generic",
  "count": 5020473,
  "size": 1847534064,
  "avgObjSize": 368,
  "storageSize": 2600636416,
  "numExtents": 19,
  "nindexes": 2,
  "lastExtentSize": 680280064,
  "paddingFactor": 1,
  "systemFlags": 1,
  "userFlags": 0,
  "totalIndexSize": 1785352240,
  "indexSizes": {
    "_id_": 162898624,
    "props.n_1_props.v_1": 1622453616
  },
  "ok": 1
}

Как вы можете видеть, размер индекса довольно велик и составляет 1,6 ГБ, поскольку мы храним как имя свойства, так и его значение в индексе. Это цена получения универсального индекса! Теперь перейдем к запросу … Давайте найдем документы, где «prop1» равно 0:

> db.generic.findOne({"props.n": "prop1", "props.v": 0})
{
  "_id": ObjectId("515dd4298bff7c34610f6ae8"),
  "props": [
    {
      "n": "prop0",
      "v": 788
    },
    {
      "n": "prop1",
      "v": 0
    },
...
    {
      "n": "prop9",
      "v": 788
    }
  ]
}
> db.generic.find({"props.n": "prop1", "props.v": 0}).explain()
{
  "cursor": "BtreeCursor props.n_1_props.v_1",
  "isMultiKey": true,
  "n": 49822,
  "nscannedObjects": 5020473,
  "nscanned": 5020473,
  "nscannedObjectsAllPlans": 5020473,
  "nscannedAllPlans": 5020473,
  "scanAndOrder": false,
  "indexOnly": false,
  "nYields": 0,
  "nChunkSkips": 0,
  "millis": 252028,
  "indexBounds": {
    "props.n": [
      [
        "prop1",
        "prop1"
      ]
    ],
    "props.v": [
      [
        {
          "$minElement": 1
        },
        {
          "$maxElement": 1
        }
      ]
    ]
  },
  "server": "agmac.local:27017"
}

Это не дает ожидаемого результата: оно соответствует 50 тыс. Записей и занимает 252 с! Причина в том, что в соответствии с языком запросов n = ”prop1” и v = 0 не обязательно должны находиться в одном и том же поддокументе, если они находятся в одном документе. В основном он учитывает все комбинации «n» и «v» в документе и соответствует больше, чем должен. В то время как вы можете обсудить этот выбор для языка запросов, способ заставить конкретный поддокумент совпадать, это использовать «$ elemMatch»:

> db.generic.findOne({"props": { $elemMatch: {n: "prop1", v: 0} }})

Теперь, как используется индекс и сколько времени занимает MongoDB v2.2 для поиска документов? Давайте посмотрим:

> db.generic.find({"props": { $elemMatch: {n: "prop1", v: 0} }}).explain()
{
  "cursor": "BtreeCursor props.n_1_props.v_1",
  "isMultiKey": true,
  "n": 5024,
  "nscannedObjects": 5020473,
  "nscanned": 5020473,
  "nscannedObjectsAllPlans": 5020473,
  "nscannedAllPlans": 5020473,
  "scanAndOrder": false,
  "indexOnly": false,
  "nYields": 0,
  "nChunkSkips": 0,
  "millis": 278784,
  "indexBounds": {
    "props.n": [
      [
        "prop1",
        "prop1"
      ]
    ],
    "props.v": [
      [
        {
          "$minElement": 1
        },
        {
          "$maxElement": 1
        }
      ]
    ]
  },
  "server": "agmac.local:27017"
}

Теперь он возвращает нужные 5024 документа … Но результат такой же медленный! Как вы можете видеть в выходных данных объяснения, причина в том, что диапазон в поле «v» остается открытым. Почему это? Давайте вернемся на секунду: если не использовать $ elemMatch, то все комбинации полей могут совпадать. Невозможно создать индекс для его резервного копирования, поскольку комбинаций будет слишком много. Поэтому для MongoDB был сделан выбор поместить значения поддокумента в одну корзину btree и игнорировать комбинации (в основном поведение $ elemMatch). Но почему же тогда запрос $ elemMatch все еще медленный? Это связано с недостатком, исправленным в версии 2.4, см.  SERVER-3104 . Обновившись до 2.4, вы увидите это:

> db.generic.find({"props": { $elemMatch: {n: "prop1", v: 0} }}).explain()
{
  "cursor": "BtreeCursor props.n_1_props.v_1",
  "isMultiKey": true,
  "n": 5024,
  "nscannedObjects": 5024,
  "nscanned": 5024,
  "nscannedObjectsAllPlans": 5024,
  "nscannedAllPlans": 5024,
  "scanAndOrder": false,
  "indexOnly": false,
  "nYields": 0,
  "nChunkSkips": 0,
  "millis": 21,
  "indexBounds": {
    "props.n": [
      [
        "prop1",
        "prop1"
      ]
    ],
    "props.v": [
      [
        0,
        0
      ]
    ]
  },
  "server": "agmac.local:27017"
}

Хорошо, мы до 21 миллисекунды, это больше похоже на это! Для выполнения запросов «и» / »или« используйте операторы $ all / $ соответственно. Обратите внимание, что в случае $ all для поиска в btree будет использоваться только 1-й элемент, поэтому сначала укажите наиболее ограничивающее значение, если вы его знаете.

db.generic.find({"props": { $all: [{ $elemMatch: {n: "prop1", v: 0} },{ $elemMatch: {n: "prop2", v: 63} } ]}})

Одно предостережение с этим решением: запросы диапазона для значения свойства не ограничивают границы индекса должным образом и заканчивают сканирование слишком много. Эта ошибка SERVER-10436 будет исправлена ​​в v2.6:

> db.generic.find({ props: { $elemMatch: {n: "prop1", v: { $gte: 6, $lte: 9 } }}}).explain()
{
    "cursor" : "BtreeCursor props.n_1_props.v_1",
	"isMultiKey" : true,
	"n" : 506,
	"nscannedObjects" : 126571,
	"nscanned" : 126571,
	"nscannedObjectsAllPlans" : 126571,
	"nscannedAllPlans" : 126571,
	"scanAndOrder" : false,
	"indexOnly" : false,
	"nYields" : 1,
	"nChunkSkips" : 0,
	"millis" : 1396,
	"indexBounds" : {
		"props.n" : [
			[
				"prop1",
				"prop1"
			]
		],
		"props.v" : [
			[
				6,
				1.7976931348623157e+308
			]
		]
	},
	"server" : "agmac.local:27017"
}

Решение № 2: один индекс BLOB

Другой подход к проблеме — просто поместить комбинацию «свойство: значение» в подобъект списка. Это решение работает как с v2.2, так и с v2.4. Давайте создадим такие документы:

> for (var i = 0; i < 5000000; ++i) { var arr = []; for (var j = 0; j < 10; ++j) { var doc = {}; doc["prop" + j] =  Math.floor(Math.random() * 1000); arr.push(doc) }; db.generic2.insert({props: arr}) }
> db.generic2.findOne()
{
  "_id": ObjectId("515e5e6a71b0722678929760"),
  "props": [
    {
      "prop0": 881
    },
    {
      "prop1": 47
    },
...
    {
      "prop9": 717
    }
  ]
}

Индекс должен быть в самом списке, так как имя свойства меняется:

> db.generic2.ensureIndex({props: 1})
> db.generic2.stats()
{
  "ns": "test.generic2",
  "count": 5000000,
  "size": 1360000032,
  "avgObjSize": 272.0000064,
  "storageSize": 1499676672,
  "numExtents": 19,
  "nindexes": 2,
  "lastExtentSize": 393670656,
  "paddingFactor": 1,
  "systemFlags": 1,
  "userFlags": 0,
  "totalIndexSize": 2384023488,
  "indexSizes": {
    "_id_": 162269072,
    "props_1": 2221754416
  },
  "ok": 1
}

Как вы можете видеть, индекс даже больше, чем решение № 1, почти на 30%, поскольку сами поддокументы BSON хранятся в индексе как BLOB. Теперь на запрос:

> db.generic2.find({"props": {"prop1": 0} }).explain()
{
  "cursor": "BtreeCursor props_1",
  "isMultiKey": true,
  "n": 4958,
  "nscannedObjects": 4958,
  "nscanned": 4958,
  "nscannedObjectsAllPlans": 4958,
  "nscannedAllPlans": 4958,
  "scanAndOrder": false,
  "indexOnly": false,
  "nYields": 0,
  "nChunkSkips": 0,
  "millis": 15,
  "indexBounds": {
    "props": [
      [
        {
          "prop1": 0
        },
        {
          "prop1": 0
        }
      ]
    ]
  },
  "server": "agmac.local:27017"
}

The result is even faster than solution #1 at 15ms! But one caveat is that the predicate must always use a full JSON object. To match “prop1” between 0 and 9, the query would be:

> db.generic2.find({"props": { $elemMatch: { $gte: {"prop1": 0}, $lte: {"prop1": 9} }})

If there are other fields in the subobject, those must be part of the JSON predicate when matching (remember that the subobject is just a BLOB for MongoDB). Now say you want an open range where “prop1” exists and is greater than 6, you still should specify an upper bound otherwise it will match many more documents than expected. Ideally you could use MaxKey as upper bound but I have discovered a bug SERVER-10394 whereby a proper bound of the same type must be specified, for example a high value integer:

db.generic2.find({"props": { $elemMatch: {$gte: {"prop1": 6}, $lt: {"prop1": 99999999 } }}})

One caveat: you cannot index independently just the values. For example with solution #1, if you want to find any document that has a value “10” for any property, you can just create an index on “props.v”. This is not possible with solution #2 since the field name varies.

Conclusion

As a conclusion, you can see that MongoDB v2.4 now offers a straightforward and efficient way to build a generic index over many properties. You are now free to index and query all of the many properties of your Big Data project ?