QA Interview Hub · База знань · Тести

SQL

Структура бази

обмеження, індекси, нормалізація, міграції

Які бувають обмеження (constraints) і як їх тестувати?

  • PRIMARY KEY — унікальний ідентифікатор рядка.
  • FOREIGN KEY — посилання має вказувати на існуючий запис.
  • NOT NULL — значення обов'язкове.
  • UNIQUE — без повторів, наприклад email.
  • CHECK — довільна умова, наприклад balance >= 0.
  • DEFAULT — значення, якщо його не передали.

Для QA обмеження — це готовий список негативних кейсів: що робить застосунок, коли база відхиляє запис? Повідомлення має бути зрозумілим користувачу, а не «500 Internal Server Error» з текстом помилки бази.

ALTER TABLE accounts ADD CONSTRAINT balance_non_negative CHECK (balance >= 0);
ALTER TABLE users    ADD CONSTRAINT users_email_unique UNIQUE (email);

-- тест: реєстрація з уже існуючим email через UI/API
-- очікування: 409 або зрозуміле повідомлення, а не 500
На співбесіді: Основні обмеження — первинний і зовнішній ключі, NOT NULL, UNIQUE, CHECK і значення за замовчуванням. Для мене це готові негативні кейси: я перевіряю, як застосунок реагує, коли база відхиляє запис, — наприклад, реєстрація з існуючим email має дати зрозуміле повідомлення, а не п'ятисоту помилку з текстом із бази.

Що відбувається з дочірніми записами при видаленні батьківського?

Визначається опцією зовнішнього ключа ON DELETE:

  • RESTRICT / NO ACTION (за замовчуванням) — видалити батька не можна, поки є дочірні записи.
  • CASCADE — дочірні записи видаляються разом з батьком.
  • SET NULL — у дочірніх посилання стає NULL.

Що тестувати: видалення користувача з рахунками — помилка, каскад чи «м'яке видалення»? Чи не зникла каскадом історія, яку треба зберігати?

На співбесіді: Це визначає налаштування зовнішнього ключа: RESTRICT забороняє видалення, поки є дочірні записи, CASCADE видаляє їх разом з батьком, а SET NULL обнуляє посилання. Я перевіряю, що поведінка відповідає вимогам: наприклад, що видалення користувача не знищить каскадом історію транзакцій, яку за законом треба зберігати.

Що таке індекс і навіщо він потрібен?

Індекс — окрема структура даних (найчастіше B-дерево), яка дозволяє знаходити рядки без перегляду всієї таблиці. Аналогія — предметний покажчик у книзі.

  • Плюси: швидкі пошук, JOIN, сортування за проіндексованими стовпцями.
  • Мінуси: займає місце і сповільнює INSERT, UPDATE, DELETE, бо індекс теж треба оновлювати.

Первинний ключ і UNIQUE автоматично створюють індекс. Зовнішні ключі в PostgreSQL — ні, і їх часто індексують вручну.

Для QA: якщо пошук чи фільтр у UI повільний на великих даних, однією з перших гіпотез є відсутній індекс.

CREATE INDEX idx_transactions_account_created
  ON transactions (account_id, created_at);
На співбесіді: Індекс — це окрема структура, як покажчик у книзі, яка дозволяє знайти рядки без перегляду всієї таблиці. Він пришвидшує пошук і JOIN, але займає місце й сповільнює запис. Як QA я згадую про індекси, коли фільтр чи звіт повільний на великому обсязі даних, — тоді разом з розробником дивимося план запиту.

Що таке EXPLAIN і як ним користуватися?

EXPLAIN показує план виконання запиту: як база збирається шукати дані. EXPLAIN ANALYZE ще й реально виконує запит і показує фактичний час і кількість рядків.

  • Seq Scan — перегляд усієї таблиці. На великій таблиці з вибірковим фільтром — підозріло.
  • Index Scan / Index Only Scan — пошук через індекс.
  • Велика різниця між очікуваною і фактичною кількістю рядків — застаріла статистика.

Обережно: EXPLAIN ANALYZE для UPDATE чи DELETE реально змінить дані — запускати лише в транзакції з ROLLBACK.

EXPLAIN ANALYZE
SELECT * FROM transactions
WHERE account_id = 10 AND created_at >= '2026-09-01';

-- Index Scan using idx_transactions_account_created on transactions
--   (actual time=0.03..0.21 rows=48 loops=1)
На співбесіді: EXPLAIN показує план виконання запиту, а EXPLAIN ANALYZE ще й виконує його і показує фактичний час. Я дивлюся, чи використовується індекс, чи йде повний перегляд таблиці. Це допомагає в баг-репорті про повільну сторінку дати розробнику конкретику, а не просто «гальмує». І пам'ятаю, що ANALYZE для UPDATE реально змінює дані.

Що таке нормалізація і денормалізація?

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

  • 1НФ — в одній клітинці одне значення, без списків через кому.
  • 2НФ — кожен неключовий стовпець залежить від усього ключа, а не від його частини.
  • 3НФ — неключові стовпці не залежать один від одного: email користувача не повторюють у кожній транзакції.

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

Для QA денормалізація означає ризик розсинхронізації: дубльовані дані треба перевіряти на узгодженість.

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

Як тестувати міграції бази даних?

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

  • Кількість: рядків до й після однакова (або змінилась очікувано).
  • Значення: вибірково й агрегатами — суми, кількості за статусами збігаються.
  • Нові стовпці: як заповнені старі записи — NULL, значення за замовчуванням, обчислене?
  • Обмеження: нові NOT NULL і UNIQUE не впали на існуючих даних.
  • Застосунок: працює зі старими даними після міграції; старі записи відкриваються й редагуються.
  • Відкат: якщо є down-міграція — чи повертає вона стан без втрат.
  • Час: скільки триває міграція на обсязі, близькому до продакшн, чи не блокує таблицю.
-- до і після міграції: однакові контрольні суми
SELECT status, COUNT(*), SUM(amount)
FROM transactions
GROUP BY status
ORDER BY status;
На співбесіді: Для міграцій я порівнюю стан до і після: кількість рядків і контрольні суми за групами мають збігатися. Перевіряю, як заповнились нові стовпці для старих записів, чи не впали нові обмеження, чи працює застосунок зі старими даними і чи коректний відкат. І окремо — скільки триває міграція на великому обсязі даних, бо вона може заблокувати таблицю на проді.