Пишемо в одному порядку, база думає в іншому
SQL задумували так, щоб запит читався як речення: «вибери місто й середню зарплату з вакансій, де…». Тому SELECT стоїть першим. Але щоб порахувати середню зарплату, базі спершу треба знати, з якої таблиці брати рядки, які з них відкинути і як їх згрупувати. Ця послідовність називається : FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY, LIMIT.
Кожен крок отримує таблицю від попереднього і віддає нову. WHERE бачить окремі рядки, HAVING уже групи, ORDER BY готовий результат. З цього випливають усі правила, які здаються дивними: де видно , чому COUNT(*) не можна поставити у WHERE і чому LIMIT без ORDER BY повертає що завгодно.
Це порядок змісту, а не план роботи. вільний переставляти кроки: прочитати індекс уже в потрібному порядку й зупинитися після першого рядка, перенести умову з HAVING у WHERE, якщо в ній немає агрегатів. Він лише гарантує, що результат буде таким, ніби кроки йшли по черзі.
Один запит, сім проміжних таблиць
Запит шукає місто з найвищою середньою зарплатою серед невіддалених вакансій, де таких вакансій щонайменше дві. Угорі він так, як його пишуть, нижче кроки так, як їх виконує база. Перемикайте кроки й дивіться, що лишається від таблиці після кожного.
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
База бере всю таблицю. JOIN теж належить до цього кроку: спершу збирається одна зʼєднана таблиця, і лише потім її фільтрують.
WHERE перевіряє кожен рядок окремо, до будь-якого групування. Віддалені вакансії 4 і 7 зникають і в середню зарплату вже не потраплять.
Тому WHERE avg_salary > 3000 не працює: на цьому кроці аліаса ще немає, SELECT виконається пізніше. MySQL відповідає Unknown column 'avg_salary' in 'where clause', PostgreSQL column "avg_salary" does not exist.
GROUP BY складає рядки з однаковим містом в одну групу. Відтепер одиниця роботи це група: для неї можна порахувати COUNT(*) чи AVG(salary), а окремий salary без агрегату вже не має сенсу, бо в групі їх кілька.
HAVING це WHERE для груп. Він існує, бо умова на COUNT(*) чи AVG(salary) має сенс лише після групування, а WHERE виконується раніше. Харків з однією вакансією випадає цілою групою.
Лише тепер SELECT рахує вирази й дає їм імена. Аліас avg_salary народжується тут, а id, salary і format у результат не йдуть.
ORDER BY виконується після SELECT, тому аліас avg_salary тут уже видно. Без ORDER BY порядок рядків ніхто не обіцяє, навіть якщо на маленькій таблиці він здається стабільним.
LIMIT останній: він бере перші рядки вже відсортованого результату. Тому LIMIT без ORDER BY повертає якийсь рядок, а не найкращий.
Сім вакансій це приклад, а сценарій підготовлений заздалегідь: бази в браузері немає. Той самий запит на MySQL 8.0, PostgreSQL 16 і SQLite 3.45 повертає Київ із середнім 4500, лише друкують його по-різному: 4500.0000, 4500.0000000000000000 і 4500.0.
Де аліас з SELECT видно насправді
За стандартом аліас колонки можна вживати лише в ORDER BY, і там його розуміють усі три бази. Далі кожна йде своїм шляхом. MySQL пускає аліас ще й у GROUP BY та HAVING, PostgreSQL лише в GROUP BY, а SQLite навіть у WHERE.
Для цього сайту різниця не академічна: продакшн працює на PostgreSQL, а тести на SQLite. Запит з аліасом у WHERE пройде тести й упаде на продакшні. Переносний варіант завжди один: повторити вираз (HAVING AVG(salary) > 3000) або загорнути запит у підзапит чи CTE, де аліас стає звичайною колонкою.
+------------+------------+ | 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.
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.
yearly ------ 60000 54000
SQLite знаходить аліас навіть у WHERE, якщо в ньому немає агрегату. MySQL і PostgreSQL на цей самий запит відповідають помилкою, тож тест на SQLite тут нічого не доводить про продакшн.
Порядок викликів не важливий, порядок SQL важливий
Query Builder приймає where(), groupBy() і having() у будь-якому порядку викликів і збирає SQL у правильному. Але імена він не перекладає: якщо аліасу не видно в SQL, його не видно й з PHP.
Топ джерел трафіку в аналітиці
Звіт аналітики в панелі рахує, звідки приходять відвідувачі. У запиті видно всі кроки конвеєра. whereBetween і whereNotNull стають WHERE і відкидають перегляди поза періодом і без реферера ще до групування. groupBy('referrer_host') складає групи, selectRaw дає агрегату імʼя visits, а orderByDesc('visits') сортує саме за цим аліасом. Це працює і на PostgreSQL, і на SQLite в тестах, бо аліас стоїть лише в ORDER BY. limit() обрізає вже відсортований список.
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.Що ламається через порядок кроків
Аліас у WHERE
WHERE avg_salary > 3000 чи WHERE yearly > 50000 виконується раніше за SELECT, тож імені ще немає. MySQL і PostgreSQL відповідають помилкою, SQLite для виразу без агрегату пропускає.
Натомість: Повторіть вираз (WHERE salary * 12 > 50000) або винесіть запит у підзапит чи CTE і фільтруйте зовні.
Агрегат у 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.
Умова на рядок у HAVING
HAVING city <> 'Одеса' працює, але відкидає групу вже після того, як база зібрала й порахувала її. На великій таблиці це зайва робота, а для колонки поза GROUP BY PostgreSQL і MySQL з ONLY_FULL_GROUP_BY її не приймуть.
Натомість: Усе, що можна перевірити на окремому рядку, пишіть у WHERE. У HAVING лишаються умови на COUNT, SUM, AVG, MIN, MAX.
LIMIT без ORDER BY
База віддає рядки в тому порядку, в якому їй зручно, і він змінюється після VACUUM, нового індексу чи паралельного плану. Сторінка 2 тоді повторює рядки зі сторінки 1 або пропускає інші.
Натомість: Для пагінації завжди ORDER BY з унікальним хвостом: ORDER BY published_at DESC, id DESC.
Віконна функція у 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.
- Логічний порядок обробки
- Послідовність, у якій стандарт SQL визначає зміст запиту: FROM разом з JOIN, WHERE, GROUP BY, HAVING, SELECT, DISTINCT, ORDER BY, LIMIT. Від неї залежить, яке імʼя на якому кроці вже існує.
- Аліас
- Імʼя виразу або таблиці: AVG(salary) AS avg_salary. Аліас колонки зʼявляється на кроці SELECT, тож кроки до нього його не знають. Аліас таблиці (FROM vacancies AS v) зʼявляється у FROM і видний скрізь.
- Планувальник
- Частина бази, яка вирішує, як саме виконати запит: який індекс узяти, в якому порядку зʼєднати таблиці, коли зупинитися. Його рішення видно в EXPLAIN.
Питання зі співбесід
Бо 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.