Собеседование Data Engineer: SQL, пайплайны и системный дизайн
Подготовка к собеседованию Data Engineer: проверяемые SQL-задачи на авиаданных, зерно витрины, ETL/ELT, SCD2, оркестрация и разбор сбоя загрузки.
Содержание статьи
На собеседовании Data Engineer обычно недостаточно написать корректный SELECT. Нужно показать, откуда берётся строка данных, почему загрузка не удвоит факты после повтора и как команда заметит сломанную витрину. Состав секций зависит от вакансии, поэтому ниже нет «вопросов конкретной компании». Есть авторский кейс с выполненными запросами на учебной базе «Авиаперевозки» и схема рассуждения, которую можно перенести на собственный проект.
Коротко: что подготовить к интервью дата-инженера
Возьмите одну систему, которую вы реально строили или можете собрать на учебных данных, и опишите путь от источника до потребителя: контракт входа, зерно таблиц, ключи, расписание, проверку качества, повторный запуск, восстановление после ошибки и владельца результата. На технической встрече это даёт больше материала, чем список знакомых инструментов без примера решения.
Повторите SQL на кратность JOIN, оконные функции, фильтрацию дат и план выполнения. Отдельно потренируйте разговор о моделировании: почему таблица фактов имеет именно такое зерно, как измерение меняется со временем, что хранится при исправлении старого события. Наконец, отрепетируйте короткий ответ на инцидент: как остановить плохую публикацию, найти диапазон повреждения, восстановить его и доказать, что теперь данные полны.
Как читать вакансию Data Engineer, прежде чем готовить стек
Название роли не гарантирует одинаковую работу. В одной команде инженер строит пакетные загрузки в хранилище, в другой поддерживает поток событий, в третьей отвечает за инфраструктуру и доступы. Прочитайте глаголы вакансии: «проектировать витрины», «поддерживать оркестрацию», «оптимизировать запросы», «разрабатывать стриминг» — это разные центры тяжести. Не делайте вывод о частоте Kafka или облака по одному названию позиции.
Спросите у рекрутера и будущего руководителя о размере и свежести данных, SLA витрин, праве инженера менять схему источника, способе деплоя и дежурствах. Если точная инфраструктура не названа, готовьте переносимые принципы и показывайте, где понадобятся знания конкретного движка. Опыт в DuckDB не является доказательством владения эксплуатацией распределённого кластера; он демонстрирует проверяемое мышление о данных.
Зерно данных: первая развилка любого решения
В учебной базе «Авиаперевозки» ticket_flights содержит 1 045 726 строк, 366 733 разных билетов и 22 226 разных рейсов. Одна строка здесь — участие одного билета в одном рейсе, а не «билет» и не «рейс». Если без уточнения назвать число строк числом пассажиров или рейсов, все дальнейшие показатели будут иметь неправильный знаменатель. Проверка сделана отдельным исполняемым скриптом по Parquet-срезу.
Спросите для каждой таблицы: какой бизнес-факт создаёт строку, какой ключ её однозначно определяет, возможны ли изменения после первой записи, как выглядит удаление или отмена. В boarding_passes связь с ticket_flights проверяется по составному ключу ticket_no и flight_id. Проверка по одному номеру билета не поймает ошибочный посадочный талон на другой рейс. Такая деталь отделяет модель предметной области от схемы, нарисованной для презентации.
SELECT count(*) AS legs,
count(DISTINCT ticket_no) AS tickets,
count(DISTINCT flight_id) AS flights
FROM ticket_flights;Шесть выполненных задач на авиаданных
Задачи ниже авторские и относятся к статическому учебному срезу, сдвинутому на август 2026 года в московском времени. Они не имитируют реальный стриминг или продуктивное расписание. Запустите npx tsx research/interviews-wave-3/verify.mts из apps/site: скрипт выполняет сами запросы, сверяет результаты утверждениями и падает при расхождении.
Первая задача — определить зерно ticket_flights: 1 045 726 строк, 366 733 билета, 22 226 рейсов. Вторая — найти сегменты билетов без рейса через LEFT JOIN: 0. Третья — найти посадочные талоны без соответствующей пары билет–рейс: 0. Нулевой результат не доказывает, что все бизнес-события попали в базу; он доказывает отсутствие нарушений именно этих двух ссылочных проверок в данном срезе.
Четвёртая задача — посчитать рейсы по датам планового вылета 5, 6 и 7 августа: 548, 550 и 493. Пятая — среди 548 рейсов с фактическим вылетом 5 августа найти вылетевшие позже плана более чем на 30 минут: 28. Оговорка существенна: запрос считает плановую когорту этого дня и исключает строки без фактического вылета; «28 задержанных рейсов во всей системе» из него не следует.
Шестая задача — проверить раздувание суммы после JOIN бронирований с билетами 5 августа. Есть 5 937 бронирований на 479 377 800 денежных единиц датасета. JOIN даёт 8 258 строк, а сумма повторённого b.total_amount — 795 323 800. Это не рост выручки, а ошибка зерна. Для витрины бронирований оставьте один факт на book_ref, а показатели билетов агрегируйте до book_ref до соединения. Не лечите сумму SUM(DISTINCT total_amount): одинаковая стоимость разных бронирований будет потеряна.
| Проверка | Запрос / ключ | Результат | Что доказывает и чего не доказывает |
|---|---|---|---|
| Зерно сегмента | COUNT(*) и DISTINCT в ticket_flights | 1 045 726 / 366 733 / 22 226 | Строка ≠ билет ≠ рейс |
| Сегмент без рейса | LEFT JOIN по flight_id | 0 | Нет этих сирот, не проверена полнота источника |
| Талон без сегмента | LEFT JOIN по ticket_no + flight_id | 0 | Нет этих сирот, не проверен статус посадки |
| Дневной объём | Полуоткрытые интервалы по scheduled_departure | 548 / 550 / 493 | Контроль трёх плановых дат |
| Поздний вылет | actual_departure > scheduled + 30 min | 28 из 548 | Порог и фильтр заданы явно |
| Размножение суммы | bookings JOIN tickets vs bookings | 795 323 800 vs 479 377 800 | Сумма повторилась из-за кратности |
SQL-решения: целостность, объём и раздутая сумма
Запустите запросы по одному. Первые два ищут строки без родителя: сегмент без рейса и посадочный талон без пары билет–рейс. Для второго нужен составной ключ: билет сам по себе может содержать несколько перелётов. Третий запрос фиксирует три соседних дня планового вылета. Нулевые сироты и похожие дневные объёмы не доказывают, что источник прислал все файлы: для этого нужен отдельный контроль полноты.
Четвёртый запрос задаёт строгий порог опоздания и исключает рейсы без фактического вылета. Последние два сравнивают исходную сумму бронирований с суммой после JOIN с билетами. Если сложить b.total_amount после соединения, каждая бронь с несколькими билетами повторится; нельзя исправлять это SUM(DISTINCT total_amount), поскольку разные брони могут стоить одинаково. Для витрины сначала агрегируйте признаки билетов до book_ref, затем соединяйте. Все шесть запросов выполняются в проверочном скрипте.
SELECT count(*) AS orphan_legs
FROM ticket_flights tf LEFT JOIN flights f ON f.flight_id = tf.flight_id
WHERE f.flight_id IS NULL;
SELECT count(*) AS orphan_passes
FROM boarding_passes bp LEFT JOIN ticket_flights tf
ON tf.ticket_no = bp.ticket_no AND tf.flight_id = bp.flight_id
WHERE tf.ticket_no IS NULL;
SELECT CAST(scheduled_departure AS DATE) AS day, count(*) AS flights
FROM flights
WHERE scheduled_departure >= TIMESTAMP '2026-08-05 00:00:00'
AND scheduled_departure < TIMESTAMP '2026-08-08 00:00:00'
GROUP BY 1 ORDER BY 1;
SELECT count(*) AS departed,
count(*) FILTER (WHERE actual_departure > scheduled_departure + INTERVAL '30 minutes') AS later_than_30m
FROM flights
WHERE scheduled_departure >= TIMESTAMP '2026-08-05 00:00:00'
AND scheduled_departure < TIMESTAMP '2026-08-06 00:00:00'
AND actual_departure IS NOT NULL;
SELECT count(*) AS bookings, sum(total_amount) AS booking_amount
FROM bookings
WHERE book_date >= TIMESTAMP '2026-08-05 00:00:00'
AND book_date < TIMESTAMP '2026-08-06 00:00:00';
SELECT count(*) AS joined_rows, count(DISTINCT b.book_ref) AS bookings,
sum(b.total_amount) AS duplicated_sum
FROM bookings b JOIN tickets t ON t.book_ref = b.book_ref
WHERE b.book_date >= TIMESTAMP '2026-08-05 00:00:00'
AND b.book_date < TIMESTAMP '2026-08-06 00:00:00';SQL глубже агрегата: окна, план и индексы
Оконная функция нужна, когда вы сохраняете строки и одновременно вычисляете показатель внутри группы: например, выбрать последнюю версию записи по ROW_NUMBER() OVER (PARTITION BY business_key ORDER BY updated_at DESC, ingestion_id DESC). Второй критерий порядка обязателен, если updated_at может совпасть: иначе выбор может зависеть от плана и порядка чтения. Но «последняя запись» ещё не означает «актуальная истина», если события приходят задним числом или версия отзывается.
На вопрос об ускорении запроса не отвечайте «добавлю индекс» до измерения. Документация PostgreSQL об EXPLAIN показывает, как смотреть план, оценки и фактическое выполнение. Сначала уточните движок, объём, селективность фильтров, физическое расположение данных и конкурирующие нагрузки. Индексы ускоряют некоторые способы доступа, но увеличивают стоимость записи и не заменяют корректное зерно; для колоночного аналитического движка меры могут быть другими.
Тренировочный вопрос: запрос к фактам за семь дней сканирует всю историю. Сильный ответ — проверить тип и форму фильтра даты, партиционирование или кластеризацию, статистики и план, а затем сравнить стоимость до и после изменения. Слабый — обещать универсальное ускорение без плана и без знания движка. Для собеседования принесите один собственный пример с исходным запросом, планом, изменением и измеренным эффектом.
SELECT *
FROM (
SELECT e.*,
row_number() OVER (
PARTITION BY business_key
ORDER BY updated_at DESC, ingestion_id DESC
) AS version_rank
FROM source_events e
) ranked
WHERE version_rank = 1;Модель хранилища: звезда и история изменений
Руководство Microsoft по star schema различает таблицы фактов с измеримыми событиями и измерения, описывающие контекст. Для авиаданных можно проектировать факт сегмента билета и измерения рейса, маршрута, календаря; но сначала решить, какую именно аналитику должна обслуживать витрина. Если одни данные нужны в разрезе билета, а другие в разрезе бронирования, их слепое объединение в одну широкую таблицу создаст кратность и спорные агрегаты.
SCD type 2 хранит историю версий атрибутов измерения: прежняя строка закрывается, новая открывается с новым техническим ключом и интервалом действия. Документация Microsoft показывает этот приём. На интервью объясните, по какой дате связываете факт с версией измерения и что сделаете с запоздавшим исправлением. Не обещайте SCD2 для каждого поля: для технического исправления опечатки иногда нужен другой режим, согласованный с бизнесом.
Пример вопроса: аэропорт сменил отображаемое название, а отчёты за прошлый год должны сохранять историческую подпись. Уточните, когда изменение вступило в силу, какой справочник источник истины и пересчитываются ли старые факты. Если аналитика должна показывать только нынешнее название, другая модель может быть проще. Критерий выбора — требование к историческому ответу, а не любовь к аббревиатуре SCD.
ETL и ELT: граница преобразования должна быть видна
В ETL данные извлекают, преобразуют и затем загружают в целевое хранилище; в ELT сначала загружают сырой слой, затем преобразуют внутри целевой системы. В обоих случаях остаются вопросы: где фиксируется версия схемы, как обнаруживается частичная партия, можно ли переиграть источник, какая таблица доступна потребителю во время обработки. Название подхода не решает их автоматически.
Для учебной авиационной витрины я бы разделил сырой снимок, очищенные сущности и опубликованную витрину. В сыром слое сохранил бы исходный идентификатор и время получения. В очищенном — проверил ключи, типы и временные зоны. В опубликованном — атомарно заменил бы партицию после контрольных проверок, чтобы потребитель не увидел полупустой день. Это проектное упражнение; статический Parquet-срез не даёт основания утверждать, что такой пайплайн в тренажёре уже работает.
Оркестрация, повторы и backfill
Airflow описывает DAG как модель задач и зависимостей, а в рекомендациях по качеству DAG отдельно рассматриваются повторяемость и поведение задач при запуске. На интервью изобразите не набор названий операторов, а причинный граф: получили источник → проверили контракт → подготовили партицию → проверили качество → опубликовали → отправили сигнал. Зависимость нужна там, где следующий шаг не может быть корректным без результата предыдущего.
Если задача за 5 августа упала после загрузки половины рейсов, повторный запуск не должен добавить вторую половину к первой как новые дубликаты. Подходы: транзакционная замена партиции, запись во временную таблицу с атомарной публикацией, upsert по устойчивому ключу. Выбор зависит от хранилища и частоты изменений. Backfill старой даты выполняйте как отдельную управляемую операцию: перечислите затронутые партиции, зависимости и проверки, не полагайтесь на «запустить DAG ещё раз».
Идемпотентность не равна «просто удалить дубликаты после загрузки». Если исходная строка исправлена и пришла с тем же бизнес-ключом, нужно правило версий; если удалена, нужен tombstone или иной сигнал; если источник недоступен, повтор должен остановиться с понятной ошибкой, а не опубликовать нулевой день. Назовите эти ветки даже тогда, когда конкретная платформа вам незнакома.
Качество данных: проверка должна соответствовать обещанию
В учебном срезе проверка ticket_flights→flights дала 0 сирот, а boarding_passes→ticket_flights по составному ключу тоже дала 0. Это хороший старт для ссылочной целостности, но он ничего не говорит о том, все ли рейсы поступили вовремя. Для полноты нужен внешний ожидаемый объём или контракт с источником. Три наблюдаемых дневных объёма 548, 550 и 493 помогают заметить скачок, но сами по себе не доказывают, что третий день полон: сезонность и расписание могут меняться.
Составьте матрицу «ошибка → проверка → действие». Неправильный ключ: блокировать публикацию и показать пример строки. Изменение схемы: сравнить контракт, согласовать миграцию. Сильное падение объёма: предупредить владельца и проверить источник, иногда задержать публикацию. Нарушение SLA свежести: показать последнюю успешную дату прямо в потребительском отчёте. Важна разница между тестом данных и решением об остановке: не каждое предупреждение должно валить загрузку, но у каждого должен быть ответственный.
Кейс системного дизайна: ежедневная витрина пунктуальности
Условие авторского кейса: авиакомпания хочет к 08:00 видеть долю рейсов предыдущего дня, вылетевших позже расписания более чем на 30 минут. Вход — ежедневная выгрузка рейсов, возможны поздние исправления фактического времени, отчёт строится по локальной дате планового вылета. Сначала уточните временную зону аэропортов и в какой зоне определён «предыдущий день»; учебный срез использует московское время, что не является универсальным правилом авиакомпаний. Уточните, входит ли отменённый и ещё не вылетевший рейс в знаменатель.
Проект решения: хранить исходные события с ключом flight_id, временем события и загрузки; после дедупликации строить дневной факт с состоянием рейса. Для метрики выбрать плановую когорту и явное правило: например, только рейсы с фактическим вылетом. В тестовом срезе для 5 августа таких 548, из них 28 позже порога; доля 28/548 ≈ 5,11%. Это арифметика учебного набора, не операционная метрика компании. При другом правиле знаменатель меняется.
После каждой загрузки проверьте уникальность flight_id, непустые даты, допустимость статусов, диапазон объёма и покрытие источника. Сохраняйте версию расчёта и дату актуальности. Если позднее исправление меняет факт 5 августа, пересчитайте зависимую партицию и оставьте след: какая версия опубликована, кто её потребил и когда обновился дашборд. При падении проверки удержите прошлую опубликованную версию с заметным статусом, а не выдавайте старый день за свежий.
| Решение | Уточнение перед реализацией | Проверка приёмки |
|---|---|---|
| Зерно | Один рейс или один сегмент билета? | Уникальный flight_id в рейсовой витрине |
| День | Локальная зона и дата плана? | Граница полуоткрытого интервала |
| Знаменатель | Отменённые и без actual_departure? | Сверка категорий статуса |
| Публикация | Можно ли показывать неполный день? | Атомарная замена после проверок |
| Исправление | Как долго обновляется прошлое? | Backfill затронутой даты без дублей |
| Наблюдение | Кто получает сигнал о срыве SLA? | Статус и время последней успешной версии |
Двенадцать вопросов: слабый и сильный ответ
Это авторская таблица для самопроверки, а не сборник вопросов какого-либо работодателя. Произнесите сильный ответ своими словами и подкрепите одним из выполненных запросов или схемой собственного пайплайна. Если не можете назвать конкретный ключ, дату или последствие сбоя, ответ пока слишком общий.
| Вопрос | Проверяют | Слабый ответ | Сильный ответ | Частая ошибка |
|---|---|---|---|---|
| Что является строкой факта? | Зерно | Что-то про рейс | Один flight_id; сегменты билета отдельно | Смешать рейс и билет |
| Почему выросла сумма после JOIN? | Кратность | Удалю дубли | Сверю ключи и агрегирую билет до брони | SUM(DISTINCT сумма) |
| Как найти сироты? | Ключи | Посмотрю NULL | LEFT JOIN по полному составному ключу | Проверить лишь ticket_no |
| Как выбрать версию записи? | Окно | Возьму MAX даты | ROW_NUMBER с устойчивым tie-breaker | Недетерминированный порядок |
| Зачем EXPLAIN? | Производительность | Чтобы был индекс | Сравнить план и реальные узкие места | Оптимизация вслепую |
| Когда SCD2? | История | Всегда | Когда нужен ответ на дату факта | Хранить лишние версии |
| Что делать с поздним событием? | Пересчёт | Игнорировать | Правило окна и backfill партиции | Тихая несогласованность |
| Как повторить загрузку? | Идемпотентность | Запущу снова | Ключ, staging, атомарная публикация | Удвоить строки |
| Что проверять перед публикацией? | Качество | Количество > 0 | Ключи, объём, схема, свежесть | Нулевые сироты = всё верно |
| Что при падении источника? | Восстановление | Поставлю retry | Лимит повтора, сигнал, прежняя версия | Показать старое как новое |
| Как выбрать ETL или ELT? | Архитектура | ELT моднее | Свойства источника и движка, граница преобразования | Ответ названием продукта |
| Как доказать SLA? | Операционная зрелость | У нас мониторинг | Метка публикации, порог, владелец, действие | Путать успех DAG со свежестью данных |
Ошибки кандидата и цена каждой
Первая ошибка — говорить о таблице по имени, не определив её зерно: отсюда раздутая выручка и неверный счёт клиентов. Вторая — считать нулевые сироты доказательством полной загрузки: источник мог не прислать целую партицию. Третья — обещать «ровно один раз» без ключа и сценария повтора: повтор аварийного шага удвоит данные. Четвёртая — выбирать индекс до плана и измерения: можно ускорить не тот запрос и ухудшить запись.
Пятая — рисовать DAG без владельца результата и проверки публикации: граф завершится успешно, а пользователь получит старые данные. Шестая — отвечать «SCD2» на любое изменение справочника без вопроса об истории: вы увеличите сложность и не решите задачу. Седьмая — не отличать event time от ingestion time: запоздавшие события окажутся не в том дне. Восьмая — считать учебную проверку на статическом срезе подтверждением надёжности реальной эксплуатации. На интервью лучше признать, что осталось непроверенным, и назвать проверку.
План подготовки на 14 дней
Дни 1–2: выпишите зерно пяти таблиц из знакомого проекта или учебной базы, ключи и изменение во времени. Дни 3–4: решите шесть задач выше и повторите JOIN после самостоятельного объяснения кратности. День 5: напишите окно выбора версии с устойчивым порядком и проверьте случай одинакового времени обновления. День 6: возьмите тяжёлый запрос, получите план в вашем движке и сформулируйте одну измеряемую гипотезу ускорения. День 7: нарисуйте факт и измерения для одного отчёта, объяснив, почему не объединили разные зерна.
Дни 8–9: спроектируйте загрузку одной даты, повтор после падения и backfill прошлого дня. День 10: подготовьте матрицу качества с действиями при нарушении. День 11: проговорите кейс пунктуальности с уточнениями о знаменателе и времени. День 12: напишите историю собственного инцидента — что сломалось, как нашли, кто видел ущерб, что изменили. День 13: ответьте на двенадцать вопросов без подсказки и отметьте темы, где не хватает примера. День 14: узнайте у компании реальный стек и адаптируйте ответы к её задаче, не выдавая учебный проект за промышленный опыт.
Частые вопросы и следующий шаг
Нужно ли знать конкретный оркестратор? Если он указан в вакансии — да, хотя бы на уровне задач, расписания, повторов и наблюдаемости; один DAG не заменяет понимания данных. Чем дата-инженер отличается от аналитика данных? Инженер отвечает за надёжную поставку и модель данных, аналитик — за интерпретацию и решение; границы в командах различаются. Достаточно ли SQL? Для учебного входа он необходим, но в вакансии могут быть программирование, инфраструктура и эксплуатация. Спросят ли системный дизайн у новичка? Зависит от команды; даже начальный ответ должен назвать зерно, ключ, сбой и контроль.
Сделайте один воспроизводимый артефакт: SQL-скрипт на доступной базе, схема витрины и короткая записка «как она переживёт повторный запуск и позднее исправление». На следующем собеседовании это позволит обсуждать инженерное решение, а не коллекцию названий технологий. Для базовой практики запросов можно пройти тренажёр SQL с нуля, затем вернуться к задаче о кратности и доказать её на собственных данных.
Материалы по теме

ETL: что это простыми словами, этапы и чем ETL отличается от ELT
ETL — процесс, который забирает данные из источников, приводит их в порядок и загружает в хранилище. Этапы extract, transform, load на примере продукта, разница ETL и ELT, инкрементальная загрузка, идемпотентность и SQL-проверки качества данных.

DWH, витрина данных и data lake: что это и чем они отличаются
DWH — хранилище данных для анализа, витрина — готовая таблица под конкретную задачу, data lake — хранилище сырых файлов. Чем DWH отличается от базы приложения, из каких слоёв состоит, как выглядит витрина на SQL и почему в двух витринах бывают разные цифры.

OLAP и OLTP: в чём разница простыми словами
OLTP — системы, которые обслуживают работу продукта мелкими транзакциями, OLAP — системы для тяжёлых аналитических запросов. Чем они отличаются по запросам, схеме и хранению, почему отчёты не строят на рабочей базе и что такое OLAP-куб.