Питання на співбесіді: Database
Найпопулярніші питання з реальних Laravel/PHP співбесід для всіх рівнів
10 питань
Query Builder - плавний інтерфейс для побудови SQL-запитів без написання рядкового SQL. Працює з усіма підтримуваними СУБД і захищає від SQL-ін'єкцій через підготовлені вирази.
$users = DB::table('users')
->where('votes', '>', 100)
->orderBy('name')
->limit(10)
->get();
Повертає прості об'єкти stdClass. Eloquent побудований поверх Query Builder, тож ті самі методи (where, join, orderBy) доступні і на моделях.
Транзакція гарантує атомарність: або всі операції виконуються, або жодна.
DB::transaction(function () use ($order) {
$order->save();
$order->items()->createMany($items);
Inventory::decrement($order->product_id, $order->qty);
});
При винятку всередині замикання Laravel автоматично робить rollBack(). Ручний контроль:
DB::beginTransaction();
try {
// ...
DB::commit();
} catch (Throwable $e) {
DB::rollBack();
throw $e;
}
Другий аргумент transaction($cb, 3) задає кількість повторів при deadlock.
Індекс - структура (зазвичай B-дерево), що пришвидшує пошук і сортування за стовпцем ціною уповільнення запису та додаткового місця.
$table->index('status'); // звичайний
$table->unique('email'); // унікальний
$table->index(['user_id', 'created_at']); // композитний
Правила:
- Індексуйте стовпці у
WHERE,JOIN,ORDER BY, зовнішні ключі. - Композитний індекс корисний за префіксом стовпців (порядок важливий).
EXPLAINпоказує, чи використовується індекс.- Зайві індекси шкодять записам - балансуйте.
Підзапит - запит, вкладений в інший. Eloquent дозволяє вставляти їх у select, where, orderBy.
// додати останню дату входу кожного користувача одним запитом
User::addSelect(['last_login_at' => Login::select('created_at')
->whereColumn('user_id', 'users.id')
->latest()
->limit(1),
])->get();
// сортування за підзапитом
Destination::orderByDesc(
Flight::select('arrived_at')->whereColumn('destination_id', 'destinations.id')->latest()->limit(1)
)->get();
Підзапити допомагають уникнути N+1 і зайвих операцій JOIN, обчислюючи похідні значення в межах одного запиту.
Підключення оголошуються в config/database.php. Вибір конкретного:
DB::connection('reporting')->table('events')->get();
class AnalyticsEvent extends Model
{
protected $connection = 'reporting'; // модель завжди на цьому з'єднанні
}
Типові сценарії:
- Read/Write splitting - окремі хости для читання й запису (Laravel сам маршрутизує
SELECTна репліку):'mysql' => ['read' => [...], 'write' => [...]], - Окрема аналітична або legacy-БД.
Pessimistic locking блокує рядок у БД до завершення транзакції - інші транзакції чекають.
DB::transaction(function () {
$account = Account::lockForUpdate()->find($id); // блокування на запис
$account->balance -= 100;
$account->save();
});
sharedLock() - блокування на читання.
Optimistic locking не блокує, а перевіряє версію/updated_at перед записом; якщо хтось уже змінив рядок - оновлення відхиляється, операцію повторюють.
UPDATE accounts SET balance = ?, version = version + 1
WHERE id = ? AND version = ?
- Pessimistic - для високої конкуренції за тими ж рядками (платежі, склад).
- Optimistic - коли конфлікти рідкісні; масштабується краще, бо не тримає блокувань.
Deadlock - дві транзакції взаємно блокують одна одну, чекаючи на ресурси, які тримає інша. СУБД виявляє це й «вбиває» одну з транзакцій.
Запобігання:
- Єдиний порядок доступу до таблиць/рядків у всіх транзакціях.
- Тримати транзакції короткими, блокувати якомога пізніше.
- Правильні рівні ізоляції (не завищувати без потреби).
Обробка в Laravel - автоматичний повтор:
DB::transaction(function () {
// ...
}, attempts: 3); // повторити при deadlock
Діагностика: SHOW ENGINE INNODB STATUS (MySQL), логи БД, моніторинг частоти deadlock. Інколи допомагає optimistic locking замість тривалих блокувань.
Транзакція відкочує записи в базу - і більше нічого. Усе, що встигло вийти за її межі, лишається зробленим.
П'ять місць, де це видно:
1. Побічні ефекти не відкочуються. Лист, відправлений усередині транзакції, піде навіть після rollback - база відкотиться, а користувач уже отримав «Замовлення оформлено».
2. Завдання стартує раніше за commit. Воркер може взяти завдання до завершення транзакції й не знайти запису. Лікується afterCommit() на завданні або ShouldDispatchAfterCommit на події:
ProcessOrder::dispatch($order)->afterCommit();
3. Вкладені транзакції - не транзакції. Друга DB::transaction() усередині першої не створює окрему: використовуються savepoint-и, і зовнішній rollback скасує все одно все.
4. DDL не відкочується в MySQL. Schema::create() усередині транзакції робить неявний commit - міграції на MySQL атомарними не бувають, на відміну від PostgreSQL.
5. Довга транзакція тримає блокування. Звернення до зовнішнього API всередині транзакції означає, що рядки заблоковані на весь час мережевого очікування.
Практичне правило: усередині транзакції - лише запити до бази. Листи, черги, HTTP - після неї, за фактом успіху.
Головний ризик - несумісність схеми зі старим кодом під час деплою та блокування таблиць.
Безпечні зміни (expand → migrate → contract):
- Додати нову колонку (nullable) - старий код працює.
- Задеплоїти код, що пише і в стару, і в нову.
- Перенести дані (фоновий job), перемкнути читання.
- Окремим релізом видалити стару колонку.
Практики:
- Не покладатися на
migrate:rollbackу проді -down()може втрачати дані. Краще forward-fix. - Великі
ALTERна величезних таблицях блокують → онлайн-міграції (pt-online-schema-change, gh-ost). php artisan migrate --forceу пайплайні;--isolated, щоб не виконати паралельно на кількох воркерах.- Бекап перед руйнівними операціями; прогін міграцій на staging.
Реплікація розвантажує основний сервер: запис іде на primary, читання - на replicas. Laravel маршрутизує запити автоматично, якщо в конфізі з'єднання задані секції read/write:
'mysql' => [
'read' => ['host' => ['10.0.0.2', '10.0.0.3']], // репліки
'write' => ['host' => ['10.0.0.1']], // primary
'sticky' => true,
// ...спільні параметри
],
SELECT→ репліка,INSERT/UPDATE/DELETE→ primary.sticky => trueкритично важливе: після запису в межах того ж запиту читання теж піде з primary, інакше через replication lag можна прочитати застарілі дані.- Реплікація асинхронна → завжди закладайте можливе відставання реплік у логіці.