SQL, який видно
JOIN, групування, індекс і транзакція на справжніх рядках, які рухаються, зʼєднуються й зникають. MySQL і PostgreSQL поруч, де вони поводяться по-різному.
Лише пари, що збіглися. Kvitka Soft з трьома вакансіями дала три рядки. Lviv Pay без вакансій і вакансія 14 без компанії зникли.
Каталог тем
Запити
6 темЩо база робить з рядками, коли читає SELECT, і де вона поводиться не так, як здається.
Проєктування
4 темиТаблиці, ключі й типи. Рішення, які потім дорого переробляти на живих даних.
Продуктивність
5 темЯк база знаходить рядки швидко і як побачити, що вона цього не робить.
Транзакції
4 темиЩо бачать два запити, які працюють одночасно, і хто кого чекає.
Рушії й СУБД
4 темиЧим відрізняються MySQL, PostgreSQL і SQLite на практиці і коли реляційна база не потрібна.
База цього сайту
Компанії, вакансії й відгуки
Вакансія посилається на компанію через company_id з nullOnDelete: якщо компанію видалити, вакансія лишиться без неї. Відгук тримає і vacancy_id, і company_id, обидва з cascadeOnDelete, і має складені індекси (company_id, status) під список у кабінеті роботодавця та (vacancy_id, email) під перевірку повторного відгуку.
database/migrations/2026_09_04_000001_add_employer_cabinet.php
companies id, slug, name, ... vacancies id, company_id → companies (nullOnDelete), title, status, published_at, ... applications id, vacancy_id → vacancies (cascadeOnDelete), company_id → companies (cascadeOnDelete), status, job_snapshot JSON, ...
Відгук зберігає знімок вакансії
Вакансію можуть відредагувати, закрити чи видалити з джерела, а кандидат має бачити, на що саме відгукнувся. Тому в момент відгуку в job_snapshot копіюються назва, компанія, зарплата, формат, рівень і адреса вакансії. Це свідома денормалізація: дані дублюються, щоб історія не змінювалась заднім числом.
src/Hiring/Domain/Candidate/Application.php
-- applications.job_snapshot {"title": "Senior PHP", "company": "Kvitka Soft", "salary": "...", "format": "...", "level": "...", "path": "/jobs/..."}
Перший перегляд кожного відвідувача за день
Звіт аналітики читає кожен відвідувач-день з першого перегляду: звідки людина прийшла і на яку сторінку. У SQL це віконна функція ROW_NUMBER() з PARTITION BY visitor. Код сайту робить те саме в PHP: бере перегляди за occurred_at і лишає перший рядок кожного visitor. Хеш відвідувача змінюється щодоби, тому visitor тут і є відвідувач-день.
src/Analytics/Infrastructure/Persistence/EloquentTrafficReport.php
SELECT * FROM ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY visitor ORDER BY occurred_at, id ) AS n FROM visits ) t WHERE n = 1;
Урок про діалекти
На продакшні PostgreSQL, а тести ганяються на SQLite в памʼяті. Тому в коді немає DISTINCT ON, який є лише в PostgreSQL, а там, де діалекти розходяться, є дві гілки: міста вакансії розгортаються через jsonb_array_elements_text у PostgreSQL і через json_each у SQLite (EloquentJobCatalog).
phpunit.xml
-- лише PostgreSQL, у тестах на SQLite впаде SELECT DISTINCT ON (visitor) * FROM visits ORDER BY visitor, occurred_at;