Алексей Меленчук
вся записка
дата
объём 6 мин чтения

SQL или NoSQL: когда реляционная база перестаёт подходить

Чем отличается SQL от NoSQL, почему «NoSQL» — это четыре разных вида баз, и по какому признаку выбирать. Разбор с процедурой выбора, а не «зависит от задачи».

Коротко. Настоящий признак выбора — не объём данных, а известны ли схемы доступа заранее. Реляционная база не требует знать запросы наперёд; документная требует, и в этом её скорость и её ловушка.

Статей «SQL vs NoSQL» написано столько, что запрос выдаёт десяток на первом экране. Почти все заканчиваются одинаково: «зависит от задачи». Это правда и это бесполезно — вопрос ровно в том, от чего именно зависит.

Разберём по существу и с процедурой, которую можно применить.

«NoSQL» — это не один вид базы

Первая ошибка делается до всякого сравнения. Под одним словом лежат четыре разных класса хранилищ с несовместимыми свойствами.

Ключ-значение (Redis, DynamoDB в простом режиме). Достаёт значение по ключу. Ничего больше не умеет и потому очень быстр.

Документные (MongoDB, CouchDB). Хранят самодостаточные документы, обычно в JSON. Ищут по полям, но чем сложнее запрос, тем меньше преимущество.

Широких колонок (Cassandra, HBase). Ориентированы на запись и на выборку по ключу партиции. Модель проектируется от запроса, а не от данных.

Графовые (Neo4j). Оптимизированы для обхода связей на много шагов. Единственный класс, который решает задачу, принципиально неудобную реляционной модели.

Сравнивать «SQL и NoSQL» — то же, что сравнивать «легковой автомобиль и не-легковой транспорт». Дальше я говорю про документные и ключ-значение, потому что выбор обычно идёт между ними и PostgreSQL.

Признак, по которому стоит выбирать

Не объём. Не скорость. Известны ли схемы доступа заранее.

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

Документная база отдаёт эту гибкость в обмен на скорость. Документ собирается под конкретный сценарий чтения, данные дублируются в нём целиком. Чтение одного документа — одно обращение, без соединений. Быстро. Но появился второй сценарий, которому нужен другой разрез, — и вы либо дублируете данные ещё раз, либо делаете то, для чего база не предназначена.

Отсюда практический вопрос, который стоит задать вместо «что быстрее»:

Смогу ли я через год ответить на вопрос, которого сегодня не задаю?

Если продукт молодой и требования подвижны, ответ должен быть «да», и это довод за реляционную базу. Если сценарий один, известен и не изменится — счётчик, сессия, кэш, лента по ключу — документная или ключ-значение выиграют.

Три довода, которые устарели

«Нужна гибкая схема». PostgreSQL умеет JSONB с индексами по полям документа с 2014 года. Полуструктурированные данные лежат в реляционной базе рядом со строгими таблицами, и переходить ради этого никуда не надо. Довод был сильным десять лет назад.

«NoSQL лучше масштабируется». Горизонтальное масштабирование чтения решается репликами и в реляционной базе. Разница возникает на записи, и она не в «SQL против NoSQL», а в том, отказались ли вы от соединений и транзакций между узлами. Отказаться можно и в PostgreSQL — шардированием по ключу; тогда вы получите те же ограничения и ту же скорость.

«Реляционные базы медленные». Медленной базу делает не модель, а отсутствие нужного индекса и запрос, который планировщик выполняет не так, как вы думали. Об этом — отдельный разбор про индексы и планировщик.

Согласованность, о которой не спрашивают

Часть, которую в сравнениях пропускают, а она дороже всех остальных.

Многие документные и распределённые хранилища по умолчанию дают итоговую согласованность: запись подтверждена, но следующее чтение может вернуть старое значение, потому что попало на другую реплику.

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

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

Это тот самый размен, который описывает CAP и его расширение PACELC: за пределами аварии выбор идёт между согласованностью и задержкой, и делается он каждый день.

Цена второго хранилища

Отдельный расход, который не виден при выборе.

«Полиглотное хранение» звучит разумно: каждой задаче свой инструмент. На практике каждое хранилище — это своя схема резервного копирования и проверенного восстановления, свой мониторинг, свои отказы, свой регламент дежурства и свой человек, который умеет это чинить.

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

Процедура выбора

Вместо «зависит от задачи» — четыре вопроса по порядку.

  1. Есть ли инварианты, охватывающие несколько сущностей? Остаток не уходит в минус, у клиента одна активная подписка. Если да — нужны транзакции по нескольким записям, и это довод за реляционную базу. Где живут такие инварианты — отдельный разговор.
  2. Знаете ли вы все запросы наперёд? Если нет — реляционная.
  3. Есть ли обход связей на много шагов? «Друзья друзей», маршруты, зависимости. Если да и он в горячем пути — графовая база решает это принципиально лучше.
  4. Упирается ли запись в один узел прямо сейчас? Не «может упереться когда-нибудь», а упирается по замеру. Если да — обсуждаем шардирование, и уже неважно, реляционная база или нет: ограничения будут те же.

Три «нет» подряд означают PostgreSQL и закрытый вопрос.

Что я делаю на практике

Беру PostgreSQL как значение по умолчанию и выношу отдельные нагрузки в специализированные хранилища тогда, когда есть замер, а не ожидание.

Кэш и сессии — в Redis, потому что это ключ-значение по природе и потому что их не жалко потерять. Аналитика на больших объёмах — в колоночную базу, потому что построчное хранение для агрегатов проигрывает на порядок. Всё остальное остаётся в реляционной базе, пока не появится причина, выраженная числом.

Обратный порядок — начать с документной базы «на вырост» — стоит дорого именно потому, что цена приходит позже: первые полгода всё хорошо, а потом появляется отчёт, которого модель не предусматривала.

Где я могу быть неправ. Если команда уже умеет эксплуатировать конкретное NoSQL-хранилище и не умеет PostgreSQL, эксплуатационный довод перевешивает модельный. База, которую некому чинить в три часа ночи, — худший выбор независимо от модели данных.