Моделювання даних
Моделювання даних — це рішення про те, що зберігатиме система: сутності, що ідентифікує кожну, як вони пов’язані й що має бути істинним завжди, — до першої таблиці. Модель переживає застосунок, збудований над нею: фреймворки замінюють, схему мігрують, а недбале рішення з другого тижня досі коштує комусь на п’ятому році. Сторінка починається з шістнадцяти питань до кожної таблиці — з відповіддю, що витримує час, і ціною хибної відповіді; далі — сутності, ключі й зв’язки, нормальні форми, типи й 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 — та сама перевірка для однієї таблиці: якщо ви не можете сказати одним реченням, що таке один рядок, таблиця тримає дві речі.
Діаграма вживає нотацію «пташина лапка» (crow’s foot), яку Гордон Еверест запропонував 1976 року і яку сьогодні малює більшість інструментів. Кожен кінець лінії каже, скільки примірників сутності може там стояти. Риска означає один, коло — нуль, а тризуба лапка — багато, тож коло з лапкою читається «нуль або більше», а дві риски — «рівно один». Читайте її як речення: від одного блоку вздовж лінії до позначок на дальньому кінці. Клієнт робить нуль або більше замовлень; замовлення належить рівно одному клієнтові. Позиція замовлення посередині — це рядок 5, зроблений видимим. Замовлення містить багато товарів, а товар трапляється в багатьох замовленнях, і таблиця, що їх зв’язує, виявилася окремою сутністю — з кількістю та ціною.
Ключ — це обіцянка, що рядок можна ідентифікувати, і ключі бувають двох видів. Природний ключ — значення, яким бізнес уже користується: електронна адреса, податковий номер, ISBN. Сурогатний ключ — число чи ідентифікатор, вигаданий базою й безглуздий поза системою. Природні ключі добре читаються й заощаджують з’єднання, і мають одну спільну ваду, що повторюється скрізь: бізнесові ідентифікатори змінюються. Люди змінюють електронні адреси. Компанії перереєстровуються. Книжки 1 січня 2007 року перейшли з десятизначних ISBN на тринадцятизначні, і кожна система, що мала ISBN своїм ключем, мусила змінити кожне посилання на кожну книжку. Робоча типова відповідь — рядок 2: сурогатний первинний ключ плюс унікальне обмеження на природний, тож ідентичність стабільна, а бізнесове правило однаково забезпечено.
| Ключ | Розмір | Добре для | На що зважати |
|---|---|---|---|
| 32-бітне ціле | 4 байти | Невеликі довідникові таблиці, що ніколи не дійдуть до двох мільярдів рядків | Зупиняється на 2 147 483 647 — таблиця подій Basecamp дійшла до цього 2018 року (рядок 3) |
64-бітне ціле (bigint) | 8 байтів | Типовий внутрішній ключ: компактний, упорядкований, достатній для будь-якої реалістичної кількості | В URL видає, скільки рядків існує і як швидко їх більшає |
| UUIDv4 | 16 байтів | Id, згенеровані будь-де — на телефоні, в іншому сервісі, — яких ніхто не вгадає | Випадковий порядок розкидає вставлення по всьому індексу (див. сторінку про SQL) |
| UUIDv7 | 16 байтів | 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 жовтня. До системи платіж потрапив у понеділок 6-го, введений як 120, а в середу 8-го хтось виправив цю друкарську помилку. Три звіти за ту п’ятницю дають три відповіді. Запущений у неділю — платежу в ньому немає, бо система ще не знала. Запущений у вівторок — показує 120. Запущений у четвер — 102. Усі три мали рацію щодо того, що знала система, коли їх запускали. Модель, що зберігає обидва часи, може на вимогу відтворити кожен із них — саме цього просить аудитор. Модель, що перезаписує, може дати лише останній і не має як пояснити, чому звіт минулого тижня казав інше.
Видалення — третє питання про час (рядок 16), і його зазвичай вирішують рефлекторно. Колонка deleted_at зберігає історію й робить випадкове видалення відновлюваним — і назавжди накладає податок на кожен запит у системі: один пропущений фільтр покаже користувачеві прибрані дані. Якщо ви це вживаєте, забезпечуйте в одному місці — поданням, репозиторієм, політикою на рівні рядків, — а не в кожному запиті. До того ж м’яке видалення — не видалення в правовому сенсі. GDPRGeneral Data Protection Regulation — закон ЄС про персональні даніЄвропейська регуляція про персональні дані: що можна збирати, на якій правовій підставі, скільки зберігати і чого людина може від вас вимагати щодо своїх даних. Вона застосовується за місцем перебування людей, а не компанії, — тому дістає до софту, збудованого будь-де.Усі терміни дає людям право на стирання персональних даних (стаття 17) і обмежує, як довго їх можна зберігати (стаття 5). Тож вирішуйте для кожної сутності: справжнє видалення — для того, що нікому не потрібне; м’яке — для помилок користувачів; анонімізація — лишити замовлення, прибрати людину — там, де бізнесу потрібен запис, але не особа; і термін зберігання, після якого зникають навіть м’яко видалені рядки. Бекапи теж рахуються. Стирання, що лишає дані в бекапі минулого місяця, потребує задокументованої відповіді на день, коли цей бекап відновлять.
Моделі для читання — схема «зірка», зернистість і схема як код
Усе наведене — для системи обліку, де найважливіше, щоб кожен запис був правильним. Модель, збудована, щоб відповідати на питання по мільйонах рядків, іде на протилежний компроміс: вільно повторює дані, щоб читання були простими й швидкими. Найпоширеніший спосіб її будувати — вимірне моделювання Ральфа Кімбалла з книжки «The Data Warehouse Toolkit» (1996). У ньому чотири кроки. Обрати бізнес-процес — продажі, відвантаження, платежі. Оголосити зернистість. Визначити виміри — те, за чим фільтрують і групують. Визначити факти — числа, які підсумовують. Другий крок важить найбільше і його пропускають найчастіше.
Зернистість — це речення, що каже, чим є один рядок таблиці фактів: один товар в одному чеку, баланс одного рахунку на кінець одного дня. Це рядок 1 із шістнадцяти, застосований до аналітики. Кожне число в таблиці фактів має бути правдою рівно на цій зернистості. Змішування зернистостей рахує двічі: денна сума, збережена поруч із позиціями, підсумовується разом із ними. Розмазати грубе число на дрібнішу зернистість — ще гірше, бо це вигадує дані. Річна сума, поділена на двадцять шість, виглядає точнісінько як двотижневий дохід, але ним не є. Кейс Robodebt нижче — це речення, вписане в урядову програму.
| Підхід | Форма | Оптимізує під | Ціна |
|---|---|---|---|
| Інмон (1992) | Нормалізоване корпоративне сховище, з якого будують вітрини для відділів | Одну інтегровану, узгоджену версію правди | Повільно будувати, а запити однаково роблять до вітрин |
| Кімбалл (1996) | Схема «зірка» на кожен бізнес-процес зі спільними «узгодженими» вимірами | Швидкі запити, які аналітики можуть прочитати | Спільні виміри потребують управління між командами |
| Data Vault (Лінстедт, близько 2000) | Хаби для ключів, лінки для зв’язків, сателіти для історії | Завантаження багатьох змінних джерел із повною історією | Багато з’єднань; для читання зверху зазвичай будують «зірку» |
| Одна широка таблиця | Усе заздалегідь з’єднано, у колонковому рушії | Найпростіші запити; колонкове стиснення поглинає повтори | Кожна зміна виміру означає перебудову |
Схема як код, який читають інші люди
Хоч яку форму оберете, сприймайте схему як вихідний код, який читають інші люди. Імена — це інтерфейс. Колонка status у трьох таблицях, що означає три різні набори значень, коштує щодня, а колонка на ім’я flag за півтора року стає загадкою. Оберіть одну домовленість про однину чи множину в назвах таблиць, одну — для міток часу, одну — для зовнішніх ключів, і запишіть їх, щоб десята таблиця збігалася з першою. Тримайте міграції в репозиторії, переглядайте їх як будь-яку іншу зміну, і робіть кожну зворотною або явно позначеною як незворотна. Як змінити схему на живій системі, не заблокувавши її, — на сторінці про SQL; тут правило про зміст.
Ніколи не давайте старій колонці нового змісту. Колонку, зміст якої змінили на місці, читають код, звіти й споживачі нижче за потоком, які досі вірять у старий зміст, і жоден із них не падає гучно. Сторінка про управління ризиками розповідає найдорожчу версію цього: 2012 року Knight Capital перевикористала прапорець для нової функції, один сервер досі виконував код, що читав старий зміст, і фірма втратила близько 460 мільйонів доларів за 45 хвилин. Коли зміст змінюється, додайте нову колонку з новою назвою, переведіть на неї читачів і приберіть стару — і пам’ятайте, що серед читачів тепер і ті, хто споживає ваші таблиці через change data captureпотік змін із журналу базиПотокова передача кожної вставки, зміни й видалення з власного журналу змін бази в інші системи, як це роблять інструменти на кшталт Debezium. Вона тримає другу копію синхронною без того, щоб застосунок писав двічі, ціною прив’язки кожного споживача до структури таблиць джерела.Усі терміни, і їм їхня форма важить так само, як вашому кодові.
П’ять кейсів і те, чого в кожному бракувало
Жодному з них не знадобився ні хитрий зловмисник, ні екзотичний баг. Кожен — це одне рішення моделювання, ухвалене за замовчуванням, що зустрілося зі світом.
| Кейс | Що сталося | Що це показує |
|---|---|---|
| Basecamp, 2018 | 32-бітний ключ таблиці подій дійшов до 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 р.
Збої, на яких вчиться стаття
розділ «П’ять кейсів і те, чого в кожному бракувало»- ПостмортемUpdate on Basecamp 3 being stuck in read-only as of Nov 8, 9:22am CST (відкривається в новій вкладці)Signal v. Noise · 08.11.2018
- ПостмортемDHH — Postmortem on the read-only outage of Basecamp on November 9th, 2018 (відкривається в новій вкладці)Signal v. Noise · 12.11.2018
- ДослідженняMark Ziemann, Yotam Eren, Assam El-Osta — Gene name errors are widespread in the scientific literature (відкривається в новій вкладці)Genome Biology · 2016
- ДослідженняMandhri Abeysooriya et al. — Gene name errors: Lessons not learned (відкривається в новій вкладці)PLOS Computational Biology · 07.2021
- СтандартElspeth A. Bruford et al. — Guidelines for human gene nomenclature (відкривається в новій вкладці)Nature Genetics · 08.2020
- СтаттяHow a ‘NULL’ License Plate Landed One Hacker in Ticket Hell (відкривається в новій вкладці)WIRED · 08.2019
- ПостмортемBill Laing — Summary of Windows Azure Service Disruption on Feb 29th, 2012 (відкривається в новій вкладці)Microsoft Azure Blog · 03.2012
- Звіт регулятораReport — Royal Commission into the Robodebt Scheme (відкривається в новій вкладці)Royal Commission into the Robodebt Scheme · 07.07.2023
Дослідження
нормальні форми і модель «сутність — зв’язок»- ДослідженняE. F. Codd — A Relational Model of Data for Large Shared Data Banks (відкривається в новій вкладці)Communications of the ACM · 06.1970
- ДослідженняWilliam Kent — A Simple Guide to Five Normal Forms in Relational Database Theory (відкривається в новій вкладці)Communications of the ACM · 02.1983
- ДослідженняPeter Pin-Shan Chen — The Entity-Relationship Model: Toward a Unified View of Data (відкривається в новій вкладці)ACM Transactions on Database Systems · 03.1976
Практики й походження термінів
антипатерни, імена й гроші, час, ключі і сховища даних- КнигаBill Karwin — SQL Antipatterns (відкривається в новій вкладці)Pragmatic Bookshelf · 2010
- СтаттяPatrick McKenzie — Falsehoods Programmers Believe About Names (відкривається в новій вкладці)Kalzumeus · 17.06.2010
- КнигаRichard T. Snodgrass — Developing Time-Oriented Database Applications in SQLMorgan Kaufmann · 1999
- ДокументаціяPostgreSQL 18 — Release Notes (відкривається в новій вкладці)postgresql.org
- СтандартRFC 9562 — Universally Unique IDentifiers (UUIDs) (відкривається в новій вкладці)IETF · 05.2024
- КнигаRalph Kimball — The Data Warehouse Toolkit (відкривається в новій вкладці)Wiley · 1996
- СтандартISO 4217 — Currency codes (відкривається в новій вкладці)ISO
- СтандартRegulation (EU) 2016/679 — General Data Protection Regulation (відкривається в новій вкладці)European Parliament and Council · 27.04.2016
Дослідження з Claude. Пошук і звірку першоджерел зроблено з Claude (Anthropic). Відбір, текст і перевірку фактів — редакція Anatomy of IT Product.
Було корисно?
Поділіться з тим, хто працює над тією ж задачею.