Привет, Хабр!

Сегодня я хотел бы поговорить подробнее о языке DAX, который активно применяется продвинутыми аналитиками во всем мире. Но я уверен, что сфера использования этого мощного инструмента охватывает не только суперпрофессионалов. Именно DAX делает аналитику доступнее для бизнес-пользователей, которые могут сделать сложные расчеты без познаний в программировании вообще. В этой статье мы рассмотрим три примера, в которых DAX помогает гораздо проще и быстрее решить типовые задачи, с которыми сталкиваются аналитики. Использовали ли Вы DAX раньше — неважно! Тех, кто впервые слышит о DAX, я постараюсь порадовать примерами, а опытных пользователей приглашаю к дискуссии.

Меня зовут Антон Кондауров, и я занимаюсь разработкой движка запросов DAX в Visiology. Итак, зачем же нужен этот DAX и с чем его едят? Для ответа на этот вопрос нужно понять, как работают современные BI-платформы. Одни из них обращаются напрямую к СУБД (этот режим также называют Direct Query, push-down или Live Connect), чтобы получить данные для определенных расчетов. В этом случае не обойтись без написания скриптов на SQL (хотя бы потому, что СУБД редко понимают запросы на других языках). В результате для каждого очередного расчета BI-системе приходится посылать очередной запрос SQL к СУБД и интерпретировать полученный ответ.

Иначе работают BI-платформы, у которых есть собственный аналитический движок. Из российских продуктов — это Visiology. Данные подгружаются в собственное хранилище, оптимизируются и размещаются таким образом, чтобы быстрее давать ответы на запросы пользователей. Тут, конечно, тоже не обойтись без SQL, но он зашит глубоко внутри. Сама механика этого процесса требует отдельной статьи — и об этом можно почитать в публикации нашего архитектора Никиты Ильина «Укрощение ClickHouse: почему ДанКо делает Visiology намного быстрее».

Архитектура Visiology Platform
Архитектура Visiology Platform

Но вернемся к варианту с live connection. Что же в этом плохого? Да, собственно, ничего! По крайней мере, пока мы не приходим к сотням пользователей, терабайтам данных и стремлению специалистов (обращаю внимание, не только аналитиков!) изучить закономерности, имеющиеся в данных корпорации. В этих случаях при злоупотреблении SQL страдает производительность...а некоторые задачи оказывается достаточно сложно реализовать.

Чтобы разобраться в этом, давайте представим, что у вас есть какая-то база данных по продажам, и для нее уже собрана модель типа звезда. В Visiology она может выглядеть вот так:

Схема данных в Visiology
Схема данных в Visiology

Ну или намного сложнее. В принципе можно подключить любое количество источников данных и выстроить связи между таблицами, чтобы потом анализировать продажи, например, выяснить…

Пример 1. Кто скупил всю недорогую колбасу?

Представляете, Вы приходите в мясной магазин, а там вообще нет никакой колбасы дешевле 1000 рублей за килограмм. Согласитесь, неприятно. Вы идете к менеджеру и говорите: «Как же так, где вся доступная колбаса?». А он, чтобы разобраться с вопросом, начинает сразу искать, куда она делась.

Если использовать для этого SQL, то выкладки будут выглядеть примерно следующим образом:

SELECT `product`.`category` AS `Категория`,
      `client`.`name`      AS `Имя клиента`,
      max(`sales`.`price`) AS `Максимальная цена ниже 1000`
FROM (SELECT `sales`.`price`       AS `price`,
            `sales`.`productkey`  AS `productkey`,
            `sales`.`customerkey` AS `customerkey`
     FROM `sales`
     WHERE `sales`.`price` < 1000) AS `salesBelow1000`
        INNER JOIN `product`
                   ON `sales`.`productkey` =
                      `product`.`dimproduct_productkey`
        INNER JOIN `client`
                   ON `sales`.`customerkey` =
                      `client`.`dimcustomer_customerkey`
GROUP BY `product`.`category`,
        `client`.`name`;

А если написать этот запрос на языке DAX, логика будет проще, и операторов потребуется меньше:

EVALUATE
SUMMARIZECOLUMNS (
    Продукты'[Категория],
    'Клиенты'[Имя клиента],
    "Максимальная цена ниже 1000",
        CALCULATE (
            MAX ( 'Продажи'[Цена] ),
            FILTER ( 'Продажи', 'Продажи'[Цена] < 1000 )
        )
)

Разумеется, колбаса здесь только для примера. Подобные задачи возникают в разных сферах ежедневно — нужно визуализировать количество сотрудников, не отгулявших 20 дней отпуска, бригады, которые не выполнили запланированную норму, и так далее…

Построенный дашборд в Visiology для покупок колбасы дешевле 1000 рублей по клиентам
Построенный дашборд в Visiology для покупок колбасы дешевле 1000 рублей по клиентам

Пример 2. Сколько-сколько мы продали?

Допустим, предыдущий пример вас не убедил. Ну а что, SQL не такой и сложный. Давайте двигаться дальше. Что делать, если руководитель задает вопрос:

— А сколько сервелата мы продали за время работы нашей фирмы?

— В сумме? — спрашиваете Вы

— Нет, в разрезе каждого месяца…

Это уже не такая тривиальная задача для обращения к СУБД “в лоб”, и поэтому SQL будет посложнее:

SELECT `filtered_calendar`.`year`  AS `Год`,
      `filtered_calendar`.`month` AS `Месяц`,
      sum(`sales`.`price`)        AS `Накопленное количество по продажам с начала года до текущей даты`
FROM `sales`
        INNER JOIN (SELECT `calendar_grid`.`date`  AS `date`,
                           `calendar_grid`.`year`  AS `year`,
                           `calendar_grid`.`month` AS `month`
                    FROM (SELECT `calendar_grid`.`year`                             AS `year`,
                                 `calendar_grid`.`month`                            AS `month`,
                                 `calendar_grid`.`date`                             AS `date`,
                                 `calendar_grid`.`date` >=
                                 makeDate(toYear(max(`date_to_aggregate`)), 1, 1) AND
                                 `calendar_grid`.`date` <= max(`date_to_aggregate`) AS `condition`
                          FROM (SELECT `calendar_1`.`year`    AS `year`,
                                       `calendar_1`.`month`   AS `month`,
                                       `calendar_2`.`datekey` AS `date`,
                                       `calendar_1`.`datekey` AS `date_to_aggregate`
                                FROM `calendar` AS `calendar_1`
                                         INNER JOIN `calendar` AS `calendar_2`
                                                    ON `calendar_1`.`year` =
                                                       `calendar_2`.`year`) AS `calendar_grid`
                          GROUP BY `calendar_grid`.`year`,
                                   `calendar_grid`.`month`,
                                   `calendar_grid`.`date`
                             ) AS `calendar_grid`
                    WHERE `calendar_grid`.`condition` = true) AS `filtered_calendar`
                   ON `sales`.`datekey` =
                      `filtered_calendar`.`date`
GROUP BY `filtered_calendar`.`year`,
        `filtered_calendar`.`month`;

А на DAX все выглядит элегантно и красиво!

EVALUATE
SUMMARIZECOLUMNS (
    'Даты'[Год],
    'Даты'[Месяц],
    "Накопленное количество по продажам с начала года до текущей даты",
        TOTALYTD ( SUM ( 'Продажи'[Количество] ), 'Даты'[Дата] )
)
Дашборд в Visiology по накопленному количеству продаж по месяцам
Дашборд в Visiology по накопленному количеству продаж по месяцам

Пример 3. Сколько колбасы нужно на Новый год?

Думаю, большинство уже убедились, что написать DAX в этом случае намного проще. Но если все еще нет, то мы движемся дальше в направлении Data Discovery!

…Начальник щелкает языком и спрашивает: “А сколько же, интересно, мы продаем колбасы перед Новым годом? Не нужно ли закупиться в этом году основательнее?”. Нам ничего не остается, кроме как написать вот такого типа SQL:

SELECT `calculationresult_no_nulls`.`amount` AS `Суммарное количество по продажам в последний рабочий день года`,
      `calculationresult_no_nulls`.`year`   AS `Год`
FROM (SELECT `calculationresult`.`year`       AS `year`,
            `calculationresult`.`sum_amount` AS `amount`
     FROM (SELECT `filtered_sales`.`year`        AS `year`,
                  sum(`filtered_sales`.`amount`) AS `sum_amount`
           FROM (SELECT `grid_to_filter`.`amount` AS `amount`,
                        `grid_to_filter`.`year`   AS `year`
                 FROM (SELECT `last_day_sales`.`year`           AS `year`,
                              `last_day_sales`.`amount_2`       AS `amount`,
                              if(`last_day_sales`.`datekey` =
                                 max(`datekey_2`), true, false) AS `predicate`
                       FROM (SELECT `grid`.`year`     AS `year`,
                                    `grid`.`amount`   AS `amount_2`,
                                    `grid`.`datekey`  AS `datekey`,
                                    `facts`.`datekey` AS `datekey_2`
                             FROM `sales` AS `facts`
                                      INNER JOIN `dates`
                                                 ON `facts`.`datekey` =
                                                    `dates`.`datekey`
                                      INNER JOIN (SELECT `dates`.`year`    AS `year`,
                                                         `facts`.`amount`  AS `amount`,
                                                         `facts`.`datekey` AS `datekey`
                                                  FROM `sales` AS `facts`
                                                           INNER JOIN `dates` AS `dates`
                                                                      ON `facts`.`datekey` =
                                                                         `dates`.`datekey`) AS `grid`
                                                 ON `dates`.`year` =
                                                    `grid`.`year`) AS `last_day_sales`
                       GROUP BY `last_day_sales`.`year`,
                                `last_day_sales`.`amount_2`,
                                `last_day_sales`.`datekey`) AS `grid_to_filter`
                 WHERE `grid_to_filter`.`predicate` = true) AS `filtered_sales`
           GROUP BY `filtered_sales`.`year`) AS `calculationresult`
     WHERE `calculationresult`.`sum_amount` IS NOT NULL) AS `calculationresult_no_nulls`
ORDER BY `year` ASC,
        `Суммарное количество по продажам в последний рабочий день года` ASC;

Ну или взять и написать такого типа DAX, чтобы оценить

Суммарное количество продаж, приходящихся на последний рабочий день года :=
    CALCULATE (
        SUM ( 'Продажи'[Количество] ),
        FILTER ( 'Продажи', 'Продажи'[Дата] = MAX ( 'Продажи'[Дата] ) )
    )

DEFINE
    VAR TotalQuantityLastDay =
        SUMMARIZECOLUMNS (
            'Даты'[Год],
            "Суммарное количество по продажам в последний рабочий день года", 'Продажи'[Суммарное количество по продажам в последний рабочий день года]
        )

EVALUATE
TOPN (
    1000,
    TotalQuantityLastDay,
    'Даты'[Год], ASC,
    [Суммарное количество по продажам в последний рабочий день года], ASC
)
Дашборд в по суммарному количеству продаж в последний рабочий день года
Дашборд в Visiology по суммарному количеству продаж в последний рабочий день года

В итоге получили дашборд со всеми задачами.

Дашборд в Visiology со всеми задачами
Дашборд в Visiology со всеми задачами

Заключение

Подводя итог, хочется отметить, что DAX намного удобнее для выполнения любых запросов, которые хотя бы чуть‑чуть отличаются от прямой визуализации того, что хранится в Вашей СУБД. Более того, именно использование DAX делает более простыми процессы загрузки информации в Visiology (об этом мы расскажем подробнее чуть позже), разделение ролевого доступа («RLS в действии: как решить вопросы доступа в BI за 5 минут») и решение многих других задач.

В следующих статьях я подробнее разберу другие полезные и практичные варианты применения синтаксиса DAX в повседневной работе как аналитика, так и бизнес-пользователя.

Спасибо за уделенное время, желаю успехов в задачах Business Intelligence! :)

Комментарии (0)