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

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

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

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

Шістнадцять питань до кожної таблиці — відповідь, що витримує час, і ціна хибної

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

#ПитанняВідповідь, що витримує часЧого згодом коштує хибна відповідь
1Що таке один рядок цієї таблиці?Одне речення про одну річ: «один рядок — це одна позиція одного замовлення»Дві сутності в одній таблиці, розрізнені колонкою типу й колонками, порожніми в половині рядків
2Що ідентифікує рядок?Сурогатний первинний ключ плюс унікальне обмеження на бізнесовий ідентифікаторБізнесовий ідентифікатор як ключ — доки він не зміниться, а з ним і кожне посилання
3До якого розміру може вирости ключ?bigint або UUID; ніколи не 32-бітне ціле для того, що рахує події32-бітний ключ зупиняється на 2 147 483 647 — а з ним і вставлення
4На що вказує цей рядок і чи може цього не бути?Зовнішній ключ на боці «багатьох»; NOT NULL, якщо бізнес не дозволяє сиротиСироти, яких ніхто не може пояснити, і посилання з NULL, про яке ніхто не вирішував
5Чи є зв’язок «багато до багатьох» окремою річчю?Таблиця на зв’язок — вона зазвичай отримує кількість, роль чи датуСписок id усередині колонки, якого не перевірить жоден зовнішній ключ чи індекс
6Як зберігаються гроші?Точний десятковий тип або ціле число дрібних одиниць — і валюта поручЧисло з рухомою комою: 0.1 + 0.2 — не 0.3, а округлення в різних мовах різне
7Це мить, календарна дата чи майбутній місцевий час?Мітка часу з часовим поясом для миттєвостей; дата для дня; місцевий час плюс назва поясу для майбутньої зустрічіМітка часу без поясу: двічі на рік неоднозначна й хибна між регіонами
8Чи фіксований набір значень?CHECK, перелічуваний тип або довідникова таблицяВільний текст: «сплачено», «Сплачено» і «сплочено» вже за рік
9Чому ця колонка може бути NULL?З однієї з двох причин: необов’язкове в бізнесі або ще невідоме, коли створюють рядокNULL, що означає п’ять різних речей, — або замінник на кшталт 'N/A' чи 1900-01-01, що означає одну з них
10Чому це JSON, а не колонки?JSON лише для того, що справді різниться від рядка до рядка й ніколи не фільтрується й не обмежуєтьсяФільтри й правила на полях, яких база не бачить і не може перевірити
11Чи зберігається кожен факт рівно один раз?Кожна неключова колонка каже факт про ключ, весь ключ і нічого, крім ключаТой самий факт у двох місцях, одне з яких оновлення пропустить
12Хто тримає кожну копію правдивою?Кожна свідома копія має механізм — тригер, перебудову — або її й мають заморозитиКопія, що тихо розходиться, і їй усе одно вірять
13Це значення обчислене чи погоджене?Зберігати погоджене — ціну на момент купівлі, суму, яку прийняв клієнт; обчислювати похіднеПерерахована сума, що змінюється, коли змінюється ціна
14Чи спитає хтось, як це виглядало в березні?Історія для сутностей, яким вона потрібна, вирішена зараз: таблиця історії або періоди чинностіЛише поточний стан — тож питання не має відповіді, ніколи
15Коли це сталося і коли ми про це дізналися?Дві дати там, де розрив важить: коли це було правдою у світі й коли система це записалаМісячні підсумки, що змінюються після закриття місяця, і жодного способу пояснити чому
16Що означає це видалити?Рішення для кожної сутності: справжнє видалення, м’яке видалення, забезпечене в одному місці, або анонімізація — з терміном зберіганняПрибрані дані, показані одним запитом, що забув фільтр, — і «видалення», яке не виконує запиту на стирання

Від речень до таблиць — сутності, ключі й зв’язки

Модель зазвичай малюють тричі, на трьох рівнях деталізації, і багато суперечок про схему — це двоє людей, що говорять про різні рівні. Цей поділ сягає трирівневої архітектури комітету ANSI/SPARC 1975 року. Діаграма, яку більшість малює на першому рівні, походить від моделі «сутність — зв’язок» Пітера Чена 1976 року.

РівеньЩо він кажеХто його читаєЗвична помилка
КонцептуальнийРечі, про які говорить бізнес, і як вони пов’язані: клієнт робить замовленняБізнес і команда — разомЙого пропускають, і перша суперечка про зміст стається на код-рев’ю
ЛогічнийКожен атрибут, ключ і зв’язок, нормалізовані; рушія ще немаєРозробники й аналітикиЙого змішують із фізичним, і рішення про індекс вигинає модель
ФізичнийТаблиці, типи, індекси й партиції для одного рушіяРушій — і той, хто його налаштовуєЙого вважають моделлю, тож переробляють щоразу, коли змінюється рушій

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

Нотація «пташина лапка», яку Гордон Еверест запропонував 1976 року, позначає кожен кінець зв’язку двома символами. Ближчий до блоку — максимум: риска означає один, тризуба лапка — багато. Дальший — мінімум: коло означає нуль, риска — один. Читайте кожну лінію як речення — від одного блоку до позначок на дальньому кінці: клієнт робить нуль або більше замовлень, а замовлення належить рівно одному клієнтові. Позиція замовлення посередині розв’язує зв’язок «багато до багатьох» між замовленнями й товарами, і вона сама є сутністю — з кількістю та ціною.

Діаграма вживає нотацію «пташина лапка» (crow’s foot), яку Гордон Еверест запропонував 1976 року і яку сьогодні малює більшість інструментів. Кожен кінець лінії каже, скільки примірників сутності може там стояти. Риска означає один, коло — нуль, а тризуба лапка — багато, тож коло з лапкою читається «нуль або більше», а дві риски — «рівно один». Читайте її як речення: від одного блоку вздовж лінії до позначок на дальньому кінці. Клієнт робить нуль або більше замовлень; замовлення належить рівно одному клієнтові. Позиція замовлення посередині — це рядок 5, зроблений видимим. Замовлення містить багато товарів, а товар трапляється в багатьох замовленнях, і таблиця, що їх зв’язує, виявилася окремою сутністю — з кількістю та ціною.

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

КлючРозмірДобре дляНа що зважати
32-бітне ціле4 байтиНевеликі довідникові таблиці, що ніколи не дійдуть до двох мільярдів рядківЗупиняється на 2 147 483 647 — таблиця подій Basecamp дійшла до цього 2018 року (рядок 3)
64-бітне ціле (bigint)8 байтівТиповий внутрішній ключ: компактний, упорядкований, достатній для будь-якої реалістичної кількостіВ URL видає, скільки рядків існує і як швидко їх більшає
UUIDv416 байтівId, згенеровані будь-де — на телефоні, в іншому сервісі, — яких ніхто не вгадаєВипадковий порядок розкидає вставлення по всьому індексу (див. сторінку про SQL)
UUIDv716 байтівId, згенеровані будь-де, що все одно сортуються за часом створення (RFC 9562, 2024)Видає, коли рядок створено
Природний ключРізнийДовідкові дані, чиї коди справді ніколи не змінюються, як-от коди валют ISOБізнесові ідентифікатори змінюються, а з ними й кожне посилання (рядок 2)
Складений ключСума його колонокТаблиці зв’язків і дочірні записи, пронумеровані в межах батьківськогоКожна таблиця, що на нього посилається, несе всі його колонки
Зв’язокЯк поданоДеталь, яку пропускають
Один до багатьохЗовнішній ключ на боці «багатьох», що вказує на «один»Чи може ключ бути NULL, вирішує, чи законна сирота, — а це бізнесове питання (рядок 4)
Багато до багатьохТретя таблиця з одним рядком на пару й зовнішнім ключем до кожного бокуЦя таблиця зазвичай і сама є сутністю й отримує атрибути (рядок 5)
Один до одногоСпільний ключ або унікальне обмеження на зовнішньому ключіСправді рідкісний: зазвичай одну сутність поділили заради зберігання або сплутали дві
ІєрархіяЗовнішній ключ від рядка до іншого рядка тієї самої таблиціЦіла гілка потребує рекурсивного запиту; коли гілки читають значно частіше, ніж змінюють, зберігайте ще й шлях

Чотири форми виглядають гнучкими, але такими не є. Білл Карвін назвав їх у книжці «SQL Antipatterns» (2010), і, схоже, кожна довгоживуча схема відрощує принаймні одну:

ФормаЯк виглядаєЩо ламаєтьсяНатомість
Список у колонціtags = 'urgent,billing'Ні індексу, ні зовнішнього ключа; «billing» збігається ще й із «rebilling»Рядок на кожне значення в таблиці зв’язку (рядок 5)
Пронумеровані колонкиphone_1, phone_2, phone_3Четвертий телефон; пошук по всіх трьохДочірня таблиця
Сутність — атрибут — значенняОдна таблиця рядків (сутність, атрибут, значення), що може тримати «будь-що»Ні типів, ні обмежень, і по з’єднанню на кожен атрибут, щоб прочитати один записСправжні колонки для відомих атрибутів; JSON для справді змінної решти (рядок 10)
Поліморфний зв’язокcommentable_type плюс commentable_id, що вказують в одну з кількох таблицьЖоден зовнішній ключ не скаже «рядок в одній із цих таблиць»Таблиця зв’язку на кожен тип батьківського запису або спільна батьківська таблиця, на яку посилаються всі

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

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

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

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

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

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

ФормаЩо прибираєКоли ви з нею зустрінетеся
Бойса — Кодда (1974)Залежність від частини ключа-кандидата, що перекривається з іншимРідко: таблиця з двома ключами-кандидатами, що мають спільну колонку, — розклад із ключем «аудиторія + година» і «викладач + година»
Четверта (Фейгін, 1977)Два незалежні списки, збережені як кожна комбінація обохПредмети й мови викладача в одній таблиці: три предмети × дві мови = шість рядків
П’ята (Фейгін, 1979)Таблиця, правильна лише як з’єднання трьох меншихМайже ніколи в продуктовій схемі
Шоста (Дейт, Дарвен, Лоренцос, 2002)Кожна неключова колонка у власній таблиці, кожна з власним періодом чинностіЧасові дані, де кожен атрибут змінюється за власним розкладом

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

КопіяЗаконна, колиЩо тримає її правдивою
Назва й ціна товару в позиції замовленняЗавжди — вона фіксує, що продалиНічого не потрібно: її й мають заморозити
Сума замовленняЗавжди — це сума, на яку погодився клієнтЗаморожена під час оформлення, з перевіркою, що позиції тоді сходилися
Кількість коментарів у дописіКоли кількість читають на кожному перегляді сторінкиАтомарне збільшення в тій самій транзакції, що й коментар, або тригер
Ім’я й адреса клієнта в рахункуЗавжди — рахунок є документом у тому вигляді, в якому його видалиЗаморожені; виправлення — це новий документ
Копія для пошуку чи звітностіКоли читанням потрібна інша форма, ніж записамПеребудовується з джерела — через change data capture чи планову задачу — і ніколи не редагується вручну

Типи, NULL і відсутність — найдешевші обмеження, які взагалі бувають

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

Що ви зберігаєтеВибір, що болитьЩо робити натомість
ГрошіЧисло з рухомою комою, що не тримає 0.10 точноТочний десятковий тип або ціле число дрібних одиниць із валютою поруч — дрібні одиниці різні: в єни їх немає, в кувейтського динара — три (рядок 6)
Мить у часіМітка часу без поясуМітка часу з поясом, що зберігається в UTC і показується в поясі читача (рядок 7)
Майбутня місцева подія — зустріч о 09:00 в Алмати наступного рокуПереведена в UTC під час бронюванняМісцевий час і назва поясу: правила поясів змінюються — Казахстан перейшов на один часовий пояс 1 березня 2024 року, і годинники в Алмати перевели на годину назад
Тривалість або «через рік»Додати одиницю до поля рокуТип інтервалу й арифметика дат рушія чи бібліотеки: 29 лютого плюс рік — не дата
Фіксований набір станівВільний текстОбмеження або довідникова таблиця; друга ще й дозволяє перейменувати мітку, не чіпаючи даних (рядок 8)
Так чи ні, що насправді є станомБулеве is_cancelledМітка часу (cancelled_at) або статус: булеве губить «коли», а третій стан приходить завжди
Щось, що схоже на числоЧислова колонка для поштових індексів, телефонів, номерів рахунківТекст із перевіркою формату: нулі на початку й знак плюс — частина значення
Ім’я людиниОбов’язкові ім’я та прізвищеОдне поле повного імені — плюс те, як людина хоче, щоб до неї звертались
Електронна адресаУнікальна як введена, тож Ann@x.com і ann@x.com — два акаунтиУнікальна за значенням у нижньому регістрі
Структурована грудкаJSON-колонка для полів, за якими фільтрує кожен запитПідняти поля, за якими шукають, до колонок; JSON лишити для того, що справді різниться (рядок 10)

Рядок про ім’я заслуговує окремого речення. 2010 року Патрік Маккензі опублікував «Falsehoods Programmers Believe About Names» — перелік із сорока припущень, які ламають справжні імена: що в кожного рівно одне ім’я й одне прізвище, що імена не змінюються, що вони вміщуються в задану кількість символів, що їх пишуть однією абеткою. Кожне з цих припущень десь є визначенням колонки. Модель, що зберігає ім’я так, як його пише людина, а окремо — як до неї звертатися, переживає всі сорок.

NULL заслуговує окремого абзацу, бо це не значення. Це відсутність значення, і вона поширюється. Порівняння з NULL ні істинне, ні хибне, а невідоме, тож рядок із NULL у колонці не збігається ні з = x, ні з <> x; сума його пропускає; а унікальне обмеження в більшості рушіїв приймає скільки завгодно NULL. Повний набір несподіванок — на сторінці про SQL. Для моделі важить рядок 9: робіть кожну колонку з NULL рішенням із підставою. Добрих підстав лише дві: значення справді необов’язкове в бізнесі або ще невідоме, коли створюють рядок. Група колонок, що бувають NULL разом, — cancelled_at, cancelled_by, cancellation_reason, — це стан, чиє місце у власній таблиці чи статусі. А колонка, що буває NULL, бо стосується лише деяких різновидів рядків, означає, що дві сутності ділять одну таблицю, — це знову рядок 1.

Час та історія — яким це було і коли ви про це дізналися

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

ПідхідЩо зберігаєПитання, на яке відповідаєЧого коштує
Журнал аудитуХто що змінив і коли — лише дописуванням«Хто це змінив і коли?»Минулий стан доводиться відтворювати, програючи зміни
Таблиця історії (тип 2 за Кімбаллом)Кожна версія з valid_from і valid_to«Яким це було в березні?»Кожен запит мусить обрати версію; перекриттям треба запобігати — у PostgreSQL 18 це робить WITHOUT OVERLAPS у самому рушії
Системно-версійні таблиці (SQL:2011)Кожна стара версія рядка з міткою, яку ставить рушій«Що казала база 1 березня?»Фіксує, коли змінилася база, а не світ; залежить від рушія (SQL Server з 2016, MariaDB з 10.3, Db2)
БітемпоральнийОбидва: коли це було правдою і коли це записали«Що ми у вівторок думали про п’ятницю?»Два діапазони в кожному рядку, дві миті в кожному запиті
Event sourcingЗаписом є самі зміни; стан обчислюютьБудь-яке з наведеного — через повторне програванняФорма кожної події живе вічно; проєкції треба перебудовувати й тримати в ногу

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

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

Діаграма стежить за одним платежем протягом тижня. Клієнт заплатив 102 у п’ятницю 3 жовтня. До системи платіж потрапив у понеділок 6-го, введений як 120, а в середу 8-го хтось виправив цю друкарську помилку. Три звіти за ту п’ятницю дають три відповіді. Запущений у неділю — платежу в ньому немає, бо система ще не знала. Запущений у вівторок — показує 120. Запущений у четвер — 102. Усі три мали рацію щодо того, що знала система, коли їх запускали. Модель, що зберігає обидва часи, може на вимогу відтворити кожен із них — саме цього просить аудитор. Модель, що перезаписує, може дати лише останній і не має як пояснити, чому звіт минулого тижня казав інше.

Видалення — третє питання про час (рядок 16), і його зазвичай вирішують рефлекторно. Колонка deleted_at зберігає історію й робить випадкове видалення відновлюваним — і назавжди накладає податок на кожен запит у системі: один пропущений фільтр покаже користувачеві прибрані дані. Якщо ви це вживаєте, забезпечуйте в одному місці — поданням, репозиторієм, політикою на рівні рядків, — а не в кожному запиті. До того ж м’яке видалення — не видалення в правовому сенсі. дає людям право на стирання персональних даних (стаття 17) і обмежує, як довго їх можна зберігати (стаття 5). Тож вирішуйте для кожної сутності: справжнє видалення — для того, що нікому не потрібне; м’яке — для помилок користувачів; анонімізація — лишити замовлення, прибрати людину — там, де бізнесу потрібен запис, але не особа; і термін зберігання, після якого зникають навіть м’яко видалені рядки. Бекапи теж рахуються. Стирання, що лишає дані в бекапі минулого місяця, потребує задокументованої відповіді на день, коли цей бекап відновлять.

Моделі для читання — схема «зірка», зернистість і схема як код

Усе наведене — для системи обліку, де найважливіше, щоб кожен запис був правильним. Модель, збудована, щоб відповідати на питання по мільйонах рядків, іде на протилежний компроміс: вільно повторює дані, щоб читання були простими й швидкими. Найпоширеніший спосіб її будувати — вимірне моделювання Ральфа Кімбалла з книжки «The Data Warehouse Toolkit» (1996). У ньому чотири кроки. Обрати бізнес-процес — продажі, відвантаження, платежі. Оголосити зернистість. Визначити виміри — те, за чим фільтрують і групують. Визначити факти — числа, які підсумовують. Другий крок важить найбільше і його пропускають найчастіше.

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

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

ПідхідФормаОптимізує підЦіна
Інмон (1992)Нормалізоване корпоративне сховище, з якого будують вітрини для відділівОдну інтегровану, узгоджену версію правдиПовільно будувати, а запити однаково роблять до вітрин
Кімбалл (1996)Схема «зірка» на кожен бізнес-процес зі спільними «узгодженими» вимірамиШвидкі запити, які аналітики можуть прочитатиСпільні виміри потребують управління між командами
Data Vault (Лінстедт, близько 2000)Хаби для ключів, лінки для зв’язків, сателіти для історіїЗавантаження багатьох змінних джерел із повною історієюБагато з’єднань; для читання зверху зазвичай будують «зірку»
Одна широка таблицяУсе заздалегідь з’єднано, у колонковому рушіїНайпростіші запити; колонкове стиснення поглинає повториКожна зміна виміру означає перебудову

Схема як код, який читають інші люди

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

Ніколи не давайте старій колонці нового змісту. Колонку, зміст якої змінили на місці, читають код, звіти й споживачі нижче за потоком, які досі вірять у старий зміст, і жоден із них не падає гучно. Сторінка про управління ризиками розповідає найдорожчу версію цього: 2012 року Knight Capital перевикористала прапорець для нової функції, один сервер досі виконував код, що читав старий зміст, і фірма втратила близько 460 мільйонів доларів за 45 хвилин. Коли зміст змінюється, додайте нову колонку з новою назвою, переведіть на неї читачів і приберіть стару — і пам’ятайте, що серед читачів тепер і ті, хто споживає ваші таблиці через , і їм їхня форма важить так само, як вашому кодові.

П’ять кейсів і те, чого в кожному бракувало

Жодному з них не знадобився ні хитрий зловмисник, ні екзотичний баг. Кожен — це одне рішення моделювання, ухвалене за замовчуванням, що зустрілося зі світом.

КейсЩо сталосяЩо це показує
Basecamp, 201832-бітний ключ таблиці подій дійшов до 2 147 483 647, і Basecamp 3 майже п’ять годин працював лише на читанняТип ключа — це межа ємності (рядок 3)
Назви генів, 2016–2021Електронні таблиці перетворювали назви генів на дати в п’ятій частині перевірених статей з геноміки, згодом — майже в третині; 2020 року гени перейменувалиЗначення, схоже на дату, однаково є текстом; природні ідентифікатори змінюються (рядки 2 і 6–10)
Номер NULL, 2016–2019Водій зареєстрував номерний знак NULL і отримав штрафів на 12 049 доларів, виписаних без номераВідсутність, збережена як значення (рядок 9)
Windows Azure, 2012«Через рік від сьогодні» обчислювали, додаючи одиницю до року; 29 лютого це дало дату, якої не існуєДати й тривалості належать типам, а не арифметиці над полями (рядок 7)
Robodebt, 2016–2023Річний дохід, усереднений на 26 двотижневих періодів, сприймали як двотижневий дохід; близько 470 000 боргів нарахували хибноЧисло, розмазане на дрібнішу зернистість, вигадує дані

Basecamp — кейс для рядка 3. О 7:21 за центральним часом у четвер 8 листопада 2018 року Basecamp 3 перестав приймати зміни. Його таблиця подій фіксує майже кожну дію в продукті — від публікації повідомлення до оплесків під коментарем, — а її ключем було 32-бітне ціле, що саме дійшло до 2 147 483 647. Кожна дія, що намагалася записати подію, падала, тож увесь продукт перейшов у режим лише для читання. Зміну колонки на 64-бітне ціле на такій великій таблиці оцінили в годину й сорок хвилин, а загалом продукт працював лише на читання майже п’ять годин, до 12:11. Найгостріша деталь — та, яку Девід Гайнемаєр Ганссон сам виклав у розборі. Ruby on Rails, створений у Basecamp, зробив 64-бітні ключі типовими ще у версії 5.1 2017 року, а команда працювала саме над цією таблицею в попередні дні — вона хибно оцінила, як швидко закінчується запас. Тип ключа — це межа ємності, а межам ємності потрібен алерт задовго до того, як їм знадобиться міграція.

Кейс із назвами генів показує тип, обраний інструментом, а не людиною. 2016 року Марк Цімман, Йотам Ерен і Ассам Ель-Оста просканували додаткові електронні таблиці опублікованих статей з геноміки й знайшли назви генів, мовчки перетворені в 987 файлах із 704 статей, — приблизно в п’ятій частині статей, що подавали списки генів в Excel. SEPT2 перетворився на 2 вересня, MARCH1 — на 1 березня, а деякі ідентифікатори стали числами з рухомою комою. Електронна таблиця вгадувала тип за тим, на що схоже значення, і вгадувала хибно. Повторне дослідження 2021 року виявило, що частка зросла до 30,9%. Ліки, що зрештою спрацювали, прийшли з іншого боку. 2020 року Комітет із номенклатури генів HUGO перейменував 27 генів — SEPT1 став SEPTIN1, MARCH1 — MARCHF1, — почасти для того, щоб електронні таблиці дали їм спокій. Ціла наукова галузь змінила свої природні ідентифікатори, бо інструменти раз у раз хибно визначали їхній тип. Це рядки 2 і 6–10 водночас: ідентифікатор, схожий на число чи дату, — це текст, а природні ідентифікатори таки змінюються.

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

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

  • Схема, зліплена під теперішній екран, яку доведеться мігрувати першого ж разу, коли екран зміниться.
  • Таблиця, що тримає дві сутності, розрізнені колонкою типу й набором колонок, які бувають NULL (рядок 1).
  • Природний ключ як первинний — доки бізнес не змінить значення, з якого той зроблено (рядок 2).
  • 32-бітний ключ у таблиці, що рахує події, без жодного алерту на залишок запасу (рядок 3).
  • Гроші в колонці з рухомою комою і мітки часу без поясу (рядки 6–7).
  • Колонки з NULL за замовчуванням і замінники, що означають «невідомо» для одних читачів і нічого — для інших (рядок 9).
  • Денормалізована копія, чесності якої ніщо не тримає: вона тихо розходиться, і їй усе одно вірять (рядок 12).
  • Збережено лише поточний стан, тож перше ж питання про минулий квартал не має відповіді (рядок 14).
  • Таблиця фактів без оголошеної зернистості, тож суми рахують двічі або розмазують одне число на рядки, яких воно ніколи не описувало.
  • Стара колонка з новим змістом, яку читають код і звіти, що досі вірять у старий.

Чесна межа в тому, що модель може забезпечити лише те, що хтось зрозумів. Нормальні форми прибирають надмірність, обмеження відхиляють неможливі рядки, а типи — неможливі значення, але жодне з них не скаже, що правдоподібне число вигадано, що «рік» обчислили вручну чи що значення, яке дві системи обидві називають NULL, означає дві різні речі. Це рішення про зміст, ухвалені людьми, які знають бізнес, а схема — лише місце, де їх записано. Модель, якої ніхто не може пояснити реченнями, — ще не модель, хоч би як її нормалізували.

Практична перевірка займає пів дня. Для десяти найбільших таблиць напишіть по одному реченню про те, що таке рядок; кожна таблиця, якій потрібні два речення, — знахідка. Перелічіть усі 32-бітні ключі та їхній поточний максимум проти 2 147 483 647. Пошукайте в схемі колонки з рухомою комою, що тримають гроші, і мітки часу без поясу. Порахуйте колонки з NULL і для кожної спитайте, яка з двох добрих підстав до неї застосовна. І спитайте фінансову команду, на яке питання про минуле вона не змогла відповісти в минулому кварталі.

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

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

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

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

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

Джерела й метод

На чому стоїть ця стаття

  • 19 джерел
  • 5 задокументованих збоїв
  • Перевірено 13 жовтня 2026 р.

Збої, на яких вчиться стаття

розділ «П’ять кейсів і те, чого в кожному бракувало»

Дослідження з Claude. Пошук і звірку першоджерел зроблено з Claude (Anthropic). Відбір, текст і перевірку фактів — редакція Anatomy of IT Product.

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

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