«Выручка выросла на 18% год к году» — звучит отлично, пока не выяснится, что за год открылось двадцать новых магазинов. Рост дали новые точки, а не бизнес как таковой. Чтобы измерить органический рост, в рознице считают like-for-like (LFL), он же same-store sales — сравнивают только те точки, что работали во всех сравниваемых периодах. Разберём паттерн на DAX по статье SQLBI.
В нашем учебном наборе один год, поэтому LFL на нём не посчитать — нужен мультипериод. Поэтому примеры даю на классической рознице: магазины (Магазины) и продажи за 2024–2026. Логика универсальна — так же работает для товаров, клиентов, филиалов.
Зачем это нужно
Наивное сравнение год-к-году смешивает два эффекта:
- органический рост (та же точка стала продавать больше);
- рост от расширения (открыли новые точки).
Для управленческих решений их надо разделять. LFL отвечает на вопрос «как выросли продажи на сопоставимой базе» — только по точкам, открытым во всех годах анализа. Новые и закрытые точки из сравнения исключаются.
Шаг 1. Статус точки по периодам
Сначала строим расчётную таблицу: какая точка в каком году работала. «Работала» = были продажи.
StoreStatus =
VAR AllStores =
CROSSJOIN (
SUMMARIZE ( Продажи, Календарь[Год] ),
ALLNOBLANKROW ( Магазины[КодМагазина] )
)
VAR OpenStores =
SUMMARIZE ( Продажи, Календарь[Год], Продажи[КодМагазина] )
RETURN
UNION (
ADDCOLUMNS ( OpenStores, "Статус", "Активен" ),
ADDCOLUMNS ( EXCEPT ( AllStores, OpenStores ), "Статус", "Неактивен" )
)
Логика:
AllStores— все пары «год × магазин» (полная матрица);OpenStores— пары, где были продажи;EXCEPT— пары без продаж (точка в этот год не работала);- результат — таблица, где каждая пара «год-магазин» помечена «Активен» / «Неактивен».
Шаг 2. Мера сопоставимых продаж
Теперь считаем выручку только по точкам, активным во всех выбранных годах:
Сопоставимые продажи =
VAR _OpenStores =
CALCULATETABLE (
FILTER (
ALLSELECTED ( StoreStatus[КодМагазина] ),
CALCULATE ( SELECTEDVALUE ( StoreStatus[Статус] ) ) = "Активен"
),
ALLSELECTED ( Календарь )
)
VAR _Filter = TREATAS ( _OpenStores, Магазины[КодМагазина] )
RETURN
CALCULATE ( [Выручка], KEEPFILTERS ( _Filter ) )
Что происходит:
- отбираем магазины со статусом «Активен» по всем выбранным годам (
ALLSELECTEDуважает срез пользователя); TREATASпереносит этот список как фильтр наМагазины[КодМагазина]— та самая виртуальная связь из прошлого урока;KEEPFILTERSпересекает его с текущим контекстом, а не затирает;[Выручка]считается только по сопоставимым точкам.
LFL = «посчитать меру только по сущностям, присутствующим во всех периодах». Список таких сущностей собирается из таблицы статусов и накладывается через TREATAS + KEEPFILTERS. Дальше LFL рост % = DIVIDE ( [Сопоставимые продажи] - [Сопоставимые продажи ПГ], [Сопоставимые продажи ПГ] ).
Шаг 3. Упаковать в функцию (по желанию)
В новых версиях DAX появились пользовательские функции — паттерн можно вынести в переиспользуемую функцию, отделив универсальную логику от схемы конкретной модели:
// универсальная (не зависит от модели)
DaxPatterns.LikeForLike.ForSameEntity =
( statusKey: ANYREF, statusCol: ANYREF, entityKey: ANYREF,
dateTable: ANYREF, formula: EXPR ) =>
VAR _Open =
CALCULATETABLE (
FILTER ( ALLSELECTED ( statusKey ),
CALCULATE ( SELECTEDVALUE ( statusCol ) ) = "Активен" ),
ALLSELECTED ( dateTable )
)
RETURN CALCULATE ( formula, KEEPFILTERS ( TREATAS ( _Open, entityKey ) ) )
Дальше тонкая обёртка под вашу модель — и вызов в одну строку: Сопоставимые продажи = Local.ForSameStore ( [Выручка] ). Это убирает копипаст, когда LFL нужен на нескольких уровнях (магазины, товары, клиенты).
Пользовательские DAX-функции — относительно новый механизм (Tabular/Fabric, развивается). Если их в вашей среде ещё нет — паттерн прекрасно работает обычными мерами из шага 2. Функции — про переиспользование, а не про саму логику.
Подводные камни
- Что считать «работал». Мы взяли «были продажи». Иногда правильнее «магазин числился открытым» — тогда статус берут из справочника дат открытия/закрытия, а не из факта продаж.
- Грануляция периода. LFL по годам и по месяцам — разные меры; решите, на каком уровне «сопоставимость».
- Стоимость. Таблица статусов и
TREATASсчитаются на лету; на больших каталогах точек следите за производительностью.
Что дальше
LFL — типовой розничный паттерн, и он показывает, как связываются темы курса: time intelligence, виртуальные связи (TREATAS) и работа с контекстом. Дальше — field parameters, what-if и другие DAX-паттерны (ABC, retention, остатки).