Junior: питання на співбесіді з теми «Типи й обмеження»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
6 питань
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 і каскадне видалення будуть повільними.
| Тип | Байтів | Діапазон зі знаком | UNSIGNED |
|---|---|---|---|
TINYINT |
1 | -128..127 | 0..255 |
SMALLINT |
2 | -32 768..32 767 | 0..65 535 |
MEDIUMINT |
3 | ±8,4 млн | 0..16,7 млн |
INT |
4 | ±2,1 млрд | 0..4,29 млрд |
BIGINT |
8 | ±9,2 × 10^18 | 0..1,8 × 10^19 |
UNSIGNED - лише невід'ємні значення, зате вдвічі більший верхній діапазон.
Як обирати:
- Первинні ключі -
BIGINT UNSIGNED.INTзакінчується на 2,1 мільярда (4,29 зUNSIGNED), і в таблицях подій, логів чи лічильників це трапляється раніше, ніж очікують. Змінити тип ключа на великій таблиці - довга й ризикована операція. Laravel$table->id()створює самеBIGINT UNSIGNED. - Зовнішні ключі - того самого типу, що й ключ, на який вони посилаються, включно з
UNSIGNED. Інакше обмеження не створиться. - Прапорці й маленькі перелічення -
TINYINT.BOOLEANу MySQL - це синонімTINYINT(1).
Пастки:
INT(11)- не обмеження розміру. Число в дужках - лише «ширина відображення» для старогоZEROFILL, на діапазон не впливає й застаріло з MySQL 8.0.17.- Віднімання з
UNSIGNED:SELECT a - bдляUNSIGNED-колонок при від'ємному результаті дає помилку «out of range» (а в нестрогому режимі - величезне число). Для арифметики, де можливі від'ємні значення, -CAST(a AS SIGNED) - b. - Гроші не в
INTгривнях, а в копійках (BIGINT) чиDECIMAL. - Переповнення: у строгому режимі (
STRICT_TRANS_TABLES, за замовчуванням) вставка значення поза діапазоном - помилка; без нього значення тихо обрізається до межі.
CHAR(n)- фіксованої довжини: значення доповнюється пробілами доnсимволів при збереженні, а при читанні кінцеві пробіли прибираються.VARCHAR(n)- змінної довжини: зберігається рівно стільки символів, скільки записано, плюс 1-2 байти на довжину.
CREATE TABLE codes (
country CHAR(2), -- завжди 2 символи: 'UA', 'PL'
email VARCHAR(255) -- від 0 до 255 символів
);
Коли CHAR: значення завжди однакової довжини - коди країн і валют, хеші фіксованої довжини, коди статусів. Тоді немає зайвих байтів на довжину.
Коли VARCHAR: усе інше - імена, email, адреси, заголовки.
Нюанси:
n- у символах, а не байтах. Вutf8mb4символ займає до 4 байтів, тожVARCHAR(255)- до 1020 байтів даних. Це важливо для обмежень розміру рядка таблиці й індексу.CHARвutf8mb4у InnoDB займає змінну кількість байтів, тож перевага «фіксованої довжини» для багатобайтових кодувань майже зникає.- Кінцеві пробіли:
CHARвтрачає їх при читанні; дляVARCHARвони зберігаються, але порівняння в collations зPAD SPACEїх ігнорують ('a' = 'a '- істина). Collationsutf8mb4_0900_*у MySQL 8 -NO PAD, і там пробіли значущі. - Обмеження
n:VARCHARдо 65 535 байтів, але всі колонки рядка разом не можуть перевищити 65 535 байтів - довгі тексти виносять уTEXT.
VARCHAR(255) за звичкою - нормально як верхня межа, але для полів з відомим обмеженням (телефон, код) варто ставити реальну довжину: це й документація, і захист від сміття. Валідація довжини все одно потрібна й у застосунку, щоб користувач отримав зрозуміле повідомлення.
Розміри типів TEXT:
TINYTEXT- до 255 байтів;TEXT- до 64 КБ;MEDIUMTEXT- до 16 МБ;LONGTEXT- до 4 ГБ.
Ліміти - у байтах: в utf8mb4 TEXT вміщує від ~16 до 64 тисяч символів залежно від мови.
Відмінності від VARCHAR:
- Ліміт рядка таблиці. Усі колонки рядка разом - не більше 65 535 байтів, і
VARCHARвраховується повністю.TEXTзаймає в цьому ліміті лише 9-12 байтів (вміст зберігається окремо), тож довгі тексти - лишеTEXT. - Індекси. Індекс на
TEXTможливий лише за префіксом (INDEX (body(100))), повний унікальний індекс - ні.VARCHARіндексується цілком (в межах ліміту ключа 3072 байти). - Значення за замовчуванням. Для
TEXT- лише як вираз у дужках (з MySQL 8.0.13);VARCHARприймає звичайний літерал. - Зберігання. InnoDB з форматом рядка
DYNAMICвиносить великі значення на окремі сторінки. Запит, що вибирає такі колонки (SELECT *), читає ці сторінки - звідси повільні списки.
Як обирати:
- Обмежений короткий текст (імена, заголовки, email, URL) -
VARCHARз розумною довжиною. - Довгий текст без жорсткої межі (статті, коментарі, описи, сирий HTML) -
TEXT/MEDIUMTEXT. - Великі бінарні дані (файли) - у сховище (S3), а в базі - шлях;
BLOBлише для невеликих бінарних значень.
Практичні наслідки:
- Не вибирайте
TEXT-колонки в списках:select(['id', 'title'])замістьSELECT *. - Для пошуку по тексту
LIKE '%...%'наTEXT- повний перегляд таблиці. ПотрібенFULLTEXT-індекс або пошуковий рушій. - У Laravel
$table->string()-VARCHAR(255),$table->text()-TEXT,$table->longText()-LONGTEXT.
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.