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

Порядок виконання SELECTLogical query processing order

Запит пишуть, починаючи з SELECT, а база доходить до нього пʼятим кроком: спершу FROM, WHERE, GROUP BY і HAVING. Звідси аліас, якого не видно в WHERE, і HAVING, без якого не відфільтрувати групи.

12 хв читання Junior → Middle
01 · ДВА ПОРЯДКИ

Пишемо в одному порядку, база думає в іншому

SQL задумували так, щоб запит читався як речення: «вибери місто й середню зарплату з вакансій, де…». Тому SELECT стоїть першим. Але щоб порахувати середню зарплату, базі спершу треба знати, з якої таблиці брати рядки, які з них відкинути і як їх згрупувати. Ця послідовність називається : FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY, LIMIT.

СЛОВНИКЛогічний порядок обробки. Послідовність, у якій стандарт SQL визначає зміст запиту: FROM разом з JOIN, WHERE, GROUP BY, HAVING, SELECT, DISTINCT, ORDER BY, LIMIT. Від неї залежить, яке імʼя на якому кроці вже існує.

Кожен крок отримує таблицю від попереднього і віддає нову. WHERE бачить окремі рядки, HAVING уже групи, ORDER BY готовий результат. З цього випливають усі правила, які здаються дивними: де видно , чому COUNT(*) не можна поставити у WHERE і чому LIMIT без ORDER BY повертає що завгодно.

СЛОВНИКАліас. Імʼя виразу або таблиці: AVG(salary) AS avg_salary. Аліас колонки зʼявляється на кроці SELECT, тож кроки до нього його не знають. Аліас таблиці (FROM vacancies AS v) зʼявляється у FROM і видний скрізь.

Це порядок змісту, а не план роботи. вільний переставляти кроки: прочитати індекс уже в потрібному порядку й зупинитися після першого рядка, перенести умову з HAVING у WHERE, якщо в ній немає агрегатів. Він лише гарантує, що результат буде таким, ніби кроки йшли по черзі.

СЛОВНИКПланувальник. Частина бази, яка вирішує, як саме виконати запит: який індекс узяти, в якому порядку зʼєднати таблиці, коли зупинитися. Його рішення видно в EXPLAIN.
02 · КОНВЕЄР

Один запит, сім проміжних таблиць

Запит шукає місто з найвищою середньою зарплатою серед невіддалених вакансій, де таких вакансій щонайменше дві. Угорі він так, як його пишуть, нижче кроки так, як їх виконує база. Перемикайте кроки й дивіться, що лишається від таблиці після кожного.

SELECT city, AVG(salary) AS avg_salary FROM vacancies WHERE format <> 'remote' GROUP BY city HAVING COUNT(*) >= 2 ORDER BY avg_salary DESC LIMIT 1
vacancies
id city salary format
1 Львів 3000 hybrid
2 Київ 4000 office
3 Київ 5000 office
4 Одеса 2500 remote
5 Львів 3500 office
6 Харків 2800 hybrid
7 Київ 4500 remote
КРОК 1 · FROM7 рядків

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

vacancies
id city salary format
1 Львів 3000 hybrid
2 Київ 4000 office
3 Київ 5000 office
4 Одеса 2500 remote
5 Львів 3500 office
6 Харків 2800 hybrid
7 Київ 4500 remote
КРОК 2 · WHERE7 → 5 рядків

WHERE перевіряє кожен рядок окремо, до будь-якого групування. Віддалені вакансії 4 і 7 зникають і в середню зарплату вже не потраплять.

Тому WHERE avg_salary > 3000 не працює: на цьому кроці аліаса ще немає, SELECT виконається пізніше. MySQL відповідає Unknown column 'avg_salary' in 'where clause', PostgreSQL column "avg_salary" does not exist.

групи за city
city рядки групи COUNT(*)
Львів #1 #5 2
Київ #2 #3 2
Харків #6 1
КРОК 3 · GROUP BY5 рядків → 3 групи

GROUP BY складає рядки з однаковим містом в одну групу. Відтепер одиниця роботи це група: для неї можна порахувати COUNT(*) чи AVG(salary), а окремий salary без агрегату вже не має сенсу, бо в групі їх кілька.

групи за city
city рядки групи COUNT(*)
Львів #1 #5 2
Київ #2 #3 2
Харків #6 1
КРОК 4 · HAVING3 → 2 групи

HAVING це WHERE для груп. Він існує, бо умова на COUNT(*) чи AVG(salary) має сенс лише після групування, а WHERE виконується раніше. Харків з однією вакансією випадає цілою групою.

результат
city avg_salary
Львів 3250
Київ 4500
КРОК 5 · SELECT2 рядки, 2 колонки

Лише тепер SELECT рахує вирази й дає їм імена. Аліас avg_salary народжується тут, а id, salary і format у результат не йдуть.

результат
city avg_salary
Київ 4500
Львів 3250
КРОК 6 · ORDER BY2 рядки, Київ перший

ORDER BY виконується після SELECT, тому аліас avg_salary тут уже видно. Без ORDER BY порядок рядків ніхто не обіцяє, навіть якщо на маленькій таблиці він здається стабільним.

результат
city avg_salary
Київ 4500
Львів 3250
КРОК 7 · LIMIT2 → 1 рядок

LIMIT останній: він бере перші рядки вже відсортованого результату. Тому LIMIT без ORDER BY повертає якийсь рядок, а не найкращий.

Крок 1 з 7

Сім вакансій це приклад, а сценарій підготовлений заздалегідь: бази в браузері немає. Той самий запит на MySQL 8.0, PostgreSQL 16 і SQLite 3.45 повертає Київ із середнім 4500, лише друкують його по-різному: 4500.0000, 4500.0000000000000000 і 4500.0.

03 · АЛІАСИ В ДІАЛЕКТАХ

Де аліас з SELECT видно насправді

За стандартом аліас колонки можна вживати лише в ORDER BY, і там його розуміють усі три бази. Далі кожна йде своїм шляхом. MySQL пускає аліас ще й у GROUP BY та HAVING, PostgreSQL лише в GROUP BY, а SQLite навіть у WHERE.

Для цього сайту різниця не академічна: продакшн працює на PostgreSQL, а тести на SQLite. Запит з аліасом у WHERE пройде тести й упаде на продакшні. Переносний варіант завжди один: повторити вираз (HAVING AVG(salary) > 3000) або загорнути запит у підзапит чи CTE, де аліас стає звичайною колонкою.

MySQL 8.0 PostgreSQL 16 SQLite 3.45
1SELECT city, AVG(salary) AS avg_salary
2FROM vacancies
3GROUP BY city
4HAVING avg_salary > 3000;
5
6SELECT city, AVG(salary) AS avg_salary
7FROM vacancies
8WHERE avg_salary > 3000
9GROUP BY city;
+------------+------------+
| city       | avg_salary |
+------------+------------+
| Львів      |  3250.0000 |
| Київ       |  4500.0000 |
+------------+------------+
2 rows in set (0.00 sec)

ERROR 1054 (42S22): Unknown column 'avg_salary' in 'where clause'

Аліас у HAVING і GROUP BY це розширення MySQL. У WHERE його немає й тут: рядки фільтруються раніше, ніж рахується AVG.

1SELECT city, AVG(salary) AS avg_salary
2FROM vacancies
3GROUP BY city
4HAVING avg_salary > 3000;
5
6-- переносно: повторити вираз
7SELECT city, AVG(salary) AS avg_salary
8FROM vacancies
9GROUP BY city
10HAVING AVG(salary) > 3000;
ERROR:  column "avg_salary" does not exist
LINE 4: HAVING avg_salary > 3000;
               ^
 city  |      avg_salary
-------+-----------------------
 Львів | 3250.0000000000000000
 Київ  | 4500.0000000000000000
(2 rows)

PostgreSQL розуміє аліас у GROUP BY і в ORDER BY, але в ORDER BY лише як окреме імʼя: ORDER BY avg_salary працює, а ORDER BY avg_salary * -1 падає з тією ж помилкою column does not exist.

1SELECT salary * 12 AS yearly
2FROM vacancies
3WHERE yearly > 50000;
yearly
------
60000
54000

SQLite знаходить аліас навіть у WHERE, якщо в ньому немає агрегату. MySQL і PostgreSQL на цей самий запит відповідають помилкою, тож тест на SQLite тут нічого не доводить про продакшн.

04 · У QUERY BUILDER LARAVEL

Порядок викликів не важливий, порядок SQL важливий

Query Builder приймає where(), groupBy() і having() у будь-якому порядку викликів і збирає SQL у правильному. Але імена він не перекладає: якщо аліасу не видно в SQL, його не видно й з PHP.

php VacancyStats.php WHERE і HAVING
1DB::table('vacancies')
2 ->select('city')
3 ->selectRaw('AVG(salary) AS avg_salary')
4 ->where('format', '<>', 'remote') // рядки, до групування
5 ->groupBy('city')
6 ->havingRaw('COUNT(*) >= ?', [2]) // групи, після
7 ->orderByDesc('avg_salary') // аліас тут видно
8 ->limit(1)
9 ->first();
ЩО ПЕРЕВІРЯЄ
where() стає WHERE і відкидає рядки, havingRaw() стає HAVING і відкидає групи. orderByDesc('avg_salary') безпечний: ORDER BY бачить аліас у всіх трьох базах.
КОЛИ БРАТИ
Завжди, коли є агрегати: умову на окремий рядок у where(), умову на COUNT, SUM чи AVG у having().
php VacancyStats.php Аліас у having()
1// MySQL так, PostgreSQL ні: column "avg_salary" does not exist
2->having('avg_salary', '>', 3000)
3
4// Скрізь: той самий вираз ще раз
5->havingRaw('AVG(salary) > ?', [3000])
ЩО ПЕРЕВІРЯЄ
having() з назвою колонки підставляє її в HAVING як є. На MySQL працює, бо там аліас у HAVING дозволений, на PostgreSQL ні.
КОЛИ БРАТИ
Якщо код має працювати на кількох базах, повторюйте вираз у havingRaw() з параметром.
php VacancyStats.php Підзапит
1$stats = DB::table('vacancies')
2 ->select('city')
3 ->selectRaw('AVG(salary) AS avg_salary')
4 ->groupBy('city');
5
6DB::query()
7 ->fromSub($stats, 'stats')
8 ->where('avg_salary', '>', 3000)
9 ->get();
ЩО ПЕРЕВІРЯЄ
fromSub() кладе запит у FROM як таблицю stats. Для зовнішнього запиту avg_salary уже звичайна колонка, і where() з нею працює скрізь.
КОЛИ БРАТИ
Коли умов на обчислене значення кілька або коли це результат віконної функції, який інакше не відфільтрувати.
05 · ЯК У PHP UKRAINE

Топ джерел трафіку в аналітиці

Звіт аналітики в панелі рахує, звідки приходять відвідувачі. У запиті видно всі кроки конвеєра. whereBetween і whereNotNull стають WHERE і відкидають перегляди поза періодом і без реферера ще до групування. groupBy('referrer_host') складає групи, selectRaw дає агрегату імʼя visits, а orderByDesc('visits') сортує саме за цим аліасом. Це працює і на PostgreSQL, і на SQLite в тестах, бо аліас стоїть лише в ORDER BY. limit() обрізає вже відсортований список.

php EloquentTrafficReport.php фрагмент, скорочено
1public function topReferrers(DateTimeImmutable $from, DateTimeImmutable $to, int $limit = 15): array
2{
3 return array_values($this->visits($from, $to)
4 ->whereNotNull('referrer_host')← WHERE
5 ->selectRaw('referrer_host, count(*) as visits')← SELECT, аліас visits
6 ->groupBy('referrer_host')← GROUP BY
7 ->orderByDesc('visits')← ORDER BY бачить аліас
8 ->limit($limit)← LIMIT останній
9 ->get()
10 ->map(fn (object $r) => ['host' => (string) $r->referrer_host, 'visits' => (int) $r->visits])
11 ->all());
12}
13
14private function visits(DateTimeImmutable $from, DateTimeImmutable $to): Builder
15{
16 return VisitModel::query()->toBase()->whereBetween('occurred_at', [$from, $to]);← FROM і ще одна умова WHERE
17}
src/Content/Infrastructure/Persistence/EloquentInterviewCatalog.phpЛічильники фільтрів на /interview: selectRaw('topic, count(*) as n')->groupBy('topic')->pluck('n', 'topic'). Аліас n читає вже PHP з готового результату, у SQL він ніде не повторюється.
src/Analytics/Infrastructure/Persistence/EloquentTrafficReport.phpentries() шукає перший перегляд кожного відвідувача за день у PHP, а не віконною функцією: тести йдуть на SQLite, і запит лишається простим. ROW_NUMBER() у WHERE не поставиш у жодній базі, бо віконні функції рахуються на кроці SELECT.
06 · ТИПОВІ ПОМИЛКИ

Що ламається через порядок кроків

✕ 1

Аліас у WHERE

WHERE avg_salary > 3000 чи WHERE yearly > 50000 виконується раніше за SELECT, тож імені ще немає. MySQL і PostgreSQL відповідають помилкою, SQLite для виразу без агрегату пропускає.

Натомість: Повторіть вираз (WHERE salary * 12 > 50000) або винесіть запит у підзапит чи CTE і фільтруйте зовні.

✕ 2

Агрегат у WHERE

WHERE COUNT(*) > 1 питає про групу на кроці, де груп ще немає. MySQL: ERROR 1111 (HY000): Invalid use of group function. PostgreSQL: aggregate functions are not allowed in WHERE. SQLite: misuse of aggregate: COUNT().

Натомість: Умова на агрегат іде в HAVING COUNT(*) > 1.

✕ 3

Умова на рядок у HAVING

HAVING city <> 'Одеса' працює, але відкидає групу вже після того, як база зібрала й порахувала її. На великій таблиці це зайва робота, а для колонки поза GROUP BY PostgreSQL і MySQL з ONLY_FULL_GROUP_BY її не приймуть.

Натомість: Усе, що можна перевірити на окремому рядку, пишіть у WHERE. У HAVING лишаються умови на COUNT, SUM, AVG, MIN, MAX.

✕ 4

LIMIT без ORDER BY

База віддає рядки в тому порядку, в якому їй зручно, і він змінюється після VACUUM, нового індексу чи паралельного плану. Сторінка 2 тоді повторює рядки зі сторінки 1 або пропускає інші.

Натомість: Для пагінації завжди ORDER BY з унікальним хвостом: ORDER BY published_at DESC, id DESC.

✕ 5

Віконна функція у WHERE

WHERE ROW_NUMBER() OVER (...) = 1 не працює ніде: вікна рахуються на кроці SELECT. MySQL пише ERROR 3593 (HY000): You cannot use the window function 'row_number' in this context., PostgreSQL window functions are not allowed in WHERE.

Натомість: Порахуйте номер у підзапиті й відфільтруйте зовні: SELECT … FROM (SELECT …, ROW_NUMBER() OVER (…) AS rn FROM vacancies) ranked WHERE rn = 1.

07 · СЛОВНИК
Логічний порядок обробки
Послідовність, у якій стандарт SQL визначає зміст запиту: FROM разом з JOIN, WHERE, GROUP BY, HAVING, SELECT, DISTINCT, ORDER BY, LIMIT. Від неї залежить, яке імʼя на якому кроці вже існує.
Аліас
Імʼя виразу або таблиці: AVG(salary) AS avg_salary. Аліас колонки зʼявляється на кроці SELECT, тож кроки до нього його не знають. Аліас таблиці (FROM vacancies AS v) зʼявляється у FROM і видний скрізь.
Планувальник
Частина бази, яка вирішує, як саме виконати запит: який індекс узяти, в якому порядку зʼєднати таблиці, коли зупинитися. Його рішення видно в EXPLAIN.
08 · ПЕРЕВІР СЕБЕ

Питання зі співбесід

Усі питання з SQL →

Бо WHERE виконується до SELECT, а ORDER BY після. Аліас avg_salary зʼявляється на кроці SELECT, тож WHERE про нього ще не знає, а ORDER BY уже бачить готовий результат з цією колонкою.

WHERE відкидає окремі рядки до групування, HAVING відкидає цілі групи після нього. Тому умова на COUNT(*) чи AVG() можлива лише в HAVING, а умову на звичайну колонку краще ставити в WHERE: менше рядків піде в групування.

Стандарт на боці PostgreSQL: аліас з SELECT дозволений лише в ORDER BY. MySQL розширює правило на GROUP BY і HAVING. Переносно пишуть HAVING AVG(salary) > 3000 або фільтрують у зовнішньому запиті. Для коду, який тестують на SQLite, а запускають на PostgreSQL, це особливо важливо: SQLite пропускає ще більше.

Віконна функція рахується на кроці SELECT, тому її фільтрують на рівень вище: SELECT * FROM (SELECT v.*, ROW_NUMBER() OVER (PARTITION BY city ORDER BY salary DESC) AS rn FROM vacancies v) ranked WHERE rn = 1. У PostgreSQL є ще DISTINCT ON (city) з ORDER BY city, salary DESC, але це не переносно.

Ні. Логічний порядок визначає, який результат правильний, а не як його рахувати. Якщо є індекс під ORDER BY, планувальник прочитає його в потрібному порядку й зупиниться після першого рядка, а умову з HAVING без агрегатів перенесе у WHERE. У EXPLAIN це видно як Limit над Index Scan замість Sort над Seq Scan.