SQL для аналитика

Запросы от базовых SELECT до оконных функций. Каждый раздел — с реальными примерами кода.

SELECT, WHERE, ORDER BY — основы синтаксиса

~8 мин

Базовый синтаксис SQL-запроса. Как фильтровать строки, сортировать результат и выбирать только нужные столбцы.

SELECT user_id, email, created_at
FROM users
WHERE status = 'active'
  AND created_at >= '2024-01-01'
ORDER BY created_at DESC
LIMIT 100;

-- DISTINCT убирает дубли
SELECT DISTINCT city FROM users;

-- BETWEEN, IN, LIKE
WHERE age BETWEEN 18 AND 35
WHERE country IN ('KZ', 'RU', 'BY')
WHERE email LIKE '%@gmail.com'

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

~10 мин

INNER, LEFT, RIGHT, FULL JOIN — когда какой использовать. Частые ошибки с дублированием строк.

-- INNER JOIN: только совпадающие строки
SELECT u.email, o.amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id;

-- LEFT JOIN: все из левой + совпадения из правой
-- NULL там, где нет пары (полезно найти «без заказов»)
SELECT u.email, COUNT(o.id) AS orders_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.email;

-- Найти пользователей БЕЗ заказов:
WHERE o.id IS NULL

GROUP BY и агрегатные функции

~7 мин

COUNT, SUM, AVG, MIN, MAX. Разница между WHERE и HAVING. Группировка по нескольким столбцам.

SELECT
  DATE_TRUNC('month', created_at) AS month,
  country,
  COUNT(*)           AS orders,
  SUM(amount)        AS revenue,
  AVG(amount)        AS avg_check,
  MAX(amount)        AS max_check
FROM orders
WHERE status = 'paid'
GROUP BY 1, 2
HAVING SUM(amount) > 100000
ORDER BY 1 DESC;

-- COUNT(*) считает все строки
-- COUNT(col) игнорирует NULL
-- COUNT(DISTINCT col) считает уникальные значения

Оконные функции — ROW_NUMBER, RANK, LAG

~12 мин

Оконные функции позволяют считать нарастающий итог, ранжировать строки, брать предыдущее значение — без GROUP BY.

-- ROW_NUMBER: порядковый номер в группе
SELECT
  user_id,
  order_date,
  amount,
  ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_date) AS order_num
FROM orders;

-- Первый заказ каждого пользователя:
WITH ranked AS (
  SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_date) AS rn
  FROM orders
)
SELECT * FROM ranked WHERE rn = 1;

-- LAG / LEAD — предыдущее/следующее значение
SELECT date, revenue,
  LAG(revenue, 1) OVER (ORDER BY date) AS prev_revenue,
  revenue - LAG(revenue, 1) OVER (ORDER BY date) AS delta
FROM daily_stats;

-- Нарастающий итог (running total):
SUM(amount) OVER (PARTITION BY user_id ORDER BY order_date)

CTE (WITH) и подзапросы

~9 мин

Common Table Expressions делают сложные запросы читаемыми. Когда использовать CTE, а когда — подзапрос.

-- CTE: именованный подзапрос
WITH active_users AS (
  SELECT user_id, COUNT(*) AS sessions
  FROM sessions
  WHERE date >= CURRENT_DATE - INTERVAL '30 days'
  GROUP BY user_id
  HAVING COUNT(*) >= 3
),
user_revenue AS (
  SELECT user_id, SUM(amount) AS ltv
  FROM orders
  WHERE status = 'paid'
  GROUP BY user_id
)
SELECT
  a.user_id,
  a.sessions,
  COALESCE(r.ltv, 0) AS ltv
FROM active_users a
LEFT JOIN user_revenue r USING (user_id)
ORDER BY ltv DESC;

-- Рекурсивные CTE — для иерархий (подчинённость, категории)

Работа с датами и временем

~8 мин

DATE_TRUNC, EXTRACT, INTERVAL, DATEDIFF — типичные операции с датами, которые встречаются на каждом собесе.

-- Усечение до периода
DATE_TRUNC('week', created_at)   -- начало недели
DATE_TRUNC('month', created_at)  -- начало месяца

-- Извлечение части даты
EXTRACT(DOW FROM created_at)   -- 0=воскресенье, 6=суббота
EXTRACT(HOUR FROM created_at)  -- час (0-23)

-- Арифметика дат
created_at + INTERVAL '7 days'
CURRENT_DATE - INTERVAL '30 days'

-- Разница между датами
AGE(returned_at, created_at)   -- PostgreSQL
DATEDIFF(day, start, end)      -- SQL Server / Snowflake

-- Часто на собесе: retention D1/D7/D30
SELECT
  u.user_id,
  MIN(s.date) AS first_session,
  COUNT(DISTINCT CASE WHEN s.date = MIN(s.date) + 1 THEN s.date END) > 0 AS d1_retained
FROM users u
LEFT JOIN sessions s USING (user_id)
GROUP BY u.user_id;