Посмотрите, я понимаю. NoSQL в тренде. Это круто. Он горизонтально масштабируется как настоящий профи, и где-то около 2015 года мы все решили, что реляционные базы данных устарели, как раскладушки. Но вот неудобная правда, о которой никто не хочет говорить на технических конференциях: для большинства приложений вам, вероятно, не нужен NoSQL, и настаивать на его использовании — это всё равно что принести огнемёт на церемонию зажигания свечей.

Я видел слишком много команд, которые загоняли себя в тупик, выбирая MongoDB или Cassandra для проектов, которые были бы вполне довольны старой доброй PostgreSQL. Результат? Потерянные месяцы разработки, кошмары с согласованностью данных, и разработчики, не спящие по ночам и задающиеся вопросом, почему их база данных с «конечной согласованностью» решила, что «в конечном счёте» означает «может быть, во вторник следующей недели».

Это не нападки на NoSQL. Я использовал его, мне он понравился, и я буду использовать его снова. Но маятник качнулся слишком далеко, и пришло время честно поговорить о том, когда SQL-базы данных не только конкурируют с NoSQL, но и полностью его побеждают.

Великая маркетинговая кампания NoSQL

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

  • Нужно масштабироваться? NoSQL.
  • Есть неструктурированные данные? NoSQL.
  • Создаёте современное приложение? NoSQL.
  • Хотите выглядеть умным на митапах? NoSQL.

Но в этом повествовании удобно умалчивается о десятилетиях оптимизации, инструментарии и проверенной надёжности, которые SQL-базы данных приносят на стол. Это всё равно что отказаться от швейцарского армейского ножа, потому что кто-то изобрёл действительно модный открывашку для бутылок.

Когда SQL не просто хорош — он лучше

Соответствие ACID: незамеченный герой

Давайте представим картину. Вы создаёте платформу электронной коммерции. Клиент нажимает «Купить» на товар стоимостью 500 долларов. Ваше приложение должно:

  1. Списать сумму с его счёта.
  2. Добавить товар в историю заказов.
  3. Уменьшить количество товара на складе.
  4. Создать запись о доставке.

С 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