Введение

Работа с базами данных — это чуть ли не ключевая задача любого бэкенд-разработчика. В самом простом случае наша работа сводится к схеме “положи что-то в базу - достань что-то из базы”. Это вы и без меня сделаете.

А тут мы разберем, какие базы бывают, зачем так много, в чем отличия, когда использовать то, когда — это. Еще мы затронем вопросы оптимизации и атомарности запросов. В общем, будет интересно (или нет). Но полезно будет точно.

Какие виды баз данных существуют?

Самые популярные базы данных — реляционные (SQL), которые появились лет 40 назад. Это и маргинальный (чутка) MySQL, и богоподобный Postgres, и всякие Oracle с MSSQL, которые странно будет видеть Node.js-разработчику (но бывает всякое).

Вторые по популярности — noSQL-базы. Но тут есть подвох: мы называем noSQL все, что не SQL, а это и key-value хранилища типа Redis, и документоориентированные базы данных типа MongoDB, и графовые типа Neo4j, и столбцовые типа Сassandra, и колоночные типа Clickhouse. И все они нужны для разных вещей. Это в том числе мы и разберем вместо того, чтобы просто написать статью “SQL vs noSQL”, по сути, не имеющую никакого смысла. Да и в интернетах таких килограммы, зачем еще — непонятно.

Итак, давайте для начала о плюсах и минусах разных типов баз данных. Начнем с SQL, конечно.

SQL-базы данных

Вероятно, самый распространенный и популярный тип баз данных — реляционнные базы данных. SQL. Structured Query Language, “язык структурированных запросов”. MySQL, Postgres, Oracle RDBMS, MS SQL — все это SQL-базы данных. Всех их объединяет в первую очередь то, что они этот самый SQL поддерживают.

Вообще, SQL - это такой декларативный язык программирования. Что такое “декларативный”, мы когда-нибудь подробно обсудим, а пока скажу, что, в отличие от, к примеру, императивных языков программирования (типа JS), мы описываем не способ решения задачи, а ожидаемый результат. Не очень понятно? Это пока и не очень важно, мы тут больше о базах данных пока.

Итак, в чем прикол реляционных баз данных? Это такие хранилища данных, где данные хранятся в строго структурированных таблицах, и часто эти таблицы как-то связаны друг с другом (к примеру, в приложении-блоге наверняка будет таблица пользователей и таблица постов, и они будут связаны типом один-ко-многим — то есть один пользователь может иметь несколько постов. О типах связей мы здесь умолчим, иначе статья превратится в детальное описание реляционных баз данных, а я этого не хочу. Когда-нибудь такая статья выйдет отдельно.). В общем, эти самые связи (relations, реляции) и объясняют название таких баз данных. А язык SQL позволяет данные в эти таблицы добавлять, менять их, удалять и забирать, как угодно их комбинируя.

Когда мы используем реляционные базы данных?

  • когда известна структура базы данных, то, в какой форме мы храним данные
  • когда мы не очень хотим масштабировать серверы с базой данных горизонтально (увеличивая количество серверов), предпочитая вместо этого вертикальное масштабирование (добавить серверу CPU, RAM и так далее). Конечно, в большинстве случаев мы можем масштабировать реляционные базы данные горизонтально, но это не очень удобно.
  • когда мы хотим хранить сущности в разных таблицах, объединяя их в выборках с помощью специальных JOIN-инструкций, используя взаимосвязи этих самых таблиц

NoSQL-базы данных

Мы почему-то привыкли понимать под этим термином документоориентированные базы данных, но на самом деле типов noSQL-баз гораздо больше. Давайте разбираться, и, раз уж так вышло, начнем именно с документоориентированных баз данных.

Документоориентированные базы данных

Рассмотрим документоориентированные базы данных на сверхпопулярной MongoDB. Зачем она нужна, если есть столь прекрасные SQL-базы данных?

  • мы не всегда знаем заранее структуру базы данных. Документоориентированные базы данных позволяют обойтись без заранее заданной структуры, храня данные “как придется”
  • мы хотим хранить документы со всеми свойствами в одном месте, не дергая данные с разных таблиц. Это может быть удобнее, и это точно гораздо быстрее
  • мы хотим горизонтально масштабироваться. Документоориентированные базы данных позволяют делать это легко

Хранилища типа “ключ-значение”

Такого рода базы данных достаточно просты: значения хранятся по ключу, как это понятно из названия. Они используются тогда, когда сложность других типов баз данных не нужна, зато нужны:

  • скорость. Базы данных типа Redis хранят данные в оперативной памяти, и это невероятно быстро. Это очень удобно использовать для кэширования
  • простота. Ключ — значение: куда уж проще?
  • горизонтальная масштабируемость. Масштабировать такие базы горизонтально даже проще, чем документоориентированные базы данных

Столбцовые (колоночные) базы данных

В столбцовой БД данные каждого столбца хранятся отдельно (независимо) от других столбцов. Такой принцип хранения позволяет при выполнении запроса считывать с диска данные только тех столбцов, которые непосредственно участвуют в этом запросе. Обратная сторона такого принципа хранения заключается в том, что выполнение операций над строками становится более затратным. Когда мы хотим использовать такие базы данных?

  • мы хотим часто выполнять операции над отдельными столбцами, не трогая остальную базу
  • мы хотим очень быстро читать данные из базы в ущерб записи данных туда, не используя индексы
  • мы хотим уменьшать объем базы данных на диске, эффективно сжимая столбцы

Графовые базы данных

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

Индексы

Когда мы говорим о реляционных и документоориентированных базах данных, иногда нам хочется ускорить чтение и поиск определенных данных. Используем мы для этого индексы. Индекс — это какая-то определенная структура данных (например, бинарное дерево, которое по умолчанию используется для индекса в Postgres), которая формируется из данных определенного столбца или поля (или полей) и позволяет нам быстро искать данные по этим полям. Преимущества индексов очевидны, а недостатка всего два (но значительных):

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

Типы индексов мы здесь разбирать не будем — они зависят от самой базы данных, а мы тут просто различия разбираем.

Транзакции

Иногда нам нужно объединить несколько операций с данными в одну так, чтобы если не выполнилась одна из них, не выполнились и другие. Простой пример: у нас есть интернет-магазин, клиент покупает товар. По сути, это несколько операций: списать деньги с баланса клиента, добавить деньги на баланс магазина, уменьшить количество товара на складе. Если выполнится только одна операция из них, получится не очень хорошо.

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

Изоляции транзакций

Очевидно, что транзакции могут выполняться параллельно. Давайте простенько на примере Postgres рассмотрим уровни изоляции транзакций (то есть варианты, насколько сильно влияют друг на друга параллельно выполняющиеся транзакции).

  • READ UNCOMMITTED: самый быстрый уровень изоляции транзакций. Но есть минус: не очень-то оно и изолирует транзакции. В некоторых случаях две параллельные транзакции видят незафиксированные данные друг друга и таким образом друг на друга влияют. Этот уровень изоляции нельзя использовать для очень критичных данных.
  • READ COMMITTED: на этом уровне изоляции параллельные транзакции видят только зафиксированные (завершенные) данные друг друга. Вроде все хорошо, но возможны случаи, когда одна из транзакций видит обновленные / удаленные / добавленные другой транзакцией данные.
  • REPEATABLE READ: еще более серьезный уровень изоляции транзакций, в современной реализации Postgres полностью изолирует данные транзакций друг от друга
  • SERIALIZABLE: полная изоляция транзакций друг от друга.

Чтобы объяснить минусы последних двух уровней транзакции, придется объяснить понятие locks — это запрет на изменение данных до завершения транзакции. Так вот, чем серьезнее транзакции изолированы друг от друга, тем более вероятность появления lock. Выбор уровня изоляции зависит от того, насколько критична непересекаемость транзакций (которая чревата нежелательным изменением данных).

Финалочка

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