Модуль 1 · Подключение и первые данные

Справочник по MongoDB · модуль 1 · подключение и первые данные

Глава 1.2

1.2

Документ
и BSON

Документ — единица хранения в MongoDB: его кладут и достают целиком. Разберём, из чего он состоит, какие типы значений в нём бывают, почему он хранится не как текст и что за поле _id появляется в каждом документе само.

Категория
Модель данных
Уровень
Начальный
Связанные темы
Вставка, типы в фильтрах, проекция
Зачем это знать
Понимать, что именно лежит в базе и почему "65000" и 65000 — разные значения

Как запускать примеры этой главы

БазыГлава меняет песочницу. Перед началом верните базы в исходное состояние: docker compose run --rm reset или скрипт из главы 1.0, §6.
Архивmongodb-glava-1.2-python.zip, 56 КБmongodb-glava-1.2-ruby.zip, 56 КБmongodb-glava-1.2-go.zip, 59 КБmongodb-glava-1.2-cpp.zip, 57 КБ — все примеры главы готовыми программами, базы и стенд в Docker. В README.md — запуск в Docker, если Compass нет или он не подключается, и на установленном сервере с Compass, а также что должна напечатать каждая программа.
Файлlessons/ch1_2.py — пример вставляется в конец файлаlessons/ch1_2.rb — пример вставляется в конец файлаlessons/ch1_2.go — пример внутрь скобок в конце main, функции и типы — выше mainlessons/ch1_2.cpp — пример внутрь скобок в конце main, функции — выше main
Запускcd lessons, затем python ch1_2.pycd lessons, затем python3 ch1_2.py · в Docker: docker compose run --rm python lessons/ch1_2.pycd lessons, затем ruby ch1_2.rb · в Docker: docker compose run --rm ruby lessons/ch1_2.rbcd lessons, затем go run ch1_2.go · в Docker: docker compose run --rm go lessons/ch1_2.gocd lessons, затем g++ -std=c++17 ch1_2.cpp -o prog $(pkg-config --cflags --libs libmongocxx1) && ./prog · в Docker: docker compose run --rm cpp lessons/ch1_2.cpp
ПорядокКаждый пример вставляется вместо предыдущего и запускается один раз, по порядку, — так получены все выводы главы. Подробно — глава 1.0, §10. Значения _id и время создания у вас будут другими: их выдаёт сервер в момент вставки.

§1Документ: пары «поле — значение»

Документ — это набор пар «поле — значение». Значением может быть число, строка, дата, массив или другой документ. Поэтому карточка товара помещается в один документ целиком, а не разносится по шести таблицам.

Ниже — документ товара из базы курса:

shop.products · _id: p-012314 байт
{
  _id: "p-012",string
  sku: "SKU-PR-012",string
  title: "Монитор Dell P2422H",string
  brand: "Dell",string
  category: "периферия",string
  price: 21990,int32
  specs: ["24\"", "IPS", "75 Гц"],array
  stock: [{ warehouse: "Екатеринбург", qty: 0 }],array of object
  rating: 3.7,double
  reviews: 80,int32
  added: ISODate("2026-04-19T00:00:00Z"),date
  discount: 5int32
}

В этом документе видны три свойства модели. specs — массив строк, он лежит прямо в документе: отдельной таблицы «характеристики» нет. stock — массив документов, вложенность может быть любой глубины. Поле discount есть у этого товара и может отсутствовать у соседнего — коллекция это допускает.

§2BSON: не текст, а двоичная запись

Выглядит документ как JSON, но хранится иначе. Формат называется BSON — Binary JSON, двоичная запись документа. Отличия от JSON:

типыВ JSON число одно на всех. В BSON различаются int32, int64, double и decimal128, а ещё есть дата, двоичные данные и ObjectId — типов, которых в JSON нет.точность
длинаКаждый документ и каждая строка предваряются своей длиной. Серверу не нужно читать документ до конца, чтобы понять, где он заканчивается.скорость
порядокПорядок полей сохраняется таким, каким его записали, — в отличие от словаря во многих языках.предсказуемость

Следствие для практики: тип значения хранится вместе со значением. Строка "21990" и число 21990 — разные значения, и запрос «дороже 20000» найдёт только второе. Разбирается это в модуле 2, но возникает здесь, на уровне хранения.

§3Типы значений

Полный список типов BSON длинный, в работе встречается десяток. В таблице ниже — как тип называется в базе и во что он превращается в Python.

Тип BSONПсевдонимВ PythonГде у нас встречается
String"string"strtitle, category
32-bit integer"int"intprice, reviews
64-bit integer"long"intбольшие счётчики, время в миллисекундах
Double"double"floatrating
Decimal128"decimal"Decimal128деньги там, где нельзя терять копейки
Boolean"bool"boolpayment.paid в заказах
Date"date"datetimeadded, created
Array"array"listspecs, items
Object"object"dictdelivery, payment
ObjectId"objectId"ObjectId_id, если его не задали сами
Null"null"Noneuser_id в событиях без пользователя

Проверить тип каждого поля можно прямо из Python — драйвер уже перевёл значения в объекты языка:

Python · PyMongoтипы полей документа
one = db["products"].find_one({"_id": "p-012"})

for name, value in one.items():
    print(f"{name:10} {type(value).__name__}")
C++ · mongocxxтипы полей документа
auto doc = products.find_one(make_document(kvp("_id", "p-012")));

for (const auto& element : doc->view()) {
    std::cout << std::left << std::setw(10) << element.key()
              << bsoncxx::to_string(element.type()) << std::endl;
}
Go · mongo-driverтипы полей документа
var raw bson.Raw
products.FindOne(ctx, bson.D{{Key: "_id", Value: "p-012"}}).Decode(&raw)

elements, _ := raw.Elements()
for _, element := range elements {
    fmt.Printf("%-10s %s\n", element.Key(), element.Value().Type)
}
Ruby · mongoтипы полей документа
one = products.find({ "_id" => "p-012" }).first

one.each { |name, value| puts format("%-10s %s", name, value.class) }
Вывод в терминалрезультат запуска
_id        str
sku        str
title      str
brand      str
category   str
price      int
specs      list
stock      list
rating     float
reviews    int
added      datetime
discount   int
Вывод в терминалрезультат запуска
_id       string
sku       string
title     string
brand     string
category  string
price     int32
specs     array
stock     array
rating    double
reviews   int32
added     date
discount  int32
Вывод в терминалрезультат запуска
_id        string
sku        string
title      string
brand      string
category   string
price      32-bit integer
specs      array
stock      array
rating     double
reviews    32-bit integer
added      UTC datetime
discount   32-bit integer
Вывод в терминалрезультат запуска
_id        String
sku        String
title      String
brand      String
category   String
price      Integer
specs      Array
stock      Array
rating     Float
reviews    Integer
added      Time
discount   Integer
На полях: int и long — один тип в Python

У Python одно целое число, у BSON их два: int32 и int64. При записи драйвер выбирает подходящий сам, при чтении оба становятся int. Разница вылезает в одном месте — в запросах по типу {"$type": "int"}, где int и long уже не одно и то же.

§4Сколько весит документ

Размер документа считает сама база — оператором $bsonSize. По нему оценивают, сколько места займёт коллекция, когда документов станет много.

mongosh · оболочкаразмер двух товаров
db.products.aggregate([
  { $match: { _id: { $in: ["p-012", "p-020"] } } },
  { $project: { title: 1, size: { $bsonSize: "$$ROOT" } } }
])
Выводстенд курса
[
  { _id: 'p-012', title: 'Монитор Dell P2422H', size: 314 },
  { _id: 'p-020', title: 'Кабель USB-C 2 м', size: 323 }
]
314байт у монитора: три характеристики, один склад 323байта у кабеля: название короче, но складов больше 16 МБпредельный размер одного документа в MongoDB

У кабеля название вдвое короче, а документ на девять байт больше: размер определяется структурой — числом полей, элементов массива и вложенных объектов. Служебные байты длины и типа тоже занимают место.

Предел в 16 МБ на документ выглядит недостижимым, но он ставит границу подходу «положим всё в один документ»: список из миллиона заказов клиента в его карточку не поместится. Как выбирают между вложением и ссылкой, разобрано в модуле 6.

§5Поле _id

В каждом документе есть поле _id — первичный ключ. Правила у него три:

  • оно обязательное: если вы его не указали, сервер добавит его сам;
  • оно уникальное внутри коллекции: индекс по _id создаётся автоматически и снять его нельзя;
  • его нельзя изменить у существующего документа — только удалить документ и записать заново.

Значением _id может быть что угодно: строка, число, дата, вложенный документ. В нашей базе это осмысленные строки — "p-012", "o-0001", "c-003": по такому ключу видно, что перед вами, ещё до чтения документа. Если же _id не задать, сервер поставит ObjectId — 12-байтовый идентификатор.

Python · PyMongo_id, которого не было
result = box.insert_one({
    "sku": "SKU-AC-022", "title": "Коврик для мыши XL", "price": 1290,
})

print("присвоенный _id:", result.inserted_id)
print("тип:", type(result.inserted_id).__name__)
print("время создания:", result.inserted_id.generation_time)
C++ · mongocxx_id, которого не было
auto result = box.insert_one(make_document(
    kvp("sku", "SKU-AC-022"),
    kvp("title", "Коврик для мыши XL"),
    kvp("price", 1290)));

auto oid = result->inserted_id().get_oid().value;
std::cout << "присвоенный _id: " << oid.to_string() << std::endl;
std::cout << "время создания: " << oid.get_time_t() << std::endl;
Go · mongo-driver_id, которого не было
res, err := box.InsertOne(ctx, bson.D{
    {Key: "sku", Value: "SKU-AC-022"},
    {Key: "title", Value: "Коврик для мыши XL"},
    {Key: "price", Value: 1290},
})
if err != nil {
    log.Fatal(err)
}

oid := res.InsertedID.(bson.ObjectID)
fmt.Println("присвоенный _id:", oid.Hex())
fmt.Printf("тип: %T\n", oid)
fmt.Println("время создания:", oid.Timestamp().UTC())
Ruby · mongo_id, которого не было
result = box.insert_one({
  "sku" => "SKU-AC-022", "title" => "Коврик для мыши XL", "price" => 1290
})

oid = result.inserted_id
puts "присвоенный _id: #{oid}"
puts "тип: #{oid.class}"
puts "время создания: #{oid.generation_time}"
Вывод в терминалрезультат запуска
присвоенный _id: 6aa71d38b90e39ca06d0652f
тип: ObjectId
время создания: 2026-09-13 22:01:28+00:00
Вывод в терминалрезультат запуска
присвоенный _id: 6aa720f5a9158e554300ecd1
время создания: 1789337845
Вывод в терминалрезультат запуска
присвоенный _id: 6aa720b3dc05457956c79cee
тип: bson.ObjectID
время создания: 2026-09-13 22:16:19 +0000 UTC
Вывод в терминалрезультат запуска
присвоенный _id: 6aa7760bf7cda459834ce6ad
тип: BSON::ObjectId
время создания: 2026-09-14 04:20:27 UTC

§6ObjectId по байтам

ObjectId — 12 байт с фиксированной структурой. Записывается он как 24 шестнадцатеричных знака: каждый байт — два знака. Разберём по байтам один из полученных выше — 6aa08c51503a4f190a669c88.

6aa08c51 503a4f190a 669c88
4 байта · времяСекунды с 1 января 1970 года. Из них получают generation_time: дата создания лежит в самом ключе.
5 байт · случайное значениеУникально для процесса на машине. Нужно, чтобы два сервера, вставляющие данные в одну секунду, не столкнулись.
3 байта · счётчикРастёт с каждым новым идентификатором внутри процесса. Разводит документы, созданные в одну и ту же секунду.

Из устройства следуют два практических вывода. Первый: ObjectId примерно упорядочен по времени — сортировка по _id почти совпадает с сортировкой по дате создания, и отдельное поле «когда создан» иногда не нужно. Второй: ObjectId выдаёт время создания записи — это не всегда желательно, если идентификатор виден снаружи, в адресной строке.

Осторожно: строка — это не ObjectId

В запросе {"_id": "6aa08c51503a4f190a669c88"} база ищет строку и не находит ничего: в документе лежит значение типа ObjectId. Идентификатор из адресной строки нужно превратить в объект: from bson import ObjectId, затем {"_id": ObjectId(raw)}. Эта ошибка появляется в первом же веб-приложении на MongoDB.

§7Одна коллекция — разные формы

Коллекция не требует, чтобы документы были одинаковыми. Схемы у неё нет — по умолчанию сервер примет документ любой формы. В нашей коллекции товаров это видно сразу:

p-003со скидкой
title: "Ноутбук HP..."
price: 67900
discount: 10
specs: [3 элемента]
p-006без скидки
title: "Смартфон Xiaomi..."
price: 18990
// поля discount нет
specs: [2 элемента]
p-020два склада
title: "Кабель USB-C 2 м"
price: 790
stock: [2 склада]
specs: [2 элемента]

Это свойство даёт свободу — новое поле добавляется без миграции всей коллекции, — и оно же оставляет опечатку незамеченной: опечатка в имени поля не будет замечена. Запись salaryy вместо salary создаст новое поле. Как поставить базе правила и заставить её проверять форму документа, разберём в главе 3.1.

§8Типы в Compass

Тот же документ в Compass. Тип здесь виден по тому, как записано значение: строки в кавычках, числа без кавычек, дата — как ISODate с расшифровкой рядом, массив — как Array (3) со свёрнутым содержимым.

Документ p-012 в MongoDB Compass: строки в кавычках, числа без кавычек, ISODate у даты, Array у массивов
Compass · shop.products, фильтр { _id: "p-012" } — тот самый документ из §1, 12 полей

Явно тип пишется в табличном виде — это третья кнопка в правом верхнем углу списка. Там Compass ставит тип в шапку каждого столбца: _id String, price Int32, rating Double. И если в столбце встретится несколько типов, он покажет их списком — так опечатка вроде цены, записанной строкой, находится глазами, без единого запроса.

§9Частые ошибки

Что написаноЧто происходитКак правильно
{"price": "21990"}Записано строкой. Сравнения и суммы по этому полю работать перестанутПриводить к числу до записи: int(value)
{"_id": "6aa08c…"} для ObjectIdПустой результат: строка не равна ObjectId{"_id": ObjectId("6aa08c…")}
Попытка изменить _idWriteError: would modify the immutable field '_id'Удалить документ и вставить заново с новым ключом
Дата записана строкой "2026-04-19"Сортировка сработает случайно, а сравнение по датам — нетПисать datetime, база сохранит его типом date
Массив на десятки тысяч элементов в документеПриближение к пределу 16 МБ и медленное чтение целикомВыносить растущий список в отдельную коллекцию
Что написаноЧто происходитКак правильно
kvp("price", "21990")Записано строкой: kvp выводит тип из аргументаПередавать число: kvp("price", 21990)
get_string() на числовом полеbsoncxx::exception: requested type does not match the underlying type: expected element type k_stringСмотреть element.type() или брать get_int32()
Чтение поля, которого нетcannot get int32 from an uninitialized element: view is invalidСначала проверить: if (doc["discount"]) …
string_view пережил документМусор в строке: view ссылается на освобождённую памятьКопировать в std::string, если значение нужно дольше документа
Массив на десятки тысяч элементов в документеПриближение к пределу 16 МБ и медленное чтение целикомВыносить растущий список в отдельную коллекцию
Что написаноЧто происходитКак правильно
Price string при int32 в базеerror decoding key price: cannot decode 32-bit integer into a string typeТип поля структуры должен совпадать с типом в BSON
Поле структуры со строчной буквыПоле молча не заполняется и не записываетсяЭкспортируемые имена с большой буквы плюс тег bson:"price"
bson.M там, где важен порядокmulti-key map passed in for ordered parameterbson.D хранит порядок ключей; bson.M — обычная карта
Строка вместо bson.ObjectIDПустой результат: строка не равна ObjectIDbson.ObjectIDFromHex(raw) и фильтр по полученному значению
Массив на десятки тысяч элементов в документеПриближение к пределу 16 МБ и медленное чтение целикомВыносить растущий список в отдельную коллекцию
Что написаноЧто происходитКак правильно
{ "price" => "21990" }Записано строкой. Сравнения и суммы по этому полю работать перестанутПриводить к числу до записи: value.to_i
Строка вместо BSON::ObjectIdПустой результат: строка не равна ObjectIdBSON::ObjectId.from_string(raw)
Символ вместо строки в фильтре{ sku: "…" } и { "sku" => "…" } — разные ключи для драйвераВ фильтрах и документах ключи пишут строками
Чтение поля, которого нетВозвращается nil, ошибки нетПроверять на nil до арифметики, иначе NoMethodError вылезет дальше по коду
Массив на десятки тысяч элементов в документеПриближение к пределу 16 МБ и медленное чтение целикомВыносить растущий список в отдельную коллекцию

§10Проверьте себя

Чем BSON отличается от JSON, если на экране они выглядят одинаково?

BSON — двоичный формат: он хранит тип вместе со значением, знает длину каждого документа и строки и поддерживает типы, которых в JSON нет, — дату, ObjectId, двоичные данные, несколько числовых типов. JSON — это текст, в котором все числа одинаковы, а даты приходится изображать строками.

Мы не указали _id. Что окажется в документе?

ObjectId — 12 байт: 4 байта времени, 5 случайных, 3 счётчика. Его сгенерирует драйвер или сервер, а вернётся он в result.inserted_id.

Можно ли в одной коллекции держать документы с разным набором полей?

Да, по умолчанию сервер не проверяет форму. Это удобно при развитии проекта, но означает, что опечатку в имени поля никто не заметит: появится новое поле. Проверку формы включают отдельно — правилом $jsonSchema.

Почему у документа с более коротким названием размер оказался больше?

Потому что размер определяется структурой, а не длиной текста. У товара p-020 в массиве stock два склада вместо одного, а каждый элемент — это вложенный документ со своими служебными байтами длины и типов.

Сортировка по _id — то же самое, что сортировка по дате создания?

Почти, если _id — это ObjectId: первые четыре байта хранят время с точностью до секунды. Внутри одной секунды порядок определяет счётчик, а на нескольких серверах он может разойтись. Если порядок важен точно, заводите собственное поле с датой.

§11Шпаргалка

Посмотреть документ col.find_one({"_id": "p-012"}) # mongosh: db.products.findOne({_id:"p-012"})
Размер документа {$project: {size: {$bsonSize: "$$ROOT"}}} # предел — 16 МБ
ObjectId из строки from bson import ObjectId col.find_one( {"_id": ObjectId(raw)})
Время из ObjectId oid.generation_time # mongosh: _id.getTimestamp()
Посмотреть документ col.find_one(make_document( kvp("_id", "p-012"))) // поля: doc->view()["title"]
Размер документа {$project: {size: {$bsonSize: "$$ROOT"}}} // предел — 16 МБ
ObjectId из строки #include <bsoncxx/oid.hpp> bsoncxx::oid oid{raw}; col.find_one(make_document(kvp("_id", oid)));
Время из ObjectId oid.get_time_t() // секунды от эпохи
Посмотреть документ var doc bson.M col.FindOne(ctx, bson.D{{Key: "_id", Value: "p-012"}}). Decode(&doc)
Размер документа {$project: {size: {$bsonSize: "$$ROOT"}}} // предел — 16 МБ
ObjectId из строки oid, err := bson.ObjectIDFromHex(raw) col.FindOne(ctx, bson.D{{Key: "_id", Value: oid}})
Время из ObjectId oid.Timestamp() // возвращает time.Time
Посмотреть документ col.find({ "_id" => "p-012" }).first # mongosh: db.products.findOne({_id:"p-012"})
Размер документа {$project: {size: {$bsonSize: "$$ROOT"}}} # предел — 16 МБ
ObjectId из строки oid = BSON::ObjectId.from_string(raw) col.find({ "_id" => oid }).first
Время из ObjectId oid.generation_time # возвращает Time в UTC

Итог главы

Документ — это пары «поле — значение», где значением может быть массив или другой документ; хранится он в двоичном формате BSON, который помнит тип каждого значения и длину каждой части. Поле _id есть всегда: заданное вами или сгенерированный сервером ObjectId, в первых байтах которого лежит время создания. Форма документов внутри коллекции может различаться: это даёт свободу при развитии проекта и оставляет опечатку в имени поля незамеченной. В следующей главе начнём класть документы в базу сами.

Глава 1.2 · Документ и BSON · Справочник по MongoDB Автор: Макаров Максим Николаевич