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

Питання на співбесіді: 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) доступні і на моделях.

Докладніше в документації: Query Builder

Транзакція гарантує атомарність: або всі операції виконуються, або жодна.

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, обчислюючи похідні значення в межах одного запиту.

Докладніше в документації: Subqueries

Підключення оголошуються в 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):

  1. Додати нову колонку (nullable) - старий код працює.
  2. Задеплоїти код, що пише і в стару, і в нову.
  3. Перенести дані (фоновий job), перемкнути читання.
  4. Окремим релізом видалити стару колонку.

Практики:

  • Не покладатися на 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 можна прочитати застарілі дані.
  • Реплікація асинхронна → завжди закладайте можливе відставання реплік у логіці.

Докладніше в документації: Read/Write підключення