Во времена пика интереса к NoSQL базам данных простоватые K-V хранилища были несколько обойдены вниманием - отчасти это понятно, вещь не очень "инновационная", можно даже сказать старинная. В то же время своя "ниша" у них находится до сих пор (не считая того что они используются в более сложных БД в качестве индексов).

В то же время в обычной SQL-ной базе проекта порой "не хватает" такого общего K-V хранилища для разнородных (семантически) записей. В своих проектах я такую обычно завожу. Среди коллег этот подход порой вызывает негатив :)

Поясню ситуацию на примерах и попробую выписать "за и против" - а уважаемых знатоков приглашаю поделиться мнениями - особенно если у кого-то в схеме похожие таблички встречаются. Не для поиска несуществующей "истины", а ради дележа опытом и идеями.

Типичные ситуации

Обычно это про сущности которых по-видимому не будет много - у которых не много полей (или вообще одно) и которые не требуется дополнительно индексировать по этим полям, например (с собственного сайта):

  • админские нотификации пользователям (типа "поправь то и то у себя в профиле") - они исчезают после прочтения и поэтому "постоянно" их в пределах жалких 5 штук на 50 тыщ аккаунтов

  • зависимости между упражнениями в духе "решения задачи А не показывать если не решена задача Б" - таких в принципе не много т.к. задач всего штук 400 и подобных связок всего может штук 10

  • "закладки" на задачи для пользователей - эту "фичу" попросили на днях и пока не факт что она много кому понадобится

  • список языков (ru, es, fr...) на которые есть перевод текста данной задачи (переводы есть м.б. у полусотни задач - и переводов обычно меньше 10

  • top-of-week - полтора десятка строчек с пользователями нарешавшими больше всего за последнюю неделю

  • временные ключи для подтверждения / восстановления пароля

Академичный подход заключается...

какую самую большую диаграмму табличек в БД вам доводилось видеть?
какую самую большую диаграмму табличек в БД вам доводилось видеть?

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

create table admin_notifications (userid int, message text);
create table task_no_show_solution_deps (taskid int, dependent_task_id int);
create table task_bookmarks (userid int, taskid int);
create table task_translations (taskid int, lang text);
create table top_of_week (userid int, solved int);
create table password_reset (userid, temp_password text, ts timestamp)

При этом можно реализовывать и one-to-one, и one-to-many, и many-to-many ситуации, благо таблицы независимы - и по ходу эволюции проекта можно добавлять в них какие-то нужные поля и т.п.

Недостатки такого подхода - не то чтобы критичны:

  • на каждую подобную мелкую фичу нужно добавлять миграцию в БД с созданием новой таблицы

  • со временем получается много "мелких таблиц" однотипных по структуре, часто с небольшим количеством записей (даже порой пустых)

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

Вариант с единой K-V таблицей

Базы типа "ключ-значение" или "персистентные мэпы" обычно так просты что и рисовать нечего
Базы типа "ключ-значение" или "персистентные мэпы" обычно так просты что и рисовать нечего

Интуитивно напрашивается "вынести" название таблицы в отдельное поле и использовать одну таблицу из 3 полей:

create table stuff (id text, record_type text, value text);

в record_type предполагается записывать что-то типа adm_msg, week_top, temp_pwd и так далее - и индексом должна быть пара id, record_type.

Тут сразу видны мелкие недочёты - числовые индексы (например userid, taskid) придётся держать как строки - а record_type будет содержать много повторяющихся значений. Но это действительно мелочи пока в таблице не миллиарды записей - первый не особо аффектит вообще а второй - ну можно либо использовать короткие метки, либо сделать отдельно enum для этих значений (но enum-ам есть свои противопоказания).

Поскольку индекс составной, то в принципе почему бы не упростить таблицу до двух колонок:

create table keyval (key text, val text);

и конкатенировать "айдишник" с типом записи, в духе:

insert into keyval values ('admmsg.12345', 'Please, change your avatar');
insert into keyval values ('trans.115', 'ru fr es sk');
insert into keyval values ('bookmk.54321', '90 115 36 42');

Смысл этого подхода понятен - если для очередной фичи нужна новая простая табличка - то она уже есть - от миграций мы освободились (не считая того что с роллбэками в таблице может оставаться какой-то мусор но в большинстве случаев он никому не помешает).

Для ключей предпочтительно использовать b-tree индекс а не хэш ради того чтобы иметь возможность с его помощью выбирать сразу все значения нужной категории

select key, val from keyval where key like 'admmsg.%'

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

В то же время возможно хранение all-in-one - когда для данной категории сущностей ключ всего один и других не предвидится - например таблицу top-of-week можно хранить как один JSON по единственному ключу (всё равно она нужна только чтобы отдавать её на UI):

insert into keyval values ('weektop',
    '{["johndoe", "John X. Doe", 67], ["lyztaylor", "Elizabeth Kitty", 53], ...}');

Недостатков тут можно усмотреть немало, но остановимся на двух:

  • сложность с хранением нескольких значений по одному ключу - нужно "изобретать" какой-то формат внутри "значения" - либо простой список, а может json и т.п.

  • нужны отдельные усилия чтобы эффективно использовать такую таблицу в коде, особенно если используется ORM - не очень культурно если везде по коду будут разбросаны префиксы типа записи (эти admmsg и bookmk)

Однако поскольку такая таблица всего одна то большинство подобных неудобств и решаются в одном месте - вероятно появляется энум (но в коде!) с типами и утильные методы get / put которые умеют преобразовывать записи в массивы и объекты (по какому признаку они решают это делать - это уж на усмотрение автора).

Однозначно что использовать такую "гибкость" оказывается несколько легче в скриптовых языках с динамической типизацией - но в целом это мы уже углубляемся в отдельную тему schema-less хранения по которой много уже написано и копий сломано.

Показания и противопоказания, заключение

Помните, у католиков, перед "Адом" существует такая зона "Чистилище" - из неё в принципе можно попасть и в ад и в рай. Ну или зависнуть до второго пришествия.

картинки про чистилище лучше искать по слову "purgatory" а то больше про кино вываливается
картинки про чистилище лучше искать по слову "purgatory" а то больше про кино вываливается

Можно воспринимать такую K-V таблицу в составе обычной "операционной" БД именно как "Чистилище", для сущностей которые связаны с фичами и идеями нуждающимися в "дозревании".

Суть аналогии ясна - если спустя некоторое время (релиз-другой) мы поняли что "фича" и соответствующией ей записи (сущности) прочно устаканились в проекте и им будет хорошо иметь "собственную" таблицу - что ж, тогда её и сделаем. А если фича оказалась невостребованной или вообще выпилена - скорее всего и делать ничего не придётся.

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

Наоборот, если проект в динамичной (а то и стартовой) фазе, а аналитики (или в целом идеолухи) приходят с новыми выдумками поразительно часто и вообще склоняются к стилю работы "а-а-а, пожар, срочно выпиливаем то что срочно добавили в предыдщем спринте" - польза от такого подхода к хранению чувствительна. Можно назвать это разновидностью "schema-as-a-code" подхода.

Отдельный юзкейс связан с кастомизацией (которую мы все обожаем) - как часто оказывается, у каждого из вариантов под разных клиентов оказываются какие-то мелкие но очень важные "ништяки" и кастомизировать миграции схемы вообще болезненно (за исключением варианта когда для всех кастомеров создаются все таблицы - и нужные и ненужные). Правда здесь уже почти наверняка можно пойти дальше и организовать полноценный no-sql отдельчик в рамках sql-ной базы (с использованием JSON-колонок и запросов например).

В целом же как и многие подобные вопросы "самой лучшей организации хранения данных" эта маленькая дилемма сводится к персональным вкусам и привычкам разработчика.

Комментарии (3)


  1. Akina
    10.01.2025 05:53

    Вы рассказываете о том, что на буржуйском обычно называется Service Table. Вполне себе обычная штука.

    Key-Value таблица для описываемых целей разумна только в случае, когда она индивидуальна для конкретной БД. Если создавать таблицу такого назначения уровня сервера, то используем EAV и кладём её в служебную БД (Service Database, тоже обычная штука).

    На самом деле структура должна быть пошире. Весьма разумно держать в такой таблицы поля с информацией о создании (кто, когда) и сроке актуальности записи, последнее весьма способствует поддержанию чистоты в таблице. Класть эти данные в информационную часть - можно, но не очень разумно.

    Натуральный ключ в такой таблице - в большинстве случаев сомнительное решение. Лучше синтетика плюс индексы.

    С другой стороны, обычно записей в такой таблице не вагон. А таблица используется по варианту Записать - Найти одну запись - Удалить, никаких массированных обработок такой таблице не требуется. И тогда вполне можно пойти на KV/EAV, где информационная часть хранится в сериализованном формате (например, JSON). И даже прикрутить полнотекст.


    1. RodionGork Автор
      10.01.2025 05:53

      Спасибо за ликбез и название :) было подозрение что подобная практика существует и даже как-то называется - но это тот случай когда из-за популярности ключевых (sql, key-value, index) слов трудно найти нужный результат. Ну теперь будет что предъявить коллегам :)


  1. Ninil
    10.01.2025 05:53

    K-V таблица - очень распространненный и древний паттерн, существоваший еще "при царе Горохе". Самый частый кейс использования (в DWH) - хранение атрибутов сущностей, которые есть у очень малого числа записей. Так же различные настройки, константы и проч. И как указал предыдущий комментатор, должна быть пошире. Как минимум выделенные колонки для разных типов + даты актуальности.