В MongoDB легко не думать про _id.

db.users.insertOne({ name: "Mikhail" })

MongoDB добавит его сама и будет использовать как первичный ключ документа:

{
  _id: ObjectId("665f2a3c7b3d4e6f8a901234"),
  name: "Mikhail"
}

И долгое время этого может быть достаточно.
Документы создаются, запросы работают, индекс по _id создается вместе с коллекцией.

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

Чаще всего выбор выглядит так:

ObjectId — дефолт MongoDB.
UUID — стандартный внешний идентификатор.

Но на практике важен не только формат ID. Важны размер, порядок вставки в индекс, переносимость, публичность и то, как именно значение хранится в BSON.

_id кажется мелочью, пока не становится частью API, индексов и миграций.
_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, а не полагаться на дефолты драйвера.

Строковая запись UUID удобна глазам, но не индексу.
Строковая запись UUID удобна глазам, но не индексу.

Индексу важен порядок

Индекс _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 и системной интеграции это часто адекватная цена.

Внутренний ключ и публичный ID — разные задачи.
Внутренний ключ и публичный ID — разные задачи.

Когда выбирать ObjectId

ObjectId — нормальный выбор, если:

  1. документы живут в MongoDB;

  2. ID нужен в основном внутри приложения или временную метку не страшно открыть миру;

  3. нет требования генерировать ID вне базы;

  4. важны компактность и нормальная работа индекса на записи;

  5. _id не используется как секретный токен.

Для обычной MongoDB-коллекции это хороший и практичный дефолт.

Когда выбирать UUID

UUID имеет смысл, если:

  1. ID должен генерироваться в приложении или другом сервисе;

  2. один ID используется в нескольких системах;

  3. данные приходят из очередей, событий или внешних источников;

  4. ID является частью публичного API;

  5. MongoDB не должна диктовать формат идентификатора всей архитектуре.

В этом случае лучше сразу решить какую версию вы хотите использовать, чтобы потом не попасть в свою же ловушку.

Вывод

Сначала стоит понять: "а кому принадлежит идентификатор?"
Если документ живёт только внутри MongoDB, а наружу это поле либо почти не выходит, либо с этим нет проблем, то в таком случае ObjectId остаётся самым спокойным вариантом: он компактный, нативный и отлично ложится в индекс.

Но как только ID становится частью публичного контракта — его лучше проектировать отдельно. В таком случае MongoDB может продолжать жить со своим _id, а наружу можно отдавать public_id, как показано в примерах выше.

А если системе нужна дата создания, тогда храните её явно в created_at. Вынимать время из ObjectId или UUIDv7 — нечитаемо, неудобно и непрактично. Такая логика плохо ищется в коде и ломается при смене стратегии ID.

Михаил Миронов, Табрика co-founder.