Моделювання даних
Модель даних — це набір рішень про те, що має бути істинним, записаних один раз і забезпечуваних доти, доки існують дані. Вона переживає застосунок, збудований над нею: фреймворки замінюють, схему мігрують, — тож рішення, ухвалене недбало на другому тижні, досі коштує комусь на пʼятому році. На цій сторінці — сутності й ключі, що саме запобігає кожна нормальна форма, чому типи й 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.
- Проєктний огляд, такий довгий, що нічого не виходить, коли на питання відповіли б мала схема й шлях міграції.
Було корисно?
Поділіться з тим, хто працює над тією ж задачею.