Во времена пика интереса к 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 хранения по которой много уже написано и копий сломано.
Показания и противопоказания, заключение
Помните, у католиков, перед "Адом" существует такая зона "Чистилище" - из неё в принципе можно попасть и в ад и в рай. Ну или зависнуть до второго пришествия.
Можно воспринимать такую K-V таблицу в составе обычной "операционной" БД именно как "Чистилище", для сущностей которые связаны с фичами и идеями нуждающимися в "дозревании".
Суть аналогии ясна - если спустя некоторое время (релиз-другой) мы поняли что "фича" и соответствующией ей записи (сущности) прочно устаканились в проекте и им будет хорошо иметь "собственную" таблицу - что ж, тогда её и сделаем. А если фича оказалась невостребованной или вообще выпилена - скорее всего и делать ничего не придётся.
В проекте давно уже пережившем "бурные времена" и вошедшем в более стабильную фазу, где изменения в схеме БД редки, как праздники - ценность подобной таблички сомнительна.
Наоборот, если проект в динамичной (а то и стартовой) фазе, а аналитики (или в целом идеолухи) приходят с новыми выдумками поразительно часто и вообще склоняются к стилю работы "а-а-а, пожар, срочно выпиливаем то что срочно добавили в предыдщем спринте" - польза от такого подхода к хранению чувствительна. Можно назвать это разновидностью "schema-as-a-code" подхода.
Отдельный юзкейс связан с кастомизацией (которую мы все обожаем) - как часто оказывается, у каждого из вариантов под разных клиентов оказываются какие-то мелкие но очень важные "ништяки" и кастомизировать миграции схемы вообще болезненно (за исключением варианта когда для всех кастомеров создаются все таблицы - и нужные и ненужные). Правда здесь уже почти наверняка можно пойти дальше и организовать полноценный no-sql отдельчик в рамках sql-ной базы (с использованием JSON-колонок и запросов например).
В целом же как и многие подобные вопросы "самой лучшей организации хранения данных" эта маленькая дилемма сводится к персональным вкусам и привычкам разработчика.
Комментарии (3)
Ninil
10.01.2025 05:53K-V таблица - очень распространненный и древний паттерн, существоваший еще "при царе Горохе". Самый частый кейс использования (в DWH) - хранение атрибутов сущностей, которые есть у очень малого числа записей. Так же различные настройки, константы и проч. И как указал предыдущий комментатор, должна быть пошире. Как минимум выделенные колонки для разных типов + даты актуальности.
Akina
Вы рассказываете о том, что на буржуйском обычно называется Service Table. Вполне себе обычная штука.
Key-Value таблица для описываемых целей разумна только в случае, когда она индивидуальна для конкретной БД. Если создавать таблицу такого назначения уровня сервера, то используем EAV и кладём её в служебную БД (Service Database, тоже обычная штука).
На самом деле структура должна быть пошире. Весьма разумно держать в такой таблицы поля с информацией о создании (кто, когда) и сроке актуальности записи, последнее весьма способствует поддержанию чистоты в таблице. Класть эти данные в информационную часть - можно, но не очень разумно.
Натуральный ключ в такой таблице - в большинстве случаев сомнительное решение. Лучше синтетика плюс индексы.
С другой стороны, обычно записей в такой таблице не вагон. А таблица используется по варианту Записать - Найти одну запись - Удалить, никаких массированных обработок такой таблице не требуется. И тогда вполне можно пойти на KV/EAV, где информационная часть хранится в сериализованном формате (например, JSON). И даже прикрутить полнотекст.
RodionGork Автор
Спасибо за ликбез и название :) было подозрение что подобная практика существует и даже как-то называется - но это тот случай когда из-за популярности ключевых (sql, key-value, index) слов трудно найти нужный результат. Ну теперь будет что предъявить коллегам :)