Посмотрите, я понимаю. NoSQL в тренде. Это круто. Он горизонтально масштабируется как настоящий профи, и где-то около 2015 года мы все решили, что реляционные базы данных устарели, как раскладушки. Но вот неудобная правда, о которой никто не хочет говорить на технических конференциях: для большинства приложений вам, вероятно, не нужен NoSQL, и настаивать на его использовании — это всё равно что принести огнемёт на церемонию зажигания свечей.
Я видел слишком много команд, которые загоняли себя в тупик, выбирая MongoDB или Cassandra для проектов, которые были бы вполне довольны старой доброй PostgreSQL. Результат? Потерянные месяцы разработки, кошмары с согласованностью данных, и разработчики, не спящие по ночам и задающиеся вопросом, почему их база данных с «конечной согласованностью» решила, что «в конечном счёте» означает «может быть, во вторник следующей недели».
Это не нападки на NoSQL. Я использовал его, мне он понравился, и я буду использовать его снова. Но маятник качнулся слишком далеко, и пришло время честно поговорить о том, когда SQL-базы данных не только конкурируют с NoSQL, но и полностью его побеждают.
Великая маркетинговая кампания NoSQL
Сначала давайте признаем очевидное: NoSQL выиграл маркетинговую битву. Повествование стало простым и обольстительным:
- Нужно масштабироваться? NoSQL.
- Есть неструктурированные данные? NoSQL.
- Создаёте современное приложение? NoSQL.
- Хотите выглядеть умным на митапах? NoSQL.
Но в этом повествовании удобно умалчивается о десятилетиях оптимизации, инструментарии и проверенной надёжности, которые SQL-базы данных приносят на стол. Это всё равно что отказаться от швейцарского армейского ножа, потому что кто-то изобрёл действительно модный открывашку для бутылок.
Когда SQL не просто хорош — он лучше
Соответствие ACID: незамеченный герой
Давайте представим картину. Вы создаёте платформу электронной коммерции. Клиент нажимает «Купить» на товар стоимостью 500 долларов. Ваше приложение должно:
- Списать сумму с его счёта.
- Добавить товар в историю заказов.
- Уменьшить количество товара на складе.
- Создать запись о доставке.
С SQL-базой данных, поддерживающей транзакции ACID, это тривиально:
BEGIN TRANSACTION;
UPDATE accounts
SET balance = balance - 500
WHERE user_id = 12345 AND balance >= 500;
INSERT INTO orders (user_id, product_id, amount, status)
VALUES (12345, 67890, 500, 'pending');
UPDATE inventory
SET quantity = quantity - 1
WHERE product_id = 67890 AND quantity > 0;
INSERT INTO shipping_queue (order_id, address_id)
VALUES (LAST_INSERT_ID(), 54321);
COMMIT;
Если любой шаг завершается сбоем, вся транзакция откатывается. Ваш клиент не будет charged, и ваши запасы останутся точными. Просто. Элегантно. Надёжно.
Теперь попробуем это в системе «конечной согласованности» NoSQL. Вам нужно будет реализовать:
- Координацию транзакций на уровне приложения.
- Компенсирующие транзакции для отката.
- Проверки идемпотентности везде.
- Фоновые задания для проверки согласованности.
- Обработку ошибок, которая заставила бы заплакать инженера Kafka.
Вот как это выглядит в псевдокоде с MongoDB (спойлер: это не очень красиво):
// Это оптимистично и всё равно будет сбоить в крайних случаях
async function processOrder(userId, productId, amount) {
const session = await mongoose.startSession();
session.startTransaction();
try {
// Шаг 1: Проверка и обновление баланса
const account = await Account.findOneAndUpdate(
{ userId, balance: { $gte: amount } },
{ $inc: { balance: -amount } },
{ session, new: true }
);
if (!account) {
throw new Error('Недостаточно средств');
}
// Шаг 2: Создание заказа
const order = await Order.create([{
userId,
productId,
amount,
status: 'pending'
}], { session });
// Шаг 3: Обновление запасов
const inventory = await Inventory.findOneAndUpdate(
{ productId, quantity: { $gt: 0 } },
{ $inc: { quantity: -1 } },
{ session, new: true }
);
if (!inventory) {
throw new Error('Нет в наличии');
}
// Шаг 4: Создание записи о доставке
await ShippingQueue.create([{
orderId: order._id,
addressId: account.addressId
}], { session });
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
// Но подождите! Что, если сеть отключится прямо здесь?
// Что, если MongoDB выйдет из строя при откате?
// Поздравляем, вам теперь нужна служба сверки
throw error;
} finally {
session.endSession();
}
}
MongoDB добавила многодокументные транзакции ACID в версии 4.0, что замечательно! Но это, по сути, переосмысление того, что SQL имеет с 1970-х годов, и это сопровождается значительным снижением производительности, которое лишает смысла использование NoSQL.
Миф о гибкости схемы
«Но NoSQL даёт вам гибкость схемы!» — слышу я ваш крик. Да, и автомобиль без тормозов даёт вам гибкость ускорения. Гибкость схемы часто является недостатком, а не особенностью.
Вот что обычно происходит со схематозными базами данных:
// Неделя 1: Просто и чисто
{
"userId": "123",
"email": "[email protected]",
"created": "2025-01-15"
}
// Неделя 4: Кто-то добавляет поле
{
"userId": "456",
"email": "[email protected]",
"emailVerified": true, // Новое поле!
"created": "2025-02-10"
}
// Неделя 8: Хаос царит
{
"userId": "789",
"email": "[email protected]",
"email_verified": "yes", // Другое имя!
"created": 1709251200, // Unix timestamp теперь?
"preferences": { // Вложенный объект
"newsletter": true
}
}
// Неделя 12: Приложение выходит из строя
{
"userId": "101",
"emails": ["[email protected]", "[email protected]"], // Массив теперь?
"created": "last Tuesday", // Пожалуйста, остановите это
"userID": "101" // Дубликат ключа с другим регистром
}
Удачи с согласованным запросом этого. Ваш код приложения становится минным полем защитных проверок:
function getUserEmail(user) {
// Ужас
return user.email ||
user.Email ||
user.emails?. ||
user.emailAddress ||
user.contact?.email ||
'[email protected]';
}
Сравните это с SQL:
CREATE TABLE users (
user_id INTEGER PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
email_verified BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
preferences JSONB
);
Вы получаете гибкость там, где она нужна (поле JSONB для предпочтений), и структуру там, где она нужна (везде). Ваши данные гарантированно будут действительными. Вы не можете случайно вставить пользователя без электронной почты, и у вас не может быть два разных имени поля для одного и того же понятия.
Сложность запросов: где SQL становится суперсилой
Давайте поговорим о реальном мире. Вы не просто храните и извлекаете простые документы. Вы анализируете данные, создаёте отчёты и отвечаете на сложные бизнес-вопросы.
Пример: аналитика электронной коммерции
Бизнес хочет знать: «Покажи мне топ-10 клиентов по доходам в 2025 году, но только для клиентов, совершивших не менее 3 покупок, вместе со средним значением стоимости заказа и категориями, из которых они чаще всего покупают».
В SQL с PostgreSQL:
WITH customer_stats AS (
SELECT
c.customer_id,
c.name,
COUNT(o.order_id) AS order_count,
SUM(o.total_amount) AS total_revenue,
AVG(o.total_amount