В MongoDB легко не думать про _id.
db.users.insertOne({ name: "Mikhail" })
MongoDB добавит его сама и будет использовать как первичный ключ документа:
{ _id: ObjectId("665f2a3c7b3d4e6f8a901234"), name: "Mikhail"}
И долгое время этого может быть достаточно.
Документы создаются, запросы работают, индекс по _id создается вместе с коллекцией.
Проблемы начинаются позже — когда ID попадает в публичный API, события, другие сервисы, миграции или ссылки между системами. В этот момент _id перестаёт быть внутренней технической деталью и становится частью архитектуры.
Чаще всего выбор выглядит так:
ObjectId — дефолт MongoDB.
UUID — стандартный внешний идентификатор.
Но на практике важен не только формат ID. Важны размер, порядок вставки в индекс, переносимость, публичность и то, как именно значение хранится в BSON.
_id кажется мелочью, пока не становится частью API, индексов и миграций. Что такое ObjectId на практике
Внутри ObjectId всего 12 байт: 4 байта на timestamp, 5 байт рандомного значения и 3 байта счетчика.
Поэтому значения обычно растут по времени создания:
ObjectId("665f2a3c7b3d4e6f8a901234")ObjectId("665f2a3d7b3d4e6f8a901235")ObjectId("665f2a3e7b3d4e6f8a901236")
Это удобно для индекса: новые документы попадают ближе к концу дерева, а не распределяются по нему случайно.
Но есть два важных ограничения.
Первое: ObjectId не заменяет created_at.
В нём действительно есть timestamp, и его можно получить:
ObjectId("665f2a3c7b3d4e6f8a901234").getTimestamp()
Но порядок только приблизительный. ObjectId хранит время с точностью до секунд, а если генерация происходит на разных клиентах, то остается только молиться, чтобы системные часы были одинаковыми.
Поэтому лучше позаботиться об отдельном поле, которое будет одновременно и читаемым, и практичным:
{ _id: ObjectId("665f2a3c7b3d4e6f8a901234"), created_at: ISODate("2026-06-08T10:15:00Z")}
Второе: ObjectId — не секрет.
Если отдавать его наружу, то по нему можно извлечь примерное время создания документа. Это не делает ObjectId опасным сходу, но его не стоит использовать как reset-token, invite-token или любой другой token, где важна непредсказуемость.
UUID: важна не только версия, но и способ хранения
Когда говорят “давайте использовать UUID”, обычно имеют в виду UUIDv4:
550e8400-e29b-41d4-a716-446655440000
Он хорошо подходит для распределённых систем: его можно генерировать в приложении, передавать между сервисами, использовать в событиях, логах и других базах.
Но в MongoDB UUID лучше хранить не строкой.
Плохой вариант:
{ _id: "550e8400-e29b-41d4-a716-446655440000"}
Лучше BSON Binary subtype 4:
{ _id: UUID("550e8400-e29b-41d4-a716-446655440000")}
Разница не в красоте, а в том, что реально попадает в документ и индекс:
-
ObjectId — 12 байт значения;
-
UUID как BSON Binary subtype 4 — 16 байт значения + накладные расходы BSON;
-
UUID как строка — 36 байт текста + накладные расходы BSON.
Сначала это незаметно, но когда документов становится миллион, то строковые UUID превращаются в технический долг: раздувают документы и индексы, увеличивают расход накопителя и добавляют лишний I/O.
Отдельный нюанс — совместимость драйверов. У UUID в MongoDB исторически были разные варианты представления, поэтому лучше явно фиксировать standard representation / subtype 4, а не полагаться на дефолты драйвера.
Индексу важен порядок
Индекс _id — это B-tree. Значения в нём упорядочены.
Поэтому характер ID влияет на запись:
ObjectId:665f2a3c...665f2a3d...665f2a3e...UUIDv4:550e8400...1c2f9a10...e7b1c034...8a91ff20...
ObjectId обычно добавляется ближе к концу индекса.UUIDv4 распределяется случайно.
На небольшой нагрузке разницы можно не заметить, но если коллекция большая, запись активная, а индекс _id уже не помещается комфортно в память, то случайность ключа начинает стоить сильно дороже.
Это не аргумент уровня “никогда не используйте UUIDv4”.
Это аргумент не выбирать UUIDv4 автоматически для тяжелых по записи коллекций, где индекс большой и активно обновляется.
UUIDv7 меняет расклад
В отличие от UUIDv4, он хранит временную ветку и отсортирован по ней: старшие биты содержат timestamp в миллисекундах, а оставшаяся часть используется под случайность и дополнительные механизмы монотонности.
Поэтому UUIDv7 — это старший брат UUIDv4 в мире индексов.
Сравнение становится таким:
-
ObjectId — компактный, MongoDB-native, примерно упорядоченный.
-
UUIDv4 — стандартный, случайный, удобный между системами, но не так хорош для индексов.
-
UUIDv7 — стандартный, упорядоченный по времени, отличный для индексов.
Но UUIDv7 не отменяет нюансы хранения. В MongoDB его всё равно лучше хранить как UUID("..."), а не просто строкой.
И ещё один момент: UUIDv7, как и ObjectId, содержит временную информацию. Если цель — не светить время создания, то он также не подойдет.
Не смешивайте типы _id
MongoDB позволяет хранить в _id разные BSON-типы. Но в одной коллекции лучше так не делать:
{ _id: ObjectId("665f2a3c7b3d4e6f8a901234") }{ _id: "665f2a3c7b3d4e6f8a901234" }{ _id: UUID("550e8400-e29b-41d4-a716-446655440000") }
Смешанные типы быстро ломают ожидания: часть документов не находится, сортировка ведёт себя не так, миграции становятся сложнее, а индексам хочется только плакать. Лучше выбрать один тип для идентификатора и придерживаться его.
Внутренний и публичный ID можно разделить
Иногда спор “ObjectId или UUID” решается не выбором одного из двух, а разделением ролей.
{ _id: ObjectId("665f2a3c7b3d4e6f8a901234"), public_id: UUID("018fdd2e-7a77-7b64-a20f-6fd83f9f7b10"), email: "alice@example.com", created_at: ISODate("2026-06-08T10:15:00Z")}
_id остаётся внутренним ключом MongoDB.public_id используется в API, событиях и интеграциях.
Это хороший вариант, когда MongoDB — внутренняя деталь реализации, а внешний контракт должен быть независимым от конкретной базы.
Минус очевидный: появляется второй уникальный индекс, больше кода и накладные расходы. Но для публичных API и системной интеграции это часто адекватная цена.
Когда выбирать ObjectId
ObjectId — нормальный выбор, если:
-
документы живут в MongoDB;
-
ID нужен в основном внутри приложения или временную метку не страшно открыть миру;
-
нет требования генерировать ID вне базы;
-
важны компактность и нормальная работа индекса на записи;
-
_idне используется как секретный токен.
Для обычной MongoDB-коллекции это хороший и практичный дефолт.
Когда выбирать UUID
UUID имеет смысл, если:
-
ID должен генерироваться в приложении или другом сервисе;
-
один ID используется в нескольких системах;
-
данные приходят из очередей, событий или внешних источников;
-
ID является частью публичного API;
-
MongoDB не должна диктовать формат идентификатора всей архитектуре.
В этом случае лучше сразу решить какую версию вы хотите использовать, чтобы потом не попасть в свою же ловушку.
Вывод
Сначала стоит понять: «а кому принадлежит идентификатор?»
Если документ живёт только внутри MongoDB, а наружу это поле либо почти не выходит, либо с этим нет проблем, то в таком случае ObjectId остаётся самым спокойным вариантом: он компактный, нативный и отлично ложится в индекс.
Но как только ID становится частью публичного контракта — его лучше проектировать отдельно. В таком случае MongoDB может продолжать жить со своим _id, а наружу можно отдавать public_id, как показано в примерах выше.
А если системе нужна дата создания, тогда храните её явно в created_at. Вынимать время из ObjectId или UUIDv7 — нечитаемо, неудобно и непрактично. Такая логика плохо ищется в коде и ломается при смене стратегии ID.
Михаил Миронов, Табрика co-founder.
ссылка на оригинал статьи https://habr.com/ru/articles/1046712/