РозробкаБудь-який рівень

Моделювання даних

Модель даних — це набір рішень про те, що має бути істинним, записаних один раз і забезпечуваних доти, доки існують дані. Вона переживає застосунок, збудований над нею: фреймворки замінюють, схему мігрують, — тож рішення, ухвалене недбало на другому тижні, досі коштує комусь на пʼятому році. На цій сторінці — сутності й ключі, що саме запобігає кожна нормальна форма, чому типи й null є рішеннями моделювання, а не технічними дрібницями, і як записувати час, не втрачаючи історії.

Модель стверджує
Що має бути істинним
Нормальні форми
По одній аномалії
Тривалість життя
Довша за застосунок

Сутності, ключі й форма звʼязку

Моделювання починається з одного питання, поставленого багато разів: про які речі говорить цей продукт і що є істинним про кожну з них рівно один раз? У клієнта одна електронна адреса й багато замовлень. У замовлення один підсумок і багато позицій. Скласти цей перелік правильно — це більшість роботи, а надійний спосіб його знайти — виписати речення, які бізнес справді промовляє вголос: «один рахунок може покривати кілька поставок» — це звʼязок «багато до багатьох», сказаний звичайною мовою, і він лишиться істинним після трьох переписувань застосунку.

Ключ — це обіцянка, що рядок можна ідентифікувати. Їх два види, і вибір важить більше, ніж здається. Природний ключ — це значення, яким бізнес уже користується: електронна адреса, податковий номер, ISBN. Сурогатний ключ — це число чи ідентифікатор, вигаданий базою і безглуздий поза системою. Природні ключі добре читаються й економлять приєднання, і мають один режим збою, що повторюється скрізь: бізнесові ідентифікатори змінюються. Люди міняють адреси, компанії перереєстровують, країна змінює формат свого ідентифікатора. Коли це значення ще й ключ, кожен рядок, що на нього посилався, мусить змінитися разом із ним.

Робочий типовий вибір — сурогатний первинний ключ плюс унікальне обмеження на природний: ідентичність стабільна, а бізнес-правило все одно забезпечено. Дві деталі варто вирішити, а не успадкувати. Послідовне ціле компактне й швидке, але видає, скільки записів існує і як швидко вони зростають, — а це має значення, якщо значення потрапляє в URL. Випадковий ідентифікатор це приховує й дозволяє клієнтові згенерувати ідентифікатор до появи рядка, ціною розміру, а для цілком випадкових — ще й локальності індексу; впорядковані в часі варіанти існують саме для того, щоб мати і те, і те.

ЗвʼязокЯк поданоДеталь, яку пропускають
Один до багатьохЗовнішній ключ на боці «багатьох», що вказує на «один».Чи може ключ бути null, вирішує, чи законна сирота, — а це бізнесове питання.
Багато до багатьохТретя таблиця з одним рядком на пару й зовнішнім ключем до кожного боку.Ця таблиця зазвичай і сама є сутністю й обростає атрибутами: роль, кількість, дата.
Один до одногоСпільний ключ або унікальне обмеження на зовнішньому ключі.Справді рідкісний. Зазвичай означає, що одну сутність поділили заради зберігання або сплутали дві.
ІєрархіяЗовнішній ключ від рядка до іншого рядка тієї самої таблиці.Прочитати цілу гілку можна рекурсивним запитом; без нього глибина стає жорстко зашитим лімітом.

Кожна нормальна форма прибирає одну конкретну аномалію

Нормалізація має репутацію академічної церемонії, і ця репутація цілком походить із того, що її викладають як драбину, на яку треба вилізти, а не як три названі проблеми, яких треба уникнути. Кожна форма прибирає один конкретний спосіб, у який дані псуються, коли той самий факт зберігається в кількох місцях. Вивчіть аномалію — і правило випливе саме; вивчіть саме лише правило — і застосуєте його там, де воно не допомагає.

Кожна нормальна форма прибирає один конкретний спосіб, у який псуються дані, коли той самий факт зберігається в кількох місцях. Перша: одне значення в комірці й без повторюваних груп, тож колонку зі списком через кому — або пронумеровані колонки, що вдають список, — розкладають на рядки. Друга: у таблиці, ключованій більш ніж однією колонкою, жодне поле не має залежати лише від частини цього ключа, тож назва товару не сидить у кожній позиції замовлення. Третя: жодне поле не має залежати від іншого неключового поля, тож місто клієнта не сидить у замовленні й не розходиться після переїзду. Уся третя нормальна форма вміщується в одне речення: кожне неключове поле залежить від ключа, всього ключа й нічого, крім ключа.

Перша нормальна форма каже, що кожна комірка містить одне значення й повторюваних груп немає. Колонка tags зі значенням «терміново, рахунки, vip» виглядає нешкідливо і не піддається ні фільтруванню, ні індексації, ні підрахунку, ні перейменуванню: кожна операція над нею стає маніпуляцією рядками, а «рахунки» збігається з «перерахунки», якщо не бути обережним так, як ніхто довго не буває. Ліки — рядок на значення. Те саме правило відкидає й іншу форму повторюваної групи: phone_1, phone_2, phone_3 — це список, що вдає колонки, і він ламається того дня, коли в когось зʼявиться четвертий.

Друга нормальна форма важить лише тоді, коли таблиця ключована більш ніж однією колонкою, і каже, що жодне поле не має залежати лише від частини цього ключа. Таблиця позицій, ключована замовленням і товаром, не має нести назви товару: назва залежить лише від товару, тож повторюється в кожному замовленні, яке його колись містило, і перейменування товару означає оновлення тисяч рядків — з яких один пропустять. Третя нормальна форма поширює ту саму логіку на поля, що залежать від іншого неключового поля. Місто клієнта, збережене в замовленні, залежить від клієнта, а не від замовлення, — тож коли він переїде, старі замовлення тихо стверджуватимуть, що їх зроблено з нового міста.

Денормалізація тоді є свідомою зворотною оптимізацією, а не невдалою нормалізацією, і різниця в тому, чи можете ви сказати, що вона купила. Копіювати значення, щоб уникнути приєднання, варто, коли читання гаряче, значення рідко змінюється й щось тримає копію чесною: тригер, планове звіряння або той факт, що копію й має бути заморожено. Збережений підсумок у замовленні — класичний законний випадок: це не кешована сума позицій, це та сума, на яку погодився клієнт, і вона не має змінюватися, коли зміниться ціна. Незаконний випадок виглядає точнісінько так само й не має механізму, що тримає його істинним.

Типи й null — це рішення моделювання

Тип — найдешевше обмеження, яке взагалі буває, і добре обраний тип робить цілі категорії вад непредставними. Надійний інстинкт — брати найвужчий тип, що вміщує кожне законне значення й жодного незаконного, і опиратися спокусі зберігати все як текст, бо текст вміщує все. Текст справді вміщує все — і ще приймає «N/A», «невідомо», « 12,50 » і порожній рядок, а щось із цього хтось туди зрештою покладе.

Що ви зберігаєтеВибір, що болитьЩо робити натомість
ГрошіЧисло з рухомою комою: воно не подає 0.10 точно й округлює по-різному в кожній мові.Точний десятковий тип або ціле число найдрібніших одиниць — і зберігайте валюту поруч.
Мить у часіЛокальна позначка часу без зони: двічі на рік неоднозначна й безглузда між регіонами.Позначка часу з зоною для миттєвостей; проста дата лише там, де справді йдеться про календарний день.
Фіксований набір станівКолонка вільного тексту: за рік у ній накопичуються «сплачено», «Сплачено» і «сплочено».Обмеження або довідникова таблиця: друга ще й дозволяє перейменувати мітку, не чіпаючи даних.
Структурована грудкаJSON-колонка для полів, за якими ви шукаєте й фільтруєте на кожному запиті.Підніміть поля, за якими шукаєте, до колонок; JSON лишіть для того, що справді різниться в кожному рядку.

Null заслуговує на окремий абзац, бо це не значення, а відсутність значення, — і воно поширюється. Порівняння з null не істинне й не хибне, а невідоме, тож рядок із null у колонці ніколи не збігається з = x і так само ніколи не збігається з <> x; сума його пропускає, підрахунок за цією колонкою пропускає, а унікальне обмеження в більшості рушіїв дозволяє мати скільки завгодно null. Ніщо з цього не є вадою, і все це хоч раз дивує.

Тож робіть кожну колонку з null рішенням із підставою. Добрих підстав лише дві: значення справді необовʼязкове в бізнесі або воно ще невідоме тієї миті, коли створюється рядок. Усе інше — замаскована проблема моделювання. Група колонок, що бувають null разом — cancelled_at, cancelled_by, cancellation_reason, — це стан, чиє місце у власній таблиці або у власному переліченому статусі. А колонка, що є null тому, що стосується лише деяких різновидів рядків, означає, що у вас дві сутності ділять одну таблицю.

Як це виявляється в реальній поставці

Запит, на якому спотикається більшість схем, — «а як це виглядало в березні». Таблиця, що зберігає поточний стан, відповідає на питання про зараз і ні на що більше, а історію потім не відновити: її ніколи не було записано. Вирішуйте рано, яким сутностям потрібна історія, бо доробити її згодом означає вибачення, а не міграцію. Дешевий варіант — журнал змін лише на дописування поруч із поточним рядком. Повний варіант зберігає період чинності кожної версії й питає кожен запит, яку мить він має на увазі, — це чесніше й більше роботи.

Два різновиди дат плутають, а не варто: коли щось сталося у світі й коли про це дізналася ваша система. Платіж, зроблений у пʼятницю й імпортований у понеділок, має обидві, і кожне звіряння, звіт та аудиторське питання залежать від того, чи знаєте ви, про яку саме йдеться. Зберігання лише однієї з них — причина, чому місячний підсумок змінюється після закриття місяця й ніхто не може пояснити чому.

Мʼяке видалення теж варто назвати рішенням, бо його зазвичай беруть рефлекторно. Колонка deleted_at зберігає історію й робить випадкове видалення відновлюваним — і назавжди накладає податок на кожен запит у системі: один пропущений фільтр покаже користувачеві прибрані дані. Якщо ви це використовуєте, забезпечуйте в одному місці — поданням, репозиторієм, політикою, — а не в кожному запиті. І памʼятайте, що мʼяке видалення не є видаленням у правовому сенсі, тож системі зі справжнім обовʼязком стирання все одно потрібне справжнє.

Нарешті, сприймайте схему як вихідний код, який читають інші люди. Імена — це інтерфейс: status у трьох таблицях, що означає три різні набори значень, коштує щодня, а колонка на імʼя flag за півтора року стає загадкою. Оберіть одну домовленість про однину чи множину в назвах таблиць, одну для позначок часу, одну для зовнішніх ключів — і запишіть її, щоб десята таблиця збігалася з першою. Тримайте міграції в репозиторії, переглядайте їх як будь-яку іншу зміну й робіть кожну зворотною або явно позначеною як незворотна.

Де це вироджується

  • Схема, зліплена під теперішній екран: її доведеться мігрувати першого ж разу, коли екран зміниться.
  • Список через кому в колонці: з ним не працює ні індекс, ні фільтр, ні підрахунок.
  • Природний ключ як первинний — доки бізнес не змінить значення, з якого той зроблено.
  • Гроші в колонці з рухомою комою: це інцидент округлення, що чекає на достатню кількість транзакцій.
  • Позначки часу без зони: двічі на рік неоднозначні й хибні між регіонами.
  • Колонки з null, додані за замовчуванням: кожен шлях читання несе розгалуження, якого ніхто не тестував.
  • Денормалізована копія, чесність якої ніщо не тримає: вона тихо розходиться, і їй усе одно вірять.
  • Одна таблиця на дві сутності, розрізнені колонкою типу й набором колонок, що бувають null.
  • Збережено лише поточний стан: перше ж питання про минулий квартал не має відповіді взагалі.

Коли це застосовувати

Застосовуйте, коли

  • До появи першої таблиці: кожна пізніша зміна — це міграція й розгортання, а не редагування.
  • Щоразу, коли в продукті зʼявляється нова сутність: хай приходить із вирішеними ключами, обмеженнями й типами.
  • Коли той самий факт починає зʼявлятися у двох місцях: це сигнал, що одну форму пропущено.
  • Перед переходом на документне чи ширококолонкове сховище, де модель має бути правильною з першого разу.

Уникайте, коли

  • Нормалізація звітної чи аналітичної таблиці, де широка денормалізована форма і є її сенсом.
  • Гонитва за нормальними формами, вищими за третю, у звичайній продуктовій схемі: випадки, які вони покривають, там не трапляються.
  • Моделювання справді змінного вмісту — тіла вебхука, відповіді третьої сторони — як пʼятдесяти колонок із null.
  • Проєктний огляд, такий довгий, що нічого не виходить, коли на питання відповіли б мала схема й шлях міграції.

Було корисно?

Поділіться з тим, хто працює над тією ж задачею.