fix(dq): устранено переполнение памяти проверок
- Зачем: - параллельный DQ выгружал деловые ключи в Python и исчерпывал 640 МиБ планировщика. - Что: - четыре сверки агрегируют дневные итоги и ограниченную диагностику в ClickHouse. - коробка планировщика поднята до 1,5 ГиБ, локальные пределы задач удалены. - измерения и граница между SQL и Python записаны в документации. - Проверка: - make lint, make config-test, make smoke, make check-clickhouse, make check-services. - полный etl_pipeline, красный и восстановительный прогоны dq_check.
This commit is contained in:
1 parent
26b2e5fea1
commit
54686651df
10 files changed
+222
-117
No files matched your search
+4
-1
@@ -310,7 +310,10 @@ services:
|
|||||||
- ./infra/airflow/init.sh:/opt/airflow/init.sh:ro
|
- ./infra/airflow/init.sh:/opt/airflow/init.sh:ro
|
||||||
- airflow_logs:/opt/airflow/logs
|
- airflow_logs:/opt/airflow/logs
|
||||||
- airflow_auth:/opt/airflow/auth
|
- airflow_auth:/opt/airflow/auth
|
||||||
mem_limit: 640m
|
# LocalExecutor запускает здесь до четырех процессов задач одновременно.
|
||||||
|
# 640 МиБ дали OOM, а 1 ГиБ - давление на memory.max при штатном dq_check;
|
||||||
|
# 1,5 ГиБ оставляет запас при AIRFLOW__CORE__PARALLELISM=4.
|
||||||
|
mem_limit: 1536m
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "curl -sf http://127.0.0.1:8974/health >/dev/null"]
|
test: ["CMD-SHELL", "curl -sf http://127.0.0.1:8974/health >/dev/null"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
|
|||||||
@@ -279,11 +279,6 @@ def replacements(scope: dict[str, object]) -> list[dict[str, object]]:
|
|||||||
# Два прогона разом собирали бы один и тот же двойник: второй затёр бы
|
# Два прогона разом собирали бы один и тот же двойник: второй затёр бы
|
||||||
# чужую сборку между вставкой и заменой партиций.
|
# чужую сборку между вставкой и заменой партиций.
|
||||||
max_active_runs=1,
|
max_active_runs=1,
|
||||||
# Потолок на задачи в прогоне. Размноженная замена норовит выстрелить
|
|
||||||
# всеми днями сразу, а каждая задача — свой процесс исполнителя, и
|
|
||||||
# планировщику отведено 640 МиБ (compose.yaml). Замерено: тринадцать дней
|
|
||||||
# двух сущностей без потолка кончаются SIGKILL от контрольной группы.
|
|
||||||
max_active_tasks=3,
|
|
||||||
template_searchpath=str(SQL_ROOT),
|
template_searchpath=str(SQL_ROOT),
|
||||||
tags=["dds"],
|
tags=["dds"],
|
||||||
)
|
)
|
||||||
@@ -355,10 +350,6 @@ def dds_transform():
|
|||||||
conn_id=CLICKHOUSE_CONNECTION,
|
conn_id=CLICKHOUSE_CONNECTION,
|
||||||
sql="dds/order_replace.sql",
|
sql="dds/order_replace.sql",
|
||||||
do_xcom_push=False,
|
do_xcom_push=False,
|
||||||
# Замены не зависят друг от друга, но каждая — отдельный процесс
|
|
||||||
# LocalExecutor. Три разом получили SIGKILL при лимите scheduler
|
|
||||||
# 640 МиБ; последовательность не меняет деловой результат.
|
|
||||||
max_active_tis_per_dag=1,
|
|
||||||
).expand_kwargs(replacements(days))
|
).expand_kwargs(replacements(days))
|
||||||
|
|
||||||
rebuild >> replace
|
rebuild >> replace
|
||||||
@@ -420,8 +411,6 @@ def dds_transform():
|
|||||||
conn_id=CLICKHOUSE_CONNECTION,
|
conn_id=CLICKHOUSE_CONNECTION,
|
||||||
sql="dds/session_replace.sql",
|
sql="dds/session_replace.sql",
|
||||||
do_xcom_push=False,
|
do_xcom_push=False,
|
||||||
# Причина единицы разобрана у order.replace выше.
|
|
||||||
max_active_tis_per_dag=1,
|
|
||||||
).expand_kwargs(replacements(days))
|
).expand_kwargs(replacements(days))
|
||||||
|
|
||||||
rebuild >> replace
|
rebuild >> replace
|
||||||
|
|||||||
@@ -76,10 +76,6 @@ def replacements(scope: dict[str, object]) -> list[dict[str, object]]:
|
|||||||
start_date=START_DATE,
|
start_date=START_DATE,
|
||||||
is_paused_upon_creation=False,
|
is_paused_upon_creation=False,
|
||||||
max_active_runs=1,
|
max_active_runs=1,
|
||||||
# Каждая задача LocalExecutor — отдельный процесс. После появления
|
|
||||||
# dq_check три одновременные сборки DM получили SIGKILL при лимите
|
|
||||||
# scheduler 640 МиБ; две оставляют запас и сохраняют параллельность.
|
|
||||||
max_active_tasks=2,
|
|
||||||
template_searchpath=str(SQL_ROOT),
|
template_searchpath=str(SQL_ROOT),
|
||||||
tags=["dm"],
|
tags=["dm"],
|
||||||
)
|
)
|
||||||
@@ -137,11 +133,6 @@ def dm_transform():
|
|||||||
conn_id=CLICKHOUSE_CONNECTION,
|
conn_id=CLICKHOUSE_CONNECTION,
|
||||||
sql="dm/revenue_daily_replace.sql",
|
sql="dm/revenue_daily_replace.sql",
|
||||||
do_xcom_push=False,
|
do_xcom_push=False,
|
||||||
# Airflow 3 применяет этот лимит и к размноженным экземплярам
|
|
||||||
# (сверено через Context7). Каждая замена - отдельный процесс
|
|
||||||
# LocalExecutor; три разом исчерпали память контейнера scheduler,
|
|
||||||
# а самим дням параллельность ничего не дает.
|
|
||||||
max_active_tis_per_dag=1,
|
|
||||||
).expand_kwargs(replacements(days))
|
).expand_kwargs(replacements(days))
|
||||||
|
|
||||||
rebuild >> replace
|
rebuild >> replace
|
||||||
@@ -199,8 +190,6 @@ def dm_transform():
|
|||||||
conn_id=CLICKHOUSE_CONNECTION,
|
conn_id=CLICKHOUSE_CONNECTION,
|
||||||
sql="dm/purchase_vs_orders_replace.sql",
|
sql="dm/purchase_vs_orders_replace.sql",
|
||||||
do_xcom_push=False,
|
do_xcom_push=False,
|
||||||
# Причина единицы разобрана у revenue_daily.replace выше.
|
|
||||||
max_active_tis_per_dag=1,
|
|
||||||
).expand_kwargs(replacements(days))
|
).expand_kwargs(replacements(days))
|
||||||
|
|
||||||
rebuild >> replace
|
rebuild >> replace
|
||||||
|
|||||||
+61
-62
@@ -11,7 +11,6 @@ from __future__ import annotations
|
|||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from airflow.exceptions import AirflowException
|
from airflow.exceptions import AirflowException
|
||||||
@@ -69,41 +68,27 @@ def _world_position() -> int:
|
|||||||
return position
|
return position
|
||||||
|
|
||||||
|
|
||||||
def _write_summary(
|
def _publish_summary(
|
||||||
check_name: str,
|
check_name: str,
|
||||||
comparisons: list[tuple[object, ...]],
|
summaries: list[tuple[object, ...]],
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
"""Свернуть строки деловых ключей в дневную сводку и малый срез ошибок."""
|
"""Записать дневную сводку и вернуть малый срез готовой диагностики."""
|
||||||
if not comparisons:
|
if not summaries:
|
||||||
raise AirflowException(f"{check_name}: область проверки пуста")
|
raise AirflowException(f"{check_name}: область проверки пуста")
|
||||||
|
|
||||||
totals: dict[datetime.date, list[int]] = defaultdict(lambda: [0, 0, 0])
|
|
||||||
diagnostics: list[str] = []
|
diagnostics: list[str] = []
|
||||||
for (
|
|
||||||
data_date,
|
|
||||||
business_key,
|
|
||||||
reference_present,
|
|
||||||
actual_present,
|
|
||||||
reference_value,
|
|
||||||
actual_value,
|
|
||||||
failed,
|
|
||||||
) in comparisons:
|
|
||||||
counts = totals[data_date]
|
|
||||||
counts[0] += int(reference_present)
|
|
||||||
counts[1] += int(actual_present)
|
|
||||||
counts[2] += int(failed)
|
|
||||||
if failed and len(diagnostics) < DETAIL_LIMIT:
|
|
||||||
reference_text = reference_value if reference_present else "нет ключа"
|
|
||||||
actual_text = actual_value if actual_present else "нет ключа"
|
|
||||||
diagnostics.append(
|
|
||||||
f"{data_date} key={business_key}: "
|
|
||||||
f"эталон={reference_text}, объект={actual_text}"
|
|
||||||
)
|
|
||||||
|
|
||||||
context = get_current_context()
|
context = get_current_context()
|
||||||
load_id = context["run_id"]
|
load_id = context["run_id"]
|
||||||
load_ts = datetime.datetime.now(datetime.UTC)
|
load_ts = datetime.datetime.now(datetime.UTC)
|
||||||
rows = [
|
rows = []
|
||||||
|
for (
|
||||||
|
data_date,
|
||||||
|
reference_rows,
|
||||||
|
actual_rows,
|
||||||
|
failed_rows,
|
||||||
|
details,
|
||||||
|
) in summaries:
|
||||||
|
rows.append(
|
||||||
(
|
(
|
||||||
data_date,
|
data_date,
|
||||||
check_name,
|
check_name,
|
||||||
@@ -114,10 +99,9 @@ def _write_summary(
|
|||||||
load_id,
|
load_id,
|
||||||
load_ts,
|
load_ts,
|
||||||
)
|
)
|
||||||
for data_date, (reference_rows, actual_rows, failed_rows) in sorted(
|
|
||||||
totals.items()
|
|
||||||
)
|
)
|
||||||
]
|
diagnostics.extend(details[: DETAIL_LIMIT - len(diagnostics)])
|
||||||
|
|
||||||
_clickhouse_hook().bulk_insert_rows(
|
_clickhouse_hook().bulk_insert_rows(
|
||||||
"dm.dq_summary_stage_dist",
|
"dm.dq_summary_stage_dist",
|
||||||
rows,
|
rows,
|
||||||
@@ -126,17 +110,31 @@ def _write_summary(
|
|||||||
return diagnostics
|
return diagnostics
|
||||||
|
|
||||||
|
|
||||||
|
def _build_sql_check(
|
||||||
|
check_name: str,
|
||||||
|
sql_name: str,
|
||||||
|
parameters: dict[str, object] | None = None,
|
||||||
|
) -> list[str]:
|
||||||
|
"""Выполнить SQL-сверку и записать ее дневные итоги в донор."""
|
||||||
|
query = (SQL_ROOT / "dq" / sql_name).read_text(encoding="utf-8")
|
||||||
|
summaries = _clickhouse_hook().get_records(query, parameters=parameters)
|
||||||
|
return _publish_summary(check_name, summaries)
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task
|
||||||
def build_sql_check(check_name: str, sql_name: str) -> list[str]:
|
def build_sql_check(check_name: str, sql_name: str) -> list[str]:
|
||||||
"""Выполнить одну SQL-сверку и записать ее дневные итоги в донор."""
|
"""Собрать проверку, которой не нужны координаты модельного мира."""
|
||||||
query = (SQL_ROOT / "dq" / sql_name).read_text(encoding="utf-8")
|
return _build_sql_check(check_name, sql_name)
|
||||||
parameters = (
|
|
||||||
{"position": _world_position()}
|
|
||||||
if check_name == "sessions_vs_reference"
|
@task
|
||||||
else None
|
def build_sessions_vs_reference() -> list[str]:
|
||||||
|
"""Сверить сессии только в пределах прожитых дней мира."""
|
||||||
|
return _build_sql_check(
|
||||||
|
"sessions_vs_reference",
|
||||||
|
"sessions_vs_reference.sql",
|
||||||
|
parameters={"position": _world_position()},
|
||||||
)
|
)
|
||||||
comparisons = _clickhouse_hook().get_records(query, parameters=parameters)
|
|
||||||
return _write_summary(check_name, comparisons)
|
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task
|
||||||
@@ -158,19 +156,29 @@ def build_classes_vs_inventory() -> list[str]:
|
|||||||
for data_date, mismatch_class, count in _clickhouse_hook().get_records(query)
|
for data_date, mismatch_class, count in _clickhouse_hook().get_records(query)
|
||||||
if (data_date, mismatch_class) in expected
|
if (data_date, mismatch_class) in expected
|
||||||
}
|
}
|
||||||
comparisons = [
|
summaries = []
|
||||||
(
|
for data_date in sorted({day for day, _ in expected}):
|
||||||
data_date,
|
details = []
|
||||||
mismatch_class,
|
reference_rows = 0
|
||||||
1,
|
actual_rows = 0
|
||||||
int((data_date, mismatch_class) in actual),
|
failed_rows = 0
|
||||||
reference_count,
|
for (day, mismatch_class), reference_count in sorted(expected.items()):
|
||||||
actual.get((data_date, mismatch_class)),
|
if day != data_date:
|
||||||
reference_count != actual.get((data_date, mismatch_class)),
|
continue
|
||||||
|
actual_key = (day, mismatch_class)
|
||||||
|
actual_count = actual.get(actual_key)
|
||||||
|
reference_rows += 1
|
||||||
|
actual_rows += int(actual_key in actual)
|
||||||
|
if reference_count != actual_count:
|
||||||
|
failed_rows += 1
|
||||||
|
actual_text = actual_count if actual_count is not None else "нет ключа"
|
||||||
|
details.append(
|
||||||
|
f"{day} key={mismatch_class}: "
|
||||||
|
f"эталон={reference_count}, "
|
||||||
|
f"объект={actual_text}"
|
||||||
)
|
)
|
||||||
for (data_date, mismatch_class), reference_count in sorted(expected.items())
|
summaries.append((data_date, reference_rows, actual_rows, failed_rows, details))
|
||||||
]
|
return _publish_summary("classes_vs_inventory", summaries)
|
||||||
return _write_summary("classes_vs_inventory", comparisons)
|
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task
|
||||||
@@ -186,7 +194,7 @@ def publication_scope() -> list[dict[str, dict[str, str]]]:
|
|||||||
|
|
||||||
@task
|
@task
|
||||||
def drop_stale_partitions() -> None:
|
def drop_stale_partitions() -> None:
|
||||||
"""Убрать дни, которые после полного прогона не покрывает ни одна сторона."""
|
"""Убрать дни, исчезнувшие после починки с обеих сторон сверки."""
|
||||||
hook = _clickhouse_hook()
|
hook = _clickhouse_hook()
|
||||||
stale_days = hook.get_records(
|
stale_days = hook.get_records(
|
||||||
"""
|
"""
|
||||||
@@ -237,10 +245,6 @@ def assert_check(check_name: str, diagnostics: list[str]) -> None:
|
|||||||
start_date=START_DATE,
|
start_date=START_DATE,
|
||||||
is_paused_upon_creation=False,
|
is_paused_upon_creation=False,
|
||||||
max_active_runs=1,
|
max_active_runs=1,
|
||||||
# Три одновременные сверки на живом мире исчерпали 640 МиБ scheduler и
|
|
||||||
# получили SIGKILL. Последовательность здесь дешева: весь даг укладывается
|
|
||||||
# в один учебный прогон, а общему донору параллельность ничего не даёт.
|
|
||||||
max_active_tasks=1,
|
|
||||||
template_searchpath=str(SQL_ROOT),
|
template_searchpath=str(SQL_ROOT),
|
||||||
tags=["dq"],
|
tags=["dq"],
|
||||||
)
|
)
|
||||||
@@ -253,9 +257,7 @@ def dq_check():
|
|||||||
do_xcom_push=False,
|
do_xcom_push=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
sessions = build_sql_check.override(task_id="build_sessions_vs_reference")(
|
sessions = build_sessions_vs_reference()
|
||||||
"sessions_vs_reference", "sessions_vs_reference.sql"
|
|
||||||
)
|
|
||||||
classes = build_classes_vs_inventory.override(
|
classes = build_classes_vs_inventory.override(
|
||||||
task_id="build_classes_vs_inventory"
|
task_id="build_classes_vs_inventory"
|
||||||
)()
|
)()
|
||||||
@@ -280,9 +282,6 @@ def dq_check():
|
|||||||
conn_id=CLICKHOUSE_CONNECTION,
|
conn_id=CLICKHOUSE_CONNECTION,
|
||||||
sql="dq/replace.sql",
|
sql="dq/replace.sql",
|
||||||
do_xcom_push=False,
|
do_xcom_push=False,
|
||||||
# Каждая замена — отдельный процесс LocalExecutor. Последовательность
|
|
||||||
# держит даг в измеренном лимите памяти планировщика.
|
|
||||||
max_active_tis_per_dag=1,
|
|
||||||
).expand_kwargs(days)
|
).expand_kwargs(days)
|
||||||
|
|
||||||
sessions_assertion = assert_check.override(task_id="sessions_vs_reference")(
|
sessions_assertion = assert_check.override(task_id="sessions_vs_reference")(
|
||||||
|
|||||||
@@ -19,16 +19,23 @@
|
|||||||
|
|
||||||
Нодам ClickHouse — по 4 ГиБ вместо гигабайта. Дальше в этом тексте предел
|
Нодам ClickHouse — по 4 ГиБ вместо гигабайта. Дальше в этом тексте предел
|
||||||
памяти контейнера (`mem_limit`) называется коробкой: сервер живёт внутри неё и
|
памяти контейнера (`mem_limit`) называется коробкой: сервер живёт внутри неё и
|
||||||
о самой машине ничего не знает. Остальные `mem_limit` не меняются.
|
о самой машине ничего не знает. На 1 августа остальные `mem_limit` не менялись.
|
||||||
|
|
||||||
Числа, выставленные ради прежнего бюджета, остаются в силе, но лишаются
|
Числа, выставленные ради прежнего бюджета, остаются в силе, но лишаются
|
||||||
статуса решения: `KAFKA_HEAP_OPTS`, `shared_buffers`, `max_connections`,
|
статуса решения: `KAFKA_HEAP_OPTS`, `shared_buffers`, `max_connections`,
|
||||||
`AIRFLOW__CORE__PARALLELISM` и девять неизменённых `mem_limit`. Основания,
|
`AIRFLOW__CORE__PARALLELISM` и восемь оставшихся `mem_limit`. Основания,
|
||||||
кроме отменённого, у них нет. Кто в них упрётся — меняет по первому
|
кроме отменённого, у них нет. Кто в них упрётся — меняет по первому
|
||||||
свидетельству и этот ADR не пересматривает. Срок хранения метрик Prometheus
|
свидетельству и этот ADR не пересматривает. Срок хранения метрик Prometheus
|
||||||
сюда не входит: неделя истории — осознанный учебный выбор, и цена у него
|
сюда не входит: неделя истории — осознанный учебный выбор, и цена у него
|
||||||
дисковая.
|
дисковая.
|
||||||
|
|
||||||
|
27 августа таким пределом впервые стала коробка `airflow-scheduler`:
|
||||||
|
LocalExecutor исполняет в ней до четырех процессов задач одновременно, а
|
||||||
|
640 МиБ этого не вместили. Коробка поднята до 1,5 ГиБ. Ресурсные ограничения
|
||||||
|
из отдельных дагов удалены. Общий предел параллелизма уже задает Airflow.
|
||||||
|
`max_active_runs=1` остается там, где два запуска делили бы один донор и могли
|
||||||
|
повредить публикацию.
|
||||||
|
|
||||||
Ресурсный довод ADR 0001 отозван.
|
Ресурсный довод ADR 0001 отозван.
|
||||||
|
|
||||||
## Почему
|
## Почему
|
||||||
@@ -94,8 +101,8 @@ ClickHouse сам понижает каждый до 512 МиБ, а на сте
|
|||||||
заранее, и в покое стенд занимает около 3,2 ГБ. Восемь гигабайт нужны не сумме
|
заранее, и в покое стенд занимает около 3,2 ГБ. Восемь гигабайт нужны не сумме
|
||||||
пределов, а работе с запасом.
|
пределов, а работе с запасом.
|
||||||
|
|
||||||
Остальные девять лимитов не меняются, потому что ни один из них ни разу не
|
На 1 августа остальные девять лимитов не менялись, потому что ни один из них
|
||||||
сработал. Снять их скопом — то же изменение без свидетельств, каким они были
|
не сработал. Снять их скопом — то же изменение без свидетельств, каким они были
|
||||||
выставлены, только в обратную сторону. Лечение здесь не в том, чтобы стереть
|
выставлены, только в обратную сторону. Лечение здесь не в том, чтобы стереть
|
||||||
числа, а в том, чтобы лишить их статуса закона: число без основания меняют,
|
числа, а в том, чтобы лишить их статуса закона: число без основания меняют,
|
||||||
когда оно мешает, и не защищают как принятое решение.
|
когда оно мешает, и не защищают как принятое решение.
|
||||||
@@ -151,3 +158,13 @@ to 2.00 GiB because the system has limited RAM` и такие же строки
|
|||||||
Значение `max_server_memory_usage_to_ram_ratio` = 0,9 и то, что в cgroup предел
|
Значение `max_server_memory_usage_to_ram_ratio` = 0,9 и то, что в cgroup предел
|
||||||
считается от коробки, а не от машины, сверены по документации ClickHouse через
|
считается от коробки, а не от машины, сверены по документации ClickHouse через
|
||||||
MCP Context7.
|
MCP Context7.
|
||||||
|
|
||||||
|
`airflow-scheduler` проверен отдельно 27 августа 2026 года. На свежем
|
||||||
|
контейнере две попытки запустить четыре DQ-сверки параллельно в коробке
|
||||||
|
640 МиБ дошли ровно до `memory.max` и дали `oom_kill=1`: сначала при выгрузке
|
||||||
|
57 567 ключей сессий в Python, затем и после переноса дневной агрегации в
|
||||||
|
ClickHouse. В коробке 1 ГиБ сам `dq_check` прошел, но снова коснулся верхней
|
||||||
|
границы (`memory.events.max=57`). Полный `etl_pipeline` без локальных
|
||||||
|
ограничений задач в коробке 1,5 ГиБ завершился успешно: пик - 1 013 760 000
|
||||||
|
байт, `memory.events.max=0`, `oom_kill=0`. Docker не резервирует разницу между
|
||||||
|
пиком и пределом, поэтому этот запас не увеличивает расход памяти в покое.
|
||||||
+14
-6
@@ -195,12 +195,20 @@ Distributed-таблица вернула бы каждый непарный з
|
|||||||
подтверждает, что до витрины доехал записанный в описи мир, но не проверяет
|
подтверждает, что до витрины доехал записанный в описи мир, но не проверяет
|
||||||
сам генератор: у настоящего магазина такого оракула нет.
|
сам генератор: у настоящего магазина такого оракула нет.
|
||||||
|
|
||||||
Пять сборок пишут в общий донор. Только после их успеха даг заменяет все
|
Четыре SQL-сверки считают дневные итоги в ClickHouse и возвращают Airflow
|
||||||
проверенные дневные партиции, затем именованные задачи читают опубликованную
|
только сводку и малый срез диагностики. Техническая сверка с описью читает
|
||||||
сводку и падают при `failed_rows > 0`. Расхождение успевает сохраниться до
|
небольшой файл JSON в Python. Все пять сборок пишут в общий донор. После их
|
||||||
красного завершения, а ошибка сборки не затирает прежний результат. Красная
|
успеха даг заменяет проверенные дневные партиции. Затем именованные задачи
|
||||||
задача печатает не больше двадцати нарушенных ключей со значениями сторон;
|
читают опубликованную сводку и падают при `failed_rows > 0`. Расхождение успевает
|
||||||
полный список воспроизводится запросом проверки.
|
сохраниться до красного завершения, а ошибка сборки не затирает прежний
|
||||||
|
результат. Красная задача печатает не больше двадцати нарушенных ключей за
|
||||||
|
проверку со значениями сторон. Для полного списка итоговую агрегацию SQL-сверки
|
||||||
|
заменяют на `SELECT * FROM comparison WHERE failed ORDER BY data_date,
|
||||||
|
business_key`.
|
||||||
|
|
||||||
|
Если после починки день исчез с обеих сторон, его больше нет и в новом доноре.
|
||||||
|
Такой день удаляется из сводки отдельно: старая красная строка не должна
|
||||||
|
превращаться в историю попыток.
|
||||||
|
|
||||||
## Даг самопроверки
|
## Даг самопроверки
|
||||||
|
|
||||||
|
|||||||
@@ -23,13 +23,17 @@ WITH
|
|||||||
any(known_users) AS known_users
|
any(known_users) AS known_users
|
||||||
FROM dm.daily_traffic_v
|
FROM dm.daily_traffic_v
|
||||||
GROUP BY report_date
|
GROUP BY report_date
|
||||||
)
|
),
|
||||||
|
comparison AS
|
||||||
|
(
|
||||||
SELECT
|
SELECT
|
||||||
coalesce(reference.report_date, actual.report_date) AS data_date,
|
coalesce(reference.report_date, actual.report_date) AS data_date,
|
||||||
toString(coalesce(reference.report_date, actual.report_date)) AS business_key,
|
toString(coalesce(reference.report_date, actual.report_date))
|
||||||
|
AS business_key,
|
||||||
isNotNull(reference.report_date) AS reference_present,
|
isNotNull(reference.report_date) AS reference_present,
|
||||||
isNotNull(actual.report_date) AS actual_present,
|
isNotNull(actual.report_date) AS actual_present,
|
||||||
toString(tuple(reference.visitors, reference.known_users)) AS reference_value,
|
toString(tuple(reference.visitors, reference.known_users))
|
||||||
|
AS reference_value,
|
||||||
concat(
|
concat(
|
||||||
toString(tuple(actual.visitors, actual.known_users)),
|
toString(tuple(actual.visitors, actual.known_users)),
|
||||||
', rows=',
|
', rows=',
|
||||||
@@ -39,8 +43,29 @@ SELECT
|
|||||||
OR isNull(actual.report_date)
|
OR isNull(actual.report_date)
|
||||||
OR actual.physical_rows != 1
|
OR actual.physical_rows != 1
|
||||||
OR NOT isNotDistinctFrom(reference.visitors, actual.visitors)
|
OR NOT isNotDistinctFrom(reference.visitors, actual.visitors)
|
||||||
OR NOT isNotDistinctFrom(reference.known_users, actual.known_users) AS failed
|
OR NOT isNotDistinctFrom(reference.known_users, actual.known_users)
|
||||||
|
AS failed
|
||||||
FROM reference
|
FROM reference
|
||||||
GLOBAL FULL OUTER JOIN actual USING (report_date)
|
GLOBAL FULL OUTER JOIN actual USING (report_date)
|
||||||
|
)
|
||||||
|
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),
|
||||||
|
' key=',
|
||||||
|
business_key,
|
||||||
|
': эталон=',
|
||||||
|
if(reference_present, reference_value, 'нет ключа'),
|
||||||
|
', объект=',
|
||||||
|
if(actual_present, actual_value, 'нет ключа')
|
||||||
|
),
|
||||||
|
failed
|
||||||
|
) AS diagnostics
|
||||||
|
FROM comparison
|
||||||
|
GROUP BY data_date
|
||||||
ORDER BY data_date
|
ORDER BY data_date
|
||||||
SETTINGS join_use_nulls = 1
|
SETTINGS join_use_nulls = 1
|
||||||
@@ -57,7 +57,9 @@ WITH
|
|||||||
GROUP BY
|
GROUP BY
|
||||||
order_day,
|
order_day,
|
||||||
order_id
|
order_id
|
||||||
)
|
),
|
||||||
|
comparison AS
|
||||||
|
(
|
||||||
SELECT
|
SELECT
|
||||||
coalesce(reference.order_day, actual.order_day) AS data_date,
|
coalesce(reference.order_day, actual.order_day) AS data_date,
|
||||||
coalesce(reference.order_id, actual.order_id) AS business_key,
|
coalesce(reference.order_id, actual.order_id) AS business_key,
|
||||||
@@ -82,14 +84,37 @@ SELECT
|
|||||||
isNull(reference.order_id)
|
isNull(reference.order_id)
|
||||||
OR isNull(actual.order_id)
|
OR isNull(actual.order_id)
|
||||||
OR actual.physical_rows != 1
|
OR actual.physical_rows != 1
|
||||||
OR NOT isNotDistinctFrom(reference.declared_revenue, actual.declared_revenue)
|
OR NOT isNotDistinctFrom(
|
||||||
|
reference.declared_revenue,
|
||||||
|
actual.declared_revenue
|
||||||
|
)
|
||||||
OR NOT isNotDistinctFrom(reference.items_total, actual.items_total)
|
OR NOT isNotDistinctFrom(reference.items_total, actual.items_total)
|
||||||
OR NOT isNotDistinctFrom(reference.status, actual.status)
|
OR NOT isNotDistinctFrom(reference.status, actual.status)
|
||||||
OR NOT isNotDistinctFrom(reference.mismatch_class, actual.mismatch_class)
|
OR NOT isNotDistinctFrom(
|
||||||
AS failed
|
reference.mismatch_class,
|
||||||
|
actual.mismatch_class
|
||||||
|
) AS failed
|
||||||
FROM reference
|
FROM reference
|
||||||
GLOBAL FULL OUTER JOIN actual USING (order_day, order_id)
|
GLOBAL FULL OUTER JOIN actual USING (order_day, order_id)
|
||||||
ORDER BY
|
)
|
||||||
|
SELECT
|
||||||
data_date,
|
data_date,
|
||||||
business_key
|
countIf(reference_present) AS reference_rows,
|
||||||
|
countIf(actual_present) AS actual_rows,
|
||||||
|
countIf(failed) AS failed_rows,
|
||||||
|
groupArrayIf(20)(
|
||||||
|
concat(
|
||||||
|
toString(data_date),
|
||||||
|
' key=',
|
||||||
|
business_key,
|
||||||
|
': эталон=',
|
||||||
|
if(reference_present, reference_value, 'нет ключа'),
|
||||||
|
', объект=',
|
||||||
|
if(actual_present, actual_value, 'нет ключа')
|
||||||
|
),
|
||||||
|
failed
|
||||||
|
) AS diagnostics
|
||||||
|
FROM comparison
|
||||||
|
GROUP BY data_date
|
||||||
|
ORDER BY data_date
|
||||||
SETTINGS join_use_nulls = 1
|
SETTINGS join_use_nulls = 1
|
||||||
@@ -49,16 +49,28 @@ WITH
|
|||||||
GROUP BY
|
GROUP BY
|
||||||
report_date,
|
report_date,
|
||||||
product_category
|
product_category
|
||||||
)
|
),
|
||||||
|
comparison AS
|
||||||
|
(
|
||||||
SELECT
|
SELECT
|
||||||
coalesce(reference.report_date, actual.report_date) AS data_date,
|
coalesce(reference.report_date, actual.report_date) AS data_date,
|
||||||
coalesce(reference.product_category, actual.product_category) AS business_key,
|
coalesce(reference.product_category, actual.product_category)
|
||||||
|
AS business_key,
|
||||||
isNotNull(reference.product_category) AS reference_present,
|
isNotNull(reference.product_category) AS reference_present,
|
||||||
isNotNull(actual.product_category) AS actual_present,
|
isNotNull(actual.product_category) AS actual_present,
|
||||||
toString(tuple(reference.orders, reference.units, reference.revenue, reference.aov))
|
toString(tuple(
|
||||||
AS reference_value,
|
reference.orders,
|
||||||
|
reference.units,
|
||||||
|
reference.revenue,
|
||||||
|
reference.aov
|
||||||
|
)) AS reference_value,
|
||||||
concat(
|
concat(
|
||||||
toString(tuple(actual.orders, actual.units, actual.revenue, actual.aov)),
|
toString(tuple(
|
||||||
|
actual.orders,
|
||||||
|
actual.units,
|
||||||
|
actual.revenue,
|
||||||
|
actual.aov
|
||||||
|
)),
|
||||||
', rows=',
|
', rows=',
|
||||||
toString(actual.physical_rows)
|
toString(actual.physical_rows)
|
||||||
) AS actual_value,
|
) AS actual_value,
|
||||||
@@ -71,7 +83,25 @@ SELECT
|
|||||||
OR NOT isNotDistinctFrom(reference.aov, actual.aov) AS failed
|
OR NOT isNotDistinctFrom(reference.aov, actual.aov) AS failed
|
||||||
FROM reference
|
FROM reference
|
||||||
GLOBAL FULL OUTER JOIN actual USING (report_date, product_category)
|
GLOBAL FULL OUTER JOIN actual USING (report_date, product_category)
|
||||||
ORDER BY
|
)
|
||||||
|
SELECT
|
||||||
data_date,
|
data_date,
|
||||||
business_key
|
countIf(reference_present) AS reference_rows,
|
||||||
|
countIf(actual_present) AS actual_rows,
|
||||||
|
countIf(failed) AS failed_rows,
|
||||||
|
groupArrayIf(20)(
|
||||||
|
concat(
|
||||||
|
toString(data_date),
|
||||||
|
' key=',
|
||||||
|
business_key,
|
||||||
|
': эталон=',
|
||||||
|
if(reference_present, reference_value, 'нет ключа'),
|
||||||
|
', объект=',
|
||||||
|
if(actual_present, actual_value, 'нет ключа')
|
||||||
|
),
|
||||||
|
failed
|
||||||
|
) AS diagnostics
|
||||||
|
FROM comparison
|
||||||
|
GROUP BY data_date
|
||||||
|
ORDER BY data_date
|
||||||
SETTINGS join_use_nulls = 1
|
SETTINGS join_use_nulls = 1
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
-- DQ: эталонная сессия Метрики против сессии, нарезанной слоем DDS.
|
-- DQ: эталонная сессия Метрики против сессии, нарезанной слоем DDS.
|
||||||
--
|
--
|
||||||
-- Строка результата — одна кука одного завершённого дня. Сравнение по ключу
|
-- Сопоставление идет по куке внутри завершенного дня: лишняя сессия одной
|
||||||
-- не даёт лишней сессии одной куки скрыть потерянную сессию другой.
|
-- куки не скрывает потерянную сессию другой. Результат содержит итог дня.
|
||||||
WITH
|
WITH
|
||||||
(SELECT min(EventDate) FROM ods.event_v) AS first_source_day,
|
(SELECT min(EventDate) FROM ods.event_v) AS first_source_day,
|
||||||
first_source_day + ({position:UInt16} - 1) AS last_lived_day,
|
first_source_day + ({position:UInt16} - 1) AS last_lived_day,
|
||||||
@@ -28,7 +28,9 @@ WITH
|
|||||||
GROUP BY
|
GROUP BY
|
||||||
data_date,
|
data_date,
|
||||||
client_id
|
client_id
|
||||||
)
|
),
|
||||||
|
comparison AS
|
||||||
|
(
|
||||||
SELECT
|
SELECT
|
||||||
coalesce(reference.data_date, actual.data_date) AS data_date,
|
coalesce(reference.data_date, actual.data_date) AS data_date,
|
||||||
toString(coalesce(reference.client_id, actual.client_id)) AS business_key,
|
toString(coalesce(reference.client_id, actual.client_id)) AS business_key,
|
||||||
@@ -39,7 +41,25 @@ SELECT
|
|||||||
NOT isNotDistinctFrom(reference.sessions, actual.sessions) AS failed
|
NOT isNotDistinctFrom(reference.sessions, actual.sessions) AS failed
|
||||||
FROM reference
|
FROM reference
|
||||||
GLOBAL FULL OUTER JOIN actual USING (data_date, client_id)
|
GLOBAL FULL OUTER JOIN actual USING (data_date, client_id)
|
||||||
ORDER BY
|
)
|
||||||
|
SELECT
|
||||||
data_date,
|
data_date,
|
||||||
business_key
|
countIf(reference_present) AS reference_rows,
|
||||||
|
countIf(actual_present) AS actual_rows,
|
||||||
|
countIf(failed) AS failed_rows,
|
||||||
|
groupArrayIf(20)(
|
||||||
|
concat(
|
||||||
|
toString(data_date),
|
||||||
|
' key=',
|
||||||
|
business_key,
|
||||||
|
': эталон=',
|
||||||
|
if(reference_present, reference_value, 'нет ключа'),
|
||||||
|
', объект=',
|
||||||
|
if(actual_present, actual_value, 'нет ключа')
|
||||||
|
),
|
||||||
|
failed
|
||||||
|
) AS diagnostics
|
||||||
|
FROM comparison
|
||||||
|
GROUP BY data_date
|
||||||
|
ORDER BY data_date
|
||||||
SETTINGS join_use_nulls = 1
|
SETTINGS join_use_nulls = 1
|
||||||
Reference in new issue
Block a user