feat(dm): добавлены учебные задания витрин и даг самопроверки
- Зачем:
- менти строит витрины DM сам, от DDL по правилу зерна до задачи в
конвейере; стенду нужны ТЗ с образцом и обратная связь по каждой
витрине (#159, резолюция #135).
- Что:
- docs/labs/: README и пять ТЗ — events_enriched_v, top_pages_daily,
session_overview, dq_errors_daily, utm_effectiveness; каждое ТЗ
называет урок, состав колонок, форму по зерну и самопроверку.
- dags/homework_check.py: ручной даг, задача на витрину, красная без
витрины или при расхождении; проверки — шесть файлов sql/dq/ с
инвариантами против DDS и ODS, без золотых констант; в dq_summary
не пишет.
- docs: dm.md называет homework_check и снимает «Остается нарезке»,
§7 и §10 мастер-спеки согласованы с лабами, dds/session.md указывает
на ТЗ, AGENTS.md пополнен правилом имен docs/labs/.
- Проверка:
- make lint; make smoke 21/21, make check-clickhouse 10/10,
make check-services 7/7.
- на свежем стенде все пять задач homework_check красные и называют
ТЗ; черновик каждой витрины по тексту ТЗ зеленит свою задачу,
после сноса — снова красная; строк от самопроверки в dq_summary нет.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
35d43125b1
commit
f33d75113d
17 files changed
+1367
-25
No files matched your search
@@ -157,6 +157,10 @@ Airflow) и названия из кода. Если для понятия ес
|
|||||||
дефис (`clickstream-event.md`). Ни даты, ни номера: документ не событие
|
дефис (`clickstream-event.md`). Ни даты, ни номера: документ не событие
|
||||||
истории, а текущее описание живого формата — и собирается из кода, а не
|
истории, а текущее описание живого формата — и собирается из кода, а не
|
||||||
пишется руками.
|
пишется руками.
|
||||||
|
- Имена файлов в `docs/labs/` — слаг строчными латинскими буквами через дефис
|
||||||
|
(`top-pages-daily.md`), индекс папки — `README.md`. Здесь живут ТЗ учебных
|
||||||
|
заданий. Ни даты, ни номера: задание — не событие истории, а живой документ,
|
||||||
|
и правится он вместе с тем, о чем говорит. Слаг называет витрину задания.
|
||||||
- Имена файлов в `docs/adr/` — `NNNN-краткое-имя.md`: сквозной номер из четырёх
|
- Имена файлов в `docs/adr/` — `NNNN-краткое-имя.md`: сквозной номер из четырёх
|
||||||
цифр и слаг (`0001-stand-services.md`). Решения нумеруются подряд, дата
|
цифр и слаг (`0001-stand-services.md`). Решения нумеруются подряд, дата
|
||||||
в имени не нужна.
|
в имени не нужна.
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
"""Ручная самопроверка учебных витрин DM против их источников."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from airflow.exceptions import AirflowException
|
||||||
|
from airflow.providers.clickhousedb.hooks.clickhouse import ClickHouseHook
|
||||||
|
from airflow.sdk import dag, task
|
||||||
|
|
||||||
|
START_DATE = datetime.datetime(2026, 1, 1, tzinfo=datetime.UTC)
|
||||||
|
SQL_ROOT = Path("/opt/airflow/sql/dq")
|
||||||
|
CLICKHOUSE_CONNECTION = "clickhouse_default"
|
||||||
|
DETAIL_LIMIT = 20
|
||||||
|
|
||||||
|
|
||||||
|
def _clickhouse_hook() -> ClickHouseHook:
|
||||||
|
"""Подключение к ClickHouse для чтения обеих сторон сверки."""
|
||||||
|
return ClickHouseHook(clickhouse_conn_id=CLICKHOUSE_CONNECTION)
|
||||||
|
|
||||||
|
|
||||||
|
def _mart_exists(hook: ClickHouseHook, mart: str) -> bool:
|
||||||
|
"""Проверить наличие публичной витрины на ноде подключения."""
|
||||||
|
database, name = mart.split(".", maxsplit=1)
|
||||||
|
rows = hook.get_records(
|
||||||
|
"""
|
||||||
|
SELECT count()
|
||||||
|
FROM system.tables
|
||||||
|
WHERE database = {database:String} AND name = {name:String}
|
||||||
|
""",
|
||||||
|
parameters={"database": database, "name": name},
|
||||||
|
)
|
||||||
|
return rows[0][0] == 1
|
||||||
|
|
||||||
|
|
||||||
|
@task
|
||||||
|
def check_mart(
|
||||||
|
mart: str,
|
||||||
|
lab_path: str,
|
||||||
|
sql_name: str,
|
||||||
|
scope_sql_name: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Сверить одну домашнюю витрину с независимым ответом из источника."""
|
||||||
|
hook = _clickhouse_hook()
|
||||||
|
if not _mart_exists(hook, mart):
|
||||||
|
raise AirflowException(f"витрина {mart} не найдена; выполните ТЗ {lab_path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
query = (SQL_ROOT / sql_name).read_text(encoding="utf-8")
|
||||||
|
scope_query = (
|
||||||
|
(SQL_ROOT / scope_sql_name).read_text(encoding="utf-8")
|
||||||
|
if scope_sql_name is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
except OSError as error:
|
||||||
|
raise AirflowException(
|
||||||
|
f"стенд не может прочитать запрос проверки {sql_name}: {error}; "
|
||||||
|
"сообщите наставнику"
|
||||||
|
) from error
|
||||||
|
|
||||||
|
try:
|
||||||
|
parameter_sets = [None]
|
||||||
|
if scope_query is not None:
|
||||||
|
parameter_sets = [{"day": day} for (day,) in hook.get_records(scope_query)]
|
||||||
|
summaries = [
|
||||||
|
summary
|
||||||
|
for parameters in parameter_sets
|
||||||
|
for summary in hook.get_records(query, parameters=parameters)
|
||||||
|
]
|
||||||
|
except Exception as error:
|
||||||
|
raise AirflowException(
|
||||||
|
f"не удалось проверить витрину {mart}: {error}; "
|
||||||
|
f"сверьте договор колонок и проверку в ТЗ {lab_path}"
|
||||||
|
) from error
|
||||||
|
|
||||||
|
failed_rows = sum(summary[3] for summary in summaries)
|
||||||
|
if failed_rows == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
diagnostics = [detail for summary in summaries for detail in summary[4]][
|
||||||
|
:DETAIL_LIMIT
|
||||||
|
]
|
||||||
|
for detail in diagnostics:
|
||||||
|
logging.error("%s: %s", mart, detail)
|
||||||
|
days = "; ".join(
|
||||||
|
f"{data_date}: ключей источника {reference_rows}, "
|
||||||
|
f"ключей витрины {actual_rows}, расходятся {day_failed_rows}"
|
||||||
|
for data_date, reference_rows, actual_rows, day_failed_rows, _ in summaries
|
||||||
|
if day_failed_rows > 0
|
||||||
|
)
|
||||||
|
compared_sides = diagnostics[0]
|
||||||
|
raise AirflowException(
|
||||||
|
f"витрина {mart} расходится с источником: {days}. "
|
||||||
|
f"Пример сравнения: {compared_sides}. Сверьтесь с ТЗ {lab_path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dag(
|
||||||
|
dag_id="homework_check",
|
||||||
|
schedule=None,
|
||||||
|
start_date=START_DATE,
|
||||||
|
is_paused_upon_creation=False,
|
||||||
|
max_active_runs=1,
|
||||||
|
tags=["dm", "labs"],
|
||||||
|
)
|
||||||
|
def homework_check():
|
||||||
|
"""Дать отдельный результат самопроверки по каждой домашней витрине."""
|
||||||
|
check_mart.override(task_id="events_enriched")(
|
||||||
|
"dm.events_enriched_v",
|
||||||
|
"docs/labs/events-enriched.md",
|
||||||
|
"events_enriched_vs_dds.sql",
|
||||||
|
"events_enriched_scope.sql",
|
||||||
|
)
|
||||||
|
check_mart.override(task_id="top_pages_daily")(
|
||||||
|
"dm.top_pages_daily_v",
|
||||||
|
"docs/labs/top-pages-daily.md",
|
||||||
|
"top_pages_daily_vs_dds.sql",
|
||||||
|
)
|
||||||
|
check_mart.override(task_id="session_overview")(
|
||||||
|
"dm.session_overview_v",
|
||||||
|
"docs/labs/session-overview.md",
|
||||||
|
"session_overview_vs_dds.sql",
|
||||||
|
)
|
||||||
|
check_mart.override(task_id="dq_errors_daily")(
|
||||||
|
"dm.dq_errors_daily_v",
|
||||||
|
"docs/labs/dq-errors-daily.md",
|
||||||
|
"dq_errors_daily_vs_ods.sql",
|
||||||
|
)
|
||||||
|
check_mart.override(task_id="utm_effectiveness")(
|
||||||
|
"dm.utm_effectiveness_v",
|
||||||
|
"docs/labs/utm-effectiveness.md",
|
||||||
|
"utm_effectiveness_vs_dds.sql",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
homework_check()
|
||||||
@@ -35,10 +35,9 @@
|
|||||||
ломают.
|
ломают.
|
||||||
|
|
||||||
`session_id` — не ключ сортировки и не ключ шардирования, а колонка-ручка:
|
`session_id` — не ключ сортировки и не ключ шардирования, а колонка-ручка:
|
||||||
витрины сессий читают `dds.session_v`, их зерно совпадает с зерном модели, и в
|
в соединениях он не участвует — события его не несут, мост к заказам живёт
|
||||||
соединениях id не участвует — события его не несут, мост к заказам живёт на
|
на событиях. Читатели ручки — лаба сессий (сопоставить свою нарезку со
|
||||||
событиях. Читатели ручки — лаба сессий (сопоставить свою нарезку со стендовой и
|
стендовой и назвать расхождение одним числом) и разбор DQ-расхождений.
|
||||||
назвать расхождение одним числом) и разбор DQ-расхождений.
|
|
||||||
|
|
||||||
## Состав строки
|
## Состав строки
|
||||||
|
|
||||||
@@ -110,8 +109,9 @@
|
|||||||
|
|
||||||
## Читатели
|
## Читатели
|
||||||
|
|
||||||
- Витрина обзора сессий `session_overview_v`: длительности, глубина,
|
- Витрина обзора сессий `session_overview_v`: дневные итоги визитов в
|
||||||
вход/выход; читает `dds.session_v`, зерно совпадает (#135).
|
срезе устройств; читает `dds.session_v` и поднимает зерно до дня — состав и довод
|
||||||
|
в ее [ТЗ](../../labs/session-overview.md) (#135).
|
||||||
- DQ-сверка с эталоном (см. выше).
|
- DQ-сверка с эталоном (см. выше).
|
||||||
- Лаба сессий: менти собирает нарезку сам до знакомства с кодом стенда,
|
- Лаба сессий: менти собирает нарезку сам до знакомства с кодом стенда,
|
||||||
потом сравнивает — с эталоном и со стендовой.
|
потом сравнивает — с эталоном и со стендовой.
|
||||||
|
|||||||
+10
-14
@@ -39,10 +39,11 @@
|
|||||||
образцом. Разминка — представление `events_enriched_v` (зерно события),
|
образцом. Разминка — представление `events_enriched_v` (зерно события),
|
||||||
затем агрегаты `top_pages_daily`, `session_overview` и `dq_errors_daily`,
|
затем агрегаты `top_pages_daily`, `session_overview` и `dq_errors_daily`,
|
||||||
последней — самая содержательная `utm_effectiveness` с уроком `declared_*`.
|
последней — самая содержательная `utm_effectiveness` с уроком `declared_*`.
|
||||||
Построенная витрина встаёт в конвейер руками менти: финальный пункт
|
Построенная витрина встаёт в конвейер руками менти: финальный пункт ТЗ
|
||||||
каждого ТЗ — добавить задачу своей витрины в `dm_transform`. До этого
|
каждого агрегата — добавить задачу своей витрины в `dm_transform`. До этого
|
||||||
конвейер о ней не знает и остаётся зелёным, после — витрина пересобирается
|
конвейер о ней не знает и остаётся зелёным, после — витрина пересобирается
|
||||||
наравне со стендовыми.
|
наравне со стендовыми. У разминочного представления такого пункта нет:
|
||||||
|
пересобирать нечего. Тексты заданий — в [`docs/labs/`](../labs/README.md).
|
||||||
|
|
||||||
Дашборд этапа 8 стоит только на витринах стенда: приёмка этапа не зависит
|
Дашборд этапа 8 стоит только на витринах стенда: приёмка этапа не зависит
|
||||||
от выполненных домашек. Расширение дашборда визуализациями по своим
|
от выполненных домашек. Расширение дашборда визуализациями по своим
|
||||||
@@ -212,22 +213,17 @@ business_key`.
|
|||||||
|
|
||||||
## Даг самопроверки
|
## Даг самопроверки
|
||||||
|
|
||||||
Домашки контролирует отдельный даг без расписания: менти запускает его
|
Домашки контролирует `homework_check` — отдельный даг без расписания: менти
|
||||||
рукой. Задача витрины красная, пока витрины нет или счёт не сходится, и
|
запускает его рукой. По задаче на каждое из пяти [заданий](../labs/README.md).
|
||||||
зелёная, когда построено верно; сообщение об ошибке говорит, что не так и
|
Задача красная, пока витрины нет или счёт не сходится, и зелёная, когда
|
||||||
куда смотреть. Проверки — инварианты против DDS-источников, золотые
|
построено верно; сообщение об ошибке говорит, что не так и куда смотреть. Проверки — инварианты против источников витрины,
|
||||||
константы не прибиваются. В `dq_summary` даг не пишет: он инструмент лабы,
|
золотые константы не прибиваются. В `dq_summary` даг не пишет: он инструмент
|
||||||
а не история качества конвейера.
|
лабы, а не история качества конвейера.
|
||||||
|
|
||||||
Приём проверен на соседнем учебном стенде Airflow + Greenplum:
|
Приём проверен на соседнем учебном стенде Airflow + Greenplum:
|
||||||
валидационный даг с группами проверок по слоям давал менти обратную связь
|
валидационный даг с группами проверок по слоям давал менти обратную связь
|
||||||
по каждому шагу домашки, не трогая рабочий конвейер.
|
по каждому шагу домашки, не трогая рабочий конвейер.
|
||||||
|
|
||||||
## Остается нарезке
|
|
||||||
|
|
||||||
- Имя и состав дага самопроверки домашних заданий.
|
|
||||||
- Тексты учебных заданий и точный список проверок учебного контура.
|
|
||||||
|
|
||||||
## Отклонённые варианты
|
## Отклонённые варианты
|
||||||
|
|
||||||
- **Весь слой представлениями.** Дёшево и всегда свежо, но главный
|
- **Весь слой представлениями.** Дёшево и всегда свежо, но главный
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# Учебные задания: витрины слоя DM
|
||||||
|
|
||||||
|
Пять домашек. Стенд привозит слой DM наполовину: витрины трех сюжетов
|
||||||
|
дашборда и контроль качества построены, остальные витрины строит менти.
|
||||||
|
Каждое задание — самостоятельное ТЗ: что за витрина, какое у нее зерно,
|
||||||
|
из чего она собирается и как проверить, что собралась верно.
|
||||||
|
|
||||||
|
Общее устройство слоя — [дока DM](../architecture/dm.md). Она отвечает на
|
||||||
|
вопросы «почему так», задание отвечает на вопрос «что сделать».
|
||||||
|
|
||||||
|
## Порядок
|
||||||
|
|
||||||
|
Задания идут по нарастанию: каждое следующее опирается на то, что уже
|
||||||
|
усвоено.
|
||||||
|
|
||||||
|
| № | Витрина | Чему учит |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | [`events_enriched_v`](events-enriched.md) | слой — это контракт, а не копия данных |
|
||||||
|
| 2 | [`top_pages_daily`](top-pages-daily.md) | зерно сменилось — витрина материализуется и встает в конвейер |
|
||||||
|
| 3 | [`session_overview`](session-overview.md) | зерно решает, на какие вопросы витрина уже не ответит |
|
||||||
|
| 4 | [`dq_errors_daily`](dq-errors-daily.md) | у брака свой день и свой срок жизни |
|
||||||
|
| 5 | [`utm_effectiveness`](utm-effectiveness.md) | `declared_` в имени — предупреждение, а не украшение |
|
||||||
|
|
||||||
|
## Общие правила
|
||||||
|
|
||||||
|
Они одни на все пять заданий, поэтому записаны здесь, а не повторяются в
|
||||||
|
каждом ТЗ.
|
||||||
|
|
||||||
|
**Читать — через `_v`.** Публичный договор сущности DDS — представление с
|
||||||
|
суффиксом `_v`: `dds.event_v`, `dds.session_v`. Витрина называет только
|
||||||
|
его; физические `_rep` и `_dist` служат загрузке и обслуживанию. Соседнюю
|
||||||
|
домашку витрина источником тоже не берет: витрина слоя DM читает договор
|
||||||
|
DDS, и задания остаются независимыми. Исключение одно и оно оговорено в
|
||||||
|
задании 4: у таблиц брака публичного договора нет.
|
||||||
|
|
||||||
|
**Писать — только в `_dist`.** При записи в локальную таблицу раскладку
|
||||||
|
определяет то, какая нода случайно выполняла код, а не свойство данных.
|
||||||
|
Правило и его следствия — [дока хранилища](../architecture/storage.md),
|
||||||
|
«Раскладка по шардам».
|
||||||
|
|
||||||
|
**Форма — по зерну.** Витрина, меняющая зерно источника, материализуется:
|
||||||
|
`dm.<имя>_rep` / `_dist`, донор `dm.<имя>_stage_rep` / `_stage_dist` и
|
||||||
|
публичное `dm.<имя>_v` с явным списком колонок. Витрина, сохраняющая зерно,
|
||||||
|
остается обычным представлением. Довод — [дока DM](../architecture/dm.md),
|
||||||
|
«Форма: по зерну, не по цене запроса».
|
||||||
|
|
||||||
|
**Ключ шардирования — твое решение.** Ни одно ТЗ ключа не называет.
|
||||||
|
Правило выбора и требование к донору — [дока
|
||||||
|
хранилища](../architecture/storage.md), «Раскладка по шардам»: донор
|
||||||
|
объявляется через `AS <цель>_rep` и шардируется тем же ключом, что цель,
|
||||||
|
иначе замена партиции потеряет строку на одном шарде и задвоит на другом.
|
||||||
|
Названный ключ надо уметь защитить: почему он не дает перекоса и почему
|
||||||
|
более сложный тут ничего не добавляет. Образец такой защиты —
|
||||||
|
`dm.revenue_daily_dist` в [`sql/ddl/55-dm-tables.sql`](../../sql/ddl/55-dm-tables.sql).
|
||||||
|
|
||||||
|
**Где лежит код.** DDL домашек — свой файл `sql/ddl/70-labs.sql`: он
|
||||||
|
применяется после стендовых по порядку имени, и твои объекты не мешаются
|
||||||
|
со стендовыми. У агрегатов рядом со стендовыми ложатся еще три запроса —
|
||||||
|
`sql/dm/<витрина>_scope.sql` спрашивает у хранилища, какие дни пересобирает
|
||||||
|
запуск, `_rebuild.sql` собирает их в донор, `_replace.sql` заменяет
|
||||||
|
партицию, — и задача в
|
||||||
|
[`dags/dm_transform.py`](../../dags/dm_transform.py). У разминочного
|
||||||
|
представления нет ничего из этого: весь его код — одно `CREATE VIEW`.
|
||||||
|
|
||||||
|
**Образец на все случаи** — витрина `revenue_daily`: у нее есть все пять
|
||||||
|
объектов, донор, сборка, замена дневных партиций и задача в конвейере.
|
||||||
|
Списывать с нее устройство можно и нужно; списывать состав колонок — нет,
|
||||||
|
он у каждой витрины свой.
|
||||||
|
|
||||||
|
## Самопроверка
|
||||||
|
|
||||||
|
Даг `homework_check` запускается рукой, расписания у него нет. Задача
|
||||||
|
витрины красная, пока витрины нет или счет не сходится, и зеленая, когда
|
||||||
|
собрано верно. В сводку качества конвейера `dm.dq_summary` он не пишет:
|
||||||
|
это инструмент лабы, а не история качества стенда.
|
||||||
|
|
||||||
|
Проверка у всех заданий устроена одинаково и золотых констант не содержит:
|
||||||
|
рядом с витриной независимым запросом собирается тот же ответ — прямо из
|
||||||
|
источника витрины, минуя ее саму. У четырех заданий источник эталона тот
|
||||||
|
же, что у витрины, `dds.session_v` или `dds.event_v`; у задания 4 это
|
||||||
|
таблицы брака ODS, потому что в DDS у брака представительства нет. Обе
|
||||||
|
стороны соединяются по деловому ключу через `FULL OUTER JOIN`, и считается
|
||||||
|
число строк, где стороны разошлись или одной стороны нет. Ноль — зеленый.
|
||||||
|
Полное соединение здесь обязательно: при обычном пропавший день или
|
||||||
|
пропавший ключ просто не попал бы в результат и остался бы незамечен.
|
||||||
|
|
||||||
|
Раздел «Как проверить себя» каждого ТЗ называет деловой ключ и сравниваемые
|
||||||
|
величины. Тот же прием в стендовом исполнении — [даг
|
||||||
|
`dq_check`](../../dags/dq_check.py) и запросы в [`sql/dq/`](../../sql/dq).
|
||||||
|
|
||||||
|
**Предупреждение.** Запросы самой проверки лежат в `sql/dq/` открыто, и
|
||||||
|
имена называют витрину и роль сверки. Проверка на инвариантах неизбежно
|
||||||
|
близка к ответу: чтобы сверить витрину, надо ее пересчитать. Прятать эти
|
||||||
|
файлы никто не станет — но открыв их до того, как задание сделано, ты
|
||||||
|
обменяешь урок на готовый SQL.
|
||||||
|
Смотреть туда стоит после своей сборки: сравнить два решения полезно,
|
||||||
|
списать одно — нет.
|
||||||
|
|
||||||
|
## Уборка
|
||||||
|
|
||||||
|
Черновики и опыты за собой сносить: правило проверок стенда —
|
||||||
|
[дока тестирования](../architecture/testing.md). Снесенная витрина обязана
|
||||||
|
снова покраснить свою задачу в `homework_check` — если не покраснила,
|
||||||
|
проверка ничего и не проверяла.
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
# Задание 4: `dm.dq_errors_daily`
|
||||||
|
|
||||||
|
**Чему учит:** у брака свой день и свой срок жизни. Витрина качества живет
|
||||||
|
по времени загрузки, а не по модельному дню, и переживает собственный
|
||||||
|
источник — поэтому «пересобрать все» здесь означает «стереть историю».
|
||||||
|
|
||||||
|
## Роль витрины
|
||||||
|
|
||||||
|
Сводка приемки: сколько строк за день не прошли разбор и по какой причине.
|
||||||
|
Читают ее, когда качество потока просело: сначала смотрят, вырос ли класс
|
||||||
|
брака, потом идут за сырым текстом в саму таблицу ошибок.
|
||||||
|
|
||||||
|
Витрина отвечает на вопрос «сколько и чего», а не «что именно сломалось».
|
||||||
|
Разбирательство с конкретной строкой начинается в `ods.event_errors_dist` и
|
||||||
|
`ods.order_errors_dist`, где рядом с классом лежат сырой текст и координаты
|
||||||
|
доставки.
|
||||||
|
|
||||||
|
## Источник: единственное исключение из правила `_v`
|
||||||
|
|
||||||
|
Общее правило домашек — читать через публичный договор `_v`. У таблиц брака
|
||||||
|
такого договора нет, и заводить его ради этой витрины не надо: брак — не
|
||||||
|
сущность бизнес-модели, а диагностика слоя приема. Читать нужно
|
||||||
|
распределенные таблицы напрямую:
|
||||||
|
|
||||||
|
- `ods.event_errors_dist` — брак разбора событий;
|
||||||
|
- `ods.order_errors_dist` — брак разбора слепка заказов.
|
||||||
|
|
||||||
|
Именно `_dist`, а не `_rep`: локальная таблица вернет строки одного шарда, и
|
||||||
|
витрина тихо покажет половину брака. Ошибки при этом не будет — ошибка
|
||||||
|
появится позже, когда по витрине примут решение.
|
||||||
|
|
||||||
|
## Зерно и форма
|
||||||
|
|
||||||
|
Зерно — **день загрузки × источник × класс брака**. Зерно источника —
|
||||||
|
строка брака, значит зерно сменилось и витрина материализуется:
|
||||||
|
`dm.dq_errors_daily_rep` / `_dist`, донор `_stage_rep` / `_stage_dist`,
|
||||||
|
публичное `dm.dq_errors_daily_v`. Ключ партиции — день загрузки.
|
||||||
|
|
||||||
|
День здесь — `toDate(_load_ts)`, то есть настоящее время, когда строка
|
||||||
|
приехала в хранилище, а не модельный день мира. Модельного дня у брака нет
|
||||||
|
и быть не может: он живет в содержимом, а содержимое как раз и не
|
||||||
|
разобралось.
|
||||||
|
|
||||||
|
## Состав колонок
|
||||||
|
|
||||||
|
| Колонка | Тип | Что это |
|
||||||
|
|---|---|---|
|
||||||
|
| `load_date` | `Date` | день загрузки, в который приехал брак |
|
||||||
|
| `source` | `LowCardinality(String)` | `event` или `order` |
|
||||||
|
| `error_class` | `LowCardinality(String)` | класс брака из таблицы ошибок |
|
||||||
|
| `error_rows` | `UInt64` | число забракованных строк |
|
||||||
|
| `_load_id` | `String` | запуск, собравший день |
|
||||||
|
| `_load_ts` | `DateTime64(3, 'UTC')` | когда собран |
|
||||||
|
|
||||||
|
Две похожие колонки рядом различаются намеренно: `load_date` — про день,
|
||||||
|
когда приехал брак, `_load_ts` — про запуск, который собрал витрину. Первая
|
||||||
|
деловая, вторая служебная, и совпадать они не обязаны.
|
||||||
|
|
||||||
|
Классы у двух источников свои и не пересекаются по смыслу, поэтому колонка
|
||||||
|
`source` в зерне обязательна: без нее одинаковое имя класса у событий и у
|
||||||
|
заказов слилось бы в одну строку.
|
||||||
|
|
||||||
|
## Ритм пересборки: витрина переживает источник
|
||||||
|
|
||||||
|
Таблицы брака живут месяц (`TTL`), сырье — трое суток. Витрина живет без
|
||||||
|
срока: сводка «сколько брака было в марте» нужна и после того, как сами
|
||||||
|
строки марта удалены.
|
||||||
|
|
||||||
|
Отсюда правило прогона: пересобираются **только те дни загрузки, которые
|
||||||
|
еще есть в источнике**. Спроси у источника список живых дней и замени
|
||||||
|
партиции ровно этих дней. Соблазн «собрать все заново в донор и подменить
|
||||||
|
таблицу целиком» здесь стоит дороже обычного: истекшие по `TTL` дни исчезли
|
||||||
|
бы из витрины навсегда, а вместе с ними — единственная оставшаяся память о
|
||||||
|
них.
|
||||||
|
|
||||||
|
Текущий день загрузки не замерзает до конца суток: поток событий идет, и
|
||||||
|
брак в него еще добавится. Это нормально, день просто пересоберется на
|
||||||
|
следующем прогоне.
|
||||||
|
|
||||||
|
## Ключ шардирования
|
||||||
|
|
||||||
|
Твое решение по [правилу донора](../architecture/storage.md), «Раскладка по
|
||||||
|
шардам». Витрина крошечная: классов брака у событий и заказов вместе меньше
|
||||||
|
десятка. Учти это в защите ключа — иногда правильный ответ звучит как
|
||||||
|
«перекос здесь ничего не стоит, и вот почему».
|
||||||
|
|
||||||
|
## Как проверить себя
|
||||||
|
|
||||||
|
Деловой ключ — пара `source` и `error_class` внутри `load_date`. Область
|
||||||
|
проверки — дни загрузки, которые еще живы хотя бы в одной таблице ошибок:
|
||||||
|
день за границей `TTL` сверять не с чем, и его отсутствие в источнике не
|
||||||
|
ошибка.
|
||||||
|
|
||||||
|
Собери прямой счет по обеим таблицам ошибок, соедини с
|
||||||
|
`dm.dq_errors_daily_v` полным внешним соединением по ключу и посчитай
|
||||||
|
строки, где ключа нет с одной из сторон или где расходится `error_rows`.
|
||||||
|
Ноль — зеленый.
|
||||||
|
|
||||||
|
Отдельно: сумма `error_rows` по живым дням равна общему числу строк в
|
||||||
|
`ods.event_errors_dist` плюс `ods.order_errors_dist`. Если эта сумма вышла
|
||||||
|
примерно вдвое меньше — витрина почти наверняка собрана с `_rep` вместо
|
||||||
|
`_dist` и видит один шард из двух.
|
||||||
|
|
||||||
|
**Пустая витрина — верный ответ.** На чистом стенде брака нет, обе стороны
|
||||||
|
пусты, расхождений ноль, задача зеленая. Проверка, которая требует хотя бы
|
||||||
|
одной строки, на здоровом стенде краснела бы всегда и приучала бы себя
|
||||||
|
игнорировать.
|
||||||
|
|
||||||
|
## Приемка
|
||||||
|
|
||||||
|
- [ ] пять объектов витрины созданы, донор объявлен через `AS` и шардируется
|
||||||
|
ключом цели;
|
||||||
|
- [ ] источники — `_dist`-таблицы обоих видов брака;
|
||||||
|
- [ ] прогон заменяет партиции только живых дней источника и не трогает
|
||||||
|
истекшие;
|
||||||
|
- [ ] задача `dq_errors_daily` в `homework_check` зеленая — в том числе на
|
||||||
|
чистом стенде, где брака нет;
|
||||||
|
- [ ] задача витрины добавлена в `dm_transform`.
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
# Задание 1: `dm.events_enriched_v`
|
||||||
|
|
||||||
|
**Чему учит:** слой — это контракт, а не копия данных. Витрина, которая не
|
||||||
|
меняет зерно источника, остается представлением, и копии событий ради
|
||||||
|
других имен колонок не заводит ни один боевой склад.
|
||||||
|
|
||||||
|
Это разминка. Строк она не хранит, в конвейер не встает, ключа шардирования
|
||||||
|
у нее нет. Задача одна: собрать плоское событие для BI и не поддаться
|
||||||
|
соблазну разложить его на диск.
|
||||||
|
|
||||||
|
## Роль витрины
|
||||||
|
|
||||||
|
Договор события в DDS широкий — 44 колонки, почти весь формат выгрузки:
|
||||||
|
часть из них технические координаты источника, часть — параллельные массивы
|
||||||
|
товаров. Аналитику в Superset нужен плоский набор деловых полей и пара
|
||||||
|
производных, которые иначе каждый пишет заново в каждом запросе.
|
||||||
|
|
||||||
|
Витрина отвечает на вопрос «что вообще происходило»: с нее начинают, когда
|
||||||
|
надо посмотреть на события глазами человека, а не разбирать широкую строку
|
||||||
|
источника.
|
||||||
|
|
||||||
|
Источником для витрин следующих заданий она не служит. Витрина слоя DM
|
||||||
|
читает публичный договор DDS — так устроены и стендовые витрины, и потому
|
||||||
|
задания независимы: соседнюю домашку можно делать первой, а можно не делать
|
||||||
|
вовсе.
|
||||||
|
|
||||||
|
## Зерно и форма
|
||||||
|
|
||||||
|
Зерно — **событие**: одна строка витрины равна одной строке `dds.event_v`.
|
||||||
|
Зерно источника сохранено, значит форма — обычное представление
|
||||||
|
`CREATE VIEW dm.events_enriched_v`, без физической таблицы, без донора и
|
||||||
|
без задачи в `dm_transform`. Пересобирать нечего.
|
||||||
|
|
||||||
|
Источник — `dds.event_v`, и только он.
|
||||||
|
|
||||||
|
## Состав колонок
|
||||||
|
|
||||||
|
Явный список обязателен: договор не должен расширяться сам, когда в
|
||||||
|
источник добавят колонку.
|
||||||
|
|
||||||
|
**Время и посетитель:** `event_date`, `event_time`, `event_hour`,
|
||||||
|
`client_id`, `event_id`.
|
||||||
|
|
||||||
|
**Действие:** `event_type` (`pageview`, `add_to_cart`, `purchase`), `url`,
|
||||||
|
`page_path`, `referer`, `referer_host`, `title`.
|
||||||
|
|
||||||
|
**Атрибуция:** `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`,
|
||||||
|
`utm_term`.
|
||||||
|
|
||||||
|
**Паспорт визита:** `device_category`, `browser`, `operating_system`,
|
||||||
|
`region_country`, `region_city`.
|
||||||
|
|
||||||
|
**Торговый блок, массивами как в источнике:** `product_id`, `product_name`,
|
||||||
|
`product_category`, `product_price`, `product_quantity`,
|
||||||
|
`product_event_type`, `purchase_id`, `purchase_revenue`, `purchase_coupon`.
|
||||||
|
|
||||||
|
Три колонки в источнике не лежат и считаются здесь:
|
||||||
|
|
||||||
|
| Колонка | Как считается | Зачем |
|
||||||
|
|---|---|---|
|
||||||
|
| `event_hour` | `toHour(event_time)` | суточный профиль спрашивают в каждом отчете |
|
||||||
|
| `page_path` | путь `url` без домена и параметров | группировка по странице без разбора строки в каждом запросе |
|
||||||
|
| `referer_host` | домен `referer` | «откуда пришли» одним значением, а не полным адресом |
|
||||||
|
|
||||||
|
Служебных `_load_id` и `_load_ts` у витрины нет, и это не забывчивость. Их
|
||||||
|
нет и у `dds.event_v`: событие в DDS не грузят, а читают из ODS, поэтому
|
||||||
|
координат собственной загрузки у него не бывает. Служебная колонка называет
|
||||||
|
запуск, который положил строку на диск, — здесь на диск никто ничего не
|
||||||
|
кладет.
|
||||||
|
|
||||||
|
## Ловушка задания
|
||||||
|
|
||||||
|
Массивы `product_*` тянет развернуть через `ARRAY JOIN` — так плоская
|
||||||
|
таблица становится по-настоящему плоской. Разворачивать нельзя, и вреда
|
||||||
|
здесь два.
|
||||||
|
|
||||||
|
Очевидный: у события с тремя позициями получилось бы три строки, зерно
|
||||||
|
витрины перестало бы совпадать с зерном источника, а вместе с ним развалилось
|
||||||
|
бы и правило формы — такую витрину уже пришлось бы материализовать.
|
||||||
|
|
||||||
|
Неочевидный и куда более разрушительный: `ARRAY JOIN` — соединение
|
||||||
|
внутреннее, и строку с пустым массивом он выбрасывает. Торговый блок пуст у
|
||||||
|
всех событий, кроме корзин и покупок, то есть у подавляющего большинства.
|
||||||
|
Так «плоская таблица событий» превращается в таблицу товарных позиций и
|
||||||
|
теряет девятнадцать строк из двадцати — а по имени и по составу колонок она
|
||||||
|
по-прежнему выглядит таблицей событий. Ошибка не падает и ничего не
|
||||||
|
подсвечивает.
|
||||||
|
|
||||||
|
Массивы едут в договор как есть; разворачивает их тот, кому нужна позиция
|
||||||
|
заказа, — и делает это уже в своей витрине, с другим зерном и другим именем.
|
||||||
|
|
||||||
|
## Как проверить себя
|
||||||
|
|
||||||
|
Деловой ключ — `event_id` внутри дня. Собери прямой ответ из `dds.event_v`
|
||||||
|
за тот же день, соедини с витриной полным внешним соединением и посчитай
|
||||||
|
строки, где ключа нет с одной из сторон или где различаются значения
|
||||||
|
деловых колонок. Ноль — зеленый.
|
||||||
|
|
||||||
|
Отдельно проверяются три производные колонки: `event_hour`, `page_path` и
|
||||||
|
`referer_host` должны совпасть с тем же выражением, посчитанным по
|
||||||
|
`dds.event_v` на лету.
|
||||||
|
|
||||||
|
Если строк в витрине оказалось в разы меньше, чем в источнике, а остались в
|
||||||
|
ней одни корзины и покупки — это развернутые массивы: `ARRAY JOIN` выбросил
|
||||||
|
все события с пустым торговым блоком.
|
||||||
|
|
||||||
|
## Приемка
|
||||||
|
|
||||||
|
- [ ] `dm.events_enriched_v` создано, физических таблиц под ним нет;
|
||||||
|
- [ ] список колонок в представлении явный;
|
||||||
|
- [ ] число строк и набор `event_id` совпадают с `dds.event_v` за каждый
|
||||||
|
прожитый день;
|
||||||
|
- [ ] задача `events_enriched` в `homework_check` зеленая;
|
||||||
|
- [ ] задачи в `dm_transform` **нет** — и не должно быть.
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
# Задание 3: `dm.session_overview`
|
||||||
|
|
||||||
|
**Чему учит:** агрегат хранит слагаемые, а не готовые средние и доли.
|
||||||
|
Среднее по строкам витрины не складывается никогда, а уникальных нельзя
|
||||||
|
складывать даже тогда, когда на сегодняшних данных сумма сходится.
|
||||||
|
|
||||||
|
## Роль витрины
|
||||||
|
|
||||||
|
Дневной портрет визитов в разрезе устройств: сколько было сессий, какие они
|
||||||
|
были длинные, сколько страниц смотрели и как часто доходили до покупки.
|
||||||
|
Вопрос витрины — «как люди ходят по магазину с телефона и с настольного
|
||||||
|
компьютера, и чем эти визиты отличаются».
|
||||||
|
|
||||||
|
Сессия на стенде — расчетная сущность, ее уже нарезал слой DDS. Собирать
|
||||||
|
визиты заново из событий не нужно и нельзя: это была бы вторая нарезка,
|
||||||
|
которая тихо разойдется с первой.
|
||||||
|
|
||||||
|
Витрина с таким именем была и на прошлом стенде, но зерно у нее сменилось,
|
||||||
|
и сменилось не по вкусу. Там визитов как сущности не было, и витрина
|
||||||
|
нарезала их сама, строкой на визит. Здесь визит — сущность слоя DDS, и
|
||||||
|
витрина, повторяющая его зерно, оказалась бы обычным представлением по
|
||||||
|
[правилу формы](../architecture/dm.md): копией сессий с другими именами
|
||||||
|
колонок. Чтобы задание осталось про дневной агрегат, витрина поднялась на
|
||||||
|
зерно выше — до дня и среза.
|
||||||
|
|
||||||
|
## Зерно и форма
|
||||||
|
|
||||||
|
Зерно — **день сессии × категория устройства**. Источник —
|
||||||
|
`dds.session_v`, у него зерно визита, значит зерно сменилось и витрина
|
||||||
|
материализуется: `dm.session_overview_rep` / `_dist`, донор `_stage_rep` /
|
||||||
|
`_stage_dist`, публичное `dm.session_overview_v`. Ключ партиции — день.
|
||||||
|
|
||||||
|
Категория устройства взята из сессии как есть: у куки она постоянна, и слой
|
||||||
|
DDS уже перевел код источника в слово. Словарь расшифровки шире, чем мир
|
||||||
|
стенда: слов в нем четыре, а встречаются три — `desktop`, `mobile` и
|
||||||
|
`tablet`. Строку под `tv` заводить незачем, она возникнет сама, если такие
|
||||||
|
визиты появятся.
|
||||||
|
|
||||||
|
## Состав колонок
|
||||||
|
|
||||||
|
| Колонка | Тип | Что это |
|
||||||
|
|---|---|---|
|
||||||
|
| `report_date` | `Date` | день сессии |
|
||||||
|
| `device_category` | `LowCardinality(String)` | категория устройства |
|
||||||
|
| `sessions` | `UInt64` | число визитов |
|
||||||
|
| `visitors` | `UInt64` | число разных `client_id` |
|
||||||
|
| `bounce_sessions` | `UInt64` | визиты ровно из одного события |
|
||||||
|
| `purchase_sessions` | `UInt64` | визиты, в которых была хотя бы одна покупка |
|
||||||
|
| `duration_seconds_total` | `UInt64` | суммарная длительность визитов |
|
||||||
|
| `events_total` | `UInt64` | событий во всех визитах |
|
||||||
|
| `pageviews` | `UInt64` | просмотров страниц |
|
||||||
|
| `cart_adds` | `UInt64` | добавлений в корзину |
|
||||||
|
| `purchases` | `UInt64` | покупок |
|
||||||
|
| `_load_id` | `String` | запуск, собравший день |
|
||||||
|
| `_load_ts` | `DateTime64(3, 'UTC')` | когда собран |
|
||||||
|
|
||||||
|
Средней длительности и доли отказов в витрине нет намеренно. Хранить
|
||||||
|
`avg_duration_seconds` было бы удобнее ровно один раз — пока смотрят одну
|
||||||
|
строку. Тот, кто захочет среднее по дню целиком, сложит средние по трем
|
||||||
|
устройствам и получит неправду: средние не складываются, они взвешиваются.
|
||||||
|
Пара «`duration_seconds_total` и `sessions`» дает верный ответ на любом
|
||||||
|
срезе, а деление стоит потребителю один вызов. То же и с долей отказов:
|
||||||
|
храним `bounce_sessions` и `sessions`, долю считает тот, кто спрашивает.
|
||||||
|
|
||||||
|
С `visitors` история тоньше, и в ней главная ловушка задания. Складывать
|
||||||
|
уникальных по строкам нельзя: один и тот же посетитель, попавший в две
|
||||||
|
строки, будет посчитан дважды. Проверь на стенде — и увидишь, что сумма
|
||||||
|
`visitors` по устройствам за день **сходится** с `dm.daily_traffic_v`.
|
||||||
|
|
||||||
|
Это не отменяет правила, а показывает, откуда берется совпадение: в мире
|
||||||
|
стенда устройство приписано куке на всю жизнь ([словарь
|
||||||
|
понятий](../../CONTEXT.md), «Паспорт куки»), поэтому одна кука лежит ровно
|
||||||
|
в одной строке дня, и пересекаться строкам не в чем.
|
||||||
|
|
||||||
|
Совпадение это — свойство мира, а не свойство витрины. Появятся визиты
|
||||||
|
одной куки с двух устройств — сумма поедет, и ни одна проверка об этом не
|
||||||
|
скажет: отчет просто начнет тихо завышать аудиторию. Отсюда правило,
|
||||||
|
которое стоит унести с задания: складывать уникальных нельзя даже там, где
|
||||||
|
сегодня получается. Число посетителей дня спрашивают у
|
||||||
|
`dm.daily_traffic_v`, где оно посчитано на своем зерне.
|
||||||
|
|
||||||
|
## Ритм пересборки
|
||||||
|
|
||||||
|
Источник — сессии: день замерзает, как только прожит. Объем прогона тот же,
|
||||||
|
что в задании 2, — хвост от первого несобранного прожитого дня; механика та
|
||||||
|
же: донор и замена дневных партиций.
|
||||||
|
|
||||||
|
## Ключ шардирования
|
||||||
|
|
||||||
|
Твое решение по [правилу донора](../architecture/storage.md), «Раскладка по
|
||||||
|
шардам». Здесь у измерения всего три встречающихся значения, и распределены
|
||||||
|
они неровно: планшеты — тонкая полоса, а телефоны и настольные машины держат
|
||||||
|
основную массу. Прежде чем брать хеш от одного `device_category`, посчитай,
|
||||||
|
сколько строк ляжет на каждый шард, и назови, чем тебя устраивает результат.
|
||||||
|
|
||||||
|
## Как проверить себя
|
||||||
|
|
||||||
|
Деловой ключ — `device_category` внутри дня. Собери прямой агрегат из
|
||||||
|
`dds.session_v` за день, соедини с `dm.session_overview_v` полным внешним
|
||||||
|
соединением и посчитай строки, где ключа нет с одной из сторон или где
|
||||||
|
расходится любой счетчик. Ноль — зеленый.
|
||||||
|
|
||||||
|
Две проверки покрупнее ловят грубую ошибку сразу:
|
||||||
|
|
||||||
|
- сумма `sessions` за день равна числу строк `dds.session_v` этого дня:
|
||||||
|
ни один визит не потерян и ни один не задвоен;
|
||||||
|
- сумма `events_total` за день равна сумме `events_total` по сессиям того
|
||||||
|
же дня.
|
||||||
|
|
||||||
|
Если суммы событий сходятся, а сумма сессий больше числа визитов — почти
|
||||||
|
наверняка витрина собрана из событий, а не из сессий.
|
||||||
|
|
||||||
|
## Приемка
|
||||||
|
|
||||||
|
- [ ] пять объектов витрины созданы, донор объявлен через `AS` и шардируется
|
||||||
|
ключом цели;
|
||||||
|
- [ ] `dm.session_overview_v` перечисляет колонки явно;
|
||||||
|
- [ ] источник витрины — `dds.session_v`, своей нарезки визитов в ней нет;
|
||||||
|
- [ ] средних и долей в физической таблице нет;
|
||||||
|
- [ ] задача `session_overview` в `homework_check` зеленая;
|
||||||
|
- [ ] задача витрины добавлена в `dm_transform`.
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
# Задание 2: `dm.top_pages_daily`
|
||||||
|
|
||||||
|
**Чему учит:** зерно сменилось — значит витрина материализуется, живет
|
||||||
|
дневными партициями и встает в конвейер. Первая настоящая витрина: пять
|
||||||
|
объектов, донор, замена партиции, задача в `dm_transform`.
|
||||||
|
|
||||||
|
## Роль витрины
|
||||||
|
|
||||||
|
Популярность страниц по дням: сколько раз страницу открыли, сколько разных
|
||||||
|
посетителей ее видели и что на ней делали кроме просмотра.
|
||||||
|
|
||||||
|
Торговое событие садится на ту страницу, где случилось: корзина — на
|
||||||
|
карточку товара, покупка — на страницу подтверждения заказа. Поэтому у
|
||||||
|
карточки товара рядом стоят просмотры и корзины, и их отношение —
|
||||||
|
конверсия карточки — главный вопрос к этой витрине: какие товары кладут в
|
||||||
|
корзину, едва открыв, а какие смотрят и уходят.
|
||||||
|
|
||||||
|
Воронки «просмотр → корзина → покупка» здесь нет и быть не может: корзина и
|
||||||
|
покупка живут на разных страницах, и связать их шаги в одну цепочку на
|
||||||
|
зерне страницы не из чего. Путь одного человека собирают из событий или из
|
||||||
|
сессий, а витрина страниц отвечает про страницу.
|
||||||
|
|
||||||
|
## Зерно и форма
|
||||||
|
|
||||||
|
Зерно — **день события × страница**. Источник — события, у него зерно
|
||||||
|
события, значит зерно сменилось и витрина материализуется:
|
||||||
|
`dm.top_pages_daily_rep` / `_dist`, донор `_stage_rep` / `_stage_dist`,
|
||||||
|
публичное `dm.top_pages_daily_v`.
|
||||||
|
|
||||||
|
Страница — это `page_path`, путь `url` без домена и параметров запроса;
|
||||||
|
считается он здесь же, из колонки `url` источника. Полный адрес в зерно не
|
||||||
|
берется: с параметрами запроса одна и та же страница рассыпалась бы на
|
||||||
|
сотни строк, и витрина перестала бы отвечать на свой вопрос.
|
||||||
|
|
||||||
|
Ключ партиции — день: он же единица пересборки.
|
||||||
|
|
||||||
|
Источник — `dds.event_v`, публичный договор события. Витрина задания 1
|
||||||
|
источником не служит: витрина слоя DM читает договор DDS, как и стендовые
|
||||||
|
витрины, и не зависит от того, выполнена ли соседняя домашка.
|
||||||
|
|
||||||
|
## Состав колонок
|
||||||
|
|
||||||
|
| Колонка | Тип | Что это |
|
||||||
|
|---|---|---|
|
||||||
|
| `report_date` | `Date` | день события |
|
||||||
|
| `page_path` | `LowCardinality(String)` | путь страницы |
|
||||||
|
| `pageviews` | `UInt64` | число событий `pageview` на странице |
|
||||||
|
| `visitors` | `UInt64` | число разных `client_id` на странице |
|
||||||
|
| `cart_adds` | `UInt64` | число событий `add_to_cart` на странице |
|
||||||
|
| `purchases` | `UInt64` | число событий `purchase` на странице |
|
||||||
|
| `_load_id` | `String` | запуск, собравший день |
|
||||||
|
| `_load_ts` | `DateTime64(3, 'UTC')` | когда собран |
|
||||||
|
|
||||||
|
Счетчики условные: строку дают все три типа событий, а различает их
|
||||||
|
`countIf`. Отфильтровать источник по `event_type = 'pageview'`, как
|
||||||
|
подсказывает имя витрины, — значит получить витрину из одних просмотров:
|
||||||
|
страницы останутся на месте, а `cart_adds` и `purchases` у всех строк
|
||||||
|
обнулятся. Ошибка тихая: витрина выглядит собранной, и главный ее вопрос —
|
||||||
|
конверсия карточки — молча отвечает «ноль везде».
|
||||||
|
|
||||||
|
## Ритм пересборки
|
||||||
|
|
||||||
|
Источник — события: день замерзает сразу, как только прожит. Значит
|
||||||
|
пересобирать нужно только то, что еще не собрано: хвост дней, которых в
|
||||||
|
витрине нет, — от первого несобранного прожитого дня до последнего
|
||||||
|
прожитого. Готовые дни трогать незачем, а вот дыру, оставленную частичным
|
||||||
|
отказом, объем прогона обязан подобрать: ищи не последний собранный день, а
|
||||||
|
первый несобранный.
|
||||||
|
|
||||||
|
Механика — сборка отрезка в донор одной вставкой и замена дневных партиций
|
||||||
|
по одной, как у `revenue_daily`.
|
||||||
|
|
||||||
|
## Ключ шардирования
|
||||||
|
|
||||||
|
Твое решение по [правилу донора](../architecture/storage.md), «Раскладка по
|
||||||
|
шардам». Подумай, чем эта витрина отличается от `revenue_daily`: там кроме
|
||||||
|
дня было одно измерение из шести близких по объему значений, здесь —
|
||||||
|
страницы, которых много и которые по популярности очень неровные.
|
||||||
|
|
||||||
|
## Как проверить себя
|
||||||
|
|
||||||
|
Деловой ключ — `page_path` внутри дня. Собери прямой агрегат из
|
||||||
|
`dds.event_v` за день, соедини с `dm.top_pages_daily_v` полным внешним
|
||||||
|
соединением по ключу и посчитай строки, где ключа нет с одной из сторон или
|
||||||
|
где расходится любой из четырех счетчиков. Ноль — зеленый.
|
||||||
|
|
||||||
|
Две проверки попроще, ими удобно ловить грубую ошибку до полного
|
||||||
|
соединения:
|
||||||
|
|
||||||
|
- сумма `pageviews` за день равна числу событий `pageview` этого дня в
|
||||||
|
`dds.event_v`;
|
||||||
|
- сумма `purchases` за день равна числу событий `purchase` этого дня.
|
||||||
|
|
||||||
|
Проверки ловят разное. Фильтр `WHERE event_type = 'pageview'` первая из них
|
||||||
|
пропускает: отбор строк по типу не меняет счет событий того же типа.
|
||||||
|
Расхождение покажет только вторая — вместе с `cart_adds` обнулятся и
|
||||||
|
`purchases`. Проверку выбирают по тому, умеет ли она покраснеть на твоей
|
||||||
|
ошибке, а не по тому, как быстро она пишется: [проверка, которая не умеет
|
||||||
|
краснеть, бесполезна](../architecture/testing.md).
|
||||||
|
|
||||||
|
`visitors` складывать по строкам нельзя: один посетитель ходит по разным
|
||||||
|
страницам и в сумме посчитается несколько раз. Число посетителей дня
|
||||||
|
спрашивают у `dm.daily_traffic_v`, а не у этой витрины.
|
||||||
|
|
||||||
|
## Приемка
|
||||||
|
|
||||||
|
- [ ] пять объектов витрины созданы, донор объявлен через `AS` и шардируется
|
||||||
|
ключом цели;
|
||||||
|
- [ ] `dm.top_pages_daily_v` перечисляет колонки явно, включая `_load_id` и
|
||||||
|
`_load_ts`;
|
||||||
|
- [ ] объем прогона выводится из состояния витрины, а не задается руками;
|
||||||
|
- [ ] задача `top_pages_daily` в `homework_check` зеленая;
|
||||||
|
- [ ] задача витрины добавлена в `dm_transform`, повторный прогон конвейера
|
||||||
|
результат не меняет.
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
# Задание 5: `dm.utm_effectiveness`
|
||||||
|
|
||||||
|
**Чему учит:** приставка `declared_` в имени колонки — предупреждение, а не
|
||||||
|
украшение. Она говорит: это заявка клиента, а не деньги отчетности. Урок
|
||||||
|
здесь дает конфликт: суммы не сойдутся, и раскапывать почему предстоит
|
||||||
|
самому.
|
||||||
|
|
||||||
|
Правило стенда: **поведение и атрибуцию считаем по трекеру, деньги — по
|
||||||
|
бэкенду**. Эта витрина — единственное разрешенное исключение: атрибуции по
|
||||||
|
UTM у бэкенда нет, метки живут только в кликстриме. Раз считать иначе
|
||||||
|
нечем, клиентская сумма берется, но называется своим именем.
|
||||||
|
|
||||||
|
## Роль витрины
|
||||||
|
|
||||||
|
Эффективность рекламных каналов по дням: сколько трафика привел канал,
|
||||||
|
сколько корзин и покупок в нем случилось и на какую сумму эти покупки, по
|
||||||
|
словам клиента.
|
||||||
|
|
||||||
|
## Атрибуция: по событию, а не по визиту
|
||||||
|
|
||||||
|
Метки едут на каждом событии, и витрина группирует события по их
|
||||||
|
собственным меткам. Модели «первое касание» или «последнее касание» здесь
|
||||||
|
нет: их построение — отдельная тема, а это витрина трафика по меткам.
|
||||||
|
|
||||||
|
`dds.session_v` при этом несет UTM первого события визита, и ответ «какой
|
||||||
|
канал привел» из сессии и из этой витрины будет разным. Это не
|
||||||
|
ошибка ни одной из сторон — это два разных вопроса, и в отчете их надо
|
||||||
|
различать.
|
||||||
|
|
||||||
|
## Прямой трафик из витрины не выбрасывается
|
||||||
|
|
||||||
|
Соблазн отфильтровать строки без меток велик: витрина же про рекламу.
|
||||||
|
Отфильтровать нельзя по двум причинам. Первая деловая: прямой трафик —
|
||||||
|
обычно самый крупный «канал» магазина, и отчет, где его нет, врет о доле
|
||||||
|
рекламы. Вторая техническая: как только часть событий выпала, итог витрины
|
||||||
|
перестал сходиться с итогом источника, и проверить сборку стало нечем.
|
||||||
|
|
||||||
|
Пустые метки остаются пустыми строками, как в источнике. Своего слова для
|
||||||
|
«прямого захода» витрина не изобретает: чем меньше выдуманных значений, тем
|
||||||
|
меньше расшифровывать читателю.
|
||||||
|
|
||||||
|
## Зерно и форма
|
||||||
|
|
||||||
|
Зерно — **день события × `utm_source` × `utm_medium` × `utm_campaign`**.
|
||||||
|
Зерно источника — событие, значит витрина материализуется:
|
||||||
|
`dm.utm_effectiveness_rep` / `_dist`, донор `_stage_rep` / `_stage_dist`,
|
||||||
|
публичное `dm.utm_effectiveness_v`. Ключ партиции — день.
|
||||||
|
|
||||||
|
Источник — `dds.event_v`, публичный договор события: витрина слоя DM читает
|
||||||
|
договор DDS, а не соседнюю витрину.
|
||||||
|
|
||||||
|
## Состав колонок
|
||||||
|
|
||||||
|
| Колонка | Тип | Что это |
|
||||||
|
|---|---|---|
|
||||||
|
| `report_date` | `Date` | день события |
|
||||||
|
| `utm_source` | `LowCardinality(String)` | источник кампании |
|
||||||
|
| `utm_medium` | `LowCardinality(String)` | канал кампании |
|
||||||
|
| `utm_campaign` | `String` | имя кампании |
|
||||||
|
| `visitors` | `UInt64` | число разных `client_id` |
|
||||||
|
| `pageviews` | `UInt64` | просмотров страниц |
|
||||||
|
| `add_to_carts` | `UInt64` | добавлений в корзину |
|
||||||
|
| `purchases` | `UInt64` | покупок |
|
||||||
|
| `declared_revenue` | `Decimal(18, 2)` | выручка покупок глазами клиента |
|
||||||
|
| `_load_id` | `String` | запуск, собравший день |
|
||||||
|
| `_load_ts` | `DateTime64(3, 'UTC')` | когда собран |
|
||||||
|
|
||||||
|
`declared_revenue` складывает `purchase_revenue[1]` событий `purchase`.
|
||||||
|
Индекс не описка: блок `purchase*` едет параллельными массивами, и у события
|
||||||
|
покупки в них ровно один элемент — контракт источника обещает один номер
|
||||||
|
заказа и одну объявленную сумму на событие. Тем же индексом берет свою
|
||||||
|
сторону стендовая сверка `purchase_vs_orders`.
|
||||||
|
|
||||||
|
Приводить тип не нужно: в договоре `purchase_revenue` уже
|
||||||
|
`Array(Decimal(18, 2))`. `Float64` эта сумма была только в выгрузке
|
||||||
|
источника, и ODS перевел ее в точные деньги на приеме — сложение Decimal
|
||||||
|
точное, и где стоит `CAST`, до или после `sum`, на результат не влияет.
|
||||||
|
Витрина просто хранит то, что сложила, в `Decimal(18, 2)`.
|
||||||
|
|
||||||
|
`utm_campaign` оставлена обычной строкой: имен кампаний со временем
|
||||||
|
становится много, и словарь `LowCardinality` на растущем множестве
|
||||||
|
перестает окупаться.
|
||||||
|
|
||||||
|
## Ритм пересборки
|
||||||
|
|
||||||
|
Источник — события: день замерзает сразу. Объем прогона и механика те же,
|
||||||
|
что в задании 2: хвост несобранных дней, донор, замена дневных партиций.
|
||||||
|
|
||||||
|
## Ключ шардирования
|
||||||
|
|
||||||
|
Твое решение по [правилу донора](../architecture/storage.md), «Раскладка по
|
||||||
|
шардам». Зерно здесь составное, из трех колонок, и решение «взять хеш от
|
||||||
|
одной из них» надо защищать отдельно: назови, какая доля строк придется на
|
||||||
|
самое частое значение выбранной колонки.
|
||||||
|
|
||||||
|
## Конфликт, ради которого витрина и написана
|
||||||
|
|
||||||
|
Собери за один и тот же день две суммы: `sum(declared_revenue)` из своей
|
||||||
|
витрины и `sum(revenue)` из `dm.revenue_daily_v`. Они не сойдутся, и это
|
||||||
|
верное поведение обеих витрин.
|
||||||
|
|
||||||
|
Базы у двух сумм при этом сравнимые: `revenue_daily` складывает цену позиций
|
||||||
|
на количество и ни скидки, ни доставки в себя не берет — ровно то, что видит
|
||||||
|
клиент. Значит все расхождение до копейки объясняется тем, что случилось с
|
||||||
|
заказом, а не тем, как считали деньги. Ищи ответы среди таких причин:
|
||||||
|
|
||||||
|
- отмененный заказ из кликстрима никуда не девается: событие покупки
|
||||||
|
случилось, а `revenue_daily` считает только оплаченные;
|
||||||
|
- событие покупки может потеряться по дороге, а заказ при этом доехать —
|
||||||
|
и наоборот, событие приехать дважды;
|
||||||
|
- клиент назвал сумму, отличную от серверной суммы позиций: генератор
|
||||||
|
делает это намеренно у одного-двух заказов из ста.
|
||||||
|
|
||||||
|
Как эти же расхождения выглядят на зерне заказа, показывает стендовая
|
||||||
|
витрина `dm.purchase_vs_orders_v` с ее классами расхождений: это готовый
|
||||||
|
ответ на вопрос «а сколько именно и из-за чего». Запиши свой вывод —
|
||||||
|
в двух-трех фразах, своими словами.
|
||||||
|
|
||||||
|
Проверять сходимость этих двух сумм не нужно и нельзя: они и не должны
|
||||||
|
сходиться.
|
||||||
|
|
||||||
|
## Как проверить себя
|
||||||
|
|
||||||
|
Деловой ключ — тройка `utm_source`, `utm_medium`, `utm_campaign` внутри
|
||||||
|
дня. Собери прямой агрегат из `dds.event_v` за день, соедини с
|
||||||
|
`dm.utm_effectiveness_v` полным внешним соединением и посчитай строки, где
|
||||||
|
ключа нет с одной из сторон или где расходится любая из пяти величин. Ноль —
|
||||||
|
зеленый.
|
||||||
|
|
||||||
|
Две проверки покрупнее ловят самые частые ошибки:
|
||||||
|
|
||||||
|
- сумма `pageviews`, `add_to_carts` и `purchases` за день равна числу
|
||||||
|
событий соответствующего типа в `dds.event_v` за тот же день;
|
||||||
|
- сумма `declared_revenue` за день равна сумме `purchase_revenue[1]` по всем
|
||||||
|
событиям `purchase` этого дня.
|
||||||
|
|
||||||
|
Обе не сойдутся, если прямой трафик отфильтрован, и разойдутся ровно на его
|
||||||
|
долю — по этому следу отфильтрованные строки и находят. `visitors`, как и в
|
||||||
|
задании 3, по строкам не складывается: один посетитель приходит по разным
|
||||||
|
меткам.
|
||||||
|
|
||||||
|
## Приемка
|
||||||
|
|
||||||
|
- [ ] пять объектов витрины созданы, донор объявлен через `AS` и шардируется
|
||||||
|
ключом цели;
|
||||||
|
- [ ] строки без UTM-меток в витрине остались;
|
||||||
|
- [ ] `declared_revenue` хранится в `Decimal(18, 2)`;
|
||||||
|
- [ ] расхождение с `dm.revenue_daily_v` объяснено письменно;
|
||||||
|
- [ ] задача `utm_effectiveness` в `homework_check` зеленая;
|
||||||
|
- [ ] задача витрины добавлена в `dm_transform`.
|
||||||
@@ -512,8 +512,14 @@ Kafka день переигрывается генератором заново:
|
|||||||
`dds.identity_map_v`: опыт на ClickHouse 26.3 показал, что публичные `_v`
|
`dds.identity_map_v`: опыт на ClickHouse 26.3 показал, что публичные `_v`
|
||||||
скрывают ко-локацию от локальной подстановки.
|
скрывают ко-локацию от локальной подстановки.
|
||||||
- `events_enriched_v`, `top_pages_daily_v`, `session_overview_v`,
|
- `events_enriched_v`, `top_pages_daily_v`, `session_overview_v`,
|
||||||
`dq_errors_daily_v` — переезжают на новую модель без смены роли: источник —
|
`dq_errors_daily_v` — переезжают на новую модель без смены роли. Источник
|
||||||
`dds.event_v`, не `ods.event`.
|
и зерно у двух из них меняет сама новая модель, и это зафиксировано ТЗ
|
||||||
|
домашек ([`docs/labs/`](../labs/README.md)):
|
||||||
|
`session_overview_v` читает `dds.session_v` и поднимается на зерно «день ×
|
||||||
|
срез», потому что визит стал сущностью DDS и витрина его зерна была бы
|
||||||
|
копией сессий; `dq_errors_daily_v` читает таблицы `ods.*_errors`, у
|
||||||
|
которых в DDS представительства нет. Остальные две читают `dds.event_v`,
|
||||||
|
а не `ods.event`.
|
||||||
- `dm.dq_summary` хранит текущее качество дня и обновляется партиционной
|
- `dm.dq_summary` хранит текущее качество дня и обновляется партиционной
|
||||||
заменой (политика «без TRUNCATE»).
|
заменой (политика «без TRUNCATE»).
|
||||||
|
|
||||||
@@ -650,9 +656,12 @@ ADR 0002 с приёмочными совпадают почти целиком.
|
|||||||
расхождение часов клиент/коллектор — туман карты, вернётся своим тикетом.
|
расхождение часов клиент/коллектор — туман карты, вернётся своим тикетом.
|
||||||
- Лабы и курс: v2 — другой стенд, лабы для него пишутся с нуля отдельной
|
- Лабы и курс: v2 — другой стенд, лабы для него пишутся с нуля отдельной
|
||||||
работой после этой спеки; редизайн лаб v1 (#7) остаётся в v1 и сюда не
|
работой после этой спеки; редизайн лаб v1 (#7) остаётся в v1 и сюда не
|
||||||
переносится. Спека даёт будущим лабам только опорные точки — контрольные
|
переносится. Исключение — домашки слоя DM: ТЗ витрин менти и даг
|
||||||
числа описи (сверка, идентичность). Явное следствие: после этапа 9
|
самопроверки забраны в этап 4 резолюцией #135, потому что витрина менти и
|
||||||
стенд работает, но учебного пути на нём ещё нет.
|
конвейер, в который она встает, проектируются вместе. Единый стиль блока
|
||||||
|
домашек — за работой учебного пути. Спека даёт будущим лабам только
|
||||||
|
опорные точки — контрольные числа описи (сверка, идентичность). Явное
|
||||||
|
следствие: после этапа 9 стенд работает, но учебного пути на нём ещё нет.
|
||||||
- Инкрементальный ETL (#8) — свой issue.
|
- Инкрементальный ETL (#8) — свой issue.
|
||||||
- Реплики (2×2), HAProxy, репликационная эксплуатация — в лекцию, не в стенд.
|
- Реплики (2×2), HAProxy, репликационная эксплуатация — в лекцию, не в стенд.
|
||||||
- TLS между нодами, настоящее хранилище и ротация секретов, внешние учётные
|
- TLS между нодами, настоящее хранилище и ротация секретов, внешние учётные
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
-- DQ: дневная сводка брака против живого окна таблиц ошибок ODS.
|
||||||
|
--
|
||||||
|
-- Граница окна повторяет TTL источников; более старую историю сверять не с чем.
|
||||||
|
|
||||||
|
WITH
|
||||||
|
reference AS
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
load_date,
|
||||||
|
source,
|
||||||
|
error_class,
|
||||||
|
count() AS error_rows
|
||||||
|
FROM
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
toDate(_load_ts) AS load_date,
|
||||||
|
'event' AS source,
|
||||||
|
error_class
|
||||||
|
FROM ods.event_errors_dist
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
toDate(_load_ts) AS load_date,
|
||||||
|
'order' AS source,
|
||||||
|
error_class
|
||||||
|
FROM ods.order_errors_dist
|
||||||
|
)
|
||||||
|
WHERE load_date >= toDate(now() - INTERVAL 1 MONTH)
|
||||||
|
GROUP BY load_date, source, error_class
|
||||||
|
),
|
||||||
|
actual AS
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
load_date,
|
||||||
|
source,
|
||||||
|
error_class,
|
||||||
|
count() AS physical_rows,
|
||||||
|
any(error_rows) AS error_rows
|
||||||
|
FROM dm.dq_errors_daily_v
|
||||||
|
WHERE load_date >= toDate(now() - INTERVAL 1 MONTH)
|
||||||
|
GROUP BY load_date, source, error_class
|
||||||
|
),
|
||||||
|
comparison AS
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
coalesce(reference.load_date, actual.load_date) AS data_date,
|
||||||
|
concat(
|
||||||
|
coalesce(reference.source, actual.source),
|
||||||
|
'/',
|
||||||
|
coalesce(reference.error_class, actual.error_class)
|
||||||
|
) AS business_key,
|
||||||
|
isNotNull(reference.source) AS reference_present,
|
||||||
|
isNotNull(actual.source) AS actual_present,
|
||||||
|
reference.error_rows AS reference_value,
|
||||||
|
actual.error_rows AS actual_value,
|
||||||
|
actual.physical_rows AS physical_rows,
|
||||||
|
isNull(reference.source)
|
||||||
|
OR isNull(actual.source)
|
||||||
|
OR actual.physical_rows != 1
|
||||||
|
OR NOT isNotDistinctFrom(reference.error_rows, actual.error_rows)
|
||||||
|
AS failed
|
||||||
|
FROM reference
|
||||||
|
GLOBAL FULL OUTER JOIN actual USING (load_date, source, error_class)
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
data_date,
|
||||||
|
countIf(reference_present) AS reference_rows,
|
||||||
|
countIf(actual_present) AS actual_rows,
|
||||||
|
countIf(failed) AS failed_rows,
|
||||||
|
groupArrayIf(20)(
|
||||||
|
concat(
|
||||||
|
toString(data_date),
|
||||||
|
' source/error_class=',
|
||||||
|
business_key,
|
||||||
|
': источник=',
|
||||||
|
if(reference_present, toString(reference_value), 'нет ключа'),
|
||||||
|
', витрина=',
|
||||||
|
if(
|
||||||
|
actual_present,
|
||||||
|
concat(toString(actual_value), ', rows=', toString(physical_rows)),
|
||||||
|
'нет ключа'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
failed
|
||||||
|
) AS diagnostics
|
||||||
|
FROM comparison
|
||||||
|
GROUP BY data_date
|
||||||
|
ORDER BY data_date
|
||||||
|
SETTINGS join_use_nulls = 1
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- DQ: область посуточной сверки обогащенных событий.
|
||||||
|
--
|
||||||
|
-- Объединение сторон оставляет видимым день, потерянный целиком.
|
||||||
|
|
||||||
|
SELECT day
|
||||||
|
FROM
|
||||||
|
(
|
||||||
|
SELECT event_date AS day
|
||||||
|
FROM dds.event_v
|
||||||
|
UNION ALL
|
||||||
|
SELECT event_date AS day
|
||||||
|
FROM dm.events_enriched_v
|
||||||
|
)
|
||||||
|
GROUP BY day
|
||||||
|
ORDER BY day
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
-- DQ: обогащенные события против прямого ответа DDS за один день.
|
||||||
|
--
|
||||||
|
-- День передает даг, чтобы широкая сверка не собирала всю историю в памяти.
|
||||||
|
|
||||||
|
WITH
|
||||||
|
reference AS
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
event_date,
|
||||||
|
event_id,
|
||||||
|
tuple(
|
||||||
|
event_time,
|
||||||
|
toHour(event_time),
|
||||||
|
client_id,
|
||||||
|
event_type,
|
||||||
|
url,
|
||||||
|
path(url),
|
||||||
|
referer,
|
||||||
|
domain(referer),
|
||||||
|
title,
|
||||||
|
utm_source,
|
||||||
|
utm_medium,
|
||||||
|
utm_campaign,
|
||||||
|
utm_content,
|
||||||
|
utm_term,
|
||||||
|
device_category,
|
||||||
|
browser,
|
||||||
|
operating_system,
|
||||||
|
region_country,
|
||||||
|
region_city,
|
||||||
|
product_id,
|
||||||
|
product_name,
|
||||||
|
product_category,
|
||||||
|
product_price,
|
||||||
|
product_quantity,
|
||||||
|
product_event_type,
|
||||||
|
purchase_id,
|
||||||
|
purchase_revenue,
|
||||||
|
purchase_coupon
|
||||||
|
) AS value
|
||||||
|
FROM dds.event_v
|
||||||
|
WHERE event_date = {day:Date}
|
||||||
|
),
|
||||||
|
actual AS
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
event_date,
|
||||||
|
event_id,
|
||||||
|
count() AS physical_rows,
|
||||||
|
any(tuple(
|
||||||
|
event_time,
|
||||||
|
event_hour,
|
||||||
|
client_id,
|
||||||
|
event_type,
|
||||||
|
url,
|
||||||
|
page_path,
|
||||||
|
referer,
|
||||||
|
referer_host,
|
||||||
|
title,
|
||||||
|
utm_source,
|
||||||
|
utm_medium,
|
||||||
|
utm_campaign,
|
||||||
|
utm_content,
|
||||||
|
utm_term,
|
||||||
|
device_category,
|
||||||
|
browser,
|
||||||
|
operating_system,
|
||||||
|
region_country,
|
||||||
|
region_city,
|
||||||
|
product_id,
|
||||||
|
product_name,
|
||||||
|
product_category,
|
||||||
|
product_price,
|
||||||
|
product_quantity,
|
||||||
|
product_event_type,
|
||||||
|
purchase_id,
|
||||||
|
purchase_revenue,
|
||||||
|
purchase_coupon
|
||||||
|
)) AS value
|
||||||
|
FROM dm.events_enriched_v
|
||||||
|
WHERE event_date = {day:Date}
|
||||||
|
GROUP BY event_date, event_id
|
||||||
|
),
|
||||||
|
comparison AS
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
coalesce(reference.event_date, actual.event_date) AS data_date,
|
||||||
|
coalesce(reference.event_id, actual.event_id) AS business_key,
|
||||||
|
isNotNull(reference.event_id) AS reference_present,
|
||||||
|
isNotNull(actual.event_id) AS actual_present,
|
||||||
|
reference.value AS reference_value,
|
||||||
|
actual.value AS actual_value,
|
||||||
|
actual.physical_rows AS physical_rows,
|
||||||
|
isNull(reference.event_id)
|
||||||
|
OR isNull(actual.event_id)
|
||||||
|
OR actual.physical_rows != 1
|
||||||
|
OR NOT isNotDistinctFrom(reference.value, actual.value) AS failed
|
||||||
|
FROM reference
|
||||||
|
GLOBAL FULL OUTER JOIN actual USING (event_date, event_id)
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
data_date,
|
||||||
|
countIf(reference_present) AS reference_rows,
|
||||||
|
countIf(actual_present) AS actual_rows,
|
||||||
|
countIf(failed) AS failed_rows,
|
||||||
|
groupArrayIf(20)(
|
||||||
|
concat(
|
||||||
|
toString(data_date),
|
||||||
|
' event_id=',
|
||||||
|
toString(business_key),
|
||||||
|
': источник=',
|
||||||
|
if(reference_present, toString(reference_value), 'нет ключа'),
|
||||||
|
', витрина=',
|
||||||
|
if(
|
||||||
|
actual_present,
|
||||||
|
concat(toString(actual_value), ', rows=', toString(physical_rows)),
|
||||||
|
'нет ключа'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
failed
|
||||||
|
) AS diagnostics
|
||||||
|
FROM comparison
|
||||||
|
GROUP BY data_date
|
||||||
|
ORDER BY data_date
|
||||||
|
SETTINGS join_use_nulls = 1
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
-- DQ: дневной портрет сессий против прямого агрегата сессий DDS.
|
||||||
|
--
|
||||||
|
-- Полное объединение не прячет день или устройство, исчезнувшие с любой стороны.
|
||||||
|
|
||||||
|
WITH
|
||||||
|
reference AS
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
session_date AS report_date,
|
||||||
|
device_category,
|
||||||
|
tuple(
|
||||||
|
count(),
|
||||||
|
uniqExact(client_id),
|
||||||
|
countIf(events_total = 1),
|
||||||
|
countIf(purchases > 0),
|
||||||
|
sum(duration_seconds),
|
||||||
|
sum(events_total),
|
||||||
|
sum(pageviews),
|
||||||
|
sum(cart_adds),
|
||||||
|
sum(purchases)
|
||||||
|
) AS value
|
||||||
|
FROM dds.session_v
|
||||||
|
GROUP BY report_date, device_category
|
||||||
|
),
|
||||||
|
actual AS
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
report_date,
|
||||||
|
device_category,
|
||||||
|
count() AS physical_rows,
|
||||||
|
any(tuple(
|
||||||
|
sessions,
|
||||||
|
visitors,
|
||||||
|
bounce_sessions,
|
||||||
|
purchase_sessions,
|
||||||
|
duration_seconds_total,
|
||||||
|
events_total,
|
||||||
|
pageviews,
|
||||||
|
cart_adds,
|
||||||
|
purchases
|
||||||
|
)) AS value
|
||||||
|
FROM dm.session_overview_v
|
||||||
|
GROUP BY report_date, device_category
|
||||||
|
),
|
||||||
|
comparison AS
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
coalesce(reference.report_date, actual.report_date) AS data_date,
|
||||||
|
coalesce(reference.device_category, actual.device_category)
|
||||||
|
AS business_key,
|
||||||
|
isNotNull(reference.device_category) AS reference_present,
|
||||||
|
isNotNull(actual.device_category) AS actual_present,
|
||||||
|
reference.value AS reference_value,
|
||||||
|
actual.value AS actual_value,
|
||||||
|
actual.physical_rows AS physical_rows,
|
||||||
|
isNull(reference.device_category)
|
||||||
|
OR isNull(actual.device_category)
|
||||||
|
OR actual.physical_rows != 1
|
||||||
|
OR NOT isNotDistinctFrom(reference.value, actual.value) AS failed
|
||||||
|
FROM reference
|
||||||
|
GLOBAL FULL OUTER JOIN actual USING (report_date, device_category)
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
data_date,
|
||||||
|
countIf(reference_present) AS reference_rows,
|
||||||
|
countIf(actual_present) AS actual_rows,
|
||||||
|
countIf(failed) AS failed_rows,
|
||||||
|
groupArrayIf(20)(
|
||||||
|
concat(
|
||||||
|
toString(data_date),
|
||||||
|
' device_category=',
|
||||||
|
business_key,
|
||||||
|
': источник=',
|
||||||
|
if(reference_present, toString(reference_value), 'нет ключа'),
|
||||||
|
', витрина=',
|
||||||
|
if(
|
||||||
|
actual_present,
|
||||||
|
concat(toString(actual_value), ', rows=', toString(physical_rows)),
|
||||||
|
'нет ключа'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
failed
|
||||||
|
) AS diagnostics
|
||||||
|
FROM comparison
|
||||||
|
GROUP BY data_date
|
||||||
|
ORDER BY data_date
|
||||||
|
SETTINGS join_use_nulls = 1
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
-- DQ: дневная популярность страниц против прямого агрегата событий DDS.
|
||||||
|
--
|
||||||
|
-- Полное объединение не прячет страницу, исчезнувшую с любой стороны.
|
||||||
|
|
||||||
|
WITH
|
||||||
|
reference AS
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
event_date AS report_date,
|
||||||
|
path(url) AS page_path,
|
||||||
|
tuple(
|
||||||
|
countIf(event_type = 'pageview'),
|
||||||
|
uniqExact(client_id),
|
||||||
|
countIf(event_type = 'add_to_cart'),
|
||||||
|
countIf(event_type = 'purchase')
|
||||||
|
) AS value
|
||||||
|
FROM dds.event_v
|
||||||
|
GROUP BY report_date, page_path
|
||||||
|
),
|
||||||
|
actual AS
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
report_date,
|
||||||
|
page_path,
|
||||||
|
count() AS physical_rows,
|
||||||
|
any(tuple(pageviews, visitors, cart_adds, purchases)) AS value
|
||||||
|
FROM dm.top_pages_daily_v
|
||||||
|
GROUP BY report_date, page_path
|
||||||
|
),
|
||||||
|
comparison AS
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
coalesce(reference.report_date, actual.report_date) AS data_date,
|
||||||
|
coalesce(reference.page_path, actual.page_path) AS business_key,
|
||||||
|
isNotNull(reference.page_path) AS reference_present,
|
||||||
|
isNotNull(actual.page_path) AS actual_present,
|
||||||
|
reference.value AS reference_value,
|
||||||
|
actual.value AS actual_value,
|
||||||
|
actual.physical_rows AS physical_rows,
|
||||||
|
isNull(reference.page_path)
|
||||||
|
OR isNull(actual.page_path)
|
||||||
|
OR actual.physical_rows != 1
|
||||||
|
OR NOT isNotDistinctFrom(reference.value, actual.value) AS failed
|
||||||
|
FROM reference
|
||||||
|
GLOBAL FULL OUTER JOIN actual USING (report_date, page_path)
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
data_date,
|
||||||
|
countIf(reference_present) AS reference_rows,
|
||||||
|
countIf(actual_present) AS actual_rows,
|
||||||
|
countIf(failed) AS failed_rows,
|
||||||
|
groupArrayIf(20)(
|
||||||
|
concat(
|
||||||
|
toString(data_date),
|
||||||
|
' page_path=',
|
||||||
|
business_key,
|
||||||
|
': источник=',
|
||||||
|
if(reference_present, toString(reference_value), 'нет ключа'),
|
||||||
|
', витрина=',
|
||||||
|
if(
|
||||||
|
actual_present,
|
||||||
|
concat(toString(actual_value), ', rows=', toString(physical_rows)),
|
||||||
|
'нет ключа'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
failed
|
||||||
|
) AS diagnostics
|
||||||
|
FROM comparison
|
||||||
|
GROUP BY data_date
|
||||||
|
ORDER BY data_date
|
||||||
|
SETTINGS join_use_nulls = 1
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
-- DQ: эффективность UTM-меток против прямого агрегата событий DDS.
|
||||||
|
--
|
||||||
|
-- Полное объединение не прячет набор меток, исчезнувший с любой стороны.
|
||||||
|
|
||||||
|
WITH
|
||||||
|
reference AS
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
event_date AS report_date,
|
||||||
|
utm_source,
|
||||||
|
utm_medium,
|
||||||
|
utm_campaign,
|
||||||
|
tuple(
|
||||||
|
uniqExact(client_id),
|
||||||
|
countIf(event_type = 'pageview'),
|
||||||
|
countIf(event_type = 'add_to_cart'),
|
||||||
|
countIf(event_type = 'purchase'),
|
||||||
|
sumIf(purchase_revenue[1], event_type = 'purchase')
|
||||||
|
) AS value
|
||||||
|
FROM dds.event_v
|
||||||
|
GROUP BY report_date, utm_source, utm_medium, utm_campaign
|
||||||
|
),
|
||||||
|
actual AS
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
report_date,
|
||||||
|
utm_source,
|
||||||
|
utm_medium,
|
||||||
|
utm_campaign,
|
||||||
|
count() AS physical_rows,
|
||||||
|
any(tuple(
|
||||||
|
visitors,
|
||||||
|
pageviews,
|
||||||
|
add_to_carts,
|
||||||
|
purchases,
|
||||||
|
declared_revenue
|
||||||
|
)) AS value
|
||||||
|
FROM dm.utm_effectiveness_v
|
||||||
|
GROUP BY report_date, utm_source, utm_medium, utm_campaign
|
||||||
|
),
|
||||||
|
comparison AS
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
coalesce(reference.report_date, actual.report_date) AS data_date,
|
||||||
|
concat(
|
||||||
|
'utm_source=', coalesce(reference.utm_source, actual.utm_source),
|
||||||
|
', utm_medium=', coalesce(reference.utm_medium, actual.utm_medium),
|
||||||
|
', utm_campaign=',
|
||||||
|
coalesce(reference.utm_campaign, actual.utm_campaign)
|
||||||
|
) AS business_key,
|
||||||
|
isNotNull(reference.utm_source) AS reference_present,
|
||||||
|
isNotNull(actual.utm_source) AS actual_present,
|
||||||
|
reference.value AS reference_value,
|
||||||
|
actual.value AS actual_value,
|
||||||
|
actual.physical_rows AS physical_rows,
|
||||||
|
isNull(reference.utm_source)
|
||||||
|
OR isNull(actual.utm_source)
|
||||||
|
OR actual.physical_rows != 1
|
||||||
|
OR NOT isNotDistinctFrom(reference.value, actual.value) AS failed
|
||||||
|
FROM reference
|
||||||
|
GLOBAL FULL OUTER JOIN actual
|
||||||
|
USING (report_date, utm_source, utm_medium, utm_campaign)
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
data_date,
|
||||||
|
countIf(reference_present) AS reference_rows,
|
||||||
|
countIf(actual_present) AS actual_rows,
|
||||||
|
countIf(failed) AS failed_rows,
|
||||||
|
groupArrayIf(20)(
|
||||||
|
concat(
|
||||||
|
toString(data_date),
|
||||||
|
' ',
|
||||||
|
business_key,
|
||||||
|
': источник=',
|
||||||
|
if(reference_present, toString(reference_value), 'нет ключа'),
|
||||||
|
', витрина=',
|
||||||
|
if(
|
||||||
|
actual_present,
|
||||||
|
concat(toString(actual_value), ', rows=', toString(physical_rows)),
|
||||||
|
'нет ключа'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
failed
|
||||||
|
) AS diagnostics
|
||||||
|
FROM comparison
|
||||||
|
GROUP BY data_date
|
||||||
|
ORDER BY data_date
|
||||||
|
SETTINGS join_use_nulls = 1
|
||||||
Reference in new issue
Block a user