<? phpukraine PHP-ПЛАТФОРМА
⌕Пошук по платформі
БАЗИ ДАНИХ

SQL, який видно

JOIN, групування, індекс і транзакція на справжніх рядках, які рухаються, зʼєднуються й зникають. MySQL і PostgreSQL поруч, де вони поводяться по-різному.

JOIN ПО РЯДКАХ
SELECT c.name, v.title FROM companies c INNER JOIN vacancies v ON v.company_id = c.id
companiesc
id name
1 Kvitka Soft
2 Dnipro Labs
3 Lviv Pay
vacanciesv
id company_id title
10 1 Senior PHP
11 1 Laravel Dev
12 1 Team Lead
13 2 Middle PHP
14 NULL Symfony Dev
РЕЗУЛЬТАТ4 рядки
c.name v.title
Kvitka Soft Senior PHP
Kvitka Soft Laravel Dev
Kvitka Soft Team Lead
Dnipro Labs Middle PHP
Lviv Pay NULL
NULL Symfony Dev

Лише пари, що збіглися. Kvitka Soft з трьома вакансіями дала три рядки. Lviv Pay без вакансій і вакансія 14 без компанії зникли.

Кола Венна про JOIN брешуть. Ось як воно насправді.
01 — ТЕМИ

Каталог тем

23 розібрано
// ЯК У PHP UKRAINE

База цього сайту

Компанії, вакансії й відгуки

Вакансія посилається на компанію через 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;