Перейти к основному содержимому

SQL для системного аналитика

SQL — обязательный навык для работы с данными и проектирования баз данных.

Основы SQL

SELECT — Выборка данных

-- Простая выборка
SELECT * FROM users;

-- Выборка конкретных колонок
SELECT id, email, created_at FROM users;

-- С условием
SELECT * FROM users WHERE status = 'active';

-- Сортировка
SELECT * FROM users ORDER BY created_at DESC;

-- Ограничение количества
SELECT * FROM users LIMIT 10;

WHERE — Условия

-- Равенство
SELECT * FROM orders WHERE status = 'completed';

-- Неравенство
SELECT * FROM products WHERE price > 1000;

-- Диапазон
SELECT * FROM orders WHERE total BETWEEN 100 AND 500;

-- Список значений
SELECT * FROM users WHERE role IN ('admin', 'manager');

-- Поиск по шаблону
SELECT * FROM users WHERE email LIKE '%@gmail.com';

-- NULL значения
SELECT * FROM users WHERE phone IS NULL;

-- Комбинация условий
SELECT * FROM orders
WHERE status = 'pending'
AND created_at > '2026-01-01'
AND total > 100;

JOIN — Объединение таблиц

-- INNER JOIN (только совпадающие записи)
SELECT
orders.id,
orders.total,
users.email
FROM orders
INNER JOIN users ON orders.user_id = users.id;

-- LEFT JOIN (все из левой таблицы)
SELECT
users.email,
COUNT(orders.id) as orders_count
FROM users
LEFT JOIN orders ON users.id = orders.user_id
GROUP BY users.id, users.email;

-- Множественные JOIN
SELECT
orders.id,
users.email,
products.name,
order_items.quantity
FROM orders
INNER JOIN users ON orders.user_id = users.id
INNER JOIN order_items ON orders.id = order_items.order_id
INNER JOIN products ON order_items.product_id = products.id;

GROUP BY — Агрегация

-- Подсчет
SELECT status, COUNT(*) as count
FROM orders
GROUP BY status;

-- Сумма
SELECT user_id, SUM(total) as total_spent
FROM orders
GROUP BY user_id;

-- Среднее
SELECT category, AVG(price) as avg_price
FROM products
GROUP BY category;

-- Минимум и максимум
SELECT
category,
MIN(price) as min_price,
MAX(price) as max_price
FROM products
GROUP BY category;

-- HAVING (фильтр после группировки)
SELECT user_id, COUNT(*) as orders_count
FROM orders
GROUP BY user_id
HAVING COUNT(*) > 5;

Подзапросы

-- В WHERE
SELECT * FROM users
WHERE id IN (
SELECT DISTINCT user_id
FROM orders
WHERE total > 1000
);

-- В SELECT
SELECT
users.email,
(SELECT COUNT(*) FROM orders WHERE orders.user_id = users.id) as orders_count
FROM users;

-- В FROM
SELECT avg_total
FROM (
SELECT user_id, AVG(total) as avg_total
FROM orders
GROUP BY user_id
) as user_averages
WHERE avg_total > 500;

Практические запросы для аналитика

1. Анализ активности пользователей

-- Пользователи с количеством заказов
SELECT
u.id,
u.email,
u.created_at as registration_date,
COUNT(o.id) as orders_count,
SUM(o.total) as total_spent,
MAX(o.created_at) as last_order_date
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.email, u.created_at
ORDER BY total_spent DESC;

2. Когортный анализ

-- Пользователи по месяцам регистрации
SELECT
DATE_TRUNC('month', created_at) as cohort_month,
COUNT(*) as users_count
FROM users
GROUP BY cohort_month
ORDER BY cohort_month;

-- Retention по когортам
SELECT
DATE_TRUNC('month', u.created_at) as cohort,
DATE_TRUNC('month', o.created_at) as order_month,
COUNT(DISTINCT o.user_id) as active_users
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY cohort, order_month
ORDER BY cohort, order_month;

3. Воронка продаж

SELECT
'Посетители' as stage,
COUNT(DISTINCT user_id) as count
FROM page_views
WHERE page = 'product'

UNION ALL

SELECT
'Добавили в корзину',
COUNT(DISTINCT user_id)
FROM cart_items

UNION ALL

SELECT
'Начали оформление',
COUNT(DISTINCT user_id)
FROM orders
WHERE status != 'cart'

UNION ALL

SELECT
'Завершили покупку',
COUNT(DISTINCT user_id)
FROM orders
WHERE status = 'completed';

4. RFM анализ

SELECT
user_id,
-- Recency (дней с последнего заказа)
CURRENT_DATE - MAX(created_at::date) as recency,
-- Frequency (количество заказов)
COUNT(*) as frequency,
-- Monetary (общая сумма)
SUM(total) as monetary
FROM orders
WHERE status = 'completed'
GROUP BY user_id;

5. Топ товаров

SELECT
p.id,
p.name,
p.category,
COUNT(oi.id) as times_ordered,
SUM(oi.quantity) as total_quantity,
SUM(oi.quantity * oi.price) as total_revenue
FROM products p
INNER JOIN order_items oi ON p.id = oi.product_id
INNER JOIN orders o ON oi.order_id = o.id
WHERE o.status = 'completed'
AND o.created_at >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY p.id, p.name, p.category
ORDER BY total_revenue DESC
LIMIT 10;

Оконные функции (Window Functions)

-- Ранжирование
SELECT
name,
category,
price,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY price DESC) as rank
FROM products;

-- Накопительная сумма
SELECT
date,
revenue,
SUM(revenue) OVER (ORDER BY date) as cumulative_revenue
FROM daily_sales;

-- Скользящее среднее (за 7 дней)
SELECT
date,
revenue,
AVG(revenue) OVER (
ORDER BY date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) as moving_avg_7d
FROM daily_sales;

-- Сравнение с предыдущим периодом
SELECT
date,
revenue,
LAG(revenue, 1) OVER (ORDER BY date) as prev_day_revenue,
revenue - LAG(revenue, 1) OVER (ORDER BY date) as diff
FROM daily_sales;

CTE (Common Table Expressions)

-- Более читаемый код
WITH active_users AS (
SELECT user_id
FROM orders
WHERE created_at >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY user_id
HAVING COUNT(*) >= 3
),
user_stats AS (
SELECT
u.id,
u.email,
COUNT(o.id) as orders_count,
SUM(o.total) as total_spent
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE u.id IN (SELECT user_id FROM active_users)
GROUP BY u.id, u.email
)
SELECT * FROM user_stats
ORDER BY total_spent DESC;

Проектирование баз данных

Нормализация

1NF (Первая нормальная форма)

  • Атомарные значения (не массивы)
  • Уникальные строки
  • Первичный ключ

Плохо:

users
id | name | phones
1 | Ivan | +7123, +7456

Хорошо:

users
id | name
1 | Ivan

user_phones
id | user_id | phone
1 | 1 | +7123
2 | 1 | +7456

2NF (Вторая нормальная форма)

  • Соответствует 1NF
  • Нет частичных зависимостей от составного ключа

3NF (Третья нормальная форма)

  • Соответствует 2NF
  • Нет транзитивных зависимостей

Типы связей

One-to-Many (1:M)

-- Один пользователь — много заказов
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL
);

CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
total DECIMAL(10,2)
);

Many-to-Many (M:N)

-- Много товаров в много заказов
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(255)
);

CREATE TABLE orders (
id SERIAL PRIMARY KEY,
created_at TIMESTAMP
);

-- Связующая таблица
CREATE TABLE order_items (
id SERIAL PRIMARY KEY,
order_id INTEGER REFERENCES orders(id),
product_id INTEGER REFERENCES products(id),
quantity INTEGER,
price DECIMAL(10,2)
);

One-to-One (1:1)

-- Один пользователь — один профиль
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255)
);

CREATE TABLE user_profiles (
user_id INTEGER PRIMARY KEY REFERENCES users(id),
first_name VARCHAR(100),
last_name VARCHAR(100),
bio TEXT
);

Индексы

-- Простой индекс
CREATE INDEX idx_users_email ON users(email);

-- Составной индекс
CREATE INDEX idx_orders_user_status ON orders(user_id, status);

-- Уникальный индекс
CREATE UNIQUE INDEX idx_users_email_unique ON users(email);

-- Частичный индекс
CREATE INDEX idx_orders_pending ON orders(created_at)
WHERE status = 'pending';

Когда создавать индексы:

  • Колонки в WHERE
  • Колонки в JOIN
  • Колонки в ORDER BY
  • Foreign keys

Когда НЕ создавать:

  • Маленькие таблицы (< 1000 строк)
  • Колонки с низкой селективностью (пол, булевы)
  • Часто обновляемые колонки

Оптимизация запросов

EXPLAIN — Анализ плана выполнения

EXPLAIN ANALYZE
SELECT * FROM orders
WHERE user_id = 123
AND status = 'completed';

Что смотреть:

  • Seq Scan — полное сканирование (плохо для больших таблиц)
  • Index Scan — использование индекса (хорошо)
  • Cost — оценка стоимости
  • Rows — количество строк

Типичные проблемы

1. N+1 запросов

Плохо:

-- В коде: для каждого пользователя отдельный запрос
SELECT * FROM users;
-- Затем для каждого:
SELECT * FROM orders WHERE user_id = ?;

Хорошо:

-- Один запрос с JOIN
SELECT
u.*,
o.*
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;

2. SELECT *

Плохо:

SELECT * FROM users; -- Получаем все колонки

Хорошо:

SELECT id, email, name FROM users; -- Только нужные

3. Отсутствие LIMIT

Плохо:

SELECT * FROM orders; -- Может вернуть миллионы строк

Хорошо:

SELECT * FROM orders LIMIT 100;

Транзакции

BEGIN;

-- Перевод денег между счетами
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

-- Если все ок
COMMIT;

-- Если ошибка
ROLLBACK;

ACID свойства:

  • Atomicity — все или ничего
  • Consistency — данные остаются консистентными
  • Isolation — транзакции не мешают друг другу
  • Durability — результат сохраняется

Практические задачи

Задача 1: Найти пользователей без заказов

SELECT u.id, u.email
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL;

Задача 2: Второй по величине заказ

SELECT total
FROM orders
ORDER BY total DESC
LIMIT 1 OFFSET 1;

Задача 3: Дубликаты email

SELECT email, COUNT(*) as count
FROM users
GROUP BY email
HAVING COUNT(*) > 1;

Задача 4: Пользователи с ростом заказов

WITH monthly_orders AS (
SELECT
user_id,
DATE_TRUNC('month', created_at) as month,
COUNT(*) as orders_count
FROM orders
GROUP BY user_id, month
)
SELECT
user_id,
month,
orders_count,
LAG(orders_count) OVER (PARTITION BY user_id ORDER BY month) as prev_month
FROM monthly_orders
WHERE orders_count > LAG(orders_count) OVER (PARTITION BY user_id ORDER BY month);

Инструменты

  • DBeaver — универсальный SQL клиент
  • pgAdmin — для PostgreSQL
  • MySQL Workbench — для MySQL
  • DataGrip — от JetBrains (платный)
  • TablePlus — современный, красивый

Полезные ресурсы

Чек-лист навыков SQL для аналитика

  • SELECT, WHERE, ORDER BY, LIMIT
  • JOIN (INNER, LEFT, RIGHT)
  • GROUP BY, HAVING
  • Агрегатные функции (COUNT, SUM, AVG, MIN, MAX)
  • Подзапросы
  • UNION, UNION ALL
  • CASE WHEN
  • Работа с датами (DATE_TRUNC, INTERVAL)
  • Оконные функции (ROW_NUMBER, RANK, LAG, LEAD)
  • CTE (WITH)
  • EXPLAIN для анализа запросов
  • Понимание индексов
  • Проектирование таблиц (нормализация)
  • Типы связей (1:M, M:N, 1:1)