Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

SQL: NULL, типи й обмеження

20 питань · ~20 хв · Версія v3.0

Увійдіть, щоб продовжити

Тризначна логіка NULL, вибір типів для грошей, дат, тексту й JSON, кодування й порівняння рядків, обмеження цілісності - питання для MySQL і PostgreSQL, від junior до senior.

За спробу
20
У пулі
70
Проходжень
0
Середній бал
-
Пройшли на 70%+
-

Питання для підготовки

25 питань

ENUM - колонка, що приймає одне значення з фіксованого списку. SET - кілька значень з фіксованого списку одночасно.

CREATE TABLE orders (
    status ENUM('new', 'paid', 'shipped', 'cancelled') NOT NULL DEFAULT 'new',
    flags SET('gift', 'urgent', 'fragile')
);

Всередині ENUM зберігається як номер значення в списку (1-2 байти) - компактно й з перевіркою допустимих значень.

Підводні камені ENUM:

  • Сортування за номером, а не за абеткою. ORDER BY status упорядкує в порядку оголошення (new, paid, shipped, cancelled). Інколи це зручно, частіше - несподівано.
  • Числа в контексті чисел. WHERE status = 1 порівнює з номером, а не з рядком '1'. Для ENUM('0', '1', '2') це джерело плутанини.
  • Зміна списку - ALTER TABLE. Додати значення в кінець у MySQL 8 можна миттєво (лише метадані). Додати в середину, перейменувати чи видалити - перебудова таблиці.
  • Нестрогий режим: недопустиме значення перетворюється на порожній рядок '' з номером 0 замість помилки. У строгому режимі (за замовчуванням у MySQL 8) - помилка.
  • Перенесення між СУБД: це нестандартний тип MySQL.

SET - ще специфічніший: значення зберігаються як бітова маска (до 64 елементів), шукати за ними незручно (FIND_IN_SET), а індекси майже не допомагають. Майже завжди краще окрема таблиця зв'язку.

Альтернативи ENUM:

  • VARCHAR + CHECK (MySQL 8.0.16+): status VARCHAR(20) CHECK (status IN ('new', 'paid', ...)) - зміна переліку не чіпає дані.
  • Таблиця-довідник із зовнішнім ключем - коли значення змінюються без деплою чи мають атрибути (назва, колір, порядок).
  • PHP enum у застосунку + рядок у базі - логіка значень живе в коді, а база зберігає просте значення.

$table->enum() у Laravel-міграціях на MySQL створює саме нативний ENUM.

Докладніше в документації: Тип ENUM

NULL у SQL означає «невідоме значення». Будь-яке порівняння з невідомим дає теж невідоме - NULL, а не true чи false. А WHERE пропускає лише рядки, де умова true.

SELECT * FROM users WHERE deleted_at = NULL;   -- завжди порожньо
SELECT * FROM users WHERE deleted_at IS NULL;  -- правильно
SELECT * FROM users WHERE deleted_at IS NOT NULL;

Де ще NULL поводиться несподівано:

  • NULL = NULL - теж NULL. Для «рівні, з урахуванням NULL» є IS NOT DISTINCT FROM у PostgreSQL і оператор <=> у MySQL.
  • COUNT(*) рахує всі рядки, COUNT(column) - лише ті, де значення не NULL.
  • SUM, AVG ігнорують NULL. AVG з трьох значень, одне з яких NULL, ділить на 2.
  • Арифметика з NULL дає NULL: price * NULL. Підставити значення за замовчуванням допомагає COALESCE(discount, 0).
  • WHERE status <> 'banned' не поверне рядки зі status IS NULL.

Порада: колонки, де значення обов'язкове, позначайте NOT NULL. Тоді питання «а що, як тут NULL» просто не виникає.

Докладніше в документації: Оператори порівняння

Первинний ключ (PRIMARY KEY) однозначно ідентифікує рядок: значення унікальне й не може бути NULL. Під нього база автоматично створює унікальний індекс.

Зовнішній ключ (FOREIGN KEY) гарантує, що значення в колонці посилається на існуючий рядок іншої таблиці.

CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,
    user_id BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
    total NUMERIC(10, 2) NOT NULL
);

Що дає зовнішній ключ:

  • Неможливо створити замовлення для неіснуючого користувача - помилка буде одразу, а не через місяць у звіті.
  • Видалення батьківського рядка керується явно: CASCADE (видалити й залежні), SET NULL, RESTRICT (заборонити, поки є залежні).

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

Нюанс MySQL: зовнішні ключі підтримує лише InnoDB. У PostgreSQL під FK індекс на колонці, що посилається, не створюється автоматично - його варто додати, інакше JOIN і каскадне видалення будуть повільними.

Докладніше в документації: Обмеження

FLOAT і DOUBLE зберігають числа у двійковій формі з плаваючою комою. Більшість десяткових дробів так точно не записати: 0.1 + 0.2 дає 0.30000000000000004. На мільйонах операцій копійки «губляться», а сума в звіті не сходиться з сумою платежів.

Два правильні варіанти:

  1. NUMERIC / DECIMAL - точний десятковий тип:
amount NUMERIC(12, 2) NOT NULL
  1. Ціле число в найменших одиницях - копійках чи центах (BIGINT). Так роблять Stripe та більшість платіжних систем: 1 999 копійок замість 19.99 грн.

Що ще важливо:

  • Валюта поруч із сумою. 100 без валюти нічого не означає. Окрема колонка currency CHAR(3) (ISO 4217).
  • У PHP теж не float: суму тримають як int у копійках або як рядок і рахують через bcmath чи бібліотеки на кшталт brick/money.
  • Округлення - явне й в одному місці. Розподіл 100 грн на три частини має дати 33.34 + 33.33 + 33.33, а не загубити копійку.
  • Тип money у PostgreSQL не радять: його формат залежить від налаштувань локалі сервера.

Докладніше в документації: Числові типи

Правило: зберігати момент часу в UTC (або в типі, що зберігає саме момент), а в часовий пояс користувача перетворювати лише під час показу.

PostgreSQL:

  • timestamptz (timestamp with time zone) зберігає момент часу. Під час запису значення переводиться в UTC, під час читання - у пояс сесії (TimeZone). Назва вводить в оману: сам пояс не зберігається.
  • timestamp (без поясу) - просто дата й час «на годиннику», без прив'язки до поясу. Годиться для «зустріч о 10:00 за місцевим часом», але не для моментів подій.

MySQL:

  • TIMESTAMP перетворюється в UTC за поясом сесії й назад, але має межу 2038 року.
  • DATETIME зберігає значення як є, без перетворень. Застосунок сам відповідає за те, щоб писати в UTC.

Типові проблеми:

  • Сервер БД, PHP і застосунок мають різні пояси, і час «з'їжджає» на кілька годин. Явно налаштований пояс на кожному шарі прибирає цей клас багів.
  • Перехід на літній час: «щодня о 02:30» за київським часом раз на рік не існує, а раз на рік буває двічі.
  • Порівняння DATE(created_at) = '2026-10-04' у UTC і в місцевому часі дає різні множини рядків.

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

Докладніше в документації: Типи дати й часу

Прочитати - ще не значить знати

20 питань, по одному на екран, ~20 хв. Після завершення - розбір кожної помилки з посиланням на питання.