SQL запити: повний розбір від базових SELECT до оптимізації

SQL-запити залишаються основним інструментом роботи з реляційними базами даних у 2026 році. Вони дозволяють точно витягувати, змінювати й аналізувати дані без написання складних процедурних алгоритмів. Мова запитів розвивається, але фундаментальні принципи — декларативність і робота з множинами — не змінилися з 1970-х.

Сучасні СУБД (PostgreSQL, MySQL, SQL Server, Oracle) обробляють запити через оптимізатор, який будує план виконання. Розуміння цього механізму допомагає писати швидкі й надійні запити як початківцям, так і досвідченим аналітикам.

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

Історичний шлях SQL-запитів: від теорії Кодда до стандарту ISO

У 1970 році Едгар Кодд опублікував роботу «A Relational Model of Data for Large Shared Data Banks». Він запропонував зберігати дані у вигляді таблиць (відношень) і оперувати ними за допомогою реляційної алгебри. Це радикально відрізнялося від ієрархічних і мережевих моделей того часу, де програміст мав явно вказувати шляхи навігації.

Дональд Чемберлін і Реймонд Бойс у 1974 році створили мову SEQUEL (Structured English Query Language) для проєкту System R в IBM. Пізніше через юридичні причини назву скоротили до SQL. Перша комерційна реалізація з’явилася в Oracle у 1979 році. Стандарт ANSI SQL-86 (пізніше ISO 9075) зафіксував базовий синтаксис, а наступні версії (SQL-92, SQL:1999, SQL:2003, SQL:2011, SQL:2016, SQL:2023) додавали віконні функції, CTE, JSON-підтримку та інші можливості.

Сьогодні SQL залишається де-факто стандартом. Навіть NoSQL-системи часто пропонують SQL-подібні інтерфейси, бо декларативний підхід виявився зручнішим для більшості аналітичних і транзакційних задач.

Анатомія сучасного SQL-запиту: як працює двигун

Коли СУБД отримує текст запиту, вона проходить кілька етапів. Спочатку парсер перевіряє синтаксис і будує дерево. Далі оптимізатор оцінює можливі плани виконання, враховуючи статистику таблиць, наявність індексів і вартість операцій (сканування, сортування, з’єднання). Найдешевший план передається виконавцю.

Ключові частини запиту виконуються в строгому логічному порядку, який не завжди збігається з порядком написання:

  • FROM і JOIN — формують початковий набір рядків
  • WHERE — відфільтровує рядки
  • GROUP BY — групує
  • HAVING — фільтрує групи
  • SELECT — обчислює вирази та віконні функції
  • ORDER BY — сортує
  • LIMIT / OFFSET або FETCH — обмежує результат

Розуміння цього порядку пояснює, чому фільтр у WHERE працює швидше за HAVING, а віконні функції не можна безпосередньо використовувати в WHERE без підзапиту або CTE.

За моїм досвідом використання PostgreSQL протягом останнього року на таблицях із десятками мільйонів рядків, правильне розміщення фільтрів і використання індексів скорочувало час виконання запитів у 10–50 разів.

Базовий набір для початківців: SELECT і фільтрація з прикладами

Найпростіший запит виглядає так:

text
SELECT product_name, price
FROM products
WHERE price > 1000
ORDER BY price DESC
LIMIT 20;

SELECT вказує потрібні стовпці, FROM — джерело, WHERE — умови, ORDER BY — сортування, LIMIT — обмеження кількості рядків. Уникайте SELECT * у продакшені: він тягне зайві дані, ускладнює підтримку й часто заважає оптимізатору використовувати покриваючі індекси.

Корисні оператори фільтрації:

  • BETWEEN — діапазон значень
  • IN — входження в список
  • LIKE / ILIKE — шаблони рядків
  • IS NULL / IS NOT NULL — робота з порожніми значеннями
  • AND, OR, NOT — логічні комбінації

Для початківців важливо запам’ятати: NULL не дорівнює жодному значенню, навіть самому собі. Порівняння з NULL завжди дає UNKNOWN, тому використовуйте саме IS NULL.

Складні конструкції: JOIN, підзапити, CTE та віконні функції

JOIN з’єднує таблиці за ключами. INNER JOIN повертає лише збіги, LEFT JOIN зберігає всі рядки лівої таблиці, FULL OUTER JOIN — обидві сторони. Уникайте декартового добутку: якщо забути умову ON, кількість рядків зростає експоненційно.

Підзапити можуть бути в SELECT, FROM, WHERE. Корельовані підзапити виконуються для кожного рядка зовнішнього запиту й часто стають вузьким місцем. Замість них у більшості випадків краще писати JOIN або EXISTS.

Common Table Expressions (CTE) з’явилися в SQL:1999 і стали стандартом читабельності:

text
WITH monthly_sales AS (
  SELECT customer_id, SUM(amount) AS total
  FROM orders
  WHERE order_date >= '2026-01-01'
  GROUP BY customer_id
)
SELECT c.name, ms.total
FROM customers c
JOIN monthly_sales ms ON c.id = ms.customer_id
WHERE ms.total > 50000;

CTE дозволяють розбивати складну логіку на іменовані кроки. У PostgreSQL 12+ і більшості сучасних СУБД CTE за замовчуванням інлайняться, якщо використовуються один раз.

Віконні функції (ROW_NUMBER, RANK, LAG, LEAD, SUM OVER) обчислюють значення поверх набору рядків без згортання їх у групи. Вони ідеально підходять для рейтингів, накопичувальних сум і порівняння з попереднім періодом.

У нашій практиці ми стикалися з випадком, коли запит із корельованим підзапитом на таблиці замовлень виконувався 47 секунд. Після заміни на віконну функцію ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date DESC) час скоротився до 1,2 секунди.

Поширені помилки, які сповільнюють або ламають запити

Багато проблем виникають через, здавалося б, незначні деталі:

  • Використання функцій над стовпцем у WHERE (WHERE YEAR(order_date) = 2026). Індекс не працює. Краще: WHERE order_date >= ‘2026-01-01’ AND order_date < ‘2027-01-01’.
  • OR у фільтрах замість UNION або кількох умов, які оптимізатор може переписати.
  • SELECT * у великих таблицях або JOIN-ах.
  • Відсутність індексів на стовпцях, що використовуються в JOIN і WHERE.
  • OFFSET для глибокої пагінації. Краще keyset-пагінація (WHERE id > last_seen_id).
  • Ігнорування NULL у порівняннях і агрегаціях.

Ще один міф: «CTE завжди повільніші за підзапити». У сучасних двигунах це вже не так. Продуктивність залежить від того, чи матеріалізується CTE і скільки разів на нього посилаються.

Оптимізація продуктивності станом на 2026 рік

Головне правило — дати оптимізатору максимум інформації. Оновлюйте статистику (ANALYZE у PostgreSQL, UPDATE STATISTICS у SQL Server). Перевіряйте план виконання через EXPLAIN (ANALYZE, BUFFERS).

Ефективні прийоми:

  • Створюйте композитні індекси в порядку фільтрації та сортування.
  • Уникайте SELECT * і вибирайте лише потрібні стовпці.
  • Фільтруйте дані якомога раніше.
  • Для аналітики використовуйте матеріалізовані представлення або попередньо агреговані таблиці.
  • Віконні функції часто швидші за самоз’єднання або корельовані підзапити.
  • Для глибокої пагінації застосовуйте keyset-підхід замість великих OFFSET.

У 2026 році більшість хмарних сховищ (Snowflake, BigQuery, Redshift) і сучасних версій PostgreSQL / MySQL добре оптимізують CTE та віконні функції. Головне — писати sargable умови й не змушувати двигун робити повне сканування таблиці.

Міні-кейс з практики та чек-лист самоперевірки

У одному з проєктів аналітики електронної комерції запит «топ-5 товарів за виручкою в кожній категорії за останній місяць» спочатку писали через корельований підзапит. На 8 млн рядків він працював понад хвилину. Переписали через CTE + ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY revenue DESC). Час виконання впав до 3–4 секунд, а код став читабельнішим.

Чек-лист перед запуском запиту в продакшен:

  1. Чи вказані лише потрібні стовпці?
  2. Чи всі умови WHERE sargable (без функцій над стовпцем)?
  3. Чи є індекси на стовпцях JOIN і WHERE?
  4. Чи перевірено план виконання (EXPLAIN)?
  5. Чи немає SELECT * і великих OFFSET?
  6. Чи оброблено NULL правильно?
  7. Чи можна замінити корельований підзапит на JOIN, EXISTS або віконну функцію?

FAQ: відповіді на типові питання

Чим відрізняється WHERE від HAVING?
WHERE фільтрує рядки до групування, HAVING — після. WHERE може використовувати індекси, HAVING — ні.

Коли краще CTE, а коли підзапит?
CTE зручніші, коли логіку потрібно розбити на кроки або використати кілька разів. Для простого одноразового фільтра підзапит часто достатній.

Чи можна використовувати віконні функції в WHERE?
Ні, безпосередньо. Спочатку обчисліть їх у підзапиті або CTE, потім фільтруйте.

Чому мій запит раптом став повільним?
Найчастіші причини: застаріла статистика, зростання обсягу даних, відсутність або фрагментація індексів, зміна плану виконання.

Як безпечно передавати параметри в динамічні запити?
Використовуйте параметризовані запити або функції форматування з екрануванням (%I, %L у PostgreSQL). Ніколи не конкатенуйте рядки безпосередньо.

SQL-запити — це не просто синтаксис, а спосіб мислення про дані. Чим точніше ви описуєте потрібний результат і чим краще розумієте, як двигун його досягає, тим швидше й надійніше працюють ваші системи. Практикуйте на реальних обсягах, читайте плани виконання й поступово переходьте від простих SELECT до складних аналітичних конструкцій.

Денис Романенко

Денис Романенко

Київський IT-інженер. Почав з OS/2 у кінці 90-х, сидів на os2.kiev.ua, портував софт. Пізніше перейшов на Linux. Зараз DevOps/SRE: Kubernetes, безпека, VPN, автоматизація. Блог samm.kiev.ua веде з 2026-го — без хайпу, тільки те, що сам перевірив руками. Пише рідко, але по суті. Живе в Києві. Багато кави, мало сну, термінал майже завжди відкритий.

Leave a Reply

Your email address will not be published. Required fields are marked *