Запросы от базовых SELECT до оконных функций. Каждый раздел — с реальными примерами кода.
Базовый синтаксис 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'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 NULLCOUNT, 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) считает уникальные значенияОконные функции позволяют считать нарастающий итог, ранжировать строки, брать предыдущее значение — без 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)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 — для иерархий (подчинённость, категории)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;