Реляційна модель: таблиці, ключі, зв'язки
Зміст
— це спосіб, у який майже всі бізнес-бази даних (PostgreSQL, MySQL, SQL Server, Oracle) організують дані: не одна суцільна купа, а окремі таблиці, поєднані ключами (keys). Для тестувальника це фундамент. Коли ти перевіряєш, що замовлення справді збереглося за потрібним користувачем, що видалення акаунта не лишило «висячих» рядків, що email не можна зареєструвати двічі — ти працюєш саме з реляційною моделлю, навіть якщо не пишеш жодного SELECT (див. навіщо QA взагалі БД).
«Чим PRIMARY KEY відрізняється від UNIQUE?», «як реалізувати зв'язок many-to-many?», «що станеться, якщо видалити рядок, на який посилаються інші?» — за цими питаннями стоїть одне: не розуміючи ключів, неможливо ні прочитати схему застосунку, ні відрізнити реальний баг цілісності даних від штатної поведінки БД. Розберемося з механізмом, а не із зубрінням.
Таблиця, рядок, стовпець, схема
Базова одиниця реляційної БД — таблиця (table). Це двовимірна структура: стовпці задають, які дані ми зберігаємо, рядки — конкретні записи.
- Стовпець (column, поле) — це атрибут із фіксованим :
idтипуinteger,emailтипуvarchar,created_atтипуtimestamp. Тип — це контракт: у стовпецьintegerне покласти рядок, і це БД перевіряє сама. - Рядок (row, запис) — один екземпляр сутності: один користувач, одне замовлення. Усі рядки таблиці мають однаковий набір стовпців.
Ключова властивість реляційної моделі: порядок рядків не гарантований. Таблиця — це множина записів, а не список. Тому запит без ORDER BY може повертати рядки в різному порядку на різних прогонах — типове джерело -перевірок, коли автотест очікує «перший рядок» без явного сортування.
Слово схема (schema) має два значення, і їх плутають:
- Схема даних — опис структури: які є таблиці, стовпці, типи, ключі й обмеження. Саме її змінюють міграції.
- Схема як простір імен (namespace) — у PostgreSQL це логічна група об'єктів усередині бази (наприклад,
public.users). На trainee-рівні достатньо першого значення; друге просто май на увазі, щоб не сплутати.
Первинний ключ: природний проти сурогатного
(primary key, PK) — це стовпець (або кілька), що однозначно ідентифікує кожен рядок таблиці. Він має три властивості, що й роблять його ідентифікатором:
- унікальний — двох рядків з однаковим PK бути не може;
- не порожній (
NOT NULL) — «невідомий» ідентифікатор нічого не ідентифікує; - один на таблицю — головний спосіб адресувати запис.
Далі — головна розвилка проєктування. Ключ буває природний або сурогатний.
Природний ключ (natural key) — значення, що вже існує в й саме собою унікальне: email користувача, ІПН, ISBN книги, код валюти USD. Спокуса очевидна: не треба вигадувати зайвий стовпець.
(surrogate key) — штучний ідентифікатор без бізнес-сенсу, який генерує БД: автоінкрементне число (SERIAL/IDENTITY у PostgreSQL, AUTO_INCREMENT у MySQL) або UUID.
Чому на практиці майже завжди беруть сурогатний — і це те, що варто вміти пояснити:
- Бізнес-значення змінюються. Людина змінює прізвище, компанія — назву, користувач — email. Якщо на цьому полі тримається ідентичність рядка й на нього посилаються інші таблиці, зміна перетворюється на біль.
- Природна унікальність часто ілюзорна. «Ім'я + прізвище» не унікальні, номер телефону переходить до іншої людини.
- Природний ключ буває складеним і громіздким, а на нього ще й посилаються — тягати пару-трійку стовпців усюди незручно.
Звідси практичний висновок для QA: сурогатний id гарантує лише технічну унікальність рядка, але не бізнес-унікальність. Щоб email не зареєстрували двічі, потрібне окреме обмеження UNIQUE на цьому стовпці — про це нижче.
Ключ може бути й складеним (composite) — з кількох стовпців разом. Класичний приклад — зв'язку M:N, де ключем є пара (order_id, product_id).
Зовнішній ключ і посилальна цілісність
Таблиці стають реляційними, коли починають посилатися одна на одну. Механізм посилання — (foreign key, FK): стовпець, значення якого мусить збігатися з первинним ключем іншої таблиці (у стандарті SQL — з PK або з унікальним стовпцем).
Приклад: таблиця orders має стовпець user_id, що є FK на users.id. Це буквально означає «це замовлення належить он тому користувачеві».
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
created_at TIMESTAMP NOT NULL DEFAULT now()
);
Найважливіша річ, яку дає FK, — (referential integrity): гарантія, що посилання завжди веде на реальний рядок. БД сама, без жодного коду застосунку, не дасть:
- вставити замовлення з
user_id, якого немає вusers(немає на кого послатися); - видалити користувача, поки на нього посилаються замовлення, — якщо не задано, що робити з «дітьми».
Ось цей другий пункт — постійне джерело питань «це баг чи ні». Поведінку при видаленні/оновленні батьківського рядка задають діями ON DELETE / ON UPDATE:
NO ACTION— дія за замовчуванням, якщо жодної не написали. Заборонити операцію, поки є посилання: спроба видалити користувача з замовленнями завершиться помилкою на кшталтviolates foreign key constraint.RESTRICT— теж заборона, але не синонімNO ACTION: цю пару слів PostgreSQL і MySQL трактують по-різному, і про це нижче окремим блоком.CASCADE— видалити (чи оновити) разом із батьком усі дочірні рядки. Видалив користувача — його замовлення зникли автоматично.SET NULL— обнулити FK у дітей (замовлення лишиться, але «без власника»). Потребує, щоб стовпецьNULL.
NO ACTION проти RESTRICT: та сама пара слів, дві різні реалізації
Найпідступніша діалектна розбіжність у зовнішніх ключах: обидві СУБД знають обидва ключові слова стандарту — і роблять ними різне. Тому подаємо обидві поведінки явно, не оголошуючи жодну «типовою».
| PostgreSQL | MySQL / InnoDB | |
|---|---|---|
| Дія за замовчуванням | ON DELETE NO ACTION | RESTRICT, він же NO ACTION |
NO ACTION і RESTRICT — те саме? | Ні, це різні дії | Так, для InnoDB еквівалентні |
| Чи можна відкласти перевірку до кінця транзакції | NO ACTION — так, RESTRICT — ні | Ні, операцію відхиляє негайно |
У PostgreSQL перевірку зовнішнього ключа можна оголосити відкладеною (DEFERRABLE INITIALLY DEFERRED). Тоді NO ACTION не падає одразу: база чекає кінця , і наступні команди встигають «полагодити» стан — перевісити замовлення на іншого користувача або видалити їх слідом. RESTRICT строгіший саме тим, що відкладення не допускає: він забороняє видалити рядок, на який посилаються, і крапка.
У MySQL/InnoDB цієї розвилки немає: NO ACTION там еквівалентний RESTRICT — операцію над батьківською таблицею відхиляє негайно, а написати RESTRICT (чи NO ACTION) — те саме, що взагалі не писати клозу ON DELETE/ON UPDATE. Відкладена перевірка в MySQL є лише в рушії NDB, не в InnoDB.
Для QA висновок подвійний. По-перше, схему з NO ACTION не можна читати однаково на двох стендах: на PostgreSQL із відкладеним обмеженням послідовність «видалив батька → полагодив дітей → COMMIT» пройде, а на MySQL перша ж команда впаде. По-друге, фраза «у нас NO ACTION, тобто те саме, що RESTRICT» правильна для MySQL і неправильна для PostgreSQL — на співбесіді це якраз той нюанс, за яким видно, чи людина працювала з обома.
Зверни увагу: FK, який допускає NULL, означає опційний зв'язок. orders.manager_id з NULL — це замовлення, якому ще не призначили менеджера; це нормальний стан, а не «зіпсовані дані».
Для тестувальника FK — це і страховка, і об'єкт перевірки. Порушення цілісності (замовлення посилається на неіснуючого користувача, «сирітські» рядки після кривої міграції) — класичний клас багів на рівні даних, який не завжди видно в UI (див. тестування цілісності даних і міграцій). А самі FK — це те, за чим потім «зшивають» таблиці у JOIN.
PRIMARY KEY проти UNIQUE
Обидва обмеження гарантують унікальність значень і обидва створюють під собою індекс (тому пошук за ними швидкий — див. індекси). Через це їх постійно плутають. Різниця — принципова:
| Аспект | PRIMARY KEY | UNIQUE |
|---|---|---|
| Скільки на таблицю | Рівно один | Скільки завгодно |
NULL | Заборонено (неявний NOT NULL) | Дозволено |
| Роль | Головний ідентифікатор рядка | Додаткове бізнес-обмеження |
| Ціль зовнішнього ключа | За замовчуванням посилаються сюди | Теж можна, але рідше |
Практичний сенс різниці видно на прикладі таблиці users: первинний ключ — сурогатний id (на нього посилаються замовлення), а UNIQUE на email не дає зареєструвати двох людей з однаковою поштою. Це два різні завдання, і одне обмеження їх не закриває.
Нюанс про NULL, який легко проґавити: у PostgreSQL і MySQL стовпець UNIQUE за замовчуванням допускає кілька рядків із NULL, бо за логікою SQL NULL не дорівнює NULL, тож формально дублікатами вони не вважаються. У деяких СУБД (історично — SQL Server) поведінка інша — дозволений лише один NULL. Тому «UNIQUE + NULL» — місце, де варто звірятися з конкретною СУБД, а не покладатися на пам'ять.
Зв'язки: 1:1, 1:N, M:N
Реляційна модель тримається на трьох типах зв'язків між таблицями. Уміти впізнати кожен — половина роботи з незнайомою схемою.
Один-до-багатьох (1:N) — найпоширеніший. Один рядок батьківської таблиці пов'язаний із багатьма дочірніми, але кожен дочірній — рівно з одним батьком. Один користувач має багато замовлень; кожне замовлення належить одному користувачеві. Реалізація проста: FK на боці „багато“. У orders є user_id — і все.
Один-до-одного (1:1) — рідкісний. Один рядок відповідає рівно одному рядку в іншій таблиці. Навіщо взагалі різати сутність надвоє: винести рідко потрібні або великі поля (профіль користувача з аватаркою й біографією окремо від users), або відокремити чутливі дані. Реалізація — FK, який водночас є UNIQUE (щоб на один users.id не почепилося двох профілів), або спільний первинний ключ.
Багато-до-багатьох (M:N) — і головна пастка новачків. Один рядок з обох боків пов'язаний з багатьма з іншого: одне замовлення містить багато товарів, і один товар входить у багато замовлень. Прямим зовнішнім ключем це не реалізується — не можна покласти «список» id у клітинку, реляційна модель цього не передбачає.
Розв'язання — проміжна (звʼязувальна, associative/junction) таблиця. Зв'язок M:N завжди розкладається на два зв'язки 1:N через окрему таблицю, кожен рядок якої — один факт зв'язку:
CREATE TABLE order_items (
order_id INTEGER NOT NULL REFERENCES orders(id),
product_id INTEGER NOT NULL REFERENCES products(id),
quantity INTEGER NOT NULL,
PRIMARY KEY (order_id, product_id)
);
Тут order_items тримає два зовнішні ключі, а її первинний ключ — складений, з пари (order_id, product_id): один товар не має двічі фігурувати в одному замовленні окремими рядками. Бонус проміжної таблиці — на ній зручно вішати атрибути самого зв'язку: quantity, ціна на момент покупки. Класичні приклади того самого патерну: студенти й курси через таблицю записів, статті й теги через таблицю тегування.
Як читати ER-діаграму
(entity-relationship diagram) — це схема бази «з висоти пташиного польоту»: прямокутники-сутності (таблиці), їхні атрибути й лінії зв'язків між ними. Її показують на онбордингу, у документації фічі, іноді — прямо на співбесіді з проханням «поясни, що тут відбувається».
Найпоширеніша нотація ліній — «воронячі лапки» (crow's foot). Символ на кінці лінії читається як кардинальність: риска | — «один», кружечок o — «нуль» (тобто необов'язково), а розгалуження, схоже на пташину лапку, — «багато». Їх комбінують: «один і тільки один», «нуль або один», «один або багато», «нуль або багато». Позначки PK і FK біля атрибутів кажуть, де первинний, а де зовнішній ключ.
Ось наша схема інтернет-магазину цілком — з усіма трьома типами зв'язків:
Читаємо діаграму вголос, і схема сама розповідає правила застосунку: користувач розміщує нуль або багато замовлень (||--o{) — 1:N; користувач має рівно один профіль (||--||) — 1:1; замовлення містить один або багато рядків (||--|{), і товар входить у нуль або багато рядків — а разом ці два зв'язки 1:N через ORDER_ITEMS і дають M:N між замовленнями й товарами. Побачив таблицю, що складається переважно з двох зовнішніх ключів, — майже напевно перед тобою проміжна таблиця, тобто десь є зв'язок many-to-many.
Для QA ER-діаграма — це карта того, що можна зламати: кожна лінія зв'язку — це FK-обмеження, яке варто перевірити (а що буде при видаленні? а nullable він чи ні?), а кожен UK/UNIQUE — бізнес-правило унікальності, яке має ловити дублікати.
Прив'язати теорію до автотесту нескладно: підготувавши дані, часто корисно перевірити цілісність прямо в БД, а не лише через UI чи API (як дані виглядають на межі застосунку — див. REST API та формати даних):
// Перевірка, що після видалення юзера не лишилось сирітських замовлень
const { rows } = await pool.query(
`SELECT COUNT(*)::int AS orphans
FROM orders o
LEFT JOIN users u ON u.id = o.user_id
WHERE u.id IS NULL`,
);
expect(rows[0].orphans).toBe(0);
Типові помилки
- Виглядає як «PK і UNIQUE — одне й те саме», а насправді PK один на таблицю й неявно
NOT NULL, аUNIQUEможе бути кілька й він допускаєNULL. Це різні інструменти для різних завдань. - Виглядає як «M:N зробимо двома FK, що дивляться одне на одного», а насправді це не масштабується й веде до дублювання; коректне рішення — окрема проміжна таблиця з двох зовнішніх ключів.
- Виглядає як баг «дані замовлень зникли після видалення юзера», а насправді штатно відпрацював
ON DELETE CASCADE. Перш ніж заводити дефект — глянь на визначення зовнішнього ключа. - Виглядає як баг «не можу видалити рядок, БД лається», а насправді спрацювала заборона на FK (
NO ACTIONабоRESTRICT): на рядок посилаються дочірні записи, і це захист цілісності, а не збій. - Виглядає як «
NO ACTIONіRESTRICT— синоніми», а насправді синоніми вони лише в MySQL/InnoDB; у PostgreSQLNO ACTIONдозволяє відкласти перевірку до кінця транзакції, аRESTRICT— ні. - Виглядає як «сурогатний
idзахищає від дублікатів», а насправдіidунікальний завжди, а бізнес-дублікат (два акаунти з тим самим email) пролізе, якщо наemailнемає окремогоUNIQUE. - Виглядає як «
NULLу зовнішньому ключі — це зіпсовані дані», а насправді nullable FK означає опційний зв'язок (замовлення без призначеного менеджера) — легальний стан, а не сирітський рядок.
Підсумок
- Первинний ключ однозначно ідентифікує рядок: він унікальний,
NOT NULLі один на таблицю; на практиці беруть сурогатний, бо бізнес-значення змінюються. - Зовнішній ключ забезпечує посилальну цілісність — БД сама не дасть послатися на неіснуючий рядок; що станеться з «дітьми» при видаленні батька, вирішує
ON DELETE(NO ACTION/RESTRICT/CASCADE/SET NULL). NO ACTIONіRESTRICT— не синоніми: у PostgreSQLNO ACTION(дефолт) допускає відкладення перевірки до кінця транзакції, аRESTRICTні; у MySQL/InnoDB обидва слова означають негайне відхилення операції.PRIMARY KEY— головний ідентифікатор (один, безNULL);UNIQUE— додаткове бізнес-правило унікальності (багато, зNULL). Сурогатнийidне замінюєUNIQUEна email.- Зв'язок 1:N реалізується зовнішнім ключем на боці «багато»; 1:1 — зовнішнім ключем із
UNIQUE; M:N — тільки через проміжну таблицю з двох зовнішніх ключів. - На ER-діаграмі «воронячі лапки» кодують кардинальність; таблиця з двох FK — ознака зв'язку many-to-many.
Можливі питання
- «Чим PRIMARY KEY відрізняється від UNIQUE?» — найчастіше питання розділу. Інтерв'юер перевіряє, чи назвеш обидві відмінності: кількість на таблицю й ставлення до
NULL, а не лише «обидва унікальні». - «Природний чи сурогатний ключ — що обереш і чому?» — тут дивляться на аргументацію: чи розумієш, що бізнес-значення змінюються й тому не годяться в ідентифікатор, на який посилаються інші таблиці.
- «Як реалізувати зв'язок many-to-many?» — очікувана відповідь одна: проміжна таблиця з двома зовнішніми ключами. Плюс бали, якщо згадаєш складений первинний ключ і атрибути на самому зв'язку.
- «Що таке посилальна цілісність?» та «що станеться, якщо видалити користувача, на якого посилаються замовлення?» — перевіряють, чи розумієш, що цілісність стежить сама БД, і чи знаєш варіанти
ON DELETE. Сильний кандидат одразу уточнює: «залежить від дії зовнішнього ключа». - «Чим
NO ACTIONвідрізняється відRESTRICT?» — питання-розділювач. Відповідь «нічим, це синоніми» правильна тільки для MySQL/InnoDB. Сильна відповідь називає обидва діалекти: у PostgreSQLNO ACTION(дефолт) допускає відкладену перевірку до кінця транзакції,RESTRICT— ні. - Наскрізний сигнал, який шукає інтерв'юер у QA: чи вмієш ти відрізнити реальний баг цілісності даних (сирітські рядки, дублікати) від штатної, хай і несподіваної, поведінки обмежень.
Джерела
Таблиця, рядок, стовпець, схема
- PostgreSQL 18 — 2.2. Concepts — таблиця як іменований набір рядків з однаковим набором стовпців, і пряме «SQL не гарантує порядку рядків жодним чином».
- E. F. Codd — A Relational Model of Data for Large Shared Data Banks — першоджерело: відношення як множина n-ок, порядок рядків неістотний, порядок стовпців — істотний.
- PostgreSQL 18 — 5.10. Schemas — друге значення слова «схема»: простір імен усередині бази й типовий
public. - PostgreSQL 18 — Chapter 8. Data Types — тип стовпця як контракт: що саме база погоджується прийняти в поле.
Первинний ключ: природний проти сурогатного
- PostgreSQL 18 — 5.5. Constraints — первинний ключ як
UNIQUEплюсNOT NULL, один на таблицю, і складена форма з кількох стовпців. - E. F. Codd — A Relational Model of Data for Large Shared Data Banks — першоджерело поняття ключа й складеного (composite) ключа.
- PostgreSQL 18 — 8.1. Numeric Types — сурогатний ключ у PostgreSQL:
serialіGENERATED … AS IDENTITY. - MySQL 8.4 — 5.6.9 Using AUTO_INCREMENT — той самий сурогатний ключ у MySQL через
AUTO_INCREMENT.
Зовнішній ключ і посилальна цілісність
- PostgreSQL 18 — 5.5. Constraints — бік PostgreSQL: посилальна цілісність, дії
ON DELETE/ON UPDATE, дефолтNO ACTION, відкладувана перевірка йRESTRICT, який відкладення не допускає; nullable FK як легальний опційний зв'язок. - MySQL 8.0 — FOREIGN KEY Constraints — бік MySQL/InnoDB тієї самої розбіжності:
NO ACTIONеквівалентнийRESTRICT, операцію відхиляють негайно, а написатиRESTRICT— те саме, що клозу не писати.
- PostgreSQL 18 — 5.5. Constraints —
PRIMARY KEYяк один на таблицю й неявнийNOT NULLпротиUNIQUE, якого може бути скільки завгодно; обидва автоматично створюють індекс, іNULL-и вUNIQUEдублікатами не вважаються. - MySQL 8.4 — 15.1.15 CREATE INDEX Statement — той самий бік MySQL: унікальний індекс допускає кілька
NULL, боNULLне дорівнюєNULL.
- PostgreSQL 18 — 5.5. Constraints — 1:N через зовнішній ключ на боці «багато» і M:N через проміжну таблицю з двох FK зі складеним первинним ключем.
- E. F. Codd — A Relational Model of Data for Large Shared Data Banks — чому «список id у клітинці» реляційною моделлю не передбачений.
- Mermaid — Entity Relationship Diagrams — нотація «воронячих лапок»: як символи на кінцях ліній кодують кардинальність і як позначають ключі.
- PostgreSQL 18 — 5.5. Constraints — чому кожна лінія на діаграмі — це FK-обмеження, а
UK— окреме бізнес-правило унікальності. - MySQL 8.0 — JOIN Clause — форма запиту, якою шукають сирітські рядки:
LEFT JOINіз перевіркоюIS NULLна боці батька.
Що таке таблиця, рядок і стовпець у реляційній БД?
Таблиця (table) — базова одиниця : сітка, у якій стовпці описують структуру даних, а рядки наповнюють її конкретними записами. Стовпець (column, поле) — атрибут із жорстко заданим типом: id — integer, email — varchar, created_at — timestamp; тип працює як контракт, який СУБД перевіряє сама — текст у цілочисловий стовпець не запишеться. Рядок (row, запис) — один екземпляр сутності (один користувач, одне замовлення); набір стовпців у всіх рядків таблиці однаковий. Для QA це фундамент: перевіряючи, що замовлення збереглося за потрібним користувачем, ти маєш справу саме з таблицями та зв'язками між ними, навіть без жодного написаного SELECT.
Чому порядок рядків у таблиці не гарантований і чим це загрожує автотестам?
Таблиця — це множина записів, а не список, тому реляційна модель принципово не обіцяє жодного порядку рядків. Запит без ORDER BY може повертати рядки в різній послідовності на різних прогонах, на різних версіях СУБД чи після зміни . Це класичне джерело -перевірок: автотест бере «перший рядок» видачі й його, а насправді «перший» щоразу інший. Практичне правило — якщо тест покладається на конкретний порядок, він мусить явно задати ORDER BY за детермінованим ключем; інакше зелений сьогодні тест почервоніє завтра без жодної зміни в продукті.
Що таке первинний ключ і які властивості роблять його ідентифікатором?
(primary key, PK) — це стовпець або набір стовпців, що однозначно ідентифікує кожен рядок таблиці. Він має три властивості: унікальний (два рядки не можуть мати однаковий PK), не порожній (неявний NOT NULL — ідентифікатор без значення не вказує на жоден рядок) і один на таблицю (саме ним запис адресують). Саме на первинний ключ за замовчуванням посилаються інших таблиць. Ключ може бути й складеним (composite) — з кількох стовпців разом, як пара (order_id, product_id) у зв'язку.
Природний чи сурогатний ключ — що обереш і чому?
На практиці майже завжди беруть сурогатний, і вміння це аргументувати — те, що перевіряють на співбесіді. Природний ключ (natural key) — унікальне значення, яке дані вже мають самі по собі: email, ІПН, ISBN, код валюти USD. Сурогатний (surrogate key) — технічний ідентифікатор, позбавлений бізнес-змісту, який видає сама БД: автоінкремент (SERIAL у PostgreSQL, AUTO_INCREMENT у MySQL) або UUID. Сурогатний виграє з трьох причин: бізнес-значення нестабільні (людина змінює прізвище, користувач — email), а на ідентифікатор уже посилаються інші таблиці; унікальність «природних» значень часто оманлива (номер телефону з часом дістається іншій людині); природний ключ буває складеним і громіздким, і тягати його в кожне посилання незручно. Важливий наслідок для QA: сурогатний id дає лише технічну унікальність рядка, а не бізнес-унікальність.
Що таке зовнішній ключ і посилальна цілісність?
Зовнішній ключ (foreign key, FK) — стовпець, чиї значення зобов'язані мати відповідник серед первинних (чи унікальних) ключів іншої таблиці; наприклад, orders.user_id як FK на users.id читається як «це замовлення належить конкретному користувачеві». Найважливіше, що дає FK, — (referential integrity): БД власними обмеженнями, без коду застосунку, гарантує, що кожне посилання вказує на реальний рядок — не дозволить ані вставити замовлення з неіснуючим user_id, ані видалити користувача, поки замовлення на нього посилаються (якщо не задано інше). Для тестувальника FK — це і страховка, і об'єкт перевірки: биті посилання та «сирітські» рядки після кривої міграції — типовий клас дефектів у даних, які з UI часто непомітні.
Що станеться, якщо видалити користувача, на якого посилаються замовлення?
Правильна відповідь починається з «залежить від дії зовнішнього ключа» — і це саме те, чого чекає інтерв'юер. Поведінку при видаленні батьківського рядка задають діями ON DELETE: NO ACTION (дія за замовчуванням) і RESTRICT блокують операцію, поки існують посилання, — спроба впаде з помилкою violates foreign key constraint. CASCADE тягне видалення за собою: разом із користувачем автоматично зникнуть і всі його замовлення. SET NULL розриває зв'язок, виставляючи FK дочірніх рядків у NULL (замовлення живе далі, просто без власника), — для цього стовпець мусить бути nullable. Практичний висновок для QA: перш ніж заводити дефект «дані зникли після видалення» чи «БД не дає видалити рядок», глянь на визначення FK — часто це штатний CASCADE або заборона, а не баг.
Чим NO ACTION відрізняється від RESTRICT у зовнішньому ключі?
Відповідь «нічим, це синоніми» правильна рівно наполовину — і саме тому питання й ставлять. У MySQL/InnoDB вони справді еквівалентні: обидва відхиляють операцію над батьківською таблицею негайно, а задати RESTRICT (чи NO ACTION) — те саме, що взагалі не писати клозу ON DELETE/ON UPDATE. У PostgreSQL це різні дії: дефолт — ON DELETE NO ACTION, і його перевірку можна оголосити відкладеною (DEFERRABLE INITIALLY DEFERRED), тож база дочекається кінця , а команди всередині неї встигнуть «полагодити» стан і видалення пройде. RESTRICT строгіший: він відкладення не й забороняє видалення одразу. Практичний наслідок для QA: та сама схема з NO ACTION на PostgreSQL-стенді пропустить послідовність «видалив батька → полагодив дітей → COMMIT», а на MySQL-стенді впаде на першій же команді.
Чим PRIMARY KEY відрізняється від UNIQUE?
Це найчастіше питання розділу, і сильна відповідь називає обидві відмінності, а не лише «обидва унікальні». Спільне: обидва гарантують унікальність значень і обидва підкріплені індексом, тож пошук за ними швидкий. Відмінності принципові: PRIMARY KEY — рівно один на таблицю й неявно NOT NULL; UNIQUE — скільки завгодно на таблицю й допускає NULL. За роллю PK — головний ідентифікатор рядка, на який за замовчуванням посилаються зовнішні ключі, а UNIQUE — додаткове бізнес-обмеження. Практичний приклад: у таблиці users первинним ключем роблять сурогатний id, на який посилаються замовлення, а окремий UNIQUE на email блокує реєстрацію двох акаунтів з однаковою поштою — це різні завдання, і жодне одне обмеження обох не закриє.
Чи гарантує сурогатний id, що email не зареєструють двічі?
Ні — і це поширена пастка. Сурогатний id унікальний завжди, але він забезпечує лише технічну унікальність рядка, а не бізнес-унікальність за іншим полем. Двоє людей із тим самим email спокійно отримають різні id й обидва запишуться в таблицю, бо для БД це два різні рядки. Щоб цього не сталося, потрібне окреме обмеження UNIQUE на стовпці email. Для QA це прямий тест-кейс: спробувати зареєструвати той самий email двічі й переконатися, що система відхиляє дублікат на рівні бази, а не лише перевіркою у формі, яку легко обійти прямим запитом до API.
Скільки значень NULL допускає стовпець UNIQUE?
Залежить від СУБД — і саме тому переносити висновок з одного стенда на інший не можна. У PostgreSQL і MySQL стовпець UNIQUE за замовчуванням допускає кілька рядків із NULL: порівняння NULL з NULL у SQL не дає «істину», тож такі рядки не рахуються дублікатами. У деяких СУБД (історично — SQL Server) поведінка інша: дозволений лише один NULL. Тому поведінку «UNIQUE + NULL» варто перевіряти в документації конкретної СУБД, а не відповідати з пам'яті. Практичний наслідок: якщо ти тестуєш на nullable-стовпці, поведінка з порожніми значеннями може відрізнятися між середовищами на різних СУБД.
Що означає зовнішній ключ, який допускає NULL?
Nullable FK означає опційний зв'язок, а не зіпсовані дані. Наприклад, orders.manager_id зі значенням NULL — замовлення, у якого менеджера поки просто немає: стан цілком штатний. Тому не варто заводити баг «сирітський рядок» щоразу, коли бачиш NULL у зовнішньому ключі: спершу з'ясуй, чи цей зв'язок за схемою обов'язковий. Різниця важлива для QA-перевірок: обов'язковий FK оголошують NOT NULL (замовлення без користувача не має існувати), а опційний свідомо лишають nullable — і тест має перевіряти саме той інваріант, який закладено в схему, а не абстрактне «FK завжди має бути заповнений».
Які бувають типи зв'язків між таблицями і як реалізується кожен?
Реляційна модель тримається на трьох типах зв'язків. Один-до-багатьох (1:N) — найпоширеніший: один користувач має багато замовлень, кожне замовлення належить одному користувачеві; реалізація проста — FK на боці «багато» (user_id у orders). Один-до-одного (1:1) — рідкісний: рядку однієї таблиці відповідає щонайбільше один рядок іншої; реалізують через FK з накладеним UNIQUE або через спільний первинний ключ. Багато-до-багатьох (M:N) — одне замовлення містить багато товарів, і один товар входить у багато замовлень; самими лише FK у двох таблицях його не зібрати — потрібна проміжна таблиця. Хто впізнає ці три патерни з першого погляду, той уже наполовину розібрався в незнайомій схемі.
Як реалізувати зв'язок many-to-many?
Очікувана відповідь одна: через проміжну (зв'язувальну, junction) таблицю з двома зовнішніми ключами. Прямим зовнішнім ключем M:N не зробити — «список» id у клітинку не запишеш, реляційна модель такого не знає. M:N завжди розпадається на пару зв'язків 1:N через окрему таблицю, де один рядок фіксує один факт зв'язку: order_items з order_id і product_id. Сильна відповідь додає, що первинний ключ такої таблиці зазвичай складений — пара (order_id, product_id), щоб той самий товар не потрапив у замовлення двома рядками, — і що на самому зв'язку зручно тримати атрибути: quantity, ціну на момент покупки. Той самий патерн: студенти й курси (таблиця записів на курс), статті й теги (таблиця тегування).
Навіщо взагалі зв'язок 1:1, якщо можна тримати все в одній таблиці?
Технічно 1:1 справді можна злити в одну таблицю, тож питання перевіряє, чи розумієш мотивацію розділення. Причин дві: перша — прибрати з «гарячої» users важкі чи рідко потрібні поля (біографія, аватарка), щоб основні запити лишалися легкими; друга — ізолювати чутливі дані з окремими правами доступу. Реалізують 1:1 через FK з обмеженням UNIQUE (інакше до одного users.id причепиться кілька профілів) або через спільний первинний ключ. Для QA важливо, що обидва рядки мають лишатися синхронними: створення користувача без профілю чи навпаки — потенційний баг, залежно від того, як застосунок гарантує пару.
Як читати ER-діаграму й нотацію «воронячих лапок»?
(entity-relationship diagram) — погляд на базу згори: прямокутники-сутності (таблиці), їхні атрибути й лінії зв'язків. Найуживаніша нотація ліній — «воронячі лапки» (crow's foot), де кардинальність закодована символом на кінці лінії: «один» малюється рискою |, «нуль» (участь необов'язкова) — кружечком o, «багато» — трипалим розгалуженням, за яке нотація й дістала назву. Символи комбінуються в «один і тільки один», «нуль або один», «один або багато», «нуль або багато»; атрибути підписують PK/FK, щоб було видно, де який ключ. Читаючи діаграму вголос, ти фактично проговорюєш правила застосунку: «користувач розміщує нуль або багато замовлень» — це зв'язок 1:N. Для QA кожна лінія — FK-обмеження, що заслуговує на окрему перевірку, а кожна позначка UK/UNIQUE — правило унікальності, яке мусить відсікати дублікати.
Як за схемою впізнати, що десь є зв'язок many-to-many?
Ознака — проміжна таблиця, яка складається переважно з двох зовнішніх ключів. Побачив таблицю на кшталт order_items з парою FK (order_id, product_id) і, можливо, кількома атрибутами самого зв'язку (quantity) — з високою ймовірністю це розкладений зв'язок M:N між двома «головними» сутностями. Її первинний ключ зазвичай складений — з тієї самої пари зовнішніх ключів. На ER-діаграмі це видно як дві лінії 1:N, що сходяться до однієї таблиці посередині. Для тестувальника така таблиця — окремий об'єкт перевірок: чи не дублюються факти зв'язку, чи коректно поводяться обидва FK при видаленні батьків, чи не лишаються «висячі» рядки зв'язку після видалення товару або замовлення.
Тест очікує конкретний рядок, а результат плаває між прогонами. З чого почати розбір?
Найімовірніша причина — відсутність детермінованого сортування, а не баг продукту. Оскільки порядок рядків без ORDER BY не гарантований, будь-яка перевірка виду «візьми перший рядок і асертни його поле» стає лотереєю: сьогодні БД повернула один порядок, завтра інший. Перше, що варто зробити, — переконатися, що запит (чи ORM за ним) явно сортує за стабільним ключем, наприклад за id або created_at. Другий підозрюваний — недостатня : паралельні прогони чи залишки від попереднього тесту домішують «чужі» рядки у видачу. Тобто симптом «зелено поодинці, червоно в наборі» тут читається як проблема сортування або ізоляції стану, а не як дефект застосунку.
Замовлення посилається на user_id, якого немає в users. Це взагалі можливо і про що каже?
За наявності коректного зовнішнього ключа з посилальною цілісністю це неможливо: БД не дасть ані вставити таке замовлення, ані видалити користувача, лишивши . Тож якщо такий рядок реально існує, це серйозний сигнал — найчастіше про те, що зовнішній ключ узагалі не оголошений (посилання є лише «на папері», а обмеження в схемі немає), або його вимкнули під час масової міграції даних і не ввімкнули назад. Це справжній баг цілісності даних, який часто не видно в UI, поки застосунок не спробує підтягнути неіснуючого користувача й не впаде. Для QA це привід перевірити не лише дані, а й саму схему: чи є REFERENCES там, де за логікою мусить бути, і чи не лишили міграції «дірку» в цілісності.
Три кейси, де вирішує, знайшов ти реальний баг цілісності чи погнався за примарою: читання як карти перевірок, розбір «це баг чи штатний ON DELETE?» на живій ситуації й перевірка цілісності прямо в БД замість сліпої віри в UI. Скрізь — що дивитися і чому.
Кейс 1. ER-діаграма як карта того, що можна зламати
Тобі дали схему інтернет-магазину на онбордингу й попросили «прикинути, що тут тестувати». Спершу читаємо діаграму вголос — і вона сама розповідає правила застосунку.
Кожна лінія й позначка перетворюється на конкретну перевірку:
| Зв'язок на схемі | Як читається | Що перевіряє QA |
|---|---|---|
| USERS до ORDERS, «нуль або багато» | 1:N, user_id — FK | замовлення не створюється на неіснуючого користувача; що з замовленнями при видаленні юзера |
| USERS до USER_PROFILES, «один-до-одного» | 1:1 через FK з UNIQUE | на один users.id не чіпляється двох профілів; пара користувач-профіль синхронна |
| ORDER_ITEMS з двох FK | проміжна таблиця, отже M:N | той самий товар не дублюється в одному замовленні окремими рядками |
email з позначкою UK | бізнес-правило унікальності | дублікат email відхиляється на рівні БД, не лише у формі |
Що дивитися і чому:
- Таблиця з двох — майже завжди зв'язок many-to-many.
ORDER_ITEMSтримаєorder_idіproduct_id, її складений з цієї пари. Побачив такий патерн — знай, що десь ховається M:N, і його треба покрити окремо від «головних» таблиць. oпроти|на кінці лінії — це питання «а nullable він чи ні?».||--o{(нуль або багато) користувача без жодного замовлення — легальний стан. Якби там стояло||--|{(один або багато), користувач без замовлень був би порушенням; це різні тест-кейси.UK(unique key) — не те саме, щоPK. Первинний ключUSERS— сурогатнийid(на нього посилаються замовлення), аemailлишеUNIQUE. Дублікат email пролізе, якщо це обмеження забули: перевіряти його треба прямим повторним запитом, а не довірою до валідації у формі.
Кейс 2. «Це баг чи штатна поведінка?» — розбір по ON DELETE
Прилітає тікет: «Видалив тестового користувача через адмінку — разом із ним зникли всі його замовлення. Втрата даних, критичний баг!». Перш ніж заводити дефект, дивимося на визначення зовнішнього ключа.
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE
);
Ось ON DELETE CASCADE — і «зникнення» замовлень тут штатне: так спроєктовано, видалення батька каскадить на дітей. Це не баг застосунку, а поведінка БД, закладена в схему. для трьох варіантів:
| Симптом | Визначення FK | Це баг? |
|---|---|---|
| замовлення зникли разом із юзером | ON DELETE CASCADE | ні — штатний каскад |
БД не дає видалити юзера, лається violates foreign key constraint | ON DELETE NO ACTION або RESTRICT | ні — захист цілісності |
замовлення лишились, але user_id став NULL | ON DELETE SET NULL | ні — «без власника» за задумом |
замовлення посилається на user_id, якого немає в users | FK не оголошений або вимкнений | так — реальний баг цілісності |
Що дивитися і чому:
- Перш ніж писати «втрата даних», відкрий схему. Різниця між багом і фічею тут — один рядок у визначенні FK. Сильний QA у тікеті одразу уточнює: «поведінка задана
ON DELETE CASCADE, це очікувано; питання лише в тому, чи каскад — бажаний продуктовий сценарій». - Заборона виглядає як збій, а насправді захист. Скарга «не можу видалити рядок, БД лається» — це не поламка, а спрацьоване обмеження: на рядок посилаються дочірні записи. Баг був би, якби видалення пройшло й лишило .
- Другий рядок таблиці читається по-різному на двох стендах.
NO ACTIONіRESTRICTеквівалентні лише в MySQL/InnoDB. У PostgreSQLNO ACTION(дефолт) можна оголосити відкладеним (DEFERRABLE INITIALLY DEFERRED) — тоді помилки не буде взагалі, якщо доCOMMITдітей встигли перевісити або видалити;RESTRICTвідкладення не допускає. Тож «на моїй базі така сама схема поводиться інакше» тут — не містика, а діалект. - Справжній дефект — четвертий рядок таблиці. Якщо в базі реально є замовлення на неіснуючого користувача, значить, FK узагалі немає або його вимкнули під час міграції. Оце варто ескалювати: цілісність «на папері» без обмеження в схемі рано чи пізно впаде в проді.
Кейс 3. Перевірка цілісності прямо в БД, а не через UI
UI показує, що користувача видалено, а форма реєстрації «наче» не пускає дублікат email. «Наче» — недостатньо: інваріанти цілісності надійніше перевіряти прямо в базі, бо UI й API можуть брехати, а обмеження БД — ні.
import { test, expect } from '@playwright/test';
import { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
test('після видалення юзера не лишилось сиріт-замовлень', async () => {
// orphan = замовлення, чий user_id не має пари в users
const { rows } = await pool.query(
`SELECT COUNT(*)::int AS orphans
FROM orders o
LEFT JOIN users u ON u.id = o.user_id
WHERE u.id IS NULL`,
);
expect(rows[0].orphans, 'знайдено сирітські замовлення — порушено цілісність').toBe(0);
});
Бізнес-унікальність email — окрема перевірка: сурогатний id її не гарантує, тож тестуємо саме обмеження UNIQUE, а не форму.
test('email не можна зареєструвати двічі — на рівні БД', async () => {
const email = `dup-${Date.now()}@example.com`;
await pool.query(`INSERT INTO users(email) VALUES ($1)`, [email]);
// друга спроба того самого email має впертися в UNIQUE, а не створити рядок
await expect(
pool.query(`INSERT INTO users(email) VALUES ($1)`, [email]),
).rejects.toThrow(/duplicate key value|unique constraint/i);
});
Що дивитися і чому:
LEFT JOIN ... WHERE u.id IS NULL— канонічний пошук сирітських записів. Ліве з'єднання лишає всі замовлення, аu.id IS NULLвиловлює ті, для яких пари вusersне знайшлося. Нуль сирітських записів — цілісність ціла; будь-яке число більше нуля — привід копати міграцію чи вимкнений FK.- Дублікат email ловиться помилкою БД, а не «тихим» другим рядком. Якщо
UNIQUEна місці, друга вставка падає зduplicate key value. Якщо ж обидві вставки пройшли — обмеження немає, і валідація у формі лише маскує діру: прямий запит до API створить два акаунти. - Не плутай технічну й бізнес-унікальність у тесті. Перевіряти
idна унікальність безглуздо — його гарантує сама БД. Цінність має перевірка саме бізнес-ключа (email), бо це той інваріант, який реально ламається на практиці.
Таблиця, рядок, схема
- Розумію, чому порядок рядків без
ORDER BYне гарантований (таблиця — множина, не список) і чому «перший рядок без сортування» — джерело , що лікується явнимORDER BYза стабільним ключем. - Знаю, що стовпець має фіксований тип-контракт, який БД перевіряє сама (рядок у
integer-стовпець не покласти). - Розрізняю два значення слова «схема»: опис структури (таблиці, типи, ключі — його змінюють міграції) і простір імен (namespace) на кшталт
public.users.
Первинний ключ
- Знаю три властивості : унікальний,
NOT NULL(неявно) і один на таблицю. - Можу пояснити різницю природний (natural) vs сурогатний (surrogate) ключ і чому на практиці майже завжди беруть сурогатний.
- Знаю, що первинний ключ буває складеним (composite) — з кількох стовпців разом, як пара
(order_id, product_id).
Зовнішній ключ і посилальна цілісність
- Можу пояснити, що (FK) — це стовпець, значення якого мусить збігатися з PK (чи
UNIQUE) іншої таблиці. - Розумію, що — гарантія БД, що посилання завжди веде на реальний рядок, без коду застосунку.
- Знаю дії
ON DELETE:NO ACTION(за замовчуванням) іRESTRICT— заборонити,CASCADE— видалити дітей,SET NULL— обнулити FK (потребує nullable-стовпця). - Знаю, що
NO ACTIONіRESTRICT— синоніми лише в MySQL/InnoDB: у PostgreSQLNO ACTIONвідкладену до кінця перевірку, аRESTRICT— ні. - Можу відрізнити штатний
CASCADE/заборону на FK від бага: перш ніж заводити дефект «дані зникли»/«не дає видалити», дивлюся на визначення FK. - Розумію, що nullable FK — це опційний зв'язок (замовлення без менеджера), а не сирітський рядок.
PRIMARY KEY проти UNIQUE
- Можу назвати обидві відмінності: PK один на таблицю й неявно
NOT NULL, аUNIQUE— багато й допускаєNULL. - Знаю спільне: обидва гарантують унікальність і обидва створюють під собою індекс.
- Розумію, що сурогатний
idне замінюєUNIQUEнаemail— бізнес-дублікат пролізе без окремого обмеження. - Пам'ятаю нюанс про
NULL: у PostgreSQL/MySQLUNIQUE-стовпець за замовчуванням допускає кількаNULL, у SQL Server історично — лише один.
Зв'язки й ER-діаграма
- Розрізняю три типи зв'язків і реалізацію кожного: 1:N — FK на боці «багато»; 1:1 — FK з
UNIQUE; M:N — junction-таблиця з двох FK, бо «список» id у клітинку не покласти. - Знаю, навіщо різати сутність на 1:1: винести великі/рідкісні поля або відокремити чутливі дані.
- Умію читати «воронячі лапки» (crow's foot):
|— один,o— нуль/необов'язково, розгалуження — багато. - Впізнаю за ознакою «складається переважно з двох FK» — сигнал, що десь є зв'язок many-to-many.
- Розумію, що для QA — карта того, що можна зламати: кожна лінія це FK-перевірка, кожен
UK— правило унікальності.
Квіз
Перед стартом
- Питань: 13
- Поріг «зараховано»: ≥70% правильних відповідей.
- Результат впливає на прогрес; завалені питання підуть у чергу повторення.
- Квіз впливає на компліт теми: тема стає «пройдено», лише коли прочитано теорію І квіз складено на ≥70%.
Питання
Чому порядок рядків у таблиці без ORDER BY не гарантований?


