SQL для Data Engineer на собеседовании
Содержание:
Чем DE-SQL отличается от аналитического
Аналитический SQL — это про корректный ответ на бизнес-вопрос. DE-SQL — про производительность и инкрементальность пайплайна.
На собесе аналитика спрашивают: «Напиши retention за 30 дней». На собесе DE: «Напиши retention за 30 дней так, чтобы запрос работал на 10 миллиардах строк и пересчитывался ежечасно».
Главные акценты:
- Оптимизация и понимание плана запроса
- Партиционирование и его учёт в фильтрах
- Оконные функции с edge cases (RANGE BETWEEN, LAG с дефолтами)
- Транзакции и уровни изоляции при батчах
- Антипаттерны (cartesian, dependent subqueries, неэффективные JOINs)
EXPLAIN и план запроса
DE должен уметь читать план запроса. На собесе показывают EXPLAIN ANALYZE и просят найти узкое место.
Что искать:
- Seq Scan на больших таблицах — индекс пропущен
- Sort на большом объёме — попробовать индекс или денормализацию
- Hash Join vs Nested Loop — если выбран Nested Loop на больших таблицах, planner мог недооценить размер
- Estimated rows vs actual rows — большой разрыв означает устаревшую статистику (
ANALYZEне обновлён) - Buffers: shared hit vs shared read — много read = читаем с диска, проблемы с кешем
Типичный кейс — задача на оптимизацию: «Запрос работает 5 минут. Сократи до 30 секунд». Подход:
- EXPLAIN, найти долгие операции
- Убрать ненужные JOIN, упростить подзапросы
- Проверить индексы, добавить покрывающий
- Проверить partition pruning (есть ли фильтр по partition key)
- Если ничего не помогло — материализовать промежуточный результат
Оконные функции продвинутого уровня
Базовый ROW_NUMBER на собесе DE не спросят. Спросят:
Running totals с RANGE BETWEEN
SELECT
user_id,
event_date,
amount,
SUM(amount) OVER (
PARTITION BY user_id
ORDER BY event_date
RANGE BETWEEN INTERVAL '7 days' PRECEDING AND CURRENT ROW
) AS rolling_7d_sum
FROM events;Тонкость: ROWS считает строки, RANGE — значения. Если есть пропуски в датах — RANGE учтёт это, ROWS — нет.
Gap-fill в данных
«У вас данные с пропусками по дням. Заполни пропуски нулями.» — стандартная DE-задача:
WITH dates AS (
SELECT generate_series(
'2026-01-01'::DATE,
'2026-01-31'::DATE,
INTERVAL '1 day'
)::DATE AS day
),
events AS (
SELECT day, COUNT(*) AS cnt
FROM event_log
WHERE day BETWEEN '2026-01-01' AND '2026-01-31'
GROUP BY day
)
SELECT
d.day,
COALESCE(e.cnt, 0) AS cnt
FROM dates d
LEFT JOIN events e USING (day);Дедупликация с ROW_NUMBER в CTE
Оконные функции в WHERE не работают:
-- плохо
SELECT *
FROM users
WHERE ROW_NUMBER() OVER (PARTITION BY email ORDER BY created_at DESC) = 1;
-- хорошо
WITH ranked AS (
SELECT *, ROW_NUMBER() OVER (PARTITION BY email ORDER BY created_at DESC) AS rn
FROM users
)
SELECT * FROM ranked WHERE rn = 1;Партиционирование и pruning
DE проектирует таблицы с партиционированием. На собесе:
- По какому ключу партиционировать (обычно — дата события)
- Range vs List partitioning
- Что такое partition pruning и почему фильтры по partition key критичны
- Как партиционирование меняет план запроса
Типичная задача: «У нас events таблица 50 млрд строк, партиционирована по дате. Аналитики жалуются, что запросы медленные. Что проверить?»
Ответ: фильтр по partition key должен быть жёстким (event_date = '2026-05-01'), а не функциональным (DATE_TRUNC('day', event_date) = ...) — последнее ломает pruning.
Транзакции и изоляция
DE пишет батчи, которые работают долго и не должны сломать ничего по дороге. Уровни изоляции:
- Read Uncommitted — почти не используется в Postgres
- Read Committed — дефолт. Видит закоммиченные данные. Подвержен non-repeatable read
- Repeatable Read — снимок на старте транзакции. В Postgres — Snapshot Isolation
- Serializable — самый строгий, может откатывать транзакции при конфликтах
Типичный вопрос: «Ты запустил длинный батч на час. Параллельно идут UPDATE в той же таблице. Что произойдёт при Read Committed vs Repeatable Read?»
Ответ: при RC в разных запросах в рамках одной транзакции вы можете увидеть разные значения (non-repeatable read). При RR увидите снимок на момент старта.
Типичные задачи на собесе
1. Оптимизация медленного запроса
Дают EXPLAIN или сам запрос. Ищите Seq Scan на больших таблицах, лишние JOIN, неэффективные подзапросы.
2. Найти дубликаты и причины
«В таблице events есть дубликаты по (user_id, event_id). Найди их, посчитай долю, объясни откуда могут появиться.»
SELECT user_id, event_id, COUNT(*) AS dup_count
FROM events
GROUP BY user_id, event_id
HAVING COUNT(*) > 1;3. Инкрементальный пайплайн
«У вас events — append-only. Каждый час нужно дотягивать данные в агрегатную таблицу. Напиши SQL.»
INSERT INTO events_hourly_agg (hour, user_id, cnt)
SELECT
DATE_TRUNC('hour', event_time) AS hour,
user_id,
COUNT(*) AS cnt
FROM events
WHERE event_time >= (SELECT MAX(hour) FROM events_hourly_agg)
GROUP BY 1, 2
ON CONFLICT (hour, user_id) DO UPDATE
SET cnt = EXCLUDED.cnt;4. Slowly Changing Dimensions (SCD Type 2)
«У вас таблица customer с историей изменений. Напиши SQL, который для каждого клиента показывает версию, актуальную на 2026-01-15.»
SELECT *
FROM customer_history
WHERE valid_from <= '2026-01-15'
AND (valid_to > '2026-01-15' OR valid_to IS NULL);Частые ошибки
Использовать SELECT * в production-пайплайне. Изменение схемы исходной таблицы сломает downstream. Всегда явный список колонок.
Игнорировать NULL в JOIN. LEFT JOIN с WHERE на правой таблице — это INNER JOIN. Если хотите оставить NULL — фильтр в ON.
COUNT(DISTINCT) в оконке Postgres. Не поддерживается. Альтернатива — коррелированный подзапрос или CTE с предварительной агрегацией.
Integer division. 5 / 20 * 100 в Postgres = 0. Кастуйте в numeric: 5::NUMERIC / 20 * 100 = 25.
Не использовать NULLIF в делениях. Деление на ноль уронит весь батч. value / NULLIF(denom, 0) — стандарт.
FAQ
Какой диалект SQL учить для собеса DE?
Postgres — базовый. Если идёте в Big Data команду — добавить ClickHouse, Spark SQL, Trino. Hive — устаревает, но иногда требуется в legacy-командах.
Сколько SQL-задач прорешать перед собесом?
50–100 задач разного уровня. На Карьернике 500+ SQL-задач, отдельный фокус на DE-сложности.
Это официальная информация?
Нет. Статья основана на публичных источниках и опыте кандидатов.