8 SQL-запитів, які розв’язують 80% робочих завдань
З прикладами коду
SQL здається величезним. Сотні функцій, десятки типів JOIN'ів, незліченна кількість варіантів синтаксису. Але насправді багато реальних робочих завдань розв’язуються невеликим набором запитів, які ви будете використовувати знову і знову.
Тож у цій статті виділимо найнеобхідніші SQL-запити, які розв’язують безліч типових задач: як знайти потрібні дані серед мільйонів записів, як порахувати суми, середні та інші метрики, як знайти дублікати, пропуски, аномалії та багато інших.
Запит 1: Базова фільтрація з умовами (WHERE)
Умовно кажучи, якщо перед вами стоїть завдання знайти всі замовлення за останній місяць зі статусом «виконано», його можна розв’язати цим запитом. Це найпростіший та найчастіший запит у роботі. Вам потрібно вибрати конкретні записи з таблиці за певними критеріями.
Приклад коду:
SELECT
order_id,
customer_name,
order_date,
total_amount
FROM orders
WHERE order_date >= '2024-02-01'
AND order_date < '2024-03-01'
AND status = 'completed';
Ще приклади умов:
-- Кілька значень
WHERE status IN ('completed', 'shipped', 'delivered')
-- Діапазон
WHERE total_amount BETWEEN 100 AND 500
-- Пошук по тексту
WHERE customer_name LIKE '%Іванов%'
-- Комбінація умов
WHERE (status = 'pending' OR status = 'processing')
AND total_amount > 1000
Цей запит ідеально підходить, коли потрібно відфільтрувати дані за датою, статусом, категорією. Також він підійде і для пошуку конкретних записів та як основа для майже будь-якого аналізу.
А ще не забувайте перевіряти формат дат у вашій БД (PostgreSQL, MySQL можуть по-різному обробляти дати).
Запит 2: Агрегація та групування (GROUP BY + функції)
Часто стоїть ще завдання підрахувати загальний дохід за категоріями. Коли потрібно не просто вибрати дані, а порахувати суми, середні значення або кількість — варто використовувати агрегатні функції з GROUP BY.
Приклад коду:
SELECT
category,
SUM(revenue) as total_revenue,
COUNT(*) as number_of_sales,
AVG(price) as average_price
FROM sales
GROUP BY category
ORDER BY total_revenue DESC;
У результаті отримуємо щось подібне:
category | total_revenue | number_of_sales | average_price
------------------|---------------|-----------------|---------------
Електроніка | 450000 | 120 | 3750
Одяг | 280000 | 540 | 518
Книги | 95000 | 380 | 250
Основні агрегатні функції:
-- Загальна сума
SUM(column_name)
-- Кількість записів
COUNT(*) -- всі рядки
COUNT(column_name) -- рядки де значення не NULL
-- Середнє значення
AVG(column_name)
-- Мінімум та максимум
MIN(column_name)
MAX(column_name)
Фільтрація після групування (HAVING):
-- Показати тільки категорії з доходом понад 100000
SELECT
category,
SUM(revenue) as total_revenue
FROM sales
GROUP BY category
HAVING SUM(revenue) > 100000;
Цей запит часто використовується у звітах по продажах за категоріями, регіонами, менеджерами, для підрахунку кількості користувачів, замовлень, транзакцій та для аналізу середніх чеків, конверсій, метрик.
Запит 3: З'єднання таблиць (JOIN)
У реальних базах даних інформація майже завжди розподілена по кількох таблицях. Клієнти зберігаються в одній, замовлення — в іншій, а товари — в третій. JOIN дозволяє об'єднати ці дані в один результат. Уявіть, що у вас є дві таблиці:
Таблиця customers:
customer_id | name | email
------------|----------------|------------------
1 | user 1 | user1@mail.com
2 | user 2 | user2@mail.com
3 | user 3 | user3@mail.com
Таблиця orders:
order_id | customer_id | order_date | amount
---------|-------------|-------------|-------
101 | 1 | 2024-02-15 | 1500
102 | 1 | 2024-02-20 | 2300
103 | 2 | 2024-02-18 | 800
Потрібно отримати список замовлень разом з іменами клієнтів.
INNER JOIN — тільки ті записи, що мають пару
Приклад коду:
SELECT
c.name,
c.email,
o.order_id,
o.order_date,
o.amount
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
ORDER BY o.order_date DESC;
Результат:
name | email | order_id | order_date | amount
---------------|-----------------|----------|-------------|-------
User 1 | user1@mail.com | 102 | 2024-02-20 | 2300
User 2 | user2@mail.com | 103 | 2024-02-18 | 800
User 3 | user3@mail.com | 101 | 2024-02-15 | 1500
Якщо в таблиці є запис про юзера, який не робив замовлення, — всі інші значення будуть null і юзера не буде включено до таблиці.
LEFT JOIN — всі записи з лівої таблиці + дані з правої (якщо є)
SELECT
c.name,
c.email,
o.order_id,
o.order_date,
o.amount
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
ORDER BY c.name;
Результат:
name | email | order_id | order_date | amount
---------------|--------------------|----------|-------------|-------
User 1 | user1@mail.com | NULL | NULL | NULL
User 2 | user2@mail.com | 101 | 2024-02-15 | 1500
User 3 | user3@mail.com | 102 | 2024-02-20 | 2300
User 4 | user4@mail.com | 103 | 2024-02-18 | 800
У цьому сценарії юзер зʼявляється в результаті, але з NULL-ами в колонках замовлень, бо в нього їх немає.
Кожен тип JOIN використовується в різних сценаріях.
- INNER JOIN — коли потрібні тільки повні дані (список проданих товарів, замовлення з інформацією про клієнтів, транзакції з деталями платежів).
- LEFT JOIN — коли потрібні всі записи з основної таблиці, навіть якщо немає зв'язку.
Також є інші види JOIN-запиту.
- RIGHT JOIN — рідко використовується (зазвичай переписують як LEFT JOIN, міняючи таблиці місцями).
- FULL OUTER JOIN — коли потрібно все з обох таблиць (найчастіше для звірки даних з двох систем або для пошуку розбіжностей між таблицями).
Запит 4: Підзапити (Subqueries)
Підзапит — це запит всередині іншого запиту. Він дозволяє спочатку порахувати якесь значення, а потім використати його у фільтрації. Уявімо, що потрібно знайти продукти, які продаються краще за середнє.
Приклад коду:
-- Знайти продукти, які продалися краще за середнє
SELECT
product_name,
total_sales
FROM products
WHERE total_sales > (
SELECT AVG(total_sales)
FROM products
)
ORDER BY total_sales DESC;
Отже, що відбувається? Спочатку виконується підзапит (SELECT AVG(total_sales) FROM products) → повертає, наприклад, 1500. Потім основний запит фільтрує: WHERE total_sales > 1500
Підзапит у FROM (віртуальна таблиця)
-- Топкатегорії з доходом > 100000
SELECT
category,
total_revenue
FROM (
SELECT
category,
SUM(revenue) as total_revenue
FROM sales
GROUP BY category
) as category_sales
WHERE total_revenue > 100000;
Підзапит з IN (список значень)
-- Клієнти, які робили замовлення в лютому
SELECT name, email
FROM customers
WHERE customer_id IN (
SELECT DISTINCT customer_id
FROM orders
WHERE order_date >= '2024-02-01'
AND order_date < '2024-03-01'
);
Є кілька сценаріїв, коли підзапит кращий за JOIN. Перш за все — коли потрібно порахувати агрегат для порівняння. Також коли потрібен список значень для фільтрації, або коли результат підзапиту — одне значення.
Запит 5: Знаходження дублікатів (HAVING)
Дублікати в базі — типова проблема. Користувач міг зареєструватися двічі або дані імпортувалися з помилками. HAVING допомагає їх знайти.
Приклад коду:
-- Знайти email'и, які зустрічаються більше ніж один раз
SELECT
email,
COUNT(*) as duplicate_count
FROM users
GROUP BY email
HAVING COUNT(*) > 1
ORDER BY duplicate_count DESC;
Результат:
email | duplicate_count
-------------------|----------------
user@mail.com | 3
test@gmail.com | 2
Щоб знайти всі рядки з дублікатами, можна зробити так:
SELECT u.*
FROM users u
WHERE u.email IN (
SELECT email
FROM users
GROUP BY email
HAVING COUNT(*) > 1
)
ORDER BY u.email, u.created_at;
Видалити ж їх можна кількома способами. З одного боку, можна залишити найновіший запис:
-- PostgreSQL
DELETE FROM users
WHERE user_id NOT IN (
SELECT MAX(user_id)
FROM users
GROUP BY email
);
З іншого боку, можна залишити й найстаріший:
DELETE FROM users
WHERE user_id NOT IN (
SELECT MIN(user_id)
FROM users
GROUP BY email
);
Втім, найбезпечніше буде використати window functions, щоби спершу перевірити, що саме буде видалено.
-- Перевірка
WITH duplicates AS (
SELECT
user_id,
email,
ROW_NUMBER() OVER (PARTITION BY email ORDER BY created_at DESC) as rn
FROM users
)
SELECT * FROM duplicates WHERE rn > 1;
-- Видалення
WITH duplicates AS (
SELECT
user_id,
ROW_NUMBER() OVER (PARTITION BY email ORDER BY created_at DESC) as rn
FROM users
)
DELETE FROM users
WHERE user_id IN (
SELECT user_id FROM duplicates WHERE rn > 1
);
Цей запит використовується в ситуаціях, коли потрібно:
- Очистити дані після імпорту
- Знайти помилки в реєстрації користувачів
- Виявити повторні транзакції
- Знайти товари з однаковими артикулами
Запит 6: Рейтинги й топи (RANK / ROW_NUMBER)
Window Functions (віконні функції) дозволяють присвоювати номери, ранги та обчислювати агрегати в межах груп без GROUP BY.
Приклад коду:
-- Топ 5 продуктів у кожній категорії
WITH ranked_products AS (
SELECT
category,
product_name,
total_sales,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY total_sales DESC) as rank
FROM products
)
SELECT *
FROM ranked_products
WHERE rank <= 5
ORDER BY category, rank;
Результат:
category | product_name | total_sales | rank
-------------|---------------|-------------|-----
Електроніка | item | 500000 | 1
Електроніка | item | 450000 | 2
Електроніка | item | 300000 | 3
Одяг | item | 120000 | 1
Одяг | item | 95000 | 2
Запит 7: Робота з датами (DATE functions)
Робота з датами — одна з найчастіших задач в аналітиці. Потрібно фільтрувати за періодами, порівнювати місяці, рахувати різницю в днях. SQL має потужний набір функцій для цього. Уявімо, що потрібно порівняти продажі цього місяця з минулим.
Основні функції для роботи з датами
CURRENT_DATE / NOW() — поточна дата/час:
SELECT CURRENT_DATE; -- 2024-03-02
SELECT NOW(); -- 2024-03-02 14:30:45
SELECT CURRENT_TIMESTAMP; -- те саме, що NOW()
EXTRACT — витягнути частину дати:
SELECT
order_date,
EXTRACT(YEAR FROM order_date) as year,
EXTRACT(MONTH FROM order_date) as month,
EXTRACT(DAY FROM order_date) as day,
EXTRACT(DOW FROM order_date) as day_of_week -- 0=неділя, 6=субота
FROM orders;
DATE_TRUNC — округлити дату (PostgreSQL):
SELECT
DATE_TRUNC('month', order_date) as month, -- 2024-02-01
DATE_TRUNC('week', order_date) as week, -- початок тижня
DATE_TRUNC('year', order_date) as year -- 2024-01-01
FROM orders;
INTERVAL — додавання/віднімання періодів:
-- Дати за останні 30 днів
SELECT * FROM orders
WHERE order_date >= CURRENT_DATE - INTERVAL '30 days';
-- Дати місяць тому
SELECT * FROM orders
WHERE order_date >= CURRENT_DATE - INTERVAL '1 month';
До прикладу, ось як можна порівняти продажі цього і минулого місяця:
WITH monthly_sales AS (
SELECT
DATE_TRUNC('month', order_date) as month,
SUM(amount) as total_sales
FROM orders
WHERE order_date >= CURRENT_DATE - INTERVAL '2 months'
GROUP BY DATE_TRUNC('month', order_date)
)
SELECT
TO_CHAR(month, 'YYYY-MM') as month_name,
total_sales,
LAG(total_sales) OVER (ORDER BY month) as previous_month_sales,
total_sales - LAG(total_sales) OVER (ORDER BY month) as difference,
ROUND(
(total_sales - LAG(total_sales) OVER (ORDER BY month)) * 100.0 /
LAG(total_sales) OVER (ORDER BY month),
2
) as growth_percent
FROM monthly_sales
ORDER BY month DESC;
В результаті отримуємо:
month_name | total_sales | previous_month_sales | difference | growth_percent
-----------|-------------|----------------------|------------|---------------
2024-03 | 450000 | 380000 | 70000 | 18.42
2024-02 | 380000 | NULL | NULL | NULL
Цей запит суперкорисний для:
- Фільтрації за періодами (останній тиждень, місяць, квартал)
- Групування за датами (продажі по днях/місяцях)
- Порівняння періодів (цей місяць vs минулий)
- Розрахунку термінів (час доставки, затримки)
- Аналізу трендів та сезонності
Запит 8: Умовна логіка (CASE WHEN)
CASE WHEN — це SQL-еквівалент конструкції if-else. Дозволяє створювати нові колонки з умовною логікою прямо в запиті. Наприклад, якщо потрібно категоризувати клієнтів за рівнем витрат.
Приклад коду:
-- Розподілити клієнтів за категоріями VIP/Regular/New
SELECT
customer_id,
name,
total_spent,
CASE
WHEN total_spent >= 10000 THEN 'VIP'
WHEN total_spent >= 1000 THEN 'Regular'
ELSE 'New'
END as customer_category
FROM customers
ORDER BY total_spent DESC;
Результат:
customer_id | name | total_spent | customer_category
------------|-----------|-------------|------------------
101 | User 1 | 25000 | VIP
102 | User 2 | 5000 | Regular
103 | User 3 | 500 | New
Використання в агрегації
-- Порахувати кількість клієнтів у кожній категорії
SELECT
CASE
WHEN total_spent >= 10000 THEN 'VIP'
WHEN total_spent >= 1000 THEN 'Regular'
ELSE 'New'
END as category,
COUNT(*) as customer_count,
SUM(total_spent) as total_revenue
FROM customers
GROUP BY category
ORDER BY total_revenue DESC;
CASE у WHERE (фільтрація)
-- Різні критерії для різних категорій
SELECT *
FROM products
WHERE
CASE
WHEN category = 'Електроніка' THEN price > 1000
WHEN category = 'Одяг' THEN price > 500
ELSE price > 100
END;
Заміна NULL-значень
-- Якщо discount NULL, показати 0
SELECT
product_name,
price,
CASE
WHEN discount IS NULL THEN 0
ELSE discount
END as discount,
-- або простіше через COALESCE:
COALESCE(discount, 0) as discount_safe
FROM products;
Створення індикаторів (0/1 flags)
-- Прапорці для різних умов
SELECT
order_id,
amount,
CASE WHEN amount > 1000 THEN 1 ELSE 0 END as is_large_order,
CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END as is_cancelled,
CASE WHEN delivery_date > promised_date THEN 1 ELSE 0 END as is_delayed
FROM orders;
Маніпуляції з умовною логікою використовують для:
- Сегментації клієнтів, продуктів, замовлень
- Створення категорій на основі числових значень
- Перетворення кодів у зрозумілі назви
- Обробки NULL-значень
- Умовних обчислень (різні формули для різних груп)