09:00. Витрина за вчера пустая, за позавчера удвоена
Чему научишься
- читать витрину как показания приборов: где день пуст, а где посчитан дважды
- искать причину в журнале прогонов, а не в переписке
- различать «загрузка упала» и «загрузка отработала дважды» — эти аварии лечат по-разному
- строить загрузку дня так, чтобы её можно было запускать сколько угодно раз: очистить диапазон даты и записать заново
Тикет №01. «Отчёт врёт»
09:00, 15 марта 2184 года. Ты заступаешь на приёмный уровень станции «Хранилище-9» — тусклый грузовой ярус, куда лента приёмки всю ночь втаскивает архивные снимки и раскладывает их по хранилищу. Утром её работу читает Академия.
В очереди один тикет, открытый в 08:41 смотрителем архива:
«Суточный отчёт врёт. За вчера пусто — как будто лента стояла. За позавчера выручка вдвое больше, чем в самих снимках. Что из этого правда?»
Ответ неприятный: правды в отчёте нет вообще. Вчера лента шла, а позавчера в снимках ровно половина того, что показывает витрина. Обе строки тикета — про один и тот же участок, но про две разные ночи.
Ленту вёл предшественник. С девяти утра она твоя.
КВЕРИ: Доброе утро, архивист. Предшественник оставил тебе журнал прогонов и две недописанные проверки. Я всю ночь дремал в фоновом процессе — и всё видел. Пустая ночь и удвоенная ночь — разные аварии; начни с того, чтобы их различить.
Витрина dm_daily_revenue — конец ленты, последняя полка: одна строка на день и город. Перед ней стоят отдельные слои загрузки. Их устройство разберём в следующем уроке; пока достаточно знать, что витрина хранит итог.
Сегодня задача проще: прочитать показания и назвать аварию своим именем.

Две разные аварии в одной таблице
Показания сняты — теперь назовём то, что видно.
Данных за вчера в витрине нет — не «нули», а именно отсутствие строк. Данные за позавчера записаны дважды: строк вдвое больше нормы, и записаны они двумя разными run_id.
Со стороны это выглядит как одна поломка отчёта. На деле поломки две, и лечат их по-разному:
| Симптом | Что произошло | Чем лечится |
|---|---|---|
| дня нет | шаг не доехал до конца | проверить попытку; после остановки — перезапустить дату |
| день дважды | шаг доехал ДВАЖДЫ и оба раза дописал | перезапуск не поможет — нужна идемпотентность |
Разница принципиальная. В первом случае повтор — лекарство, во втором — добавка к болезни: он положит в витрину третий комплект строк.
Второй случай требует идемпотентности: свойства шага давать один и тот же результат, сколько бы раз его ни запустили.
Пока запомни симптом: два run_id на один день — это признак повторной загрузки, которая дописала, вместо того чтобы заменить.
Но симптом — ещё не история. Что происходило ночью, показывает журнал прогонов etl_runs. Его ведут не для отчётности: только там остаётся след каждой попытки, в том числе запущенной автоматически.
КВЕРИ: Разворачиваю журнал за обе ночи. Люди помнят, что они запускали. Журнал помнит, что запустилось.
load_mart — той, что кладёт день в витрину. Одна строка здесь — одна ПОПЫТКА за одну логическую дату. Сумм и группировок нарочно нет: улика видна только в отдельных попытках. Читай построчно: состояние, rows_out, время начала и конца.Что случилось ночью
Журнал показывает всю историю — по попыткам, а не по итогу.
13 марта — три попытки одной задачи вместо одной.
990001, 02:31–02:33,up_for_retry,rows_out = 0— попытка не доехала и ничего не записала;990002, 02:41–02:44,success,rows_out = 22— повтор посчитал день и положил его в витрину: двадцать два города, двадцать две строки;8812, 03:24–03:28,failed,rows_out = 1552— ещё одна попытка того же дня, которая упала, но уже успела дойти до записи.
Вот и объяснение сорока четырём строкам: день положили в витрину больше одного раза, потому что загрузчик при повторе дописывал к тому, что там уже лежало.
Никто не заметил по простой причине: успешная попытка в списке есть, а зелёная попытка выглядит как «всё хорошо». Авария спряталась не за отсутствием сигнала, а за сигналом об успехе рядом с чужой записью.
Про rows_out у неуспешных попыток. 1552 — это не «столько строк в витрине». Счётчик пишется по ходу работы, и у попытки, которая не дошла до конца, он подтверждает ровно одно: она успела дойти до записи.
Сколько именно строк дошло до витрины и дошли ли они вообще — по нему не определить. Поэтому смотрят сначала на состояние, и только потом на числа.
14 марта. Та же задача до сих пор в состоянии running, finished_at пуст, и rows_out у неё тоже не ноль — но это не доказывает, что строки уже попали в витрину: попытка не завершилась, а дня там нет.
Значит, задача не упала, а зависла. Данных за вчера нет, потому что загрузка их ещё не привезла; что с ними на предыдущих слоях, витрина не говорит.
Чего журнал НЕ говорит: какая именно попытка положила какие именно строки. У витрины свой run_id загрузки, у журнала — свой номер попытки, и связь между ними появится в главе de2.
Но на главный вопрос смены он ответил: попыток было несколько, и одна из них упала, дойдя до записи.
Отсюда правило смены, к которому курс будет возвращаться десять глав подряд:
Сначала спроси у журнала прогонов, что происходило, и только потом — у данных, что в них лежит. Данные показывают следствие, журнал показывает причину.
Голый INSERT — это не загрузка
Причина задвоения — в одном глаголе. Загрузчик витрины дописывал: считал день и добавлял результат к тому, что уже лежит в таблице.
Дописывать правильно только тогда, когда событие происходит один раз. Загрузка за дату — не такое событие: она случается столько раз, сколько её запустят, а запустят её обязательно больше одного раза — ретраем ночью или руками утром.
Нужное свойство шага называется идемпотентность: сколько бы раз его ни выполнили с одними и теми же входами, состояние получается одно и то же. Не «шаг не упадёт при повторе», а именно «после второго прогона в таблице ровно то же, что после первого».
Чтобы этого добиться, загрузчик меняет единицу записи. Он работает не с отдельными строками, а с диапазоном одной даты — куском витрины, который целиком относится только к этому дню. Такой кусок будем называть диапазоном дня.
Тогда шаг из «посчитать и дописать» превращается в пару:
DELETEвсего, что относится к этой дате;INSERT … SELECTтого, что посчитали сейчас.
Смысл пары виден на повторах. Первый запуск удаляет ноль строк и кладёт двадцать две. Второй удаляет двадцать две и кладёт двадцать две. Десятый — то же самое.
Соседние даты при этом не страдают: удаляется СВОЙ диапазон, а не таблица целиком.
Одна оговорка про : без между DELETE и INSERT витрина пуста за этот день, и отчёт, попавший в эту щель, покажет ноль. В транзакционной базе обе операции поэтому обычно объединяют. В песочнице курса управлять транзакцией нельзя — BEGIN не пропустит валидатор, — поэтому ниже пара идёт просто двумя операторами подряд.
Соберём такой загрузчик и запустим его дважды подряд, ровно как это сделал ночной ретрай.
run_id, а выручка вдвое выше, чем в самих снимках.Что изменилось. Было: 44 строки за 13 марта, два run_id, выручка 2 931 242.80 — вдвое больше, чем в снимках. Стало после ДВУХ прогонов подряд: 22 строки, один run_id, выручка 1 465 621.40 — ровно то же, что после одного прогона.
Это и есть идемпотентность в готовом виде: второй запуск ничего не добавил к первому.
Пара DELETE диапазона + INSERT … SELECT — прямой способ сделать загрузку идемпотентной. Есть и другие способы (INSERT OVERWRITE, MERGE по ключу); их механика здесь не важна. Важна единица перезаписи: логическая дата прогона, а не строка и не таблица целиком.
Почему не таблица целиком — увидишь в de1l5, где ровно это решение сломает соседний день.
Вопрос с собеседования
Как это спрашивают на собеседовании. «Ночная загрузка упала и была перезапущена. Какие два проблемных состояния данных можно увидеть утром и как отличить одно от другого?»
Ответ, который ждут: либо день не доехал (шаг не дошёл до конца — после проверки текущей попытки нужен перезапуск), либо день посчитан дважды (шаг дописал вместо замены — перезапуск сделает хуже).
Отличают их по витрине и журналу прогонов вместе: есть ли строки за дату, сколько прогонов их записали, сколько было попыток и чем они закончились.
Следующий вопрос почти всегда — «как сделать шаг идемпотентным». Здесь ждут: перезаписывать диапазон логической даты (DELETE + INSERT … SELECT, INSERT OVERWRITE или MERGE по ключу), а не дописывать.
Первый из трёх вариантов ты только что увидел в работе; доказывать его тестом будешь в de1l5.
run_id и вдвое больше строк, чем в соседние дни. Как исправить саму витрину и устранить причину удвоения?- Повторяющиеся вакансииEASY
- Просмотры и уникальная аудитория по днямEASY
- Дневные обороты по банковским продуктамEASY
Главное из урока
- Витрина
dm_daily_revenue: 14 марта отсутствует, 13 марта задвоен двумяrun_id. - Причину показал
etl_runs: 13-го у задачиload_martтри попытки вместо одной — одна не доехала, одна записала день, одна упала, дойдя до записи; 14-го она до сих пор висит вrunningс пустымfinished_at. rows_outу незавершённой попытки доказывает лишь, что она дошла до записи: какие строки попали в витрину, по счётчику не определить. Сначала состояние, потом числа.- Пустой день после проверки и остановки зависшей попытки исправляют перезапуском. Удвоенный день перезапуском НЕ исправить — нужен шаг, который при повторе заменяет, а не дописывает.
- Лекарство показано и проверено на живых данных:
DELETEдиапазона даты +INSERT … SELECT. Два прогона подряд дали ровно один день: 22 строки, одинrun_id, выручка вдвое меньше прежней — то есть настоящая.
Дальше по ленте: где физически лежат данные между источником и витриной — и почему полок именно столько.