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: за пределами аварии выбор идёт между согласованностью и задержкой, и делается он каждый день.
Цена второго хранилища
Отдельный расход, который не виден при выборе.
«Полиглотное хранение» звучит разумно: каждой задаче свой инструмент. На практике каждое хранилище — это своя схема резервного копирования и проверенного восстановления, свой мониторинг, свои отказы, свой регламент дежурства и свой человек, который умеет это чинить.
Второе хранилище должно окупать не разницу в скорости запроса, а весь этот список. Поэтому граница у меня жёсткая: новое хранилище заводится, когда есть замер, показывающий, что текущее не справляется, и когда понятно, кто будет его эксплуатировать.
Процедура выбора
Вместо «зависит от задачи» — четыре вопроса по порядку.
- Есть ли инварианты, охватывающие несколько сущностей? Остаток не уходит в минус, у клиента одна активная подписка. Если да — нужны транзакции по нескольким записям, и это довод за реляционную базу. Где живут такие инварианты — отдельный разговор.
- Знаете ли вы все запросы наперёд? Если нет — реляционная.
- Есть ли обход связей на много шагов? «Друзья друзей», маршруты, зависимости. Если да и он в горячем пути — графовая база решает это принципиально лучше.
- Упирается ли запись в один узел прямо сейчас? Не «может упереться когда-нибудь», а упирается по замеру. Если да — обсуждаем шардирование, и уже неважно, реляционная база или нет: ограничения будут те же.
Три «нет» подряд означают PostgreSQL и закрытый вопрос.
Что я делаю на практике
Беру PostgreSQL как значение по умолчанию и выношу отдельные нагрузки в специализированные хранилища тогда, когда есть замер, а не ожидание.
Кэш и сессии — в Redis, потому что это ключ-значение по природе и потому что их не жалко потерять. Аналитика на больших объёмах — в колоночную базу, потому что построчное хранение для агрегатов проигрывает на порядок. Всё остальное остаётся в реляционной базе, пока не появится причина, выраженная числом.
Обратный порядок — начать с документной базы «на вырост» — стоит дорого именно потому, что цена приходит позже: первые полгода всё хорошо, а потом появляется отчёт, которого модель не предусматривала.
Где я могу быть неправ. Если команда уже умеет эксплуатировать конкретное NoSQL-хранилище и не умеет PostgreSQL, эксплуатационный довод перевешивает модельный. База, которую некому чинить в три часа ночи, — худший выбор независимо от модели данных.