Статьи

Разработка схемы в MongoDB против Разработка схемы в MySQL

Этот пост прибывает из Стефан Комбоудон в блоге MySQL Performance.

Для людей, привыкших к реляционным базам данных, использование NoSQL-решений, таких как MongoDB, приносит интересные проблемы. Одним из них является проектирование схемы: в то время как в реляционном мире нормализация — хороший способ начать, как мы должны проектировать наши коллекции при создании нового приложения MongoDB?

Давайте посмотрим на простом примере, как мы будем создавать структуру данных для MySQL (или любой реляционной базы данных) и для MongoDB. В этом посте мы будем предполагать, что мы хотим хранить информацию о людях (их имя) и детали из их паспорта (страна и дата действия).

Реляционный дизайн

В реляционном мире основная идея — попытаться придерживаться третьей нормальной формы и создать две таблицы (для ясности я опущу индексы и внешние ключи — MongoDB поддерживает индексы, но не внешние ключи):

mysql> select * from people;
+----+------------+
| id | name       |
+----+------------+
|  1 | Stephane   |
|  2 | John       |
|  3 | Michael    |
|  4 | Cinderella |
+----+------------+
mysql> select * from passports;
+----+-----------+---------+-------------+
| id | people_id | country | valid_until |
+----+-----------+---------+-------------+
|  4 |         1 | FR      | 2020-01-01  |
|  5 |         2 | US      | 2020-01-01  |
|  6 |         3 | RU      | 2020-01-01  |
+----+-----------+---------+-------------+

Одна из хороших вещей в таком дизайне заключается в том, что одинаково легко выполнять любой запрос (если мы не рассматриваем объединения как нечто сложное в использовании):

  • Вы хотите количество людей?
    SELECT count(*) FROM people
  • Вы хотите знать срок действия паспорта Стефана?
    SELECT valid_until from passports ps join people pl ON ps.people_id = pl.id WHERE name = 'Stephane'
  • Вы хотите знать, сколько людей не имеют паспорта? Бег
    SELECT name FROM people pl LEFT JOIN passports ps ON ps.people_id = pl.id WHERE ps.id IS NULL
  • И т.п.

MongoDB дизайн

Теперь, как мы должны разрабатывать наши коллекции в MongoDB, чтобы упростить запросы?

Использование 3-й нормальной формы, конечно, возможно, но это, вероятно, будет неэффективно, поскольку все объединения должны выполняться в приложении. Таким образом, из 3-х приведенных выше запросов можно легко выполнить только запрос № 1. Итак, какие еще проекты мы могли бы иметь?

Первый вариант — хранить все в одной коллекции:

> db.people_all.find().pretty()
{
	"_id" : ObjectId("51f7be1cd6189a56c399d3bf"),
	"name" : "Stephane",
	"country" : "FR",
	"valid_until" : ISODate("2019-12-31T23:00:00Z")
}
{
	"_id" : ObjectId("51f7be3fd6189a56c399d3c0"),
	"name" : "John",
	"country" : "US",
	"valid_until" : ISODate("2019-12-31T23:00:00Z")
}
{
	"_id" : ObjectId("51f7be4dd6189a56c399d3c1"),
	"name" : "Michael",
	"country" : "RU",
	"valid_until" : ISODate("2019-12-31T23:00:00Z")
}
{ "_id" : ObjectId("51f7be5cd6189a56c399d3c2"), "name" : "Cinderella" }

Кстати, мы видим здесь, что MongoDB не имеет схемы: нет проблем с хранением документов, которые не имеют одинаковую структуру.

Недостатком является то, что больше не ясно, какие атрибуты принадлежат паспорту, поэтому, если вы хотите получить всю паспортную информацию для Михаэля, вам нужно будет правильно понять всю структуру данных.

Второй вариант — встроить паспортную информацию в информацию о людях — MongoDB поддерживает расширенные документы:

> db.people_embed.find().pretty()
{
	"_id" : ObjectId("51f7c0048ded44d5ebb83774"),
	"name" : "Stephane",
	"passport" : {
		"country" : "FR",
		"valid_until" : ISODate("2019-12-31T23:00:00Z")
	}
}
{
	"_id" : ObjectId("51f7c70e8ded44d5ebb83775"),
	"name" : "John",
	"passport" : {
		"country" : "US",
		"valid_until" : ISODate("2019-12-31T23:00:00Z")
	}
}
{
	"_id" : ObjectId("51f7c71b8ded44d5ebb83776"),
	"name" : "Michael",
	"passport" : {
		"country" : "RU",
		"valid_until" : ISODate("2019-12-31T23:00:00Z")
	}
}
{ "_id" : ObjectId("51f7c7258ded44d5ebb83777"), "name" : "Cinderella" }

Или мы могли бы встроить другой путь (однако это выглядит немного сомнительно, поскольку у некоторых людей может не быть паспорта, как у Золушки в нашем примере):

> db.passports_embed.find().pretty()
{
	"_id" : ObjectId("51f7c7e58ded44d5ebb8377b"),
	"country" : "FR",
	"valid_until" : ISODate("2019-12-31T23:00:00Z"),
	"person" : {
		"name" : "Stephane"
	}
}
{
	"_id" : ObjectId("51f7c7ec8ded44d5ebb8377c"),
	"country" : "US",
	"valid_until" : ISODate("2019-12-31T23:00:00Z"),
	"person" : {
		"name" : "John"
	}
}
{
	"_id" : ObjectId("51f7c7fa8ded44d5ebb8377d"),
	"country" : "RU",
	"valid_until" : ISODate("2019-12-31T23:00:00Z"),
	"person" : {
		"name" : "Michael"
	}
}
{
	"_id" : ObjectId("51f7c8058ded44d5ebb8377e"),
	"person" : {
		"name" : "Cinderella"
	}
}

Это много вариантов! Как мы можем выбрать? Вот где вы должны знать о принципиальном различии между MongoDB и реляционными базами данных, когда речь идет о разработке схемы:

Коллекции внутри MongoDB должны разрабатываться с учетом наиболее частых шаблонов доступа приложения, в то время как в реляционном мире вы можете забыть, как будут осуществляться доступ к данным, если ваши таблицы будут нормализованы.

Так…

  • Если вы читаете информацию о людях в 99% случаев, хорошим решением может быть наличие 2 отдельных коллекций: оно позволяет избежать хранения в памяти данных, которые почти никогда не используются (паспортные данные), и когда вам нужна вся информация для данного человека, может быть приемлемо сделать соединение в приложении.
  • То же самое, если вы хотите отобразить имена людей на одном экране и паспортные данные на другом экране.
  • Но если вы хотите отобразить всю информацию для данного человека, лучшим решением будет хранение всего в одной коллекции (с вложением или с плоской структурой).

Вывод

В этом посте мы увидели одно из фундаментальных различий между MySQL и MongoDB, когда речь идет о создании правильной структуры данных для приложения: с MongoDB вам необходимо знать схему доступа к данным приложения. Этим не следует пренебрегать, так как создание неправильной схемы схемы является причиной катастрофы: запросы будут трудны для написания и оптимизации, они будут медленными, и их иногда придется заменять пользовательским кодом. Все это может привести к снижению производительности и разочарованию.

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