From 4693bc5af9303302e86e0a93223ca43643287968 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Sun, 18 Oct 2026 18:09:43 +0200 Subject: [PATCH] =?UTF-8?q?docs(labs):=20=D1=83=D0=BF=D1=80=D0=BE=D1=89?= =?UTF-8?q?=D0=B5=D0=BD=20=D1=8F=D0=B7=D1=8B=D0=BA=20=D1=83=D1=87=D0=B5?= =?UTF-8?q?=D0=B1=D0=BD=D1=8B=D1=85=20=D0=B7=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: начинающим трудно читать задания из-за необъясненных терминов и неестественных формулировок. - Что: переработаны вводная и 21 задание, названы темы разделов, объяснены термины и сокращены повторы; согласованы правила учебного текста и ссылки. - Проверка: сохранены 25 блоков SQL и команд и таблицы состава колонок; проверены 153 локальные ссылки и 32 входящие ссылки на разделы; git diff --check прошел. --- README.md | 2 +- docs/labs/1-1-events-enriched.md | 50 ++-- docs/labs/1-2-top-pages-daily.md | 82 ++++--- docs/labs/1-3-session-overview.md | 83 ++++--- docs/labs/1-4-dq-errors-daily.md | 100 ++++---- docs/labs/1-5-utm-effectiveness.md | 79 ++++--- docs/labs/2-1-superset-chart.md | 37 +-- docs/labs/3-1-sessions-by-hand.md | 65 ++--- docs/labs/4-1-session-pipeline.md | 110 ++++----- docs/labs/5-1-order-checks.md | 74 +++--- docs/labs/5-2-order-parsing.md | 82 +++---- docs/labs/5-3-order-pipeline.md | 89 +++---- docs/labs/6-1-event-parsing.md | 112 ++++----- docs/labs/7-1-monitoring.md | 60 ++--- docs/labs/README.md | 222 ++++++++++-------- docs/labs/optional/duplicate-purchases.md | 26 +- docs/labs/optional/identity-over-time.md | 25 +- .../optional/incremental-daily-traffic.md | 24 +- docs/labs/optional/missing-purchases.md | 23 +- docs/labs/optional/order-amount-difference.md | 17 +- docs/labs/optional/revenue-over-time.md | 41 ++-- docs/labs/optional/superset-dashboard.md | 27 +-- docs/labs/optional/superset-missing-object.md | 26 +- docs/specs/2026-09-01-labs-program.md | 10 +- 24 files changed, 782 insertions(+), 684 deletions(-) diff --git a/README.md b/README.md index 38cac01..4043b06 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ Kafka, затем заново создаст схему хранилища и из `CLICKHOUSE_STUDENT_PASSWORD` в `.env`. Он читает те же источники, а создавать, заполнять и удалять таблицы может только в базе `default`. Для создания учебных витрин используйте подключение, описанное -в [инструкции к заданиям](docs/labs/README.md#как-применить-свой-ddl). +в [инструкции к заданиям](docs/labs/README.md#выполнение-sql-в-dbeaver). Нативные порты `29000` и `29001` нужны `clickhouse-client`, но не DBeaver. Остальные пользователи и роли описаны в [справочнике стенда](docs/architecture/stand.md#пользователи-и-роли-clickhouse). diff --git a/docs/labs/1-1-events-enriched.md b/docs/labs/1-1-events-enriched.md index 18625a5..5469a32 100644 --- a/docs/labs/1-1-events-enriched.md +++ b/docs/labs/1-1-events-enriched.md @@ -1,37 +1,38 @@ -# Задание 1: `dm.events_enriched_v` +# Задание 1: события для анализа, `dm.events_enriched_v` -**Чему учит:** подготовить удобный для анализа набор полей события -и обойтись представлением, сохранив одну строку на событие. +Вы подготовите представление с полями для анализа событий. Оно будет +добавлять вычисляемые поля при чтении, сохраняя одну строку на событие. -## Сначала прочитать +## Подготовка - [«Хранилище и его слои»](../handbook/storage-layers.md). -- [Как применить свой DDL](README.md#как-применить-свой-ddl): файл для решения +- [Выполнение SQL в DBeaver](README.md#выполнение-sql-в-dbeaver): файл для решения и подключение с правами на создание витрины. -## Для чего нужна витрина +## Данные для анализа событий В `dds.event_v` 44 колонки, включая технические поля источника и массивы -товаров. Для анализа событий в Superset нужны деловые поля и три готовых -значения: час события, путь страницы и домен перехода. Тогда вам не придется +товаров. Для анализа событий в Superset нужны сведения о посетителе и его +действиях, а также три вычисляемых поля: час события, путь страницы и домен, +с которого пришел посетитель. Тогда вам не придется повторять эти выражения в каждом запросе. Витрины следующих заданий читают DDS напрямую. Использовать `dm.events_enriched_v` как источник для них не нужно: задания независимы, их можно выполнять в любом порядке. -## Что сделать +## Создание представления Создайте обычное представление `CREATE VIEW dm.events_enriched_v`. -Единственный источник — `dds.event_v`. Зерно — **событие**: каждой строке -источника соответствует одна строка витрины. +Единственный источник: `dds.event_v`. Каждой строке источника должна +соответствовать ровно одна строка витрины: одно событие. -Физическая таблица, таблица-донор и задача в `dm_transform` не нужны. -Представление не хранит строки, поэтому ключ шардирования и пересборка ему -не требуются. +Представление выполняет запрос при чтении и не хранит отдельную копию данных. +Для него не нужно создавать таблицы или добавлять задачу в `dm_transform`. Укажите колонки явно. Иначе новая колонка источника попадет в витрину -автоматически, и договор с потребителем изменится без вашего участия. +автоматически. Отчет получит ее, даже если вы не собирались менять его набор +полей. | Группа | Колонки | |---|---| @@ -57,9 +58,7 @@ DDS читают из ODS без отдельной загрузки. Эти с ## Типичная ошибка Не разворачивайте массивы товаров через `ARRAY JOIN`. У события с тремя -позициями получится три строки. Зерно изменится. Витрину с новым зерном на -стенде хранят готовым агрегатом — причины описаны в разделе [«Как принято на -ступени 1»](README.md#как-принято-на-ступени-1). +позициями получится три строки вместо одной. Главная опасность — потеря событий с пустым массивом товаров: `ARRAY JOIN` исключает их из результата. Товары есть только в событиях корзины и покупки. @@ -68,9 +67,9 @@ DDS читают из ODS без отдельной загрузки. Эти с по-прежнему будет выглядеть таблицей событий. Сохраните массивы как в источнике. Для анализа отдельных товарных позиций -нужна другая витрина с другим зерном и именем. +нужна отдельная витрина, где одна строка соответствует товарной позиции. -## Как проверить себя +## Проверка результата После применения DDL откройте Airflow по [адресу из README](../../README.md#адреса-и-пароли), найдите даг `homework_check` и запустите его вручную. В новом запуске откройте @@ -79,16 +78,15 @@ DDS читают из ODS без отдельной загрузки. Эти с расхождениях с источником — дни и примеры несовпадений. Задачи еще не созданных витрин будут красными; это ожидаемо и не мешает проверке первой. -Деловой ключ — `event_id` внутри дня. Получите результат прямо из -`dds.event_v` за тот же день. Соедините его с витриной полным внешним -соединением по `event_id`. Посчитайте строки, где ключ отсутствует с одной -из сторон или различаются значения деловых колонок. Расхождений должно быть -ноль. +Выберите день и прочитайте его события из `dds.event_v` и своей витрины. +Сопоставьте строки по `event_id` через `FULL OUTER JOIN`. +Найдите события, которые есть только с одной стороны, и события, у которых +различаются перенесенные из источника поля. Расхождений должно быть ноль. Отдельно сверьте `event_hour`, `page_path` и `referer_host` с теми же выражениями, рассчитанными прямо по `dds.event_v`. -## Что должно получиться +## Результат работы - [ ] у вас есть `dm.events_enriched_v` с явно перечисленными колонками; - [ ] вы сверили число строк и набор `event_id` с `dds.event_v` за каждый diff --git a/docs/labs/1-2-top-pages-daily.md b/docs/labs/1-2-top-pages-daily.md index bd1741c..4635c17 100644 --- a/docs/labs/1-2-top-pages-daily.md +++ b/docs/labs/1-2-top-pages-daily.md @@ -1,14 +1,15 @@ -# Задание 2: `dm.top_pages_daily` +# Задание 2: показатели страниц по дням, `dm.top_pages_daily` -**Чему учит:** собрать дневные показатели страниц и настроить их -обновление в `dm_transform` через замену партиций. +В этой работе вы рассчитаете показатели страниц за каждый день и сохраните +их в таблице. Затем подключите обновление витрины к `dm_transform`: +новый расчет будет заменять дневные партиции таблицы. -## Сначала прочитать +## Подготовка - [«Хранилище и его слои»](../handbook/storage-layers.md); - [«Потоки Airflow и даги модельного времени»](../handbook/airflow-dags.md). -## Для чего нужна витрина +## Дневные показатели страниц По дневным показателям страниц аналитик сравнивает просмотры, посетителей и действия с товарами. Добавление в корзину происходит на карточке товара, @@ -19,19 +20,23 @@ действия происходят на разных страницах, а связи между ними в агрегате нет. Путь человека исследуют по событиям или сессиям. -## Что сделать +## Создание витрины -Источник — `dds.event_v`. Зерно витрины — **день события × страница**. -Читайте DDS напрямую: так выполнение задания не зависит от витрины -задания 1. +Читайте события из `dds.event_v`. В результате нужна одна строка на страницу +за день. Например, все просмотры одной карточки товара за один день попадут +в одну строку. Витрина задания 1 для этого расчета не нужна. -На стенде витрину с новым зерном хранят как готовый агрегат, чтобы не -рассчитывать ее заново для каждого запроса. Причины описаны в разделе -[«Как принято на ступени 1»](README.md#как-принято-на-ступени-1). Создайте -пять объектов: -`dm.top_pages_daily_rep` / `_dist`, донор `_stage_rep` / `_stage_dist` и -публичное представление `dm.top_pages_daily_v`. Ключ партиции — день, он же -единица пересборки. +Сохраните рассчитанные показатели в таблице, чтобы отчеты читали готовые +итоги. Создайте пять объектов: + +- основные таблицы `dm.top_pages_daily_rep` и `dm.top_pages_daily_dist`; +- промежуточные таблицы `dm.top_pages_daily_stage_rep` и `dm.top_pages_daily_stage_dist`; +- представление для чтения `dm.top_pages_daily_v`. + +Их назначение и порядок обновления разобраны во вводной: +[«Создание и обновление витрин»](README.md#создание-и-обновление-витрин). +Разбейте данные на партиции по дням, чтобы можно было заменить результат +одного дня целиком. ### Состав колонок @@ -50,25 +55,26 @@ адрес разделил бы показатели одной страницы на сотни строк. Учитывайте все три типа событий, а их счетчики вычисляйте через `countIf`. -### Ритм пересборки +### Дни для пересчета -События прожитого дня больше не меняются. Определяйте объем прогона по -состоянию витрины: от первого несобранного прожитого дня до последнего -прожитого. Отсчет от последнего собранного дня оставит пропуски после -частичного сбоя. +События завершившегося модельного дня больше не меняются. Найдите первый +такой день, которого еще нет в витрине, и рассчитайте все дни от него до +последнего завершившегося. Если начать после последнего дня, который уже +есть в витрине, можно пропустить более ранний день, не загруженный из-за сбоя. -Как у `revenue_daily`, заполните донор за весь диапазон одной вставкой, -затем замените дневные партиции по одной. +Как у `revenue_daily`, запишите расчет за все выбранные дни в промежуточную +таблицу одной вставкой. Затем замените дневные партиции основной таблицы по +одной. Добавьте запуск этих запросов в `dm_transform`. ### Ключ шардирования -Выберите ключ самостоятельно. У `revenue_daily` кроме дня одно измерение с -шестью близкими по объему значениями. Здесь страниц много, и число событий -на них сильно различается. +Выберите ключ самостоятельно. У `revenue_daily` строки сгруппированы по дню +и категории товара; категорий всего шесть. Здесь страниц много, и число +событий на них сильно различается. -У цели и донора используйте одинаковый ключ: иначе при замене партиции -строка потеряется на одном шарде и удвоится на другом. О том, [как устроен -донор](../architecture/storage.md), читайте в разделе «Раскладка по шардам». +У основной и промежуточной таблиц используйте одинаковый ключ шардирования. +Замена партиции не перераспределяет строки между шардами. Подробнее об этом +в разделе [«Раскладка по шардам»](../architecture/storage.md#раскладка-по-шардам). ## Типичная ошибка @@ -82,13 +88,13 @@ Выбирайте проверку под возможную ошибку. Этот подход описан в [справочнике проверок](../architecture/testing.md). -## Как проверить себя +## Проверка результата -Деловой ключ — `page_path` внутри дня. Получите прямой агрегат из -`dds.event_v` за день и соедините его с `dm.top_pages_daily_v` полным -внешним соединением по ключу. Посчитайте строки, где ключ отсутствует с -одной из сторон или различается любой из четырех счетчиков. Расхождений -должно быть ноль. +Рассчитайте те же показатели по `dds.event_v` за выбранный день. +Сопоставьте результат со строками `dm.top_pages_daily_v` за этот день +через `FULL OUTER JOIN` по `page_path`. Найдите страницы, которые есть +только с одной стороны, и строки, где различается хотя бы один из четырех +счетчиков. Расхождений должно быть ноль. Перед полным сравнением проверьте дневные суммы: @@ -98,10 +104,10 @@ Сумма `visitors` по страницам повторно учитывает посетителя, открывшего несколько страниц. Число посетителей за день берите из `dm.daily_traffic_v`. -## Что должно получиться +## Результат работы -- [ ] у вас есть пять объектов витрины; донор объявлен через `AS` и - использует тот же ключ шардирования, что и целевая таблица; +- [ ] у вас есть пять объектов витрины; промежуточная таблица объявлена через + `AS` и использует тот же ключ шардирования, что и основная; - [ ] в `dm.top_pages_daily_v` явно перечислены колонки, включая `_load_id` и `_load_ts`; - [ ] ваш запрос сам определяет дни пересборки по состоянию витрины; diff --git a/docs/labs/1-3-session-overview.md b/docs/labs/1-3-session-overview.md index 3e71140..7c0dc24 100644 --- a/docs/labs/1-3-session-overview.md +++ b/docs/labs/1-3-session-overview.md @@ -1,14 +1,15 @@ -# Задание 3: `dm.session_overview` +# Задание 3: визиты по устройствам, `dm.session_overview` -**Чему учит:** хранить слагаемые для расчета средних и долей и понимать, +Вы соберете дневные показатели визитов по устройствам. В таблице будут +храниться суммы и счетчики для расчета средних и долей. Заодно проверите, когда сложение числа посетителей по устройствам завышает аудиторию. -## Сначала прочитать +## Подготовка - [«Хранилище и его слои»](../handbook/storage-layers.md); - [«Потоки Airflow и даги модельного времени»](../handbook/airflow-dags.md). -## Для чего нужна витрина +## Показатели визитов Витрина нужна, чтобы по дням сравнивать визиты с телефона и компьютера: их число, длительность, просмотры страниц и частоту покупок. @@ -17,13 +18,19 @@ устройствам. Почему такие итоги хранят в отдельной таблице, объясняет [справочник DM](../architecture/dm.md). -## Что сделать +## Создание витрины -Источник — `dds.session_v`. Зерно — **день сессии × категория устройства**. -Зерно новое, поэтому витрина хранится готовым агрегатом — см. [«Как принято -на ступени 1»](README.md#как-принято-на-ступени-1). Создайте пять объектов: -`dm.session_overview_rep` / `_dist`, донор `_stage_rep` / `_stage_dist` и -публичное представление `dm.session_overview_v`. Ключ партиции — день. +Читайте готовые сессии из `dds.session_v`. В результате нужна одна строка +на категорию устройства за день: например, итоги всех визитов с телефонов +за один день. + +Создайте пять объектов по [общим правилам ступени 1](README.md#создание-и-обновление-витрин): + +- основные таблицы `dm.session_overview_rep` и `dm.session_overview_dist`; +- промежуточные таблицы `dm.session_overview_stage_rep` и `dm.session_overview_stage_dist`; +- представление для чтения `dm.session_overview_v`. + +Разбейте данные на партиции по дням. ### Состав колонок @@ -47,19 +54,20 @@ категорий встречаются `desktop`, `mobile` и `tablet`. Строку для `tv` создавайте только при появлении таких визитов. -Вместо `avg_duration_seconds` храните слагаемые. Среднее из средних по трем -устройствам даст неверный ответ за день: число сессий в категориях -различается, поэтому средние нужно взвешивать. Потребитель делит сумму -`duration_seconds_total` на сумму `sessions`. Долю отказов считают так же: -сумму `bounce_sessions` делят на сумму `sessions`, а не усредняют готовые -доли. +Для расчета средней длительности храните общую длительность и число сессий. +Готовую среднюю `avg_duration_seconds` сохранять не нужно. Число сессий +в категориях различается, поэтому среднее из трех средних даст неверный +ответ за день. Чтобы получить его правильно, разделите сумму +`duration_seconds_total` на сумму `sessions`. Для доли отказов разделите +сумму `bounce_sessions` на сумму `sessions`. Усреднение готовых долей тоже +даст неверный результат. -### Ритм пересборки +### Дни для пересчета -Сессии прожитого дня больше не меняются. Как в задании 2, определяйте -диапазон от первого несобранного прожитого дня до последнего прожитого. -Соберите его в донор и замените дневные партиции. Добавьте задачу в -`dm_transform`. +Сессии завершившегося модельного дня больше не меняются. Как в задании 2, +найдите первый завершившийся день, которого еще нет в витрине. Рассчитайте +все дни от него до последнего завершившегося, запишите их в промежуточную +таблицу и замените дневные партиции основной. Добавьте задачу в `dm_transform`. ### Ключ шардирования @@ -68,18 +76,18 @@ `device_category`, сначала посчитайте строки на каждом шарде. Затем объясните, почему такое распределение вас устраивает. -Ключ донора — тот же, что у цели (см. [«Как принято на ступени -1»](README.md#как-принято-на-ступени-1) и раздел «Раскладка по шардам» -[справочника хранилища](../architecture/storage.md)). +У основной и промежуточной таблиц должен быть одинаковый ключ шардирования: +см. [общие правила ступени 1](README.md#создание-и-обновление-витрин). -## Почему сумма `visitors` сходится с `dm.daily_traffic_v` +## Число посетителей за день Сравните сумму `visitors` по устройствам за день с `dm.daily_traffic_v`. На -стенде значения совпадут: устройство приписано куке на всю жизнь, поэтому за -день она учитывается только в одной строке. Это свойство мира стенда; см. +стенде значения совпадут: одному `client_id` всегда соответствует одно +устройство, поэтому за день он учитывается только в одной категории. +Это свойство мира стенда; см. раздел «Паспорт куки» в [словаре понятий](../../CONTEXT.md). -Если появятся визиты одной куки с двух устройств, сумма начнет завышать +Если появятся визиты одного `client_id` с двух устройств, сумма начнет завышать аудиторию. Проверки счетчиков по категориям этого не обнаружат. Число посетителей дня берите из `dm.daily_traffic_v`, где оно рассчитано для дня целиком. @@ -91,12 +99,13 @@ событий совпало с источником, а число сессий завышено, вероятная причина — сборка витрины из событий вместо сессий. -## Как проверить себя +## Проверка результата -Деловой ключ — `device_category` внутри дня. Получите прямой агрегат из -`dds.session_v` за день. Соедините его с `dm.session_overview_v` полным -внешним соединением по ключу. Посчитайте строки, где ключ отсутствует с -одной из сторон или различается любой счетчик. Расхождений должно быть ноль. +Рассчитайте те же показатели по `dds.session_v` за выбранный день. +Сопоставьте результат со строками `dm.session_overview_v` за этот день +через `FULL OUTER JOIN` по `device_category`. Найдите категории, которые +есть только с одной стороны, и строки, где различается хотя бы один +счетчик. Расхождений должно быть ноль. Еще две проверки дневных итогов: @@ -104,12 +113,12 @@ повторного учета визитов; - сумма `events_total` равна сумме `events_total` по сессиям того же дня. -## Что должно получиться +## Результат работы -- [ ] у вас есть пять объектов витрины; донор объявлен через `AS` и - использует ключ шардирования целевой таблицы; +- [ ] у вас есть пять объектов витрины; промежуточная таблица объявлена через + `AS` и использует ключ шардирования основной; - [ ] вы явно перечислили колонки в `dm.session_overview_v` и собрали показатели из готовых сессий `dds.session_v`; -- [ ] в таблице хранятся слагаемые для средних и долей; +- [ ] в таблице хранятся суммы и счетчики для расчета средних и долей; - [ ] проверка `session_overview` в `homework_check` прошла успешно; - [ ] витрина обновляется при запуске `dm_transform`. diff --git a/docs/labs/1-4-dq-errors-daily.md b/docs/labs/1-4-dq-errors-daily.md index a9506f5..423c658 100644 --- a/docs/labs/1-4-dq-errors-daily.md +++ b/docs/labs/1-4-dq-errors-daily.md @@ -1,28 +1,42 @@ -# Задание 4: `dm.dq_errors_daily` +# Задание 4: ошибки загрузки по дням, `dm.dq_errors_daily` -**Чему учит:** считать брак по дню загрузки и сохранять сводку дольше -исходных строк. +Вы соберете дневную сводку ошибок загрузки. Ее итоги должны сохраниться +и после того, как исходные сообщения будут удалены из хранилища. -## Сначала прочитать +## Подготовка - [«Хранилище и его слои»](../handbook/storage-layers.md); - [«Потоки Airflow и даги модельного времени»](../handbook/airflow-dags.md). -## Для чего нужна витрина +## Ошибки загрузки и их источники -Сводку открывают при росте брака в потоке: смотрят, каких ошибок стало -больше и сколько строк забраковано. Причину конкретной ошибки исследуют -в `ods.event_errors_dist` и `ods.order_errors_dist`, где есть сырой текст и -координаты доставки. +Если сообщение не удалось разобрать, стенд записывает его в таблицу ошибок. +Такие строки дальше называются браком. Вы соберете сводку, по которой можно +увидеть, каких ошибок стало больше и сколько строк забраковано. -## Что сделать +Источники сводки: -Зерно — **день загрузки × источник × класс брака**. В источнике зерно — -строка брака. Зерно новое, поэтому витрина хранится готовым агрегатом — см. -[«Как принято на ступени 1»](README.md#как-принято-на-ступени-1). Создайте -пять объектов: `dm.dq_errors_daily_rep` / `_dist`, донор `_stage_rep` / -`_stage_dist` и публичное представление `dm.dq_errors_daily_v`. Ключ -партиции — день загрузки. +- `ods.event_errors_dist`: ошибки разбора событий; +- `ods.order_errors_dist`: ошибки разбора выгрузки заказов. + +В этих таблицах есть исходный текст сообщения и сведения о его доставке: +они пригодятся при поиске причины ошибки. Представлений `_v` у таблиц ошибок +нет, создавать их для задания не нужно. Чтение локальной таблицы `_rep` +даст строки только одного шарда из двух, поэтому используйте `_dist`. + +## Создание сводки + +В результате нужна одна строка на день загрузки, источник и класс ошибки. +Например, ошибки `field_invalid` в заказах за один день загрузки попадут +в одну строку с их количеством. + +Создайте пять объектов по [общим правилам ступени 1](README.md#создание-и-обновление-витрин): + +- основные таблицы `dm.dq_errors_daily_rep` и `dm.dq_errors_daily_dist`; +- промежуточные таблицы `dm.dq_errors_daily_stage_rep` и `dm.dq_errors_daily_stage_dist`; +- представление для чтения `dm.dq_errors_daily_v`. + +Разбейте данные на партиции по дням загрузки. ### Состав колонок @@ -41,16 +55,17 @@ Служебная `_load_ts` витрины описывает сборку сводки; ее дата может отличаться от `load_date`. -У событий и заказов свои классы брака. `source` входит в зерно, чтобы -одинаковые имена классов с разным смыслом не объединялись в одну строку. +У событий и заказов свои классы брака. Группируйте строки также по `source`, +чтобы одноименные классы разных источников не объединялись. -### Ритм пересборки +### Пересчет и хранение итогов -Брак хранится месяц по `TTL`, сырые данные — трое суток. Сводка хранится -бессрочно: итоги нужны и после удаления исходных строк. +Правило `TTL` удаляет строки брака через месяц, сырые данные через трое суток. +Сводку храните бессрочно: итоги нужны и после удаления исходных строк. -Получите список дней загрузки, еще имеющихся в источниках. Соберите их в -донор и замените только эти партиции, сохранив остальные. +Получите список дней загрузки, еще имеющихся в источниках. Рассчитайте их +итоги в промежуточной таблице и замените только эти партиции основной таблицы. +Более старые итоги сохраните. В течение текущих суток ошибки еще поступают: пересчитайте этот день на следующем прогоне. @@ -60,36 +75,23 @@ Выберите ключ самостоятельно. Классов брака у событий и заказов вместе меньше десятка. Объясните, насколько существенен перекос при таком объеме. -Ключ донора — тот же, что у цели (см. [«Как принято на ступени -1»](README.md#как-принято-на-ступени-1) и раздел «Раскладка по шардам» -[справочника хранилища](../architecture/storage.md)). - -## Источники: таблицы `_dist` - -Только это задание читает данные не через `_v`: брак нужен для диагностики -приема и не входит в бизнес-модель. Публичных представлений у таблиц ошибок -нет, создавать их для задания не нужно. Источники: - -- `ods.event_errors_dist` — ошибки разбора событий; -- `ods.order_errors_dist` — ошибки разбора слепка заказов. - -Чтение локальной `_rep` даст строки одного шарда из двух: часть брака не -попадет в сводку, хотя запрос выполнится без ошибки. +У основной и промежуточной таблиц должен быть одинаковый ключ шардирования: +см. [общие правила ступени 1](README.md#создание-и-обновление-витрин). ## Типичная ошибка -Если заменить всю таблицу данными из нового донора, исчезнут итоги дней, -истекших по `TTL` источника. Восстановить их неоткуда. +Если заменить всю сводку новым расчетом, исчезнут итоги дней, за которые +исходные строки уже удалены по `TTL`. Восстановить эти итоги неоткуда. -## Как проверить себя +## Проверка результата -Деловой ключ — `source` и `error_class` внутри `load_date`. Проверяйте дни, -оставшиеся хотя бы в одной таблице ошибок. Если дня за пределами `TTL` уже -нет в источнике, это не ошибка: сверять его уже не с чем. +Сопоставляйте строки по `load_date`, `source` и `error_class`. Проверяйте дни, +оставшиеся хотя бы в одной таблице ошибок. Старые итоги, для которых исходные +строки уже удалены по `TTL`, сверять не с чем. Посчитайте строки прямо в обеих таблицах ошибок и соедините результат с -`dm.dq_errors_daily_v` полным внешним соединением по ключу. Затем посчитайте -строки, где ключ отсутствует с одной из сторон или различается `error_rows`. +`dm.dq_errors_daily_v` через `FULL OUTER JOIN` по этим трем полям. Посчитайте +строки, которые есть только с одной стороны или у которых различается `error_rows`. Расхождений должно быть ноль. Сумма `error_rows` по проверяемым дням равна общему числу строк в @@ -99,10 +101,10 @@ Если в источниках нет брака, витрина должна быть пустой. В этом случае расхождений тоже ноль и проверка успешна. -## Что должно получиться +## Результат работы -- [ ] у вас есть пять объектов витрины; донор объявлен через `AS` и - использует ключ шардирования целевой таблицы; +- [ ] у вас есть пять объектов витрины; промежуточная таблица объявлена через + `AS` и использует ключ шардирования основной; - [ ] сводка включает ошибки событий и заказов из таблиц `_dist`; - [ ] ваш запрос пересобирает только дни, еще доступные в источниках, сохраняя более старые итоги; diff --git a/docs/labs/1-5-utm-effectiveness.md b/docs/labs/1-5-utm-effectiveness.md index e2735ab..8bbe5cc 100644 --- a/docs/labs/1-5-utm-effectiveness.md +++ b/docs/labs/1-5-utm-effectiveness.md @@ -1,30 +1,38 @@ -# Задание 5: `dm.utm_effectiveness` +# Задание 5: показатели рекламы, `dm.utm_effectiveness` -**Чему учит:** различать сумму покупки со слов клиента (`declared_`) и -серверную выручку, находить причины их расхождения. +Вы подготовите показатели рекламных каналов по событиям сайта. Суммы покупок +в этой витрине будут отличаться от выручки по заказам магазина. В конце +работы вы сравните их и объясните причины расхождения. -## Сначала прочитать +## Подготовка - [«Хранилище и его слои»](../handbook/storage-layers.md); - [«Потоки Airflow и даги модельного времени»](../handbook/airflow-dags.md). -## Для чего нужна витрина +## Покупки по рекламным каналам Аналитик сравнивает рекламные каналы по дням: трафик, добавления в корзину, -покупки и заявленные клиентом суммы. +покупки и их суммы из событий сайта. -На стенде поведение и атрибуцию считают по трекеру, деньги — по бэкенду. -Здесь сделано исключение: UTM-метки есть только в кликстриме, поэтому для -оценки покупок по меткам используют клиентскую сумму. +События сайта показывают действия посетителя и рекламные метки его переходов. +Заказы магазина служат источником выручки. Но в заказах нет UTM-меток, поэтому +в этой витрине вы возьмете суммы из событий покупки. Их передает браузер +посетителя; дальше они называются клиентскими суммами. Приставка `declared_` +в имени показателя напоминает об этом источнике. -## Что сделать +## Создание витрины -Читайте `dds.event_v` напрямую, без соседних витрин DM. Зерно — **день -события × `utm_source` × `utm_medium` × `utm_campaign`**. Зерно новое, -поэтому витрина хранится готовым агрегатом — см. [«Как принято на ступени -1»](README.md#как-принято-на-ступени-1). Создайте пять объектов: -`dm.utm_effectiveness_rep` / `_dist`, донор `_stage_rep` / `_stage_dist` и -публичное представление `dm.utm_effectiveness_v`. Ключ партиции — день. +Читайте события из `dds.event_v`, без других витрин DM. В результате нужна +одна строка на день и сочетание трех меток: `utm_source`, `utm_medium`, +`utm_campaign`. События одного дня с одинаковыми метками попадают в одну группу. + +Создайте пять объектов по [общим правилам ступени 1](README.md#создание-и-обновление-витрин): + +- основные таблицы `dm.utm_effectiveness_rep` и `dm.utm_effectiveness_dist`; +- промежуточные таблицы `dm.utm_effectiveness_stage_rep` и `dm.utm_effectiveness_stage_dist`; +- представление для чтения `dm.utm_effectiveness_v`. + +Разбейте данные на партиции по дням. ### Состав колонок @@ -54,31 +62,32 @@ Для `utm_campaign` выбран тип `String`: имен кампаний со временем становится много, и затраты на словарь `LowCardinality` перестают окупаться. -### Ритм пересборки +### Дни для пересчета -События прожитого дня больше не меняются. Как в задании 2, соберите в донор -дни от первого несобранного до последнего прожитого и замените дневные -партиции. Добавьте задачу в `dm_transform`. +События завершившегося модельного дня больше не меняются. Как в задании 2, +найдите первый завершившийся день, которого еще нет в витрине. Рассчитайте +все дни от него до последнего завершившегося, запишите их в промежуточную +таблицу и замените дневные партиции основной. Добавьте задачу в `dm_transform`. ### Ключ шардирования Выберите ключ самостоятельно. Если берете хеш одной из трех UTM-колонок, посчитайте долю строк с ее самым частым значением и обоснуйте выбор. -Ключ донора — тот же, что у цели (см. [«Как принято на ступени -1»](README.md#как-принято-на-ступени-1) и раздел «Раскладка по шардам» -[справочника хранилища](../architecture/storage.md)). +У основной и промежуточной таблиц должен быть одинаковый ключ шардирования: +см. [общие правила ступени 1](README.md#создание-и-обновление-витрин). -## Атрибуция по событию, а не по визиту +## Рекламные метки событий Группируйте события по их собственным меткам. Модели «первое касание» и «последнее касание» здесь не рассчитываются. В `dds.session_v` записаны UTM первого события визита. Распределение по каналам может отличаться от этой витрины, и это не означает ошибку в -расчете. В отчете различайте атрибуцию визитов и событий. +расчете. Укажите в отчете, что распределяете по рекламным каналам отдельные +события. Такое распределение называют атрибуцией. -## Прямой трафик остается в витрине +## События без рекламных меток Сохраните события без меток: прямой трафик обычно крупнейший канал, и без него доля рекламы будет завышена. Если отфильтровать такие строки, дневные @@ -108,13 +117,13 @@ причины можно посмотреть в `dm.purchase_vs_orders_v`, где выделены классы расхождений. -## Как проверить себя +## Проверка результата -Деловой ключ — `utm_source`, `utm_medium`, `utm_campaign` внутри дня. -Получите прямой агрегат из `dds.event_v` за день и соедините его с -`dm.utm_effectiveness_v` полным внешним соединением по ключу. Посчитайте -строки, где ключ отсутствует с одной из сторон или различается любая из пяти -величин. Расхождений должно быть ноль. +Рассчитайте те же показатели по `dds.event_v` за выбранный день. +Сопоставьте результат со строками `dm.utm_effectiveness_v` за этот день +через `FULL OUTER JOIN` по `utm_source`, `utm_medium` и `utm_campaign`. +Найдите сочетания меток, которые есть только с одной стороны, и строки, +где различается хотя бы один из пяти показателей. Расхождений должно быть ноль. Дополнительно сверьте дневные итоги: @@ -126,10 +135,10 @@ Сумма `visitors` по строкам повторно учитывает одного посетителя, приходящего по разным меткам. Для числа посетителей дня она не подходит. -## Что должно получиться +## Результат работы -- [ ] у вас есть пять объектов витрины; донор объявлен через `AS` и - использует ключ шардирования целевой таблицы; +- [ ] у вас есть пять объектов витрины; промежуточная таблица объявлена через + `AS` и использует ключ шардирования основной; - [ ] строки без UTM-меток сохранены, а `declared_revenue` хранится в `Decimal(18, 2)`; - [ ] вы записали, почему сумма отличается от `dm.revenue_daily_v`; diff --git a/docs/labs/2-1-superset-chart.md b/docs/labs/2-1-superset-chart.md index fcb68eb..3d59b54 100644 --- a/docs/labs/2-1-superset-chart.md +++ b/docs/labs/2-1-superset-chart.md @@ -1,24 +1,27 @@ # Свой график в Superset -**Чему учит:** увидеть, как собственный результат из хранилища становится -отчетом в BI. +В этой работе вы построите график по своей витрине и проверите числа +на нем SQL-запросом. -## Сначала прочитать +## Подготовка - [«Немного Superset»](../handbook/superset.md); - [задание 3: `dm.session_overview`](1-3-session-overview.md). -## На какой вопрос ответит график +## График отказов по устройствам -В задании 3 вы собрали витрину `dm.session_overview_v`. Теперь покажите, с -каких устройств чаще уходят после первой страницы. Постройте столбиковый -график доли отказов по `device_category` за весь период витрины. Измерение -одно — `device_category`, метрика тоже одна. +В задании 3 первой ступени вы собрали витрину `dm.session_overview_v`. +Теперь покажите, с каких устройств чаще уходят после единственного события. +Такие визиты в стенде считаются отказами. Постройте столбиковый график доли +отказов по `device_category` за весь период витрины. Каждый столбик должен +показывать долю для одной категории устройств. -## Что сделать +## Настройка графика -Заведите физический датасет в подключении к `ClickHouse`: схема `dm`, объект -`session_overview_v`. Назовите по-русски хотя бы две колонки: +Добавьте представление как источник данных для графиков. В Superset такой +источник называется физическим датасетом. Выберите подключение `ClickHouse`, +схему `dm` и объект `session_overview_v`. Задайте русские подписи хотя бы двум +колонкам: - `device_category` — «Категория устройства»; - `report_date` — «Дата отчета». @@ -32,10 +35,10 @@ | `SQL expression` | `SUM(bounce_sessions) / SUM(sessions)` | | `D3 format` | `.1%` | -Доля считается из сумм слагаемых: среднее готовых долей соврет. Поэтому -метрике нужно это выражение. Сохраните ее в датасете, а не в графике: -подписи и формат задаются в датасете, а график их использует. Так во всех -будущих графиках у метрики будут одинаковые подписи. Это правило описано в +Выражение делит общее число отказов на общее число сессий. Усреднение дневных +долей может дать неверный результат: число сессий в разные дни различается. +Сохраните метрику в датасете, чтобы все будущие графики использовали одно +выражение, подпись и формат. Это правило описано в [справочнике Superset](../architecture/superset.md#дерево-импорта). Не используйте вкладку «Custom SQL» конструктора графика. Созданная там @@ -58,7 +61,7 @@ FROM dm.session_overview_v; Стендовые объекты не меняйте. Что переживает `make up`, а что стирает `make clean`, описано в главе [«Немного Superset»](../handbook/superset.md#стендовые-объекты-и-ваши). -## Как проверить себя +## Проверка результата Автоматической проверки этого задания нет. Сверьте числа на графике с результатом запроса в SQL Lab: @@ -76,7 +79,7 @@ ORDER BY device_category; проценты с одним знаком, как формат `.1%`, поэтому числа должны совпасть один в один. -## Что должно получиться +## Результат работы - [ ] у вас есть физический датасет `dm.session_overview_v` в подключении `ClickHouse`; diff --git a/docs/labs/3-1-sessions-by-hand.md b/docs/labs/3-1-sessions-by-hand.md index 937b00c..4b1da26 100644 --- a/docs/labs/3-1-sessions-by-hand.md +++ b/docs/labs/3-1-sessions-by-hand.md @@ -1,35 +1,35 @@ -# Нарежьте сессии сами +# Сессии из событий -**Чему учит:** собрать сессии из событий, сравнить их число с `VisitID` -источника и найти события, из-за которых расчеты расходятся. +Вы самостоятельно объедините события сайта в сессии и сравните их число +с готовыми визитами источника, обозначенными `VisitID`. Затем найдете +события, из-за которых расчеты расходятся. -## Сначала прочитать +## Подготовка - [Хранилище и его слои, раздел DDS](../handbook/storage-layers.md#dds-язык-бизнеса); - [формат события: Идентификаторы и время](../formats/clickstream-event.md#идентификаторы-и-время). -## Для чего нужны сессии +## События и визиты Визит, или сессия, объединяет действия посетителя за один заход на сайт. По визитам аналитик считает длительность и глубину просмотра. В хранилище сессии собираются из отдельных событий. -Сверьте самостоятельный расчет с `VisitID`, чтобы увидеть, как из событий -получается сессия. -## Что сделать +## Расчет сессий -### Нарезка +### Границы визитов В SQL Lab или DBeaver под `analyst` напишите запрос к `ods.event_v`. -Для своей нарезки и эталона возьмите одни и те же строки за все доступные +Для своего расчета и эталона возьмите одни и те же строки за все доступные дни: для расследования понадобятся события соседних дней. Выберите `ClientID`, `UTCEventTime`, `EventDate`, `EventType` и `VisitID`. В `dds.event_v` эталонного `VisitID` нет. -Для начала считайте визитом последовательные события одной куки (`ClientID`); -пауза дольше 30 минут открывает новый визит. +`ClientID` хранится в cookie браузера и обозначает посетителя в событиях +сайта. Для начала считайте визитом последовательные события одного +`ClientID`: пауза дольше 30 минут открывает новый визит. Учитывайте события всех типов. Упорядочите их по `UTCEventTime`. -Присвойте каждому событию номер своей сессии внутри куки. +Присвойте каждому событию номер своей сессии внутри `ClientID`. Подсказка: оконные функции. Днем сессии считайте `EventDate` ее первого события: дата из `UTCEventTime` @@ -38,7 +38,8 @@ ### Сверка с эталоном -В эталоне посчитайте число визитов каждой куки за модельный день: +Посчитайте по эталонному `VisitID` число визитов каждого `ClientID` +за модельный день: ```sql SELECT ClientID, EventDate, uniqExact(VisitID) AS reference_sessions @@ -47,46 +48,48 @@ GROUP BY ClientID, EventDate ORDER BY EventDate, ClientID; ``` -Посчитайте свои сессии по куке и дню сессии. -Соедините два готовых агрегата через `FULL OUTER JOIN` по куке и дню. -В результате полного соединения останутся ключи обеих сторон: при расхождении -на вашей стороне ключа может не быть совсем. Учтите такие строки при сравнении. -Выведите сводку по дням с числом разошедшихся кук и список этих кук -с днем и обоими значениями счета. Общая сумма за день могла бы скрыть -взаимную компенсацию ошибок разных кук. По списку видно, у кого искать причину. +Посчитайте свои сессии по `ClientID` и дню сессии. Соедините результаты двух +расчетов через `FULL OUTER JOIN` по этим полям. Учтите строки, которые есть +только с одной стороны: полное соединение сохранит их в результате. + +Выведите сводку по дням с числом `ClientID`, для которых расчеты разошлись, +и список этих идентификаторов с днем и обоими значениями. Общая сумма за день +могла бы скрыть ошибки: для одного посетителя вы посчитали лишнюю сессию, +для другого пропустили. По списку видно, чьи события нужно исследовать. Расхождений будет всего несколько. Если их нет, мир прожил мало дней. Прогоните несколько дней через [даги модельного времени](../handbook/airflow-dags.md#даги-мира) и повторите сверку. -### Расследование +### Поиск причины расхождения -Выберите одну разошедшуюся куку. Выведите ее события за два соседних дня +Выберите один `ClientID` с расхождением. Выведите его события за два соседних дня с `UTCEventTime`, `EventDate`, `VisitID` и номером своей сессии. -Сопоставьте сессии через события: общего ключа у двух нарезок нет, +Сопоставьте сессии через события: общего ключа у двух расчетов нет, зато у каждого события есть и `VisitID`, и присвоенный вами номер. Найдите два соседних события, между которыми границы визитов разошлись. -Запишите куку, день, время обоих событий и причину одним предложением. +Запишите `ClientID`, день, время обоих событий и причину одним предложением. Уточните определение визита и добейтесь нуля расхождений во всех днях. -## Как проверить себя +## Проверка результата Сохраните исходный и уточненный расчеты, результаты сверки и найденные строки. Таблицы и представления создавать не нужно. Задачи в `homework_check` для этого задания нет: вы проверяете расчет своим запросом. В `sql/dds/session_rebuild.sql` и -`sql/dq/sessions_vs_reference.sql` лежат готовые запросы нарезки и сверки. +`sql/dq/sessions_vs_reference.sql` лежат готовые запросы расчета сессий и сверки. Откройте их после собственного решения и расследования. Сравните запросы со своими. -Сопоставление со стендовой нарезкой по `session_id` остается на ступень 4. +На ступени 4 вы сравните свой результат с готовыми сессиями стенда +по `session_id`. -## Что должно получиться +## Результат работы - [ ] ваш запрос собирает сессии из всех событий `ods.event_v` за все доступные дни; -- [ ] у вас есть сводка расхождений по дням и список кук, полученные через +- [ ] у вас есть сводка расхождений по дням и список `ClientID`, полученные через `FULL OUTER JOIN`; -- [ ] вы объяснили причину одного расхождения и записали куку, день и два +- [ ] вы объяснили причину одного расхождения и записали `ClientID`, день и два события, на которых оно видно; - [ ] уточненный расчет дает ноль расхождений во всех днях. diff --git a/docs/labs/4-1-session-pipeline.md b/docs/labs/4-1-session-pipeline.md index cb16984..05a3f9c 100644 --- a/docs/labs/4-1-session-pipeline.md +++ b/docs/labs/4-1-session-pipeline.md @@ -1,16 +1,17 @@ -# Перепишите нарезку стенда +# Расчет сессий в стенде -**Чему учит:** переписать расчет сессий в готовом конвейере и проверить +На прошлой ступени вы рассчитали сессии отдельным запросом. Теперь +перепишете расчет внутри стенда, запустите его через Airflow и проверите результат по исходным событиям. -## Сначала прочитать +## Подготовка -- [задание «Нарежьте сессии сами»](3-1-sessions-by-hand.md); +- [задание «Сессии из событий»](3-1-sessions-by-hand.md); - [модель сессии DDS](../architecture/dds/session.md); - [дневной конвейер трансформаций](../architecture/etl.md#объём-прогона); - [раскладка по шардам](../architecture/storage.md#раскладка-по-шардам). -## Что предстоит сделать +## Файлы для изменения Даг, таблицы сессий и проверка их числа уже созданы. Перепишите только три файла: @@ -19,56 +20,57 @@ - `sql/dds/session_rebuild.sql`; - `sql/dds/session_replace.sql`. -Удалите из них готовый SQL и соберите свою реализацию. Параметры и порядок -запросов заданы в `dds_transform`. DDL задает целевые таблицы и донор. Донор — -служебная таблица, где день собирается перед заменой. В -`sessions_vs_reference.sql` число сессий сравнивается с `VisitID` из ODS. +Удалите из них готовый SQL и напишите свой. Даг `dds_transform` передаст +параметры и выполнит запросы по порядку. Таблицы уже созданы: основные хранят +готовые сессии, промежуточные нужны для нового расчета перед заменой данных. +Запрос `sessions_vs_reference.sql` сравнивает число сессий с `VisitID` из ODS. -## Как даг запускает ваши запросы +## Выполнение запросов в Airflow Объем прогона — дни, которые соберет один запуск. Его вычисляет первый файл. | Файл | Вход | Результат | |---|---|---| -| `session_scope.sql` | `{position:UInt16}` — номер первого несыгранного дня | `first_day` и `last_day` | +| `session_scope.sql` | `{position:UInt16}` — номер первого еще не прожитого модельного дня | `first_day` и `last_day` | | `session_rebuild.sql` | `{first_day:Date}`, `{last_day:Date}`, `{load_id:String}` | Сессии в `dds.session_stage_dist` | | `session_replace.sql` | День в `{{ params.day }}` | Партиция дня в `dds.session_rep` | -Первый прожитый день, которого еще нет в DDS, становится `first_day`. -`last_day` — последний прожитый день. Начало шкалы нужно взять из событий ODS: -`position` считает дни от него. Живой модельный день еще не закончен, поэтому -сессии за него не собираются. +`first_day` должен быть первым завершившимся модельным днем, которого еще +нет в DDS, а `last_day` последним завершившимся днем. Начальную дату возьмите +из событий ODS: `position` считает дни от нее. Текущий модельный день еще +не закончен, поэтому сессии за него не рассчитываются. -Если в ODS нет ни одного события, запрос не возвращает строк. Если DDS уже -догнал мир, запрос возвращает строку с `first_day = NULL`. В этом случае группа -сессий в Airflow получает состояние `skipped`. +Если в ODS нет ни одного события, запрос не возвращает строк. Если все +завершившиеся дни уже есть в DDS, запрос возвращает строку с `first_day = NULL`. +Тогда Airflow пропускает группу задач расчета сессий: ее состояние будет `skipped`. `session_rebuild.sql` сначала очищает `dds.session_stage_rep` на всем кластере. -Затем он собирает в `dds.session_stage_dist` все дни отрезка. Добавьте к +Затем он записывает в `dds.session_stage_dist` расчет за все выбранные дни. Добавьте к вставке `SETTINGS distributed_foreground_insert = 1`. Так строки успеют попасть на шарды до замены партиций. -Цель `session_replace.sql` — локальная таблица `dds.session_rep`, источник — -локальный донор `dds.session_stage_rep`. Операция выполняется на всем кластере. +Запрос `session_replace.sql` заменяет партицию в основной `dds.session_rep` +готовой партицией из промежуточной `dds.session_stage_rep`. +Операция выполняется на всем кластере, на локальных таблицах каждого шарда. Не ставьте точку с запятой в конце `session_scope.sql`. Драйвер добавляет формат -ответа, и точка с запятой превратит строку в мультизапрос, который ClickHouse +ответа. С точкой с запятой получится несколько команд, которые ClickHouse отклонит. День для замены подставляет шаблон Jinja в Airflow. Параметр ClickHouse в `ALTER ... PARTITION` здесь не работает. -## Нарежьте сессии +## Правила расчета сессий Источник сборки — `dds.event_v`. Одна строка результата представляет один -визит. На третьей ступени вы уже нарезали сессии: +визит. На третьей ступени вы уже получили правила разделения событий на сессии: - сессия не пересекает модельные сутки; - пауза больше 30 минут открывает новую сессию; -- в нарезке участвуют события всех типов. +- в расчете участвуют события всех типов. -Пересборка добавляет требования к повторяемости. При одинаковом времени -порядок событий задает `event_id`, поэтому полный порядок окон — +Повторный расчет должен давать тот же результат. При одинаковом времени +упорядочивайте события по `event_id`: порядок в оконных функциях должен быть `(event_time, event_id)`. Один визит должен получать одинаковый `session_id` -при каждом запуске. Тогда его можно сравнить с сохраненным результатом. +при каждом запуске. Вычислите идентификатор как `cityHash64(client_id, min(event_time))`. Состав и типы строки заданы в `dds.session_rep`. Заполните начало, конец и @@ -76,11 +78,11 @@ ClickHouse в `ALTER ... PARTITION` здесь не работает. покупки. Возьмите входную и выходную страницы, UTM первого события, устройство и город. Заполните также служебные колонки из схемы таблицы. -Пишите через `dds.session_stage_dist`: место строки определит ключ -`client_id`. При записи прямо в локальную таблицу оно зависело бы от ноды, +Пишите через `dds.session_stage_dist`: шард для строки определяется по +`client_id`. При записи прямо в локальную таблицу он зависел бы от узла, которая выполнила запрос. -## Запустите свою реализацию +## Запуск нового расчета Готовые дни повторно не собираются. Чтобы новый SQL точно выполнился, выберите последний собранный день: @@ -90,8 +92,8 @@ SELECT max(session_date) FROM dds.session_v; ``` -Берите последний день: более ранняя дыра расширит объем до всех последующих -прожитых дней. +Берите последний день: если удалить более ранний, расчет придется повторить +и за все последующие завершившиеся дни. Сохраните десять строк с идентификатором, границами, длительностью и счетчиками: @@ -113,7 +115,7 @@ ORDER BY client_id, started_at, session_id LIMIT 10; ``` -Создайте в DBeaver подключение к первой ноде по [инструкции из +Создайте в DBeaver подключение к первому узлу по [инструкции из README](../../README.md#подключиться-из-dbeaver). Укажите пользователя `etl` и пароль `CLICKHOUSE_ETL_PASSWORD` из `.env`. У этой учетной записи есть право менять таблицы DDS на кластере. Подставьте найденную дату и удалите только ее @@ -128,19 +130,18 @@ DROP PARTITION 'YYYY-MM-DD'; группу `session`: в ней должны выполниться задачи `scope` и `rebuild`, а также `replace` для выбранного дня. Если после третьей ступени мир ушел вперед, в группе появятся задачи `replace` и для последующих прожитых дней. Следом -выполнятся витрины и `dq_check`. +обновятся витрины и выполнится `dq_check`. Если прогон дошел до замены, но упал на проверке, ошибочная партиция уже находится в DDS. После правки снова удалите тот же день перед следующим запуском. Иначе день останется заполненным, а новая сборка для него не выполнится. -## Как проверить себя +## Проверка результата Сначала повторите запрос с десятью строками. `session_id`, границы, -длительность и счетчики должны совпасть с сохраненным результатом. Так вы -завершите отложенное с третьей ступени сопоставление со стендовой нарезкой по -`session_id`. +длительность и счетчики должны совпасть с сохраненным результатом. +Это проверит, что ваш расчет восстановил те же сессии с теми же идентификаторами. Задача `dq_check.sessions_vs_reference` должна стать зеленой. Та же проверка видна в хранилище: @@ -152,33 +153,34 @@ WHERE check_name = 'sessions_vs_reference' ORDER BY data_date; ``` -Эта сверка сравнивает число сессий каждой куки за день с `VisitID`. Нулевой -`failed_rows` не подтверждает границы, страницы и UTM: две ошибки могут -оставить тот же счет. Возьмите поштучную сверку из третьей ступени. Для одной -куки выбранного дня сопоставьте границы `VisitID` по событиям с границами -строк `dds.session_v`. По исходным событиям проверьте страницы, UTM, устройство +Эта сверка сравнивает число сессий каждого `client_id` за день с `VisitID`. +При `failed_rows = 0` число сессий совпало, но их границы, страницы и UTM +еще могут различаться. Как на третьей ступени, проверьте отдельные сессии. +Для одного `client_id` выбранного дня найдите начало и конец каждого +`VisitID` по событиям и сравните с границами строк `dds.session_v`. +По исходным событиям проверьте страницы, UTM, устройство и город. Страницы и UTM не входят в сохраненные десять строк: готовый SQL стенда выбирал их через `argMin` и `argMax` только по `event_time`. При равном времени ожидаемое значение определяйте по исходным событиям и полному порядку `(event_time, event_id)`. -Осталась физическая раскладка. Напишите отдельный запрос к -`dds.session_dist`. Для каждой строки он должен: +Проверьте также, на каком шарде лежит каждая строка. Напишите отдельный +запрос к `dds.session_dist`. Он должен: -1. получить фактическую ноду через `hostName()`; -2. пересчитать ожидаемую ноду по `cityHash64(client_id)`; -3. посчитать строки, у которых ноды не совпали. +1. получить фактический узел строки через `hostName()`; +2. определить ожидаемый узел по `cityHash64(client_id)`; +3. посчитать строки, у которых фактический и ожидаемый узлы не совпали. У кластера два равных шарда: остаток 0 соответствует `clickhouse-01`, остаток -1 — `clickhouse-02`. Сгруппируйте ответ по фактической ноде. На каждой ноде +1 — `clickhouse-02`. Сгруппируйте ответ по фактическому узлу. На каждом узле число строк на чужом шарде должно быть нулевым. Проверка числа сессий читает Distributed-таблицу и такую ошибку не заметит. После зеленого результата запустите `etl_pipeline` еще раз, не удаляя -партицию. Расчет объема должен пропустить группу сессий: новых прожитых дней и -дыр в слое нет. +партицию. Группа сессий должна быть пропущена: все завершившиеся дни уже +рассчитаны, новых дней и пропусков нет. -## Что должно получиться +## Результат работы - [ ] вы переписали три файла `sql/dds/session_*.sql`, сохранив даг, таблицы и проверку стенда; @@ -186,6 +188,6 @@ Distributed-таблицу и такую ошибку не заметит. - [ ] вы сверили десять сохраненных строк: `session_id`, границы, длительность и счетчики совпали после пересборки; - [ ] вы получили ноль расхождений в `sessions_vs_reference` за все - прожитые дни и вручную сверили границы и атрибуты сессии одной куки; + прожитые дни и вручную сверили границы и поля сессии одного `client_id`; - [ ] ваш запрос с `hostName()` не нашел строк на чужом шарде; - [ ] повторный прогон без удаления данных пропустил группу сессий. diff --git a/docs/labs/5-1-order-checks.md b/docs/labs/5-1-order-checks.md index 3906d56..0de0c17 100644 --- a/docs/labs/5-1-order-checks.md +++ b/docs/labs/5-1-order-checks.md @@ -1,9 +1,10 @@ -# Научите проверки замечать ошибки в заказах +# Проверки качества заказов -**Чему учит:** написать проверки заказов до изменения их обработки -и убедиться на намеренной ошибке, что проверки ее замечают. +Перед изменением обработки заказов вы добавите две проверки качества данных. +Затем намеренно испортите один заказ и убедитесь, что проверки обнаружили +ошибки. -## Сначала прочитать +## Подготовка - [модель заказа DDS](../architecture/dds/order.md); - [дневной конвейер](../architecture/etl.md#объём-прогона); @@ -13,30 +14,29 @@ и на ее включение в [`dq_check`](../../dags/dq_check.py). Готового ответа для двух проверок из этого задания в репозитории нет. -## Что предстоит сделать +## Новые проверки Добавьте две проверки и включите их в даг: - `sql/dq/order_items_total.sql` проверяет сумму позиций заказа; - `sql/dq/orders_vs_ods.sql` сверяет DDS с его источником; -- `dags/dq_check.py` запускает обе проверки, публикует их строки в +- `dags/dq_check.py` запускает обе проверки, записывает их результаты в `dm.dq_summary` и создает для них отдельные задачи. Разбор ODS и сборку DDS вы перепишете в следующих заданиях. Здесь сохраните -их, даги загрузки, DDL, таблицы-доноры, Kafka и службы как есть. +их, даги загрузки, DDL, промежуточные таблицы, Kafka и службы как есть. Перед опытами остановите автоматическое движение мира: поставьте `world_live` на паузу и дождитесь завершения активных прогонов. До завершения опытов не запускайте руками `world_next_day`, `world_live_day`, `world_initialize` и -`world_recreate`. Это условие стабильного модельного времени: одной паузы -`world_live` для него мало. +`world_recreate`, чтобы новые данные не меняли результат между проверками. -## Что будут проверять ваши запросы +## Требования к запросам Чтобы `dq_check` мог запустить проверку, ее запрос возвращает по одной строке на день с колонками `data_date`, `reference_rows`, `actual_rows`, `failed_rows`, `diagnostics`. Даг сохраняет в `dm.dq_summary` только счетчики, -статус и координаты запуска. Примеры из `diagnostics` ищите в +статус и сведения о запуске. Примеры из `diagnostics` ищите в журнале упавшей задачи Airflow. Ограничьте их тем же малым числом, что и существующие запросы в `sql/dq/`. @@ -44,46 +44,48 @@ Имя проверки - `order_items_total`. Для каждого заказа из `dds.order_v` посчитайте сумму `item_price * item_quantity` и сравните ее с `items_total`. -`reference_rows` считает ключи со стороны `items_total`, `actual_rows` - ключи -со стороны вычисленной суммы, а `failed_rows` - несовпавшие значения. В -исправном наборе первые два счетчика равны; несовпадения сумм считает -`failed_rows`. Для диагностики достаточно `order_id`, ожидаемой суммы и -суммы позиций. +В `reference_rows` запишите число заказов со стороны `items_total`, +в `actual_rows` число заказов со стороны вычисленной суммы, в `failed_rows` +число заказов, у которых суммы не совпали. На исправных данных первые два +счетчика равны, а `failed_rows` равен нулю. Для примеров расхождений +достаточно `order_id`, значения `items_total` и вычисленной суммы позиций. -Проверка относится к типизированному разбору массива `items` на границе -ODS -> DDS. Скидку, доставку и равенство `total` здесь проверять не нужно. +Проверка поможет заметить ошибки разбора `items` при переносе заказа +из ODS в DDS. Скидку, доставку и равенство `total` здесь проверять не нужно. -### DDS против ODS +### Полнота заказов в DDS Имя проверки - `orders_vs_ods`. Источник модели DDS здесь - текущее состояние `ods.order_v`. С ним вы и сравниваете DDS; обращаться к внутреннему состоянию генератора для этой проверки не нужно. -Одним запросом соберите обе стороны на зерне дня заказа и сравните: +Одним запросом рассчитайте итоги каждого дня заказа в обоих слоях и сравните: - число заказов; - максимальный `updated_at`. -День ODS вычисляется в `Europe/Samara`, как `order_date` в DDS. Область -проверки - объединение дней обеих сторон, включая день, который полностью -пропал из одного слоя. Перед сравнением приведите времена к одному часовому -поясу. `reference_rows` - число заказов ODS, `actual_rows` - число заказов DDS, -а `failed_rows` равен единице для дня, где расходится хотя бы одно значение. +День заказа в ODS вычисляйте в `Europe/Samara`, как `order_date` в DDS. +Проверьте все дни, которые есть хотя бы в одном слое: так вы заметите день, +целиком пропавший из другого. Перед сравнением приведите времена к одному +часовому поясу. `reference_rows` содержит число заказов ODS, `actual_rows` +число заказов DDS. В `failed_rows` запишите 1, если за день расходится +хотя бы одно значение, и 0, если оба совпали. Обе стороны распределены по `order_id`. Дневные агрегаты соедините через `GLOBAL FULL OUTER JOIN` с `join_use_nulls = 1`, как в существующих сверках. -Если разбор ODS и сборка DDS одинаково потеряют строку, слои останутся равны -по этой проверке. Общая потеря при разборе проявится в отдельном балансе по -`_load_id` из [следующего задания](5-2-order-parsing.md). +Если строка потерялась при разборе в ODS, она не попадет и в DDS. Эта проверка +не заметит потерю: данные слоев совпадут. В [следующем задании](5-2-order-parsing.md) +вы отдельно сравните число исходных строк с числом разобранных заказов +и ошибок за один запуск, обозначенный `_load_id`. -## Получите красный и зеленый результаты +## Проверка на намеренных ошибках Сначала запустите `dq_check` на нетронутых данных. Обе новые задачи должны завершиться успешно, а в `dm.dq_summary_v` у обоих имен не должно быть -нарушенных дней. +дней с расхождениями. Выберите оплаченный заказ из последнего слепка с непустым массивом позиций и -сохраните его `order_id` и `order_date`. Подключитесь к первой ноде из DBeaver +сохраните его `order_id` и `order_date`. Подключитесь к первому узлу из DBeaver по [инструкции из README](../../README.md#подключиться-из-dbeaver) под пользователем `etl`. Прибавьте одну копейку к `items_total` этого заказа в локальной таблице `dds.order_rep` на всем кластере: @@ -102,7 +104,7 @@ SETTINGS mutations_sync = 2; Запустите `etl_pipeline`. Текущее окно заказов будет заново собрано из ODS, и исходная строка вернется в DDS. Повторный `dq_check` должен дать зеленый -результат для нового сторожа. +результат для новой проверки. Теперь удалите тот же заказ из `dds.order_rep` на всем кластере: @@ -118,9 +120,9 @@ SETTINGS mutations_sync = 2; Если `orders_vs_ods` не обнаружил удаление, исправьте запрос до следующего задания. -## Как проверить себя +## Проверка результата -В `dm.dq_summary_v` должны быть зелены обе новые и пять прежних проверок: +В `dm.dq_summary_v` обе новые и пять прежних проверок должны быть успешны: ```sql SELECT check_name, sum(failed_rows) AS failed_rows, groupUniqArray(status) @@ -132,12 +134,12 @@ ORDER BY check_name; Проверка суммы должна охватывать каждый день `dds.order_v`. Сверка слоев должна охватывать объединение дней ODS и DDS, включая дни только одной стороны. -## Завершите опыт +## Завершение работы После восстановления заказа и успешного `dq_check` можно закончить работу или перейти к разбору заказов, оставив модельное время на паузе. -## Что должно получиться +## Результат работы - [ ] две новые проверки запускаются в `dq_check`, а их дневные результаты появляются в `dm.dq_summary`; diff --git a/docs/labs/5-2-order-parsing.md b/docs/labs/5-2-order-parsing.md index dc61cf8..a37d12a 100644 --- a/docs/labs/5-2-order-parsing.md +++ b/docs/labs/5-2-order-parsing.md @@ -1,9 +1,10 @@ -# Перепишите разбор заказов в ODS +# Разбор заказов в ODS -**Чему учит:** переписать разбор заказов и на пробных сообщениях проверить, -что годные строки и брак попадают в свои таблицы без потерь. +Вы перепишете разбор заказов в ODS. На четырех пробных сообщениях проверите, +что правильно заполненный заказ и сообщения с ошибками попадают +в соответствующие таблицы без потерь. -## Сначала прочитать +## Подготовка - [проверки заказов](5-1-order-checks.md); - [прием заказов в ODS](../architecture/orders/ingestion.md); @@ -11,24 +12,22 @@ - [`orders_ingest`](../../dags/orders_ingest.py); - [`order_parse.sql`](../../sql/ods/order_parse.sql). -Запрос для проверки пробных строк вы напишете сами. - -## Что предстоит сделать +## Порядок работы Сначала проведите опыт на готовом `order_parse.sql`. Затем удалите его SQL, соберите разбор заново и повторите опыт с другим маркером. -Не меняйте устройство `orders_ingest`, DDL, Kafka, таблиц и преобразований -DDS. Оба опыта должны закончиться адресной очисткой только своих строк. +Сохраните устройство `orders_ingest`, DDL, Kafka, таблиц и преобразований +DDS. После каждого опыта удалите только добавленные вами строки. Перед началом убедитесь, что после [предыдущего задания](5-1-order-checks.md) -все семь DQ остались зелеными. Поставьте `world_live` на паузу, дождитесь +все семь проверок качества прошли успешно. Поставьте `world_live` на паузу, дождитесь активных прогонов и до завершения опыта не запускайте `world_next_day`, `world_live_day`, `world_initialize` и `world_recreate`. Не запускайте `etl_pipeline` между отправкой пробной пачки и ее очисткой. -## Четыре строки для двух ветвей +## Пробные сообщения Сделайте четыре сообщения с общим уникальным маркером: @@ -39,9 +38,9 @@ DDS. Оба опыта должны закончиться адресной оч `field_invalid`. `not_an_object` означает, что корень JSON не объект. `keyset_mismatch` означает, -что у объекта отличается точный набор верхнеуровневых ключей. `field_invalid` -означает, что ключи на месте, но значение не прошло проверку типа, формата или -вместимости. +что у объекта есть лишние верхнеуровневые ключи или не хватает обязательных. +`field_invalid` означает, что ключи на месте, но значение имеет неверный тип, +формат или не помещается в заданный тип данных. Соберите строки в `/tmp/<маркер>.jsonl`. Заготовка дает одну годную строку и по одному примеру каждого класса брака: @@ -79,31 +78,33 @@ docker compose exec -T kafka /opt/kafka/bin/kafka-console-producer.sh \ --bootstrap-server kafka:9092 --topic orders < "$LAB_BATCH" ``` -Запустите `orders_ingest` и найдите `_load_id` по маркеру в -`stg.orders_raw_dist`. У четырех строк должен быть один идентификатор прогона. +Запустите `orders_ingest`. Найдите свои строки по маркеру в +`stg.orders_raw_dist` и сохраните их `_load_id`: это идентификатор запуска, +который принял сообщения. Он должен быть одинаковым у всех четырех строк. -## Зафиксируйте исходный результат +## Проверка готового разбора -До любого запуска `etl_pipeline` проверьте срез по найденному `_load_id`: +До запуска `etl_pipeline` проверьте строки с найденным `_load_id`: - в STG четыре строки; -- в физической `ods.order_dist` одна годная строка; +- в таблице `ods.order_dist` одна годная строка; - в `ods.order_errors_dist` три строки, по одной каждого класса; - `STG = годные + брак`; -- текущая строка `ods.order_v` совпадает по деловым полям с результатом - `argMax` по `snapshot_date` из годного сырья среза. +- поля заказа в текущей строке `ods.order_v` совпадают с полями из годного + исходного сообщения; последнюю версию для сравнения выберите через + `argMax` по `snapshot_date`. -Счет физической таблицы нужен именно по `_load_id`: представление -`ods.order_v` оставляет текущую версию сущности и не показывает, сколько строк -разобрал запуск. Запрос сверки с `argMax` напишите сами. Эталонные деловые поля -стройте только из `raw`; отдельно сравните различающиеся `created_at` и -`updated_at`. +Считайте строки `ods.order_dist` именно по `_load_id`: представление +`ods.order_v` оставляет только текущую версию каждого заказа и не показывает, +сколько строк разобрал запуск. Запрос сверки с `argMax` напишите сами. +Значения для сравнения извлеките из `raw` пробных сообщений. Отдельно +сравните `created_at` и `updated_at`: в заготовке они намеренно различаются. Проверьте раскладку годной строки через `hostName()` на `ods.order_dist`. -Фактическая нода должна совпасть с остатком `cityHash64(order_id) % 2`: +Узел, на котором лежит строка, должен соответствовать остатку `cityHash64(order_id) % 2`: 0 - `clickhouse-01`, 1 - `clickhouse-02`. -## Уберите пробную пачку +## Удаление пробных сообщений Удалите только строки маркера из локальных таблиц: @@ -135,40 +136,41 @@ SETTINGS mutations_sync = 2; запустите `etl_pipeline`: пересборка текущего окна удалит заказ из DDS и обновит витрины. -## Перепишите разбор +## Собственный разбор заказов Удалите готовый SQL из `sql/ods/order_parse.sql` и напишите свой. Чтобы `orders_ingest` мог его использовать, сохраните следующие условия: -- вход - строки `stg.orders_raw_dist` одного `{load_id:String}`; -- каждая строка среза попадает ровно в один результат: годный заказ или брак; -- произвольный `raw` не останавливает весь разбор исключением; +- запрос читает строки `stg.orders_raw_dist` с переданным `{load_id:String}`; +- каждая прочитанная строка попадает ровно в один результат: годный заказ или брак; +- ошибка в `raw` одной строки не прерывает разбор остальных; - годные строки записываются в `ods.order_dist`, остальные - в `ods.order_errors_dist`; - классы брака и их порядок - `not_an_object`, `keyset_mismatch`, `field_invalid`; - метки доставки и `_load_id` переходят из STG без пересчета; -- задача завершается после доставки обоих результатов через Distributed, а - строки распределяются по ключу целевой таблицы. +- задача завершается после записи и заказов, и ошибок на шарды через + Distributed; строки распределяются по ключу соответствующей целевой таблицы. -Точный набор полей, типы и граница строгого приема описаны в +Точный набор полей, типы и условия проверки значений описаны в [документе приема](../architecture/orders/ingestion.md#граница-строгого-приёма), а целевые колонки - в [`20-ods-tables.sql`](../../sql/ddl/20-ods-tables.sql). -Внутренность `items` остается сырым JSON: ее разбирает DDS. +Содержимое `items` сохраните как JSON: его разбирает DDS. Повторите опыт с новым маркером. Получите тот же баланс 4 = 1 + 3, те же три -класса, верную текущую строку по `snapshot_date`, разные времена и правильный -шард. Затем очистите второй срез теми же адресными командами и запустите +класса, верную текущую версию заказа по `snapshot_date`, разные значения +`created_at` и `updated_at` и правильный шард. Затем удалите строки второго +опыта теми же командами и запустите `dq_check`. -## Завершите опыт +## Завершение работы Убедитесь, что оба маркера отсутствуют в STG, ODS, таблице брака и DDS, временные файлы удалены, а семь проверок успешны. Теперь можно перейти к сборке DDS, не снимая паузу с модельного времени. Если вы прерываетесь и до опыта `world_live` работал, снимите его с паузы. -## Что должно получиться +## Результат работы - [ ] у вас сохранены результаты до переписывания: четыре строки дали один заказ и три класса брака; вы сверили поля текущей версии и шард; diff --git a/docs/labs/5-3-order-pipeline.md b/docs/labs/5-3-order-pipeline.md index a5d531d..9d3515a 100644 --- a/docs/labs/5-3-order-pipeline.md +++ b/docs/labs/5-3-order-pipeline.md @@ -1,9 +1,10 @@ -# Перепишите сборку заказов в DDS +# Сборка заказов в DDS -**Чему учит:** настроить пересборку заказов по состоянию ODS и DDS -и проверить, что она восстанавливает пропущенный день за пределами текущего окна. +Вы перепишете загрузку заказов из ODS в DDS и выбор дней для пересчета. +Затем удалите данные за один из прошлых дней и проверите, что ваша +загрузка восстановила их. -## Сначала прочитать +## Подготовка - [проверки заказов](5-1-order-checks.md); - [разбор заказов в ODS](5-2-order-parsing.md); @@ -11,7 +12,7 @@ - [дневной конвейер](../architecture/etl.md#объём-прогона); - [раскладку по шардам](../architecture/storage.md#раскладка-по-шардам). -## Что предстоит сделать +## Файлы для изменения Удалите готовый SQL и перепишите только три файла: @@ -19,18 +20,18 @@ - `sql/dds/order_rebuild.sql`; - `sql/dds/order_replace.sql`. -Даг `dds_transform`, DDL, донор, ODS и обе проверки из первого задания +Даг `dds_transform`, DDL, промежуточную таблицу, ODS и обе проверки из первого задания оставьте как есть. Перед началом убедитесь, что пробные строки предыдущего -задания удалены, а семь DQ дают зеленый результат. +задания удалены, а семь проверок качества дают зеленый результат. Поставьте `world_live` на паузу и дождитесь завершения активных прогонов. До завершения опыта не запускайте `world_live_day`, `world_initialize` и `world_recreate`. `world_next_day` понадобится только в явно указанном ниже случае. -## Как даг запускает ваши запросы +## Выполнение запросов в Airflow -Сохраните интерфейс `dds_transform`: +Даг `dds_transform` ожидает от файлов следующие параметры и результаты: | Файл | Вход | Результат | |---|---|---| @@ -38,39 +39,46 @@ | `order_rebuild.sql` | `{first_day:Date}`, `{last_day:Date}`, `{load_id:String}` | выбранные дни в `dds.order_stage_dist` | | `order_replace.sql` | день в `{{ params.day }}` | партиция дня в `dds.order_rep` | -Объем состоит из текущего окна источника и хвоста от первого дня, который есть -в ODS, но отсутствует в DDS. Текущее окно дают строки последнего -`snapshot_date`: их минимальный и максимальный день заказа. Если источник -пуст, `order_scope.sql` не возвращает строку. Если дыр нет, `first_day` равен -началу окна. Если дыра старше, непрерывный отрезок начинается с нее. +Сначала определите дни, за которые заказы еще могут измениться. Возьмите +строки с последним `snapshot_date` и найдите минимальную и максимальную +дату заказа. Этот диапазон далее называется текущим окном источника. + +Затем найдите первый день, который есть в ODS, но отсутствует в DDS. +Если он раньше начала текущего окна, начните расчет с него. Иначе начните +с начала окна. Полученная дата станет `first_day`, а конец окна `last_day`. +Один запуск пересчитывает все дни между ними. Если источник пуст, +`order_scope.sql` не возвращает строку. `order_rebuild.sql` сначала очищает все партиции `dds.order_stage_rep` на кластере, затем записывает в `dds.order_stage_dist` весь выбранный отрезок. Он читает текущее состояние `ods.order_v`, вычисляет `order_date`, переводит времена в `Europe/Samara`, разбирает `items` в три параллельных массива и -ставит координаты запуска. +заполняет служебные поля запуска. -Запись через Distributed должна завершиться до замены и использовать ключ -целевой таблицы для раскладки строк. Способ собрать весь отрезок внутри файла -выберите сами. +Дождитесь записи всех строк на шарды через Distributed перед заменой партиций. +Строки должны распределяться по ключу целевой таблицы. Способ рассчитать +выбранные дни внутри файла выберите сами. `order_replace.sql` работает с локальными `dds.order_rep` и -`dds.order_stage_rep` на всем кластере. `REPLACE PARTITION` переносит готовую -дневную партицию из донора и оставляет донор на месте. Структура, ключи и -политика хранения совпадают: DDL объявляет донор через `AS dds.order_rep`. +`dds.order_stage_rep` на всем кластере. `REPLACE PARTITION` заменяет дневную +партицию основной таблицы готовой партицией из промежуточной. Промежуточная +партиция при этом сохраняется. Структура, ключи и политика хранения таблиц +совпадают: DDL создает промежуточную через `AS dds.order_rep`. -## Восстановите ближайший закрытый день +## Восстановление пропущенного дня -Найдите последний по дате день DDS, который лежит левее начала текущего окна -ODS. Это ближайший к окну закрытый день: его удаление показывает расширение -объема на дыру без пересборки всей истории. +Найдите самый поздний день DDS перед началом текущего окна ODS. Заказы за этот +день уже не меняются, поэтому он считается закрытым. Удалив его, вы проверите, +что ваш запрос заметил пропуск и включил этот день в расчет. Если такого дня еще нет, один раз продвиньте мир через `world_next_day`, дождитесь `orders_ingest`, затем запустите `etl_pipeline`. После этого снова -зафиксируйте модельное время до конца опыта и выберите ближайший закрытый день. +оставьте модельное время неизменным до конца опыта и выберите этот день. -Сохраните для него число заказов и контрольную сумму деловых полей без -`_load_id` и `_load_ts`. Удалите только его партицию: +Сохраните для него число заказов и контрольную сумму их полей, исключив +служебные `_load_id` и `_load_ts`. Контрольная сумма позволит проверить, +что после восстановления значения остались прежними. Удалите только +партицию выбранного дня: ```sql ALTER TABLE dds.order_rep ON CLUSTER clickstream_cluster @@ -90,29 +98,30 @@ DROP PARTITION 'YYYY-MM-DD'; прогона число заказов и контрольная сумма выбранного дня должны совпасть с сохраненными. -## Проверьте раскладку и повтор +## Распределение по шардам и повторный запуск -Напишите отдельный запрос к `dds.order_dist`, который получает фактическую -ноду через `hostName()`, вычисляет ожидаемую по `cityHash64(order_id) % 2` и -считает несовпадения по каждой ноде. Остаток 0 соответствует -`clickhouse-01`, остаток 1 - `clickhouse-02`. На обеих нодах число строк на -чужом шарде должно быть нулевым. Счетные DQ читают Distributed-таблицы и такую -ошибку раскладки не обнаруживают. +Напишите отдельный запрос к `dds.order_dist`, который получает фактический +узел каждой строки через `hostName()`, вычисляет ожидаемый по +`cityHash64(order_id) % 2` и считает несовпадения по каждому узлу. +Остаток 0 соответствует `clickhouse-01`, остаток 1 - `clickhouse-02`. +На обоих узлах число строк на чужом шарде должно быть нулевым. +Проверки количества строк читают данные со всех шардов через Distributed +и такую ошибку не обнаруживают. Запустите `etl_pipeline` еще раз при неизменном модельном времени. Группа заказов снова должна пересобрать текущее изменяемое окно. Контрольные суммы -деловых полей остаются прежними, а все семь проверок дают зеленый результат. +полей заказов остаются прежними, а все семь проверок дают зеленый результат. -## Завершите опыт +## Завершение работы После восстановления партиции и успешных проверок снимите `world_live` с паузы, если он работал до упражнения. -## Что должно получиться +## Результат работы - [ ] вы переписали три файла `sql/dds/order_*.sql`, сохранив остальные части стенда; -- [ ] вы удалили ближайший к окну закрытый день и увидели ошибку DQ до +- [ ] вы удалили ближайший к окну закрытый день и увидели ошибку проверки до восстановления; - [ ] ваш расчет выбрал `first_day` с удаленного дня и заменил партиции всего отрезка до конца окна; @@ -120,4 +129,4 @@ DROP PARTITION 'YYYY-MM-DD'; с сохраненными значениями; они совпали; - [ ] `order_items_total`, `orders_vs_ods` и пять прежних проверок успешны; - [ ] ваш запрос с `hostName()` не нашел заказов на чужом шарде; -- [ ] повторный `etl_pipeline` сохранил деловые данные, проверки прошли. +- [ ] после повторного `etl_pipeline` поля заказов остались прежними, проверки прошли. diff --git a/docs/labs/6-1-event-parsing.md b/docs/labs/6-1-event-parsing.md index 8c09c92..d400cf6 100644 --- a/docs/labs/6-1-event-parsing.md +++ b/docs/labs/6-1-event-parsing.md @@ -1,9 +1,9 @@ -# Разберите сырые события рядом со стендом +# Разбор сырых событий -**Чему учит:** пакетно разобрать сообщения отдельно от живого приема и -построчно проверить свой результат по нетронутому ODS. +В этой работе вы разберете сохраненные сообщения о событиях в свою таблицу. +Каждую полученную строку сравните с результатом обработки в стенде. -## Сначала прочитать +## Подготовка - [формат события](../formats/clickstream-event.md); - [справочник хранилища](../architecture/storage.md), разделы про имена @@ -15,26 +15,26 @@ Готовый стендовый разбор находится в `sql/ddl/30-ods-views.sql`. Откройте его после собственной сверки: в нем лежит ответ на основную часть задания. -## Что предстоит сделать +## Порядок работы -Создайте в базе `default` одну свою таблицу годных событий и наполните ее одним -пакетным `INSERT SELECT` из `stg.hits_raw_dist`. Работайте с первой нодой под -пользователем `etl`. +Создайте в базе `default` свою таблицу для правильно заполненных событий. +Выберите небольшой набор сообщений из `stg.hits_raw_dist`, разберите их +и загрузите в таблицу одним `INSERT SELECT`. Работайте с первым узлом под +пользователем `etl`. Затем сравните результат с событиями в ODS. -Матвью `ods.event_mv` и `ods.event_errors_mv` продолжают принимать живой -поток. Оставьте их, STG, ODS, Kafka и даги без изменений. Пересоздание матвью -на работающем стенде оставило бы зазор в приеме, а для пакетного опыта оно не -нужно. +Материализованные представления `ods.event_mv` и `ods.event_errors_mv` +продолжают обрабатывать поступающие сообщения. Оставьте их, STG, ODS, Kafka +и даги без изменений. Если пересоздать представления на работающем стенде, +часть сообщений может остаться без обработки. Дайте своей таблице имя, которое не пересечется с чужой работой, и запишите -его вместе с запросами. В конце вы удалите только эту таблицу. +его вместе с запросами. -## Выберите сырой срез +## Выбор исходных сообщений STG хранит строки примерно три дня по `_load_ts`. Эта колонка отмечает время -поступления в хранилище. Модельный день события лежит в `EventDate`. Сначала -найдите среди недавних данных завершившийся интервал со строками. Посмотрите -счет по минутам: +поступления в хранилище. Модельный день события лежит в `EventDate`. +Посмотрите, сколько строк поступило за каждую из последних минут: ```sql SELECT @@ -48,7 +48,8 @@ ORDER BY minute DESC; ``` Выберите короткий интервал `[slice_from, slice_to)`, который уже закончился и -дает удобное для построчной проверки число строк. Если одна минута велика, +дает удобное для построчной проверки число строк. Этот набор строк дальше +называется срезом. Если за одну минуту пришло слишком много строк, сузьте ее точными границами `_load_ts`. Если недавних строк нет, откройте Airflow. При включенном `world_live` посмотрите его текущий запуск и дочерний `world_live_day`: дождитесь свежих строк от текущего или следующего планового @@ -64,9 +65,9 @@ Airflow. При включенном `world_live` посмотрите его т Во всех дальнейших запросах читайте `stg.hits_raw_dist` только с этими границами. Новые строки с более поздним `_load_ts` в срез не попадут. Повторите -опыт до очистки по TTL; после нее прежний срез уже не восстановить из ODS. +опыт до удаления исходных строк по TTL; после него прежний срез уже не восстановить из ODS. -## Соберите собственную таблицу +## Таблица для результата Таблица содержит все 47 полей источника с именами и типами `ods.event_rep`, а также `_load_ts`. Тип `purchaseRevenue` возьмите из ODS: @@ -75,14 +76,15 @@ Airflow. При включенном `world_live` посмотрите его т Обработайте повторные доставки так же, как ODS. Возьмите `ReplacingMergeTree(_load_ts)`, партицию `EventDate` и полный ключ замены: `(CounterID, EventDate, intHash32(ClientID), WatchID)`. Достаточно одной -локальной таблицы: загрузка и сверка в этом опыте выполняются на одной ноде. +локальной таблицы: загрузка и сверка в этом опыте выполняются на одном узле. -`WatchID` — логический идентификатор события, уникальный по -[договору генератора](../specs/2026-08-01-generator.md). Физический ключ ODS -шире: в `ReplacingMergeTree` версиями считаются строки с одинаковым полным -ключом сортировки. Используйте его при загрузке и сверке. +`WatchID` обозначает событие и уникален по +[правилам генератора](../specs/2026-08-01-generator.md). Но `ReplacingMergeTree` +считает версиями строки, у которых совпадает весь ключ сортировки. +Поэтому для загрузки и сверки используйте все четыре части ключа ODS, +перечисленные выше. -## Разберите годные строки +## Разбор сообщений Для каждой строки среза проверьте те же условия приема, что и на стенде: @@ -92,8 +94,10 @@ Airflow. При включенном `world_live` посмотрите его т в ожидаемые типы. Напишите разбор, который не прерывает пакет из-за одной грязной строки. Сами -выберите способ безопасного извлечения пяти опорных полей, форму общих выражений -и место фильтра. Остальные поля извлеките по описанию формата. +выберите, как извлечь пять перечисленных полей без ошибки запроса, какие +выражения вынести в общую часть и где отфильтровать неподходящие строки. +Эти пять полей дальше называются опорными. Остальные поля извлеките +по описанию формата. Перенесите `_load_ts` из STG без пересчета: это время исходной доставки и колонка версии. Время запуска вашего запроса сюда не входит. Вставьте в свою @@ -114,25 +118,25 @@ Airflow. При включенном `world_live` посмотрите его т условия отбора на этом срезе, но не классификатор ошибок. Подкладывать грязные сообщения ради задания не требуется. -## Приведите стороны к одному зерну +## Подготовка к сравнению -### Учтите повторные доставки +### Повторные доставки В STG одна строка представляет доставку. Повтор сообщения остается второй строкой. В `ods.event_v` одна строка представляет актуальное событие, потому что представление читает физическую таблицу с `FINAL`. -Свою сторону тоже читайте с `FINAL`. Ожидаемый счет равен числу различных -полных ключей среди годных доставок. Разница между числом доставок и этим -счетом показывает повторы в исходном срезе. +Свою таблицу тоже читайте с `FINAL`. Число строк должно совпасть с числом +различных полных ключей среди годных исходных сообщений. Разница между числом +годных доставок и этим числом показывает повторы в исходном срезе. -### Выберите события ODS для сравнения +### События ODS для сравнения Теперь нужно найти в ODS события из выбранного среза. Список стройте по исходным доставкам: если взять его из своей таблицы, ошибочно отвергнутое событие исчезнет с обеих сторон сверки и потеря останется незамеченной. -Снова возьмите весь исходный STG-срез. По координатам Kafka исключите +Снова возьмите все выбранные строки STG. По координатам Kafka исключите доставки, которые есть в `ods.event_errors_dist`. У оставшихся извлеките `WatchID` без риска прервать запрос и выберите эти события из свежего `ods.event_v`. Свое условие отбора здесь не применяйте: решение о годности @@ -141,36 +145,38 @@ Airflow. При включенном `world_live` посмотрите его т `WatchID` связывает доставку с событием в ODS. Для удаления повторов и сравнения полей дальше используйте полный физический ключ из схемы таблицы. -## Сверьте 47 полей +## Сверка полей Соедините свою таблицу и выбранные строки `ods.event_v` через `FULL OUTER JOIN` по полному ключу замены. Полное соединение сохраняет строки, -которых нет на одной из сторон. Разделите результат на три исхода: +которых нет на одной из сторон. Найдите три вида расхождений: - строка есть только в ODS — вы ее потеряли; - строка есть только у вас — вы приняли лишнее или иначе разобрали часть ключа; - ключ есть с обеих сторон, но различается хотя бы одно из 47 полей. -ClickHouse по умолчанию заполняет отсутствующую сторону значениями типов. +ClickHouse по умолчанию заполняет отсутствующую сторону значениями по умолчанию +для соответствующих типов, например нулем или пустой строкой. Включите для сверки `join_use_nulls = 1` или добавьте явный признак присутствия в каждую сторону. Не определяйте отсутствие по значениям полей события. Сравните все поля явно, включая массивы, `ecommerce` и точное десятичное -`purchaseRevenue`. Удобный вид диагностики выберите сами: важно, чтобы по -результату можно было назвать отличающиеся колонки одной строки. Выведите число -строк каждого из трех исходов. В верном разборе все три числа равны нулю. +`purchaseRevenue`. Выберите, как показать расхождения, чтобы было видно, +какие колонки различаются в конкретной строке. Выведите число строк каждого +из трех видов расхождений. В верном разборе все три числа равны нулю. `_load_ts` не входит в 47 полей источника и не проверяет JSON-разбор. -Проверьте его отдельно. Свяжите каждую строку своей таблицы с меткой одной из -доставок того же полного ключа в исходном срезе. После `FINAL` остается самая -поздняя метка этого ключа внутри среза. С ODS ее не сравнивайте: там может +Проверьте его отдельно. Для каждой строки своей таблицы найдите самую позднюю +метку доставки того же полного ключа в исходном срезе. Она должна совпасть +с `_load_ts`, который вы читаете после `FINAL`. С ODS эту метку не сравнивайте: там может храниться более поздняя доставка того же события из-за границ среза. -## Расследуйте расхождение +## Поиск ошибок разбора -Если сверка не пуста, возьмите одну строку. Вернитесь к независимому списку -ключей и исходному `raw`, выведите рядом координаты Kafka и `_load_ts`. +Если нашлись расхождения, выберите одну строку. Найдите ее ключ в списке, +который вы получили из STG, и исходное сообщение `raw`. Выведите рядом +координаты Kafka и `_load_ts`. Для измененной строки назовите поля, которые расходятся, и сравните каждое с сырой строкой. Для пропавшей или лишней строки проверьте пять опорных полей и @@ -178,28 +184,28 @@ ClickHouse по умолчанию заполняет отсутствующую `ods.event_errors_dist`, посмотрите `error_class`. Исправьте разбор, удалите и заново создайте только собственную таблицу, затем -повторите вставку того же среза. Остановитесь, когда счет исходных доставок по +повторите вставку того же среза. Остановитесь, когда число исходных доставок по прежним границам не изменился, число годных и отвергнутых строк в сумме дает -число исходных доставок, а построчная сверка пуста. +число исходных доставок, а построчная сверка не находит расхождений. -## Чего не подтверждает сравнение с ODS +## Ограничения проверки `ods.event_v` — образец для сравнения. Оба разбора могли повторить одну ошибку. Для пяти опорных полей у вас есть проверка преобразования в нужный тип и исходный JSON. Остальные 42 поля можно вручную сопоставить с `raw`, однако второго независимого источника для всех значений на стенде нет. -## Уберите свой объект +## Сохранение результата и удаление таблицы Сохраните DDL таблицы, запрос разбора, границы среза, число исходных, годных и отвергнутых строк и итоговую сверку без расхождений. Запишите также, чего не подтверждает сравнение с ODS и как TTL ограничивает время для повторения опыта. Затем удалите свою таблицу командой `DROP TABLE` с ее точным именем. Убедитесь через `system.tables`, что объекта больше нет. -Прикладные строки стенда вы не меняли, поэтому удалять данные из STG, ODS или +Данные стенда вы не меняли, поэтому удалять строки из STG, ODS или Kafka после опыта не нужно. -## Что должно получиться +## Результат работы - [ ] у вас сохранены границы выбранного среза, число строк и координаты доставки по партициям Kafka; diff --git a/docs/labs/7-1-monitoring.md b/docs/labs/7-1-monitoring.md index 4929a47..a983c80 100644 --- a/docs/labs/7-1-monitoring.md +++ b/docs/labs/7-1-monitoring.md @@ -1,9 +1,10 @@ # Мониторинг: куда смотреть -**Чему учит:** начать расследование с подходящего экрана, найти источник -нагрузки и объяснить свежесть данных через путь доставки. +Вы познакомитесь с дашбордами мониторинга и проведете два расследования: +найдете запросы, которые создают нагрузку, и выясните, почему события сайта +и заказы обновляются с разной частотой. -## Перед началом +## Знакомство с дашбордами Откройте Grafana и Airflow по адресам из раздела [«Адреса и пароли»](../../README.md#адреса-и-пароли). Для первого знакомства в Grafana @@ -11,7 +12,7 @@ - «Данные» — путь сообщений от источника до хранилища; - «Кластер» — процессор, память, диски и внутренняя работа ClickHouse; -- «Запросы» — выполненные запросы, их стоимость и ошибки. +- «Запросы» — выполненные запросы, затраченные на них ресурсы и ошибки. Откройте все три дашборда и запишите по одному вопросу, на который каждый из них помогает ответить. «Данные» оставьте открытым в соседней вкладке. @@ -19,7 +20,7 @@ Проведите два расследования на работающем стенде. Службы останавливать и данные портить не понадобится. -## Расследование 1. Кто читает больше всех +## Расследование 1. Какие запросы читают больше всего данных Начните в спокойный момент: не запускайте одновременно свои тяжелые запросы. Откройте дашборд «Запросы». На панели «Тяжёлые запросы: что прочитало больше @@ -27,8 +28,8 @@ Сохраните: -- границы окна и время запроса; -- место запроса в списке, пользователя и ноду; +- начало и конец выбранного периода, время запроса; +- место запроса в списке, пользователя и узел; - текст запроса; - длительность, память, число прочитанных и отданных строк. @@ -44,22 +45,25 @@ В меню панели «Кто спрашивает: сколько запросов и какой у них аппетит» откройте Inspect, затем Query. Ответьте на два вопроса: -1. Почему журнал читают сразу с обеих нод? -2. Почему из счета исключены строки начала запроса и работа шардов? +1. Почему журнал читают сразу с обоих узлов? +2. Почему при подсчете запросов исключают записи о начале выполнения и + записи о выполнении частей запроса на шардах? Проверьте второе объяснение по панели ошибок: ее фильтр не исключает записи -шардов. Объясните, почему различаются вопросы панелей, а не только их SQL. +шардов. Объясните, на какие разные вопросы отвечают эти две панели +и зачем им разные условия отбора. ## Расследование 2. Как меняется свежесть заказов Откройте дашборд «Данные». Панель «Свежесть: сколько прошло с последней -доехавшей строки» показывает текущее число, а не историю. До наблюдения -нарисуйте ожидаемую форму свежести событий и заказов в течение одного живого -дня. Отдельно отметьте момент доставки слепка заказов. +доехавшей строки» показывает, сколько времени прошло с последней загрузки. +Истории значений на ней нет. До наблюдения нарисуйте, как, по вашему ожиданию, +будут меняться эти числа для событий и заказов в течение одного запуска +`world_live_day`. Отдельно отметьте ожидаемый момент доставки слепка заказов. Если `world_live` снят с паузы, наблюдайте его текущий или следующий -`world_live_day`: новый ручной запуск встанет в очередь. Если выключатель на -паузе и активного живого дня нет, запустите один `world_live_day` вручную. +`world_live_day`: новый ручной запуск встанет в очередь. Если `world_live` на +паузе и активного `world_live_day` нет, запустите один `world_live_day` вручную. Модельные сутки занимают около 24 реальных минут. Запишите несколько наблюдений до конца дня и сразу после него: @@ -69,29 +73,29 @@ | | | | | Дашборд обновляется раз в минуту, поэтому точного совпадения момента -доставки со строкой таблицы ждать не нужно. Рост и сброс свежести заказов -перенесите на свой набросок. Объясните форму линии: какую метку времени +доставки с записью наблюдения ждать не нужно. Перенесите рост и сброс +показателя заказов на свой рисунок. Объясните форму линии: какую метку времени вычитает панель из текущего времени, почему число растет и какое событие его -сбрасывает. Модельную дату сыгранного дня подпишите отдельно от реального +сбрасывает. Модельную дату наблюдаемого дня подпишите отдельно от реального времени наблюдения. -Подтвердите сброс двумя следами: +Подтвердите, что сброс совпал с загрузкой заказов: - в Airflow завершился прием слепка `orders_ingest`; - на панели «Дневной слепок заказов: какой доехал, когда и после какой паузы» появилась новая верхняя строка. -Если вы начали после доставки, дождитесь следующего дня либо честно запишите, -какую часть формы увидели. Одно текущее значение не доказывает ни рост, ни +Если вы начали после доставки, дождитесь следующего дня либо запишите, +какую часть изменений успели увидеть. Одно текущее значение не доказывает ни рост, ни сброс. -## Объясните путь данных +## Источники показателей Не открывая справочник, объясните своими словами: -- почему свежесть считают в ODS, а поток — в STG; +- почему время с последней загрузки считают по ODS, а число поступивших сообщений по STG; - почему знаменатель доли брака берут из STG; -- почему у числителя и знаменателя доли брака одно окно. +- почему числитель и знаменатель доли брака берут за один период. Свяжите ответы с тем, что вы видели на панелях. Затем сверьте объяснение с [главой о мониторинге](../handbook/monitoring.md) и справочниками дашбордов @@ -99,20 +103,20 @@ [«Данные»](../architecture/monitoring/data.md). Устройство живого дня и доставки слепка описано в [главе об Airflow](../handbook/airflow-dags.md). -## Что должно получиться +## Результат работы - [ ] для каждого из трех дашбордов записан вопрос, на который он отвечает; -- [ ] у вас сохранены окно, текст и показатели верхнего запроса; +- [ ] у вас сохранены выбранный период, текст и показатели верхнего запроса; вы выяснили, кто его отправил и зачем; - [ ] вы нашли запрос открытых дашбордов, который прочитал больше всего строк, и панель, которая его отправила; -- [ ] вы можете объяснить, зачем журнал собирают с двух нод и какие записи +- [ ] вы можете объяснить, зачем журнал собирают с двух узлов и какие записи учитывают при подсчете запросов; - [ ] у вас есть прогноз свежести, сделанный до наблюдения, и несколько замеров; рост и сброс на рисунке объяснены через время последней доставки; - [ ] вы подтвердили доставку заказов в Airflow и на панели слепков, подписали модельную дату отдельно от реального времени наблюдений; - [ ] вы можете объяснить выбор слоев для свежести, потока и доли брака - и почему числитель и знаменатель берут за одно окно; + и почему числитель и знаменатель берут за один период; - [ ] после наблюдений службы продолжают работать, данные и стендовые дашборды остались без изменений. diff --git a/docs/labs/README.md b/docs/labs/README.md index 7b82156..4a5b743 100644 --- a/docs/labs/README.md +++ b/docs/labs/README.md @@ -1,84 +1,121 @@ # Учебные задания -Программа состоит из двух частей: основная включает семь ступеней, которые вы -проходите по порядку, а факультативы выбираете по желанию. Причины такого -порядка и формы заданий описаны в [программе учебных лаб](../specs/2026-09-01-labs-program.md#2-устройство-программы-основная-программа-и-факультативы). +Вы будете достраивать хранилище небольшого интернет-магазина. Стенд уже +загружает события сайта и заказы, обрабатывает их и строит несколько отчетов. +Сначала вы подготовите данные для новых отчетов, затем разберетесь, как +работают расчеты и загрузка, и перепишете часть из них. -## Основная программа +Основная программа состоит из семи ступеней. Проходите их по порядку, +факультативы выбирайте по желанию. Перед первым заданием прочитайте главу +[«Хранилище и его слои»](../handbook/storage-layers.md) и выполните запросы +из нее: они покажут данные, с которыми предстоит работать. -| Ступень | Задание | Чему учит | +## Порядок заданий + +| Ступень | Задания | Содержание | |---|---|---| -| 1 | Пять витрин слоя DM | построить результат для потребителя хранилища; уроки каждой витрины — в таблице ниже | -| 2 | [Свой график в Superset](2-1-superset-chart.md) | превратить свой результат из хранилища в отчет в BI | -| 3 | [Нарежьте сессии сами](3-1-sessions-by-hand.md) | сверить число сессий в своей нарезке с эталоном и найти события, из-за которых оно расходится | -| 4 | [Перепишите нарезку стенда](4-1-session-pipeline.md) | переписать расчет сессий в готовом конвейере и проверить по исходным событиям | -| 5 | [Научите проверки замечать ошибки в заказах](5-1-order-checks.md)
[Перепишите разбор заказов в ODS](5-2-order-parsing.md)
[Перепишите сборку заказов в DDS](5-3-order-pipeline.md) | написать проверки качества до рискованного изменения, затем переписать разбор и сборку заказов | -| 6 | [Разберите сырые события рядом со стендом](6-1-event-parsing.md) | изучить разбор потока отдельно от живого приема и сверить его со стендовым результатом | -| 7 | [Мониторинг: куда смотреть](7-1-monitoring.md) | выбрать, куда и на что смотреть в живой системе | +| 1 | Пять витрин для отчетов | подготовить данные о событиях, страницах, визитах, ошибках загрузки и рекламе; задания перечислены ниже | +| 2 | [Свой график в Superset](2-1-superset-chart.md) | построить график по своей витрине и проверить числа на нем | +| 3 | [Сессии из событий](3-1-sessions-by-hand.md) | объединить действия посетителя в визиты и сравнить их с визитами источника | +| 4 | [Расчет сессий в стенде](4-1-session-pipeline.md) | включить свой расчет в готовую обработку данных и проверить результат | +| 5 | [Проверки качества заказов](5-1-order-checks.md)
[Разбор заказов в ODS](5-2-order-parsing.md)
[Сборка заказов в DDS](5-3-order-pipeline.md) | сначала написать проверки качества, затем с их помощью проверить свою обработку заказов | +| 6 | [Разбор сырых событий](6-1-event-parsing.md) | извлечь поля из сообщений и сравнить результат с готовым разбором стенда | +| 7 | [Мониторинг: куда смотреть](7-1-monitoring.md) | найти тяжелые запросы и разобраться, почему данные обновляются с разной частотой | -## Ступень 1: пять витрин слоя DM +## Ступень 1: витрины для отчетов -Слой DM в стенде построен наполовину: витрины трех сюжетов дашборда и контроль -качества уже построены, остальные пять витрин строите вы. Каждая из них — -самостоятельное задание: в нем есть состав витрины, зерно, источники и способ -проверки. +Витрина готовит данные для конкретного отчета. В стенде они находятся в базе +`dm`: это слой DM. Витрины выручки, трафика, сверки покупок с заказами и +сводка качества уже готовы. Вы добавите еще пять. -| № | Результат | Чему учит | +| № | Витрина | Содержание задания | |---|---|---| -| 1 | [`events_enriched_v`](1-1-events-enriched.md) | подготовить удобный для анализа набор полей, сохранив одну строку на событие | -| 2 | [`top_pages_daily`](1-2-top-pages-daily.md) | создавать материализованную витрину с дневной пересборкой в конвейере | -| 3 | [`session_overview`](1-3-session-overview.md) | хранить слагаемые для расчета средних и долей | -| 4 | [`dq_errors_daily`](1-4-dq-errors-daily.md) | считать брак по дню загрузки и хранить итоги дольше источника | -| 5 | [`utm_effectiveness`](1-5-utm-effectiveness.md) | читать `declared_` как предупреждение о сумме со слов клиента | +| 1 | [События для анализа: `events_enriched_v`](1-1-events-enriched.md) | выбрать нужные поля и добавить вычисляемые, сохранив одну строку на событие | +| 2 | [Показатели страниц по дням: `top_pages_daily`](1-2-top-pages-daily.md) | сохранить итоги в таблице и настроить их обновление через Airflow | +| 3 | [Визиты по устройствам: `session_overview`](1-3-session-overview.md) | хранить суммы и счетчики для расчета средних и долей | +| 4 | [Ошибки загрузки по дням: `dq_errors_daily`](1-4-dq-errors-daily.md) | сохранить число ошибок даже после удаления исходных строк | +| 5 | [Показатели рекламы: `utm_effectiveness`](1-5-utm-effectiveness.md) | различать суммы покупок из событий сайта и выручку по заказам магазина | -Общее устройство слоя — [справочник DM](../architecture/dm.md). Он отвечает на -вопросы «почему так», задание отвечает на вопрос «что сделать». +Начните с [первого задания](1-1-events-enriched.md): для него достаточно +одного представления. В заданиях 2–5 понадобятся таблицы и их обновление +по дням. К общим правилам ниже возвращайтесь по мере работы. -## Как принято на ступени 1 +## Создание и обновление витрин -**Читать — через `_v`.** Физические `_rep` и `_dist` служат загрузке и -обслуживанию, а публичный договор DDS — `dds.event_v` или `dds.session_v` — -остается прежним даже при изменении физических объектов. Чтение DDS -напрямую делает задания независимыми: соседняя витрина может быть еще не -построена. -Исключение — задание 4: у таблиц брака публичного договора нет. +### Источники данных -**Писать — в `_dist`.** Так строки распределяются по ключу шардирования. При -записи в локальную таблицу распределение зависит от узла, выполнившего -запрос, и при следующем запуске может измениться. Подробности — раздел -«Раскладка по шардам» [справочника хранилища](../architecture/storage.md). +События читайте из `dds.event_v`, сессии из `dds.session_v`. Суффикс `_v` +обозначает обычное представление: оно выполняет сохраненный запрос при +чтении. Такие представления сохраняют привычные имена и состав колонок, +когда внутреннее устройство таблиц меняется. -**Форма — по зерну.** На стенде агрегат с новым зерном принято хранить -готовым: потребитель читает его много раз без повторного расчета по -источнику. Создайте `dm.<имя>_rep` / `_dist`, донор `dm.<имя>_stage_rep` / -`_stage_dist` и публичное `dm.<имя>_v` с явным списком колонок. При прежнем -зерне достаточно представления: таблица дублировала бы данные. Доводы — -раздел «Таблица или представление» [справочника -DM](../architecture/dm.md). +Каждая учебная витрина читает готовый источник и не зависит от других +учебных витрин. В задании 4 источниками служат таблицы ошибок ODS: +представлений для них нет. -**Ключ шардирования — ваше решение.** Объясните, почему выбранный ключ не -дает существенного перекоса и почему более сложный ничего не добавит. -Объявите донор через `AS <цель>_rep` с ключом цели: иначе замена партиции -потеряет строку на одном шарде и удвоит на другом. Детали — раздел -«Раскладка по шардам» [справочника хранилища](../architecture/storage.md). -Пример обоснования — `dm.revenue_daily_dist` в +### Таблицы и представления + +В заданиях 2–5 вы группируете исходные строки. Например, в `top_pages_daily` +одна строка содержит итоги для одной страницы за день. То, чему соответствует +одна строка, называют **зерном**. На стенде итоги с новым зерном сохраняют +в таблицах. Отчеты читают готовые числа. + +Новый расчет сначала записывают в промежуточную таблицу. Пока он готовится, +отчеты читают прежние данные из основной. Затем готовыми разделами +промежуточной таблицы заменяют разделы основной. Раздел называется +**партицией**; в этих заданиях он содержит данные одного дня. +Промежуточную таблицу также называют **донором**. + +На примере `top_pages_daily` пять объектов выглядят так: + +| Объект | Для чего нужен | +|---|---| +| `dm.top_pages_daily_rep` | Хранит рассчитанные строки на каждом шарде | +| `dm.top_pages_daily_dist` | Читает строки со всех шардов и распределяет запись между ними | +| `dm.top_pages_daily_stage_rep` | Хранит новый расчет до замены данных в основной таблице | +| `dm.top_pages_daily_stage_dist` | Распределяет запись нового расчета между шардами | +| `dm.top_pages_daily_v` | Дает отчетам доступ к готовой витрине; явно перечисляет ее колонки | + +### Распределение по шардам + +Каждый из двух шардов стенда хранит свою часть данных. Записывайте строки +через таблицы `_dist`: они выбирают шард по ключу шардирования. Прямая запись +в `_rep` оставит строки на том узле, где выполнен запрос. + +Ключ для своей витрины выберите сами. Проверьте распределение строк между +шардами и объясните, почему выбранного ключа достаточно. +Пример обоснования для `dm.revenue_daily_dist` есть в [`sql/ddl/55-dm-tables.sql`](../../sql/ddl/55-dm-tables.sql). -**Где лежит код.** DDL заданий — в `sql/ddl/70-labs.sql`. -При создании мира он применяется после стендовых файлов. -В уже работающем стенде сохранение файла и повторный `make up` новый DDL -не применят — выполните его сами, как описано ниже. В -`sql/dm/<витрина>_scope.sql` определяются дни прогона, в `_rebuild.sql` они -собираются в донор, в `_replace.sql` заменяется партиция. Задачу сборки -добавьте в [`dags/dm_transform.py`](../../dags/dm_transform.py): без нее -конвейер не обновит витрину. Представлению достаточно `CREATE VIEW`. +Создайте промежуточную таблицу через `AS <цель>_rep`, сохранив структуру и +ключи основной. У обеих таблиц `_dist` должен быть одинаковый ключ +шардирования: замена партиции выполняется внутри каждого шарда и не +перераспределяет строки. Подробности есть в разделе +[«Раскладка по шардам»](../architecture/storage.md#раскладка-по-шардам). -**Образец — витрина `revenue_daily`.** У нее есть пять объектов, донор, -сборка, замена дневных партиций и задача в конвейере. Используйте ее как -образец всего порядка пересборки. Состав колонок берите из своего ТЗ, иначе -получите показатели другой витрины. +### SQL-файлы и запуск расчета -## Как применить свой DDL +Команды создания таблиц и представлений (DDL) сохраняйте в +`sql/ddl/70-labs.sql`. При создании мира этот файл выполняется после +стендовых файлов. В работающем стенде примените команды сами по инструкции +ниже: сохранение файла и повторный `make up` их не выполнят. + +Для витрины с таблицами нужны три файла в `sql/dm/`: + +- `<витрина>_scope.sql` определяет, какие дни пересчитать; +- `<витрина>_rebuild.sql` рассчитывает их и записывает в промежуточную таблицу; +- `<витрина>_replace.sql` заменяет партицию одного дня в основной таблице. + +Добавьте запуск расчета в [`dags/dm_transform.py`](../../dags/dm_transform.py), +чтобы витрина обновлялась вместе с остальными. Для представления из задания 1 +достаточно выполнить `CREATE VIEW`. + +Образец этого порядка работы есть у готовой витрины `revenue_daily`. +Ее таблицы, SQL-файлы и задача Airflow разобраны в +[справочнике DM](../architecture/dm.md#dmrevenue_daily_v). +Состав колонок и выбор дней для пересчета берите из своего задания. + +## Выполнение SQL в DBeaver В DBeaver создайте подключение по [инструкции в README](../../README.md#подключиться-из-dbeaver), но укажите пользователя `etl` и пароль из `CLICKHOUSE_ETL_PASSWORD` в `.env`. @@ -88,37 +125,33 @@ DM](../architecture/dm.md). Создайте файл `sql/ddl/70-labs.sql` и сохраняйте в нем DDL своих витрин. Откройте его в SQL-редакторе этого подключения и выполните DDL текущего задания. Создавайте объекты с -`ON CLUSTER clickstream_cluster`, чтобы они появились на обеих нодах; +`ON CLUSTER clickstream_cluster`, чтобы они появились на обоих узлах; образец для представления — [`sql/ddl/60-dm-views.sql`](../../sql/ddl/60-dm-views.sql). Сохраненный файл позволит воссоздать ваши объекты при пересборке мира. -## Самопроверка ступени 1 +## Проверка витрин -Даг `homework_check` запускается руками, расписания у него нет. Задача -витрины красная, пока витрины нет или счет не сходится, и зеленая, когда -собрано верно. В сводку качества конвейера `dm.dq_summary` он не пишет: это -инструмент лабы, а не история качества стенда. +В Airflow есть даг `homework_check` с отдельной задачей для каждой учебной +витрины. Запускайте его вручную: расписания у него нет. Задача завершится +ошибкой, если витрина еще не создана или ее данные расходятся с ожидаемыми. +Причину и примеры расхождений ищите в журнале задачи. Проверки еще не +выполненных заданий тоже будут красными. -Для каждой витрины проверка независимо рассчитывает ожидаемый результат -прямо по ее источнику. У четырех заданий источник -эталона тот же, что у витрины, `dds.session_v` или `dds.event_v`; у задания -4 это таблицы брака ODS, потому что в DDS нет сущности брака. Обе стороны -соединяются по деловому ключу через `FULL OUTER JOIN`, и считается число -строк, где стороны разошлись или одной стороны нет. Ноль — зеленый. Полное -соединение сохраняет ключи обеих сторон: при внутреннем пропавший день или -ключ не попал бы в результат, и потеря осталась бы незамеченной. +Проверка заново рассчитывает показатели по источнику и сравнивает их с вашей +витриной. Строки сопоставляются по полям, которые их определяют: например, +по дню и странице. `FULL OUTER JOIN` позволяет найти и разные значения, и +строки, которых нет с одной стороны. `INNER JOIN` пропустил бы отсутствующие +строки. Когда расхождений нет, задача становится зеленой. -Раздел «Как проверить себя» каждого задания по витрине называет деловой ключ и -сравниваемые величины. Тот же прием в стендовом исполнении — [даг -`dq_check`](../../dags/dq_check.py) и запросы в [`sql/dq/`](../../sql/dq). +`homework_check` не сохраняет результаты в сводку качества `dm.dq_summary`. -Запросы проверок лежат в `sql/dq/`. Откройте их после собственного решения: +Запросы проверок лежат в [`sql/dq/`](../../sql/dq/). Откройте их после собственного решения: они заново рассчитывают показатели витрины, поэтому содержат ответ на существенную часть задания. Сравните свой способ расчета с готовым. -## Факультативы +## Задания по желанию -Эти задания вы берете по желанию. Полный список сюжетов — в разделе +Полный список сюжетов — в разделе [«Факультативы — пять разделов»](../specs/2026-09-01-labs-program.md#4-факультативы--пять-разделов) программы. @@ -129,16 +162,17 @@ DM](../architecture/dm.md). - [Куда пропали покупки](optional/missing-purchases.md): найти заказы без события, выбрать источник витрины и условие проверки полноты. - [Почему повтор покупки завышает выручку](optional/duplicate-purchases.md): - выбрать зерно и ключ расчета, проверить его на повторных событиях. + рассчитать выручку с одной строкой на заказ и проверить результат на + повторных событиях. - [Какая позиция объясняет разницу в сумме](optional/order-amount-difference.md): - разобрать позиции без потери точности и локализовать расхождение источников. + разобрать товары заказа без потери точности и найти причину разницы в суммах. - [Почему вчерашняя выручка меняется сегодня](optional/revenue-over-time.md): - обосновать окно пересчета витрины и требования к истории заказов. + определить, за какие дни пересчитывать выручку и какую историю заказов сохранять. Первые три работы используют готовые данные. Четвертая продвигает мир через Airflow; для нее вы сохраняете состояния заказов после каждого модельного дня. -### Тонкий анализ данных +### Данные о посетителях Открывается после ступени 1. @@ -146,9 +180,7 @@ Airflow; для нее вы сохраняете состояния заказо сохранить историю связей, сравнить расчеты на прежних сессиях и определить, какие сведения нужны для воспроизведения результата. -Обогащение отчета своим словарем регионов пока описано только в программе. - -### Superset и BI +### Отчеты в Superset Открывается после ступени 2. Два самостоятельных задания: @@ -157,21 +189,15 @@ Airflow; для нее вы сохраняете состояния заказо - [В DBeaver работает, в Superset нет](optional/superset-missing-object.md): выяснить, почему клиенты по-разному видят один объект хранилища. -### Инженерия стенда +### Загрузка и обновление данных -Открывается после ступени 1. Прием и хранение данных, раскладка по шардам, -повторная обработка дня и обновление витрин. +Открывается после ступени 1. - [Как обновлять витрину без полной пересборки](optional/incremental-daily-traffic.md): продолжить работу о связях пользователей, пересчитать затронутые дни и проверить повтор незавершенной загрузки. Повышенная сложность. -### Что делать, когда стенд сломался - -Открывается после ступени 7. Поломки стенда и инструкции по восстановлению -по итогам расследований; весь раздел — повышенная сложность. - -## Уборка +## Удаление временных объектов После опытов удаляйте свои временные объекты. Если удалили учебную витрину, ее задача в `homework_check` должна снова завершиться ошибкой: отсутствие diff --git a/docs/labs/optional/duplicate-purchases.md b/docs/labs/optional/duplicate-purchases.md index 09686ce..c26e3d3 100644 --- a/docs/labs/optional/duplicate-purchases.md +++ b/docs/labs/optional/duplicate-purchases.md @@ -1,19 +1,20 @@ # Почему повтор покупки завышает выручку -**Чему учит:** выбрать зерно и ключ расчета, чтобы повторные события -не умножали денежную сумму. +Несколько событий покупки могут относиться к одному заказу. Вы проверите, +как они завышают сумму, и составите расчет, который учитывает каждый заказ +один раз. Факультатив доступен после ступени 1. Работайте в DBeaver пользователем `analyst` по [инструкции](../../../README.md#подключиться-из-dbeaver). Дождитесь завершения `etl_pipeline`; оставьте `world_live` на паузе и дождитесь уже начатых прогонов, чтобы данные не менялись между запросами. -## Проверьте расчет по событиям +## Расчет по событиям покупки В ODS уже устранены повторные доставки событий. Достаточно ли этого, чтобы сложить суммы всех `purchase` для витрины заказов? Проверьте на данных. -Возьмите один закрытый день: его дата должна быть не позднее +Возьмите день, за который уже получены все слепки заказов: его дата должна быть не позднее `max(snapshot_date) - 6` из `ods.order_v`. Во всех запросах к `dds.event_v` отбирайте этот `event_date` и `event_type = 'purchase'`. @@ -22,12 +23,12 @@ Сначала посчитайте число событий и сумму всех заявленных сумм. Затем найдите номера заказов, которые встречаются несколько раз. Если таких нет, выберите -другой закрытый день. +другой день с полученными слепками. Для двух повторившихся покупок выпишите `event_id`, `client_id`, `event_time`, номер заказа и сумму. Сопоставьте число событий и заказов в `dds.order_v`. -## Соберите расчет по заказам +## Расчет по заказам Напишите запрос с одной строкой на номер заказа. Сохраните в результате номер, число исходных событий и одну заявленную сумму. До выбора суммы @@ -38,7 +39,7 @@ разных номеров в исходных событиях. Отдельно проверьте свое решение на случае двух разных заказов с одинаковой суммой: сколько денег оно учтет? -## Найдите заказы, из-за которых сумма завышена +## Заказы с повторными событиями Возьмите один повторившийся номер заказа. Сколько денег он добавил в первый расчет и сколько — во второй? Подтвердите разность его событиями. @@ -58,22 +59,23 @@ его статус и [порядок присвоения классов](../../architecture/dm.md#dmpurchase_vs_orders_v). Класс витрины не заменяет поиск всех повторов в событиях. -## Определите границы расчета +## Сравнение с выручкой магазина Сложите `revenue` из `dm.revenue_daily_v` за тот же `report_date` по всем категориям. Эта витрина считает стоимость позиций оплаченных заказов, без скидки и доставки. -Сравните результат с исправленной клиентской суммой. Совпадение не требуется: +Сравните результат с суммой по событиям после устранения повторов. Совпадение не требуется: удаление повторов устраняет одну причину расхождения. Найдите хотя бы один заказ, который объясняет, почему деньги трекера и магазина все еще могут различаться. Для этого сопоставьте события, заказ и готовую сверку. -Рядом со своим запросом укажите его зерно и ключ. Обоснуйте, где нужна одна -строка на событие, а где — одна на заказ: следует ли ради этого расчета +Рядом с запросом укажите, чему соответствует одна строка результата +и какие поля ее определяют. Обоснуйте, где нужна одна строка на событие, +а где одна на заказ: следует ли ради этого расчета удалять повторные покупки из `dds.event_v`? Используйте найденные `event_id`. -## Что должно получиться +## Результат работы - [ ] сохранен запрос с одной строкой на заказ, числом событий и суммой; - [ ] уникальность ключа и полнота набора номеров проверены по источнику; diff --git a/docs/labs/optional/identity-over-time.md b/docs/labs/optional/identity-over-time.md index 7140157..c4cbd43 100644 --- a/docs/labs/optional/identity-over-time.md +++ b/docs/labs/optional/identity-over-time.md @@ -1,7 +1,8 @@ # Как новые заказы меняют отчет о посетителях -**Чему учит:** сохранять историю связей посетителей с покупателями -и использовать ее для воспроизведения прежнего расчета. +Вы сохраните историю связей посетителей сайта с покупателями магазина. +На ее основе проверите, как новые связи меняют старые отчеты и что нужно, +чтобы повторить прежний расчет. Факультатив доступен после ступени 1. Понадобятся Airflow и DBeaver с подключением `student` по [инструкции](../../../README.md#подключиться-из-dbeaver). @@ -9,13 +10,14 @@ ваши таблицы будут доступны только на узле этого подключения. Если предложенное имя таблицы занято, выберите свое. -В событиях сайта посетитель обозначен кукой `client_id`, а в заказах -покупатель — `user_id`. Сопоставив событие покупки с заказом, можно связать -эти идентификаторы. Тогда и к прежним визитам этой куки можно добавить +В событиях сайта посетитель обозначен `client_id` из cookie браузера, +а в заказах покупатель обозначен `user_id`. Сопоставив событие покупки +с заказом, можно связать эти идентификаторы. Тогда и к прежним визитам +этого `client_id` можно добавить пользователя магазина. Вы проверите, как появление такой связи меняет число известных пользователей в отчете за прошедший день. -## Сопоставьте посетителей с покупателями +## Связи посетителей с покупателями Поставьте `world_live` на паузу, дождитесь завершения начатых прогонов генерации и обработки. Запустите `etl_pipeline` и дождитесь успеха. @@ -32,7 +34,8 @@ найдите самое раннее время события покупки, которое удалось сопоставить с заказом. Назовите эту колонку `first_purchase_at`. В событии `purchase` массив `purchase_id` содержит один номер заказа. -Статус заказа не ограничивайте: отмена покупки не меняет владельца куки. +Статус заказа не ограничивайте: отмена покупки не меняет найденную связь +посетителя с покупателем. Источники распределены по разным ключам. Для соединения используйте `GLOBAL INNER JOIN`, заключив выборки событий и заказов в подзапросы. @@ -43,7 +46,7 @@ Для одной пары покажите номера заказов и события, по которым получили `first_purchase_at`. -## Сохраните список связей после каждого модельного дня +## История связей Создайте таблицу: @@ -84,7 +87,7 @@ SELECT max(event_date) AS observation_day FROM dds.event_v; не устраняет повторы вставки. Сохраните и проверьте список перед тем, как запускать следующий модельный день. -## Сравните дату покупки с датой появления связи +## Дата покупки и дата появления связи Сравните списки за соседние даты: H0 с H1, затем H1 с H2. Найдите пары, которых не было в прежнем списке. Для нескольких таких пар покажите @@ -101,7 +104,7 @@ SELECT max(event_date) AS observation_day FROM dds.event_v; у вас нет более ранних копий списка. Для новой пары можно установить интервал между двумя сохранениями, но не точное время внутри него. -## Пересчитайте отчет за прежние дни +## Пересчет прежних отчетов Возьмите один и тот же набор сессий из `dds.session_v` за даты до H0 включительно. Постройте два дневных расчета: @@ -128,7 +131,7 @@ SELECT max(event_date) AS observation_day FROM dds.event_v; На примере найденных различий объясните, какие данные нужно сохранять, чтобы повторить прежний расчет. -## Что должно получиться +## Результат работы У вас есть запрос сопоставления посетителей с покупателями, три сохраненных списка связей и сравнение двух расчетов на одном наборе сессий. На конкретных diff --git a/docs/labs/optional/incremental-daily-traffic.md b/docs/labs/optional/incremental-daily-traffic.md index 195ee06..feaf0e7 100644 --- a/docs/labs/optional/incremental-daily-traffic.md +++ b/docs/labs/optional/incremental-daily-traffic.md @@ -1,7 +1,8 @@ # Как обновлять витрину без полной пересборки -**Чему учит:** выбирать дни для пересчета по новым данным, проверять результат -и повторять незавершенную загрузку. +Вы настроите обновление витрины только за дни, затронутые новыми данными. +Затем проверите, можно ли продолжить загрузку, которая успела записать +лишь часть результатов. Факультатив повышенной сложности продолжает работу [«Как новые заказы меняют отчет о посетителях»](identity-over-time.md). @@ -18,7 +19,7 @@ Удаление данных, исправления старых сессий и пересоздание мира потребовали бы другого способа обнаружения изменений. -## Создайте витрину и журнал успешных загрузок +## Витрина и журнал загрузок Создайте две локальные таблицы. Если имена заняты, выберите свои. @@ -52,7 +53,7 @@ ORDER BY observation_day; и успешной сверки. Максимальная дата в журнале укажет, какой список использовался в последней завершенной загрузке. -## Выполните первую загрузку целиком +## Первая загрузка Убедитесь, что `world_live` стоит на паузе и нет активных прогонов генерации и обработки. Запустите `etl_pipeline` и дождитесь успеха. Получите текущую @@ -79,7 +80,7 @@ ORDER BY observation_day; и различия обоих показателей. Учебную витрину читайте с `FINAL`. После совпадения запишите дату наблюдения в журнал один раз. -## Найдите дни, которые нужно пересчитать +## Выбор дней для пересчета Продвиньте мир на один день: `world_next_day` с `days = 1`, затем `etl_pipeline`; после каждого запуска дождитесь успеха. Сохраните новый @@ -90,7 +91,7 @@ ORDER BY observation_day; даты для пересчета без повторов. Он должен учесть два изменения: - в сессиях появились дни, которых еще нет в вашей витрине; -- в списке на H появились пары, которых не было в списке на P; у этих кук +- в списке на H появились пары, которых не было в списке на P; у этих `client_id` есть сессии, в том числе за прежние дни. Сначала выведите даты и выберите один прежний день: какая новая пара @@ -103,7 +104,7 @@ ORDER BY observation_day; найденными сравнением списков? Покажите пару, которую ограничение по дате покупки пропускает. -## Обновите выбранные дни +## Обновление витрины Напишите второй `INSERT INTO ... SELECT`. Для выбранных дат он должен рассчитать все сессии с полным списком связей на H. Прибавление числа новых @@ -117,7 +118,7 @@ ORDER BY observation_day; Запрос должен вернуть пустой результат, а витрина — остаться прежней. Повторно записывать H в журнал не требуется. -## Проверьте повторный запуск после частичной загрузки +## Повтор незавершенной загрузки Снова запустите `world_next_day` с `days = 1`, затем `etl_pipeline`, дожидаясь успеха каждого. Сохраните новый список связей с его датой H. @@ -133,7 +134,7 @@ ORDER BY observation_day; не означает, что витрина обновлена. Что изменится, если записать H в журнал до обновления витрины? -## Что должно получиться +## Результат работы Сохраните SQL первой загрузки, выбора дат, обновления и сверки. После двух новых модельных дней ваша витрина совпадает со стендовой, @@ -143,8 +144,9 @@ ORDER BY observation_day; Сравните число пересчитанных дней с полной загрузкой и перечислите, какие дополнительные данные пришлось хранить. Так можно оценить объем работы. На небольшом стенде полный пересчет может быть дешевле. -В вашей реализации поиск связей пока читает всю историю покупок — -инкрементальным стал именно пересчет дневной витрины. +В вашей реализации поиск связей пока читает всю историю покупок. +По новым данным обновляется только дневная витрина; такое обновление +называют инкрементальным. После работы удалите свои `lab_daily_traffic`, `lab_traffic_runs` и `lab_identity_history` через `DROP TABLE`, если они больше не нужны. diff --git a/docs/labs/optional/missing-purchases.md b/docs/labs/optional/missing-purchases.md index ff6333a..9d33f6a 100644 --- a/docs/labs/optional/missing-purchases.md +++ b/docs/labs/optional/missing-purchases.md @@ -1,21 +1,24 @@ # Куда пропали покупки -**Чему учит:** проверить полноту данных по независимому источнику и выбрать -источник для витрины заказов. +Вы сравните покупки из событий сайта с заказами магазина. Найденные +расхождения помогут выбрать источник для полной витрины заказов. Факультатив доступен после ступени 1. В DBeaver подключитесь пользователем `analyst` по [инструкции](../../../README.md#подключиться-из-dbeaver). Дождитесь завершения `etl_pipeline`; на время расследования оставьте `world_live` на паузе и дождитесь уже начатых прогонов. -## Выберите день +## Выбор дня Можно ли построить полную витрину заказов по событиям `purchase`? Проверьте это на одном дне: сначала сопоставьте события сайта, затем сверьте номера заказов с выгрузкой магазина. -Возьмите день, для которого уже получен последний слепок семидневного окна. -Запрос показывает самую позднюю подходящую дату: +Слепок заказов содержит их состояние на конец модельного дня. Состояние +заказа может меняться семь дней, поэтому для окончательного сравнения нужны +все слепки за этот период. Возьмите день, для которого уже получен последний +из них. Такой день называется закрытым. Запрос показывает самую позднюю +подходящую дату: ```sql SELECT max(snapshot_date) - 6 AS last_closed_day @@ -27,7 +30,7 @@ FROM ods.order_v; Сравнивайте все заказы дня, включая отмененные: сейчас вопрос в том, зарегистрировал ли магазин заказ, а не в том, был ли он оплачен. -## Найдите разрыв в событиях +## События оформления заказа При оформлении заказа посетитель видит страницу подтверждения, а трекер отправляет событие покупки. Просмотр страницы и покупка — отдельные события. @@ -42,7 +45,7 @@ FROM ods.order_v; заказа пришло ровно по одному событию каждого вида? Что можно предположить по вашему результату и чего эти два числа еще не доказывают? -## Сверьте с магазином +## Сверка с заказами магазина В событии номер заказа находится в `purchase_id[1]`, в заказах — в `order_id`. Соберите список заказов выбранного дня, для которых нет события покупки. @@ -72,7 +75,7 @@ FROM ods.order_v; Если в выбранном дне повторов нет, возьмите другой закрытый день. -## Сопоставьте вывод с готовой сверкой +## Сравнение с готовой витриной Найдите свои заказы без события в `dm.purchase_vs_orders_v` по `order_id`. Сравните их `status` и `mismatch_class`. Витрина присваивает заказу один класс, @@ -83,7 +86,7 @@ FROM ods.order_v; Возьмите один заказ из своего списка и сохраните два запроса: первый находит его в магазине, второй показывает отсутствие покупки в трекере. -## Выберите источник и условие проверки +## Выбор источника для витрины По результатам сверки укажите источник для витрины, в которой должен быть каждый зарегистрированный заказ. Обоснуйте выбор найденными номерами. @@ -93,7 +96,7 @@ FROM ods.order_v; Позволяет ли отсутствие события в `dds.event_v` установить место потери — до приема в хранилище или при обработке? Какие данные нужны, чтобы это выяснить? -## Что должно получиться +## Результат работы - [ ] сохранен запрос сверки по дате, возвращающий заказы без события; - [ ] оба равенства сходятся, влияние повторного просмотра объяснено; diff --git a/docs/labs/optional/order-amount-difference.md b/docs/labs/optional/order-amount-difference.md index 0f18a45..46a2397 100644 --- a/docs/labs/optional/order-amount-difference.md +++ b/docs/labs/optional/order-amount-difference.md @@ -1,14 +1,15 @@ # Какая позиция объясняет разницу в сумме -**Чему учит:** разобрать вложенные данные без потери точности и локализовать -расхождение двух источников до товарной позиции. +Вы сравните товары в событии покупки и заказе магазина, чтобы найти причину +разницы в суммах. При разборе вложенных данных понадобится сохранить точность +цен до копейки. Факультатив доступен после ступени 1. В DBeaver используйте пользователя `analyst` из [инструкции подключения](../../../README.md#подключиться-из-dbeaver). Дождитесь завершения `etl_pipeline`; оставьте `world_live` на паузе и дождитесь уже начатых прогонов. -## Найдите заказ +## Выбор заказа Суммы покупки в трекере и заказа в магазине различаются. Разберите их позиции и проверьте отдельно точность своего запроса и различия между событием и заказом. @@ -24,12 +25,12 @@ `declared_revenue`, если мы ищем расхождение именно в составе корзины? Проверьте свой выбор после разбора позиций. -## Разберите позиции с двух сторон +## Товарные позиции события и заказа Найдите событие покупки в `ods.event_v`: `purchaseID[1]` совпадает с номером заказа, `EventDate` — с выбранным днем, `EventType = 'purchase'`. Откройте поле `ecommerce`. Внутри JSON массив `purchase.products` содержит -позиции клиента с полями `id`, `price` и `quantity`. +товары из события покупки с полями `id`, `price` и `quantity`. Разверните этот массив в строки и получите артикул, точную цену, количество и стоимость каждой позиции. Для разбора пригодятся `JSONExtractArrayRaw` @@ -47,7 +48,7 @@ Сохраните запрос сверки для выбранных даты и номера заказа; в результате оставьте артикул, значения с обеих сторон и разность стоимостей позиций. -## Проверьте объяснение +## Проверка сумм Подтвердите три равенства: @@ -66,7 +67,7 @@ Какие строки подтверждают найденное расхождение? Есть ли в источниках сведения о том, почему оно возникло? Отделите наблюдения от предположений. -## Выберите поля для точного расчета +## Поля для точного расчета Укажите рядом с запросом, откуда брать цену, каким типом ее читать и по какому ключу сопоставлять позиции. Подтвердите выбор результатами обеих сверок: @@ -74,7 +75,7 @@ между собой. Какой результат укажет на ошибку вашего разбора, а какой потребует искать причину выше — в данных, которые получил запрос? -## Что должно получиться +## Результат работы - [ ] сохранен запрос сверки позиций, не теряющий товары без пары; - [ ] все три равенства сходятся до копейки; diff --git a/docs/labs/optional/revenue-over-time.md b/docs/labs/optional/revenue-over-time.md index 469236e..b569b47 100644 --- a/docs/labs/optional/revenue-over-time.md +++ b/docs/labs/optional/revenue-over-time.md @@ -1,16 +1,18 @@ # Почему вчерашняя выручка меняется сегодня -**Чему учит:** обосновать окно пересчета витрины и требования к истории -заказов по наблюдаемым опозданиям и изменениям статусов. +Вы проследите, как опоздавшие заказы и смена статусов меняют выручку +за прошедший день. По этим наблюдениям определите, какие дни пересчитывать +и какую историю заказов сохранять. Факультатив доступен после ступени 1. Понадобятся Airflow и DBeaver с подключением `student` по [инструкции](../../../README.md#подключиться-из-dbeaver). -Он читает данные стенда и записывает ваши наблюдения в базу `default`. +Пользователь `student` может читать данные стенда и записывать ваши наблюдения +в базу `default`. Вы будете наблюдать один день оформления заказов, продвигая мир по одному модельному дню. Проверьте, можно ли при загрузке выручки каждый раз считать только новый день. Новые данные останутся в стенде. -## Подготовьте первое наблюдение +## Подготовка Поставьте `world_live` на паузу. Дождитесь завершения начатых прогонов генерации и обработки, затем запустите `etl_pipeline` и дождитесь успеха. @@ -28,14 +30,15 @@ SELECT Дата слепка, по которому сделано очередное наблюдение, будет **S**. Это модельные даты магазина. Календарная дата запуска Airflow день D не задает. -Слепок снимается к концу суток и доставляется следующим прогоном. Поэтому -покупки последнего сыгранного дня уже видны в событиях, а их заказов в +Слепок содержит состояние заказов на конец модельных суток и доставляется +при следующем запуске генератора. Поэтому покупки последнего завершившегося +дня уже видны в событиях, а их заказов в хранилище еще нет. Проверьте это для D в `dds.order_v` и `dm.purchase_vs_orders_v`. -## Подготовьте место для наблюдений +## Таблицы для истории заказов -Публичное представление заказа показывает текущее состояние, а сырые слепки +Представление `dds.order_v` показывает текущее состояние заказов, а сырые слепки хранятся ограниченное время. Сохраните историю в двух своих таблицах: потом вы сможете сравнивать наблюдения обычными SQL-запросами в том же DBeaver. @@ -66,7 +69,7 @@ ENGINE = MergeTree ORDER BY (order_date, snapshot_date); ``` -## Сохраните первое наблюдение +## Первое наблюдение Напишите два `INSERT INTO ... SELECT` с явным списком колонок назначения: @@ -93,7 +96,7 @@ ORDER BY (order_date, snapshot_date); статусам и сумму `items_total` только у `paid`. Эта сумма должна сходиться с сохраненной выручкой, сложенной по всем категориям. -## Продвигайте мир по одному дню +## Наблюдения за следующие дни Для каждого следующего наблюдения: @@ -102,16 +105,16 @@ ORDER BY (order_date, snapshot_date); не обновляет. 3. Повторите два запроса сохранения с новой датой S и проверьте записанные строки. -Во всех запросах остается прежний день D. За один запуск проигрывайте один +Во всех запросах остается прежний день D. За один запуск добавляйте один день, чтобы сохранить промежуточные состояния. Пакетный `world_next_day` отправляет события без пауз; ждать 24 минуты живого дня здесь не нужно. -Продолжайте до слепка **D + 7** включительно. У заказов дня D окно состоит -из семи дней, от D до D + 6. Последний шаг нужен для сравнения состояния -в конце окна и за его пределами. +Продолжайте до слепка **D + 7** включительно. Заказы дня D могут меняться +семь дней, от D до D + 6. Этот период называется окном изменяемости. +Последний шаг нужен для сравнения состояния в конце окна и после него. От исходного наблюдения потребуется восемь однодневных запусков. -## Найдите изменения в уже загруженном дне +## Изменения заказов и выручки Для сравнения прочитайте `default.lab_order_history` дважды: в одном подзапросе отберите прежнюю S, в другом — новую; день D остается общим. @@ -138,7 +141,7 @@ ORDER BY (order_date, snapshot_date); Достаточно ли добавлять в старый день только опоздавшие заказы? Проверьте свой ответ на найденных изменениях статусов. -## Определите, какую историю нужно хранить +## Требования к истории заказов Возьмите отмененный заказ, у которого в вашей истории был статус `paid`. Укажите два наблюдения, доказывающих, что он был оплачен, а затем отменен. @@ -153,7 +156,7 @@ ORDER BY (order_date, snapshot_date); Какие сведения для каждой задачи должен передавать источник и сохранять хранилище? Проверьте, каких нужных сведений нет в ваших таблицах наблюдений. -## Задайте правило пересчета +## Правило пересчета витрины Сравните сохраненные состояния при S = D + 6 и S = D + 7: состав заказов, их статусы, суммы и выручку. Какой слепок последним мог изменить заказы дня D @@ -171,13 +174,13 @@ ORDER BY (order_date, snapshot_date); [слепка и его доставки](../../architecture/orders/snapshot.md#слепок-и-его-доставка) и [границ истории отмен](../../architecture/orders/fate.md#судьба-заказа). -## Что должно получиться +## Результат работы - [ ] сохранена последовательность наблюдений одного дня D с датами слепков S; - [ ] на опоздавших заказах и изменениях статусов показаны ошибки загрузки только новых дней или только новых заказов; - [ ] изменение выручки между наблюдениями сходится с изменением вкладов заказов; -- [ ] оплата одной из последующих отмен подтверждена двумя наблюдениями; +- [ ] для одного отмененного заказа найдены два наблюдения: с оплатой и с отменой; - [ ] сформулированы требования к истории расчетов и истории оплаты; - [ ] правило пересчета и момент закрытия дня обоснованы наблюдениями и правилом доставки слепков. diff --git a/docs/labs/optional/superset-dashboard.md b/docs/labs/optional/superset-dashboard.md index 48c042e..173806c 100644 --- a/docs/labs/optional/superset-dashboard.md +++ b/docs/labs/optional/superset-dashboard.md @@ -1,12 +1,13 @@ # Свой дашборд в Superset -**Чему учит:** собрать из своих витрин отчет о поведении посетителей, -подобрать графики под вопросы и разместить их на одном понятном экране. +Из своих витрин вы соберете отчет о поведении посетителей: подберете графики +и разместите их на одном экране так, чтобы отчет можно было понять +без ваших пояснений. -Это факультатив раздела «Superset и BI». Его можно пройти после ступени 2, +Это задание раздела «Отчеты в Superset». Его можно пройти после ступени 2, когда готовы витрины ступени 1. -## На какие вопросы ответит дашборд +## Отчет о поведении посетителей Соберите обзор трафика и поведения посетителей. Он должен отвечать на три вопроса: что происходило в магазине, какие страницы, устройства и рекламные @@ -17,25 +18,23 @@ стендового дашборда не нужно: выберите форму, в которой ответы читаются без пояснений рядом с экраном. -## Свои объекты — отдельно +## Отдельный дашборд для задания Не добавляйте графики в стендовый дашборд и не правьте его датасеты. При инициализации Superset импорт с `--overwrite` заменит ручные правки стендовых объектов файлами из репозитория. `make up` свой дашборд не стирает; срок жизни -таких объектов описан в [доке Superset](../../architecture/superset.md), «Импорт +таких объектов описан в [справочнике Superset](../../architecture/superset.md), «Импорт при каждой инициализации». Используйте свои датасеты и графики, соберите из них свой дашборд. -Назовите его, например, «Мои витрины DM». Это то же правило, по которому DDL -домашек лежит в `sql/ddl/70-labs.sql`: стендовые объекты уже есть, свои -создаете вы. +Назовите его, например, «Мои витрины DM». ## Датасеты Датасет `session_overview_v`, его метрика доли отказов и график уже готовы после [ступени 2](../2-1-superset-chart.md). Используйте их здесь. Добавьте недостающие датасеты, чтобы в подключении `ClickHouse` были представлены -пять публичных представлений схемы `dm`: +пять представлений ваших витрин из схемы `dm`: - `events_enriched_v`; - `top_pages_daily_v`; @@ -56,7 +55,7 @@ | `dq_errors_daily_v` | число ошибок по дню загрузки, источнику и классу | | `utm_effectiveness_v` | трафик, покупки и заявленную выручку по рекламным каналам | -Для долей используйте слагаемые из витрин. Например, доля отказов считается +Для долей используйте суммы счетчиков из витрин. Например, доля отказов считается как сумма `bounce_sessions`, деленная на сумму `sessions`. Среднее по готовым долям для такого графика не подходит. @@ -73,7 +72,7 @@ по модельному времени. Для панели ошибок выберите период загрузки отдельно и укажите его в подписи. Общий фильтр времени для всего дашборда не требуется. -## Как проверить себя +## Проверка результата Автоматической проверки этого задания нет. Числа вы сверите запросами, а понятность графиков проверите по подписям. @@ -103,9 +102,9 @@ Проверьте, можно ли найти эти ответы по одним подписям графиков. Если есть возможность, попросите другого человека прочитать экран без ваших пояснений. -## Что должно получиться +## Результат работы -- [ ] у вас есть пять датасетов поверх публичных представлений витрин заданий 1–5; +- [ ] у вас есть пять датасетов по представлениям витрин заданий 1–5; - [ ] вы собрали свой дашборд, сохранив стендовые объекты без изменений; - [ ] на дашборде есть хотя бы один график от каждой витрины; - [ ] вы проверили, что графики и подписи понятны по вопросам из раздела выше; diff --git a/docs/labs/optional/superset-missing-object.md b/docs/labs/optional/superset-missing-object.md index aff842e..2c26e58 100644 --- a/docs/labs/optional/superset-missing-object.md +++ b/docs/labs/optional/superset-missing-object.md @@ -1,16 +1,16 @@ # В DBeaver работает, в Superset нет -**Чему учит:** найти причину, по которой один клиент читает объект -хранилища, а другой его не видит. +Вы создадите представление, которое читается из DBeaver, но недоступно +в Superset. Затем найдете причину и исправите ее. Факультатив доступен после [ступени 2](../2-1-superset-chart.md). Свой дашборд для опыта не нужен. Используйте готовую витрину стенда `dm.daily_traffic_v`. -## Подготовьте опыт +## Подготовка -В DBeaver подключитесь к первой ноде пользователем `etl`, как описано -в разделе [«Как применить свой DDL»](../README.md#как-применить-свой-ddl). +В DBeaver подключитесь к первому узлу пользователем `etl`, как описано +в разделе [«Выполнение SQL в DBeaver»](../README.md#выполнение-sql-в-dbeaver). Для опыта создайте отдельное представление: ```sql @@ -28,19 +28,19 @@ SELECT count() FROM dm.bi_node_probe_v; Повторите запрос в SQL Lab Superset, выбрав подключение `ClickHouse`. Сохраните текст ошибки. -## Найдите причину +## Поиск причины Выясните, к каким серверам обращаются DBeaver и Superset. Адреса подключения показывают настройки клиентов, а имя ответившего сервера можно спросить запросом `SELECT hostName()`. -Проверьте наличие `dm.bi_node_probe_v` на обеих нодах. Для второй ноды +Проверьте наличие `dm.bi_node_probe_v` на обоих узлах. Для второго узла заведите подключение DBeaver с портом из [README](../../../README.md#подключиться-из-dbeaver). Запишите, где объект есть, и объясните, почему успешного создания в первом подключении оказалось недостаточно. -Исправьте DDL так, чтобы представление появилось на обеих нодах. Подключение +Исправьте DDL так, чтобы представление появилось на обоих узлах. Подключение Superset должно остаться прежним. Сохраните исправленный DDL и повторите исходный запрос в обоих клиентах. @@ -48,23 +48,23 @@ Superset должно остаться прежним. Сохраните исп [справочнике Superset](../../architecture/superset.md#устройство-зоны) и [описание распределенного DDL ClickHouse](https://clickhouse.com/docs/reference/statements/distributed-ddl). -## Как проверить себя +## Проверка результата В обоих клиентах запрос к вашему представлению должен вернуть столько же строк, сколько `SELECT count() FROM dm.daily_traffic_v`. Сравнивайте результаты при завершенном прогоне конвейера, пока мир не продвигается. Объясните причину ошибки через три факта: куда пришел DDL, куда пришел -запрос Superset и на каких нодах существовало представление. +запрос Superset и на каких узлах существовало представление. -После опыта удалите только `dm.bi_node_probe_v` на обеих нодах. Не включайте +После опыта удалите только `dm.bi_node_probe_v` на обоих узлах. Не включайте его в постоянный DDL домашних витрин: это временный объект расследования. -## Что должно получиться +## Результат работы - [ ] у вас сохранены успешный запрос из DBeaver и ошибка из SQL Lab; - [ ] вы можете объяснить, к каким серверам подключены клиенты, где было создано представление и почему Superset его не видел; - [ ] у вас есть исправленный DDL; оба клиента возвращают столько же строк, сколько исходная витрина; -- [ ] после опыта вы удалили учебное представление с обеих нод. +- [ ] после опыта вы удалили учебное представление с обоих узлов. diff --git a/docs/specs/2026-09-01-labs-program.md b/docs/specs/2026-09-01-labs-program.md index ed6e519..aa3c2f1 100644 --- a/docs/specs/2026-09-01-labs-program.md +++ b/docs/specs/2026-09-01-labs-program.md @@ -45,12 +45,16 @@ Пишите для учащегося: объясняйте, чему он научится, зачем нужны действия и как понять результат. Историю разработки оставляйте за пределами задания. -«Как проверить себя» описывает действия для проверки и поиска ошибок. -«Что должно получиться» — результат работы и то, что учащийся теперь может +Начинайте задание коротким описанием предстоящей работы. Заголовки должны +называть содержание разделов: «Подготовка», «Создание витрины», «Проверка +результата». Шаблонные вопросы «Чему учит» и «Что сделать» не нужны. + +Раздел проверки описывает действия для проверки и поиска ошибок. +Раздел результата перечисляет итог работы и то, что учащийся теперь может объяснить. Если проверка уже встроена в шаги упражнения, повторять ее отдельным разделом не нужно. -Остальные заголовки и структуру выбирайте по содержанию. Образцы — задания +Структуру выбирайте по содержанию. Образцы — задания про [дашборд Superset](../labs/optional/superset-dashboard.md) и [расследование подключения](../labs/optional/superset-missing-object.md).