Божественная K-V таблица для мелочей
Во времена пика интереса к 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», а то больше про кино вываливается
Можно воспринимать такую K-V таблицу в составе обычной «операционной» БД именно как «Чистилище», для сущностей которые связаны с фичами и идеями нуждающимися в «дозревании».
Суть аналогии ясна — если спустя некоторое время (релиз-другой) мы поняли что «фича» и соответствующией ей записи (сущности) прочно устаканились в проекте и им будет хорошо иметь «собственную» таблицу — что ж, тогда её и сделаем. А если фича оказалась невостребованной или вообще выпилена — скорее всего и делать ничего не придётся.
В проекте давно уже пережившем «бурные времена» и вошедшем в более стабильную фазу, где изменения в схеме БД редки, как праздники — ценность подобной таблички сомнительна.
Наоборот, если проект в динамичной (а то и стартовой) фазе, а аналитики (или в целом идеолухи) приходят с новыми выдумками поразительно часто и вообще склоняются к стилю работы «а-а-а, пожар, срочно выпиливаем то что срочно добавили в предыдщем спринте» — польза от такого подхода к хранению чувствительна. Можно назвать это разновидностью «schema-as-a-code» подхода.
Отдельный юзкейс связан с кастомизацией (которую мы все обожаем) — как часто оказывается, у каждого из вариантов под разных клиентов оказываются какие-то мелкие, но очень важные «ништяки» и кастомизировать миграции схемы вообще болезненно (за исключением варианта когда для всех кастомеров создаются все таблицы — и нужные и ненужные). Правда здесь уже почти наверняка можно пойти дальше и организовать полноценный no-sql отдельчик в рамках sql-ной базы (с использованием JSON-колонок и запросов например).
В целом же как и многие подобные вопросы «самой лучшей организации хранения данных» эта маленькая дилемма сводится к персональным вкусам и привычкам разработчика.