Files
clickstream-data-platform/docs/architecture/superset.md
T
ddadminandClaude Opus 5 b6c10ecf21 feat(superset): подача дашборда «Бизнес-сюжеты»
- Зачем:
  - экран отвечал только графиками: итога за период не было, доли категорий
    читались по высоте столбиков, а известные пользователи лежали на нуле
    рядом с посетителями.
- Что:
  - добавлены ряд крупных чисел (выручка, заказы, средний чек, посетители),
    кольцо долей категорий и линия доли известных пользователей;
  - средний чек и доля известных заведены метриками датасетов, знаменатель
    через `nullIf`: на пустом периоде деление Decimal на ноль в ClickHouse
    роняет панель;
  - фильтр «Период» сужает все панели, включая сверку по `order_day`;
  - `label_colors` закрепляет цвет за категорией и за рядами трафика;
  - деньги без копеек, единица в названии панели; подписи дат читаются как
    даты; сводная сверки развернута классами по строкам и уместилась без
    прокрутки;
  - обновлены справочник Superset и глава путеводителя.
- Проверка:
  - `make lint`;
  - `superset import-directory /app/import --overwrite` на живом стенде, экран
    снят в браузере: девять панелей отрисованы, цвет категории одинаков на
    столбиках и в кольце;
  - период 03.06–06.06 сузил все панели, включая сводную по `order_day`;
  - `SUM(revenue) / nullIf(SUM(orders), 0)` на периоде без заказов возвращает
    NULL, без `nullIf` — исключение 153.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 03:35:00 -04:00

20 KiB
Raw Blame History

Superset: витрины глазами

Документ описывает зону BI: куда Superset ходит за данными и за своими метаданными, из чего собран стендовый дашборд, как снять свежий экспорт после правки в интерфейсе и почему правка руками живет только до перезапуска.

Состав сюжетов и колонок витрин здесь не повторяется: он живет в спеке, раздел 7, и в доке слоя DM.

Устройство зоны

Superset читает ClickHouse пользователем bi на ноде 2 — так задано подключением ClickHouse (clickhousedb://bi@clickhouse-02:8123). Нода 2 здесь — ловушка правильных ошибок из спеки, раздел «Роли нод»: DDL идет с ноды 1, и забытый ON CLUSTER виден в дашборде.

Пароля в подключении нет. Его отдает штатное хранилище паролей Superset (SQLALCHEMY_CUSTOM_PASSWORD_STORE в infra/superset/superset_config.py), которое читает CLICKHOUSE_BI_PASSWORD из окружения. Поэтому файл подключения безопасно лежит в git, и экспорт из интерфейса не должен возвращать пароль обратно в YAML.

Свои метаданные — подключения, датасеты, графики, дашборды, пользователи — Superset держит в базе superset служебного postgres-metadata, рядом с базой Airflow. В ClickHouse он не пишет ничего.

Дерево импорта

infra/superset/import/ — это дашборд стенда в виде файлов. Каталог смонтирован в контейнер как /app/import.

Файл Что описывает
metadata.yaml версия формата, 1.0.0; больше в нем ничего нет
databases/clickhouse.yaml подключение к ClickHouse: URI без пароля и постоянный uuid
datasets/ClickHouse/*.yaml три витрины: revenue_daily_v, purchase_vs_orders_v, daily_traffic_v
charts/*.yaml девять графиков: четыре крупных числа, столбики выручки и кольцо долей, сводная таблица сверки, линии трафика и доля известных
dashboards/business_stories.yaml дашборд «Бизнес-сюжеты» и раскладка панелей

Связаны файлы не путями, а uuid. Датасет называет database_uuid, график — dataset_uuid, а дашборд не называет графики вовсе: единственная связь с ними лежит в его position, где узел CHART-* несет meta.uuid нужного графика. График, которого нет в position, импортируется молча и не появляется нигде.

Uuid у стенда розданы рукой: общее начало 19600000-0000-4000-8000-, дальше номер объекта в последнем блоке — 1–3 у датасетов, 101–109 у графиков, 201 у дашборда (…-000000000101). Экспорт из интерфейса приносит свои, сгенерированные; их заменяют на эти, иначе повторный импорт заведет вторую копию объекта вместо обновления первой.

Датасет несет русские имена колонок, формат чисел и метрики — а график только ссылается на них по имени. Значит, подпись или формат правятся в одном месте, а не в каждом графике. Два исключения проверены на живом стенде, Superset 6.1. Подписи оси график берет не из датасета, а из своего y_axis_format: без него ось выручки уходит в «1.2M». И разрез сводной таблицы подписан именем колонки (mismatch_class), хотя колонке даты в той же таблице русское имя достается.

Датасеты объявлены физическими (schema: dm, sql: null), хотя за именем стоит представление ClickHouse. Импорт в источник не ходит и разницы между таблицей и представлением не знает, а виртуальный датасет со своим SQL просто повторил бы запрос, который уже живет в sql/ddl/60-dm-views.sql.

Из чего собран экран

Четыре строки панелей:

  • ряд крупных чисел — выручка, заказы, средний чек, посетители;
  • выручка по дням и категориям, рядом доля категорий кольцом;
  • сводная сверки purchase и заказов;
  • посетители и известные пользователи, рядом доля известных.

Правая панель пары отвечает на вопрос «сколько из целого» и берет цвета левой: категория окрашена одинаково на столбиках и в кольце, доля известных — как линия известных пользователей рядом. Цвет закрепляет label_colors в metadata дашборда. Шесть категорий выписаны в него руками по каталогу генератора (generator/src/clickstream_generator/catalog.py): седьмая останется без закрепленного цвета, пока файл не поправят.

Средний чек и доля известных считаются метриками датасета — SUM(revenue) / nullIf(SUM(orders), 0) и то же для доли известных. Знаменатель обернут не зря: на периоде без заказов деление Decimal на ноль в ClickHouse — не nan, а исключение, и панель показала бы ошибку вместо пустоты. У витрины выручки есть готовая колонка aov, но среднее по ней дало бы другое число: день с тремя заказами получил бы тот же вес, что день с тремя сотнями.

«Период» — единственный фильтр дашборда, тип filter_time с пустой целью (targets: [{}]). Такой фильтр колонку не называет: он подставляет выбранный период в то временное условие, которое уже стоит у каждого графика. Поэтому сверка сужается по order_day, а остальные панели — по report_date.

Импорт при каждой инициализации

Последняя строка infra/superset/init.sh:

superset import-directory /app/import --overwrite

Служба superset-init отрабатывает перед подъемом самого Superset, то есть при каждом make up. Без --overwrite второй прогон падал бы на первом же знакомом uuid; с ним объект обновляется на месте, и повторная инициализация проходит без ошибок.

Отсюда правило зоны: стендовый дашборд не расширяют руками в интерфейсе. Правка живет до следующей инициализации, а потом молча заменяется тем, что лежит в git. Пробовать интерфейс менти никто не мешает — но все, что должно пережить перезапуск, приезжает файлом.

Это правило относится только к стендовым объектам. Свои датасеты, графики и дашборды менти живут в той же базе метаданных Superset, а импорт трогает только объекты с известными ему uuid. Поэтому свои объекты переживают make up и make rebuild-storage. Их стирает только make clean или адресный сброс базы метаданных по рецепту ниже.

Как снять свежий экспорт

Маршрут один: правишь на живом стенде, забираешь экспорт, кладешь дерево в git.

  1. Собрать или поправить график и дашборд в интерфейсе — на стенде, а не в файлах. Ключи params графиков велики, недокументированы и меняются от версии к версии; неверный ключ не ошибается, а рисует пустоту. Интерфейс здесь единственный надежный оракул.

  2. Снять экспорт и достать его из контейнера:

    docker compose exec superset superset export-dashboards -f /tmp/export.zip
    docker compose cp superset:/tmp/export.zip /tmp/export.zip
    unzip -d /tmp/superset-export /tmp/export.zip
    

    Внутри архива — один каталог dashboard_export_<дата> ровно той же формы, что и дерево импорта. Команда выгружает все дашборды сразу; на стенде он один.

  3. Вычистить экспортный сор перед коммитом:

    • выбросить экспортный databases/ — наш clickhouse.yaml остается как есть, с постоянным uuid и без пароля;
    • оставить в metadata.yaml одну строку version: 1.0.0;
    • убрать из графиков params.datasource, params.slice_id и params.dashboards — это идентификаторы строк того Superset, где снимали, а импорт выводит связи из uuid;
    • убрать query_context — снимок запроса оттуда же. Импорт его не требует и обратно не заполняет: после переимпорта он остается пустым, на отрисовку это не влияет;
    • перезаписать русские строки читаемыми. Экспорт кодирует их \uXXXX-последовательностями, и после каждого круга их приходится раскодировать заново: имена графиков и подписи колонок читает менти, и в файле, и в диффе. Годится любой повторный дамп YAML с allow_unicode=True;
    • вернуть постоянные uuid из схемы выше.
  4. Скопировать вычищенное дерево в infra/superset/import/ и проверить импортом на пустых метаданных — иначе прогон проверит только обновление уже стоящих объектов, а молчаливый пропуск нового спрячется за старым. Активы Superset лежат в базе superset службы postgres-metadata, поэтому адресный сброс — эта база:

    docker compose stop superset
    docker compose exec postgres-metadata sh -c \
      'psql -U "$POSTGRES_USER" -d postgres \
         -c "DROP DATABASE superset WITH (FORCE)" \
         -c "CREATE DATABASE superset OWNER \"$SUPERSET_METADATA_USER\""'
    docker compose run --rm superset-init
    docker compose up -d superset
    

    Базу приходится создавать тут же: скрипт infra/postgres/init/ заводит ее один раз, при первой инициализации тома Postgres. make rebuild-storage для этой проверки не годится — он гоняет world_recreate в ClickHouse и метаданных Superset не касается. make clean сбросит и их, но заодно снесет весь стенд, включая метаданные Airflow в той же службе.

    После импорта дашборд смотрят глазами: это единственное доказательство — см. ловушку ниже.

Ловушки

Импорт молчит. График, чей dataset_uuid не разрешился, отбрасывается без единого слова; дашборд, у которого не сошлись ссылки на графики, пропускается. Ошибки уходят в журнал, и init.sh завершается нулем в любом случае. Поэтому импорт проверяют глазами или запросом к API — но не тем, что команда отработала.

Знак рубля приходит не из формата валюты. Символ Superset берет у Intl.NumberFormat по коду валюты и английской локали стенда: код RUB печатается как «RUB», а знак «₽» вызов роняет, и форматирование молча откатывается к числу без валюты. Поэтому единица стоит в названии панели — «Выручка, ₽», — а метрика несет обычный формат ,d.

label_colors знает имя ряда, а не подпись. Для разреза имя ряда — это значение колонки («Бытовая техника»), для графика без разреза — имя метрики (known_share), а не ее русская подпись. Цвет, закрепленный за подписью «Доля известных», не применяется.

--overwrite синхронизирует датасет разрушительно. Колонки и метрики берутся из YAML целиком: добавленная в интерфейсе метрика или подпись колонки исчезнет при следующем старте контейнера. Правка датасета — это правка файла.

Что проверяет стенд

make check-services входит в Superset, находит подключение ClickHouse, сверяет его uuid и безопасный URI и требует схему dm — то есть подтверждает живое подключение и хранилище пароля. Сами графики и дашборд ни одна цель не проверяет: их смотрят глазами после переинициализации. Полная карта целей — в доке проверок.

Отклоненные варианты

Оживить дашборд предложат снова, поэтому отказы записаны с доводами. Замерено на живом стенде 7 сентября 2026 года, Superset 6.1.

Карта регионов России. Замысел стоял на клике: щелчок по региону перестраивает весь экран. Плагин Country Map кросс-фильтр не отдает вовсе — behaviors не объявлены, setDataMask не вызывается, а щелчок приближает карту. Без клика остается картинка, и она рассказывает меньше, чем кажется: 16 городов стенда сворачиваются в 14 регионов из 83, и больше половины сессий приходится на один. Карта отдана факультативу «Добавь названия регионов в отчет»: там читатель собирает справочник сам, и она становится наградой за него.

Регион как общее измерение витрин. Чтобы фильтр по региону действовал на весь экран, регион пришлось бы завести в каждой витрине: Superset датасеты между собой не соединяет, а фильтр по колонке, которой нет в датасете панели, отбрасывается с причиной COL_NOT_IN_DATASOURCE. У заказа географии нет — она бралась бы от человека через карту идентичностей и приписывалась бы сущности, у которой ее в источнике не было. Вдобавок не сходятся ритмы: витрина от карты идентичностей не замерзает никогда, а выручка живет окном последнего слепка (дока DM). Стендовый словарь регионов отклонен раньше и по своей причине — он повторил бы урок dictGet каталога вторым разом.

Кросс-фильтрация между панелями. Общих измерений у витрин почти нет: категория есть только у выручки, а день у сверки называется order_day против report_date у остальных. Клик перестроил бы часть экрана и оставил другую нетронутой, а вывод из этого читатель делает про данные, а не про устройство витрин. Панель фильтров этой болезнью не болеет — там колонка задается явно для каждой витрины, — поэтому фильтр периода дашборду разрешен, а кросс-фильтрация нет.

Вкладки и блоки «что смотреть». Вкладки были условным вариантом: их заводят, когда экран перестает читаться одним взглядом. Четыре строки панелей читаются сразу, поэтому вкладки спрятали бы половину экрана за щелчком без выигрыша. Блок с текстом «что смотреть» рядом с панелью означал бы, что панель сама себя не объясняет: тогда чинить надо название и подписи, а не дописывать к ним пояснение.

Пороги и раскраска панелей. Правило спеки, раздел 9: на дашборд смотрят глазами, красное и зеленое живет в проверках.

Поводы вернуться к разговору: плагин карты начинает отдавать кросс-фильтр; у заказа появляется адрес доставки; витрины получают общее измерение по своей причине, не ради дашборда.