Введение: Важность упорядочивания данных в бизнес-анализе
Представьте, что вы открываете отчет о продажах, где продукты расположены хаотично — ни по алфавиту, ни по объему продаж, ни по какому-либо другому логическому принципу. Найти нужную информацию становится практически невозможно. Именно поэтому сортировка является фундаментальной операцией в аналитике данных.
В MDX сортировка работает иначе, чем в традиционных реляционных базах данных. Мы работаем не с плоскими таблицами, а с многомерными структурами, где каждый элемент может быть частью сложной иерархии. Это открывает уникальные возможности, но также создает специфические вызовы.
Основы сортировки в MDX
Функция ORDER — главный инструмент упорядочивания
В MDX для сортировки используется функция ORDER. Её структура интуитивно понятна:
Haxe
ORDER(что_сортируем, по_чему_сортируем, как_сортируем)
Более формально:
- Первый параметр — набор элементов, который нужно упорядочить
- Второй параметр — выражение, определяющее критерий сортировки
- Третий параметр — направление и тип сортировки
Типы сортировки: сохранение против разрушения иерархии
MDX предоставляет четыре варианта сортировки:
- ASC (Ascending) — возрастающий порядок с учетом иерархии
- DESC (Descending) — убывающий порядок с учетом иерархии
- BASC (Break Ascending) — возрастающий порядок, игнорируя иерархию
- BDESC (Break Descending) — убывающий порядок, игнорируя иерархию
Буква "B" означает "Break" — разрушение иерархической структуры. Это критическое отличие, которое определяет, будут ли дочерние элементы оставаться под своими родителями или сортироваться глобально.
Механизм работы сортировки
Когда MDX выполняет сортировку:
- Оценивает выражение сортировки для каждого элемента набора
- Создает упорядоченный список на основе полученных значений
- Применяет указанное направление сортировки
- Учитывает иерархические связи (если не указан флаг B)
Сортировка по числовым мерам
Простейший случай: упорядочивание по продажам
Начнем с базового примера — отсортируем субкатегории продуктов по объему интернет-продаж:
SELECT
[Measures].[Internet Sales Amount] ON COLUMNS,
ORDER(
[Product].[Subcategory].[Subcategory].Members, -- Что сортируем
[Measures].[Internet Sales Amount], -- По чему сортируем
DESC -- Как сортируем
) ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]
Этот запрос:
- Берет все элементы уровня Subcategory
- Вычисляет для каждого значение Internet Sales Amount
- Располагает их от большего к меньшему
Комбинирование фильтрации и сортировки
Часто требуется сначала отобрать релевантные данные, затем их упорядочить:
WITH
-- Определяем минимальный порог продаж
MEMBER [Measures].[Sales Threshold] AS 100000
SELECT
{[Measures].[Internet Sales Amount],
[Measures].[Internet Order Count]} ON COLUMNS,
ORDER(
-- Сначала фильтруем элементы выше порога
FILTER(
[Product].[Subcategory].[Subcategory].Members,
[Measures].[Internet Sales Amount] > [Measures].[Sales Threshold]
),
[Measures].[Internet Order Count], -- Сортируем по количеству заказов
DESC
) ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]
Работа с NULL значениями в сортировке
NULL значения требуют особого внимания:
WITH
-- Рассчитываем рентабельность
MEMBER [Measures].[ROI] AS
IIF(
[Measures].[Internet Total Product Cost] = 0,
NULL,
([Measures].[Internet Sales Amount] - [Measures].[Internet Total Product Cost]) /
[Measures].[Internet Total Product Cost]
),
FORMAT_STRING = "Percent"
-- Безопасная версия для сортировки
MEMBER [Measures].[ROI for Sort] AS
IIF(
IsEmpty([Measures].[ROI]),
-1, -- Помещаем NULL в конец при DESC сортировке
[Measures].[ROI]
)
SELECT
{[Measures].[Internet Sales Amount],
[Measures].[ROI]} ON COLUMNS,
ORDER(
NON EMPTY [Product].[Product].[Product].Members,
[Measures].[ROI for Sort], -- Используем безопасную версию
DESC
) ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]
Иерархическая сортировка
Сохранение структуры при сортировке
Рассмотрим, как работает сортировка с сохранением иерархии:
-- Создаем иерархический набор: категории и их подкатегории
WITH SET [CategoryAndSubs] AS
Generate(
[Product].[Category].[Category].Members,
{[Product].[Category].CurrentMember} + -- Категория
Children([Product].[Category].CurrentMember) -- Её подкатегории
)
SELECT
[Measures].[Internet Sales Amount] ON COLUMNS,
ORDER(
[CategoryAndSubs],
[Measures].[Internet Sales Amount],
DESC -- Подкатегории остаются под своими категориями
) ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]
Глобальная сортировка с разрушением иерархии
Теперь применим BDESC для полной пересортировки:
WITH SET [AllLevels] AS
[Product].[Category].[Category].Members +
[Product].[Subcategory].[Subcategory].Members
SELECT
{[Measures].[Internet Sales Amount],
[Product].[Product Categories].CurrentMember.Level.Name} ON COLUMNS,
ORDER(
[AllLevels],
[Measures].[Internet Sales Amount],
BDESC -- Все элементы сортируются независимо от иерархии
) ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]
Сортировка внутри каждой группы
Для локальной сортировки внутри групп:
WITH
-- Для каждой страны создаем отсортированный список регионов
SET [SortedByCountry] AS
Generate(
[Customer].[Country].[Country].Members,
{[Customer].[Country].CurrentMember} +
ORDER(
Filter(
[Customer].[State-Province].[State-Province].Members,
[Customer].[State-Province].CurrentMember.Parent =
[Customer].[Country].CurrentMember
),
[Measures].[Internet Sales Amount],
DESC
)
)
SELECT
[Measures].[Internet Sales Amount] ON COLUMNS,
[SortedByCountry] ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]
Сортировка по вычисляемым критериям
Использование расчетных мер для упорядочивания
Создадим комплексный критерий сортировки:
WITH
-- Базовые метрики
MEMBER [Measures].[Units Sold] AS
[Measures].[Order Quantity]
MEMBER [Measures].[Revenue] AS
[Measures].[Internet Sales Amount]
-- Средняя стоимость единицы
MEMBER [Measures].[Unit Price] AS
IIF(
[Measures].[Units Sold] = 0,
0,
[Measures].[Revenue] / [Measures].[Units Sold]
),
FORMAT_STRING = "Currency"
-- Комплексный индекс привлекательности
MEMBER [Measures].[Attractiveness Score] AS
-- Учитываем объем, цену и частоту покупок
[Measures].[Revenue] * 0.5 +
[Measures].[Unit Price] * 100 * 0.3 +
[Measures].[Internet Order Count] * 50 * 0.2,
FORMAT_STRING = "#,##0"
SELECT
{[Measures].[Revenue],
[Measures].[Unit Price],
[Measures].[Internet Order Count],
[Measures].[Attractiveness Score]} ON COLUMNS,
ORDER(
NON EMPTY [Product].[Subcategory].[Subcategory].Members,
[Measures].[Attractiveness Score],
DESC
) ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]
Относительная сортировка
Упорядочивание по отклонению от среднего:
WITH
-- Среднее значение по всем элементам
MEMBER [Measures].[Global Average] AS
AVG(
[Product].[Subcategory].[Subcategory].Members,
[Measures].[Internet Sales Amount]
)
-- Отклонение от среднего в процентах
MEMBER [Measures].[Variance %] AS
IIF(
[Measures].[Global Average] = 0,
0,
([Measures].[Internet Sales Amount] - [Measures].[Global Average]) /
[Measures].[Global Average] * 100
),
FORMAT_STRING = "#,##0.00"
-- Категоризация
MEMBER [Measures].[Category] AS
CASE
WHEN [Measures].[Variance %] > 100 THEN "Outstanding"
WHEN [Measures].[Variance %] > 0 THEN "Above Average"
WHEN [Measures].[Variance %] > -50 THEN "Below Average"
ELSE "Poor"
END
SELECT
{[Measures].[Internet Sales Amount],
[Measures].[Global Average],
[Measures].[Variance %],
[Measures].[Category]} ON COLUMNS,
ORDER(
NON EMPTY [Product].[Subcategory].[Subcategory].Members,
[Measures].[Variance %],
DESC
) ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]
Сортировка по атрибутам элементов
Алфавитное упорядочивание
Сортировка по названию элемента:
SELECT
{[Measures].[Internet Sales Amount],
[Measures].[Internet Order Count]} ON COLUMNS,
ORDER(
NON EMPTY [Customer].[City].[City].Members,
[Customer].[City].CurrentMember.Name, -- Сортируем по имени города
ASC
) ON ROWS
FROM [Adventure Works]
WHERE (
[Date].[Calendar Year].&[2013],
[Customer].[Country].[United States]
)
Сортировка по свойствам членов
Использование дополнительных атрибутов:
WITH
-- Получаем уникальный ключ элемента
MEMBER [Measures].[Member Key] AS
[Product].[Product].CurrentMember.UniqueName
-- Получаем уровень в иерархии
MEMBER [Measures].[Hierarchy Level] AS
[Product].[Product Categories].CurrentMember.Level.Ordinal
SELECT
{[Measures].[Internet Sales Amount],
[Measures].[Member Key],
[Measures].[Hierarchy Level]} ON COLUMNS,
ORDER(
Descendants(
[Product].[Product Categories].[All Products],
3,
SELF_AND_BEFORE
),
[Measures].[Hierarchy Level], -- Сортируем по уровню иерархии
ASC
) ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]
Продвинутые техники сортировки
Многоступенчатая сортировка
Реализация сортировки по нескольким критериям:
WITH
-- Первичный критерий: категория продукта
MEMBER [Measures].[Primary Sort] AS
CASE [Product].[Category].CurrentMember.Name
WHEN "Bikes" THEN 1
WHEN "Components" THEN 2
WHEN "Clothing" THEN 3
WHEN "Accessories" THEN 4
ELSE 5
END
-- Вторичный критерий: продажи
MEMBER [Measures].[Secondary Sort] AS
[Measures].[Internet Sales Amount]
-- Объединенный критерий
MEMBER [Measures].[Combined Sort] AS
[Measures].[Primary Sort] * 1000000 + -- Умножаем для приоритета
[Measures].[Secondary Sort]
SELECT
{[Measures].[Internet Sales Amount],
[Product].[Category].CurrentMember.Name} ON COLUMNS,
ORDER(
CrossJoin(
[Product].[Category].[Category].Members,
[Product].[Subcategory].[Subcategory].Members
),
[Measures].[Combined Sort],
ASC
) ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]
Условная сортировка
Изменение направления в зависимости от условий:
WITH
-- Определяем прибыль
MEMBER [Measures].[Profit] AS
[Measures].[Internet Sales Amount] - [Measures].[Internet Total Product Cost]
-- Флаг прибыльности
MEMBER [Measures].[Is Profitable] AS
IIF([Measures].[Profit] > 0, 1, 0)
-- Критерий сортировки с условной логикой
MEMBER [Measures].[Smart Sort] AS
IIF(
[Measures].[Is Profitable] = 1,
[Measures].[Profit], -- Прибыльные сортируем по прибыли
[Measures].[Internet Sales Amount] * -1 -- Убыточные по продажам (инверсия)
)
SELECT
{[Measures].[Internet Sales Amount],
[Measures].[Profit],
[Measures].[Is Profitable]} ON COLUMNS,
ORDER(
NON EMPTY [Product].[Product].[Product].Members,
[Measures].[Smart Sort],
DESC
) ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]
Практические примеры
Пример 1: Топ-N анализ с детализацией
WITH
-- Находим топ-5 категорий
SET [Top5Categories] AS
Head(
ORDER(
[Product].[Category].[Category].Members,
[Measures].[Internet Sales Amount],
DESC
),
5
)
-- Для каждой топ-категории берем топ-3 подкатегории
SET [DetailedTop] AS
Generate(
[Top5Categories],
{[Product].[Category].CurrentMember} +
Head(
ORDER(
Filter(
[Product].[Subcategory].[Subcategory].Members,
[Product].[Subcategory].CurrentMember.Parent =
[Product].[Category].CurrentMember
),
[Measures].[Internet Sales Amount],
DESC
),
3
)
)
-- Добавляем ранги
MEMBER [Measures].[Category Rank] AS
Rank(
[Product].[Category].CurrentMember,
[Top5Categories]
)
MEMBER [Measures].[Subcategory Local Rank] AS
IIF(
[Product].[Subcategory].CurrentMember IS [Product].[Subcategory].[All],
NULL,
Rank(
[Product].[Subcategory].CurrentMember,
ORDER(
Filter(
[Product].[Subcategory].[Subcategory].Members,
[Product].[Subcategory].CurrentMember.Parent =
[Product].[Category].CurrentMember
),
[Measures].[Internet Sales Amount],
DESC
)
)
)
SELECT
{[Measures].[Internet Sales Amount],
[Measures].[Category Rank],
[Measures].[Subcategory Local Rank]} ON COLUMNS,
[DetailedTop] ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]
Пример 2: Парето-анализ (правило 80/20)
WITH
-- Сортируем продукты по продажам
SET [SortedProducts] AS
ORDER(
NON EMPTY [Product].[Product].[Product].Members,
[Measures].[Internet Sales Amount],
DESC
)
-- Общая сумма
MEMBER [Measures].[Total Revenue] AS
SUM([SortedProducts], [Measures].[Internet Sales Amount])
-- Накопительная сумма
MEMBER [Measures].[Running Total] AS
SUM(
Head(
[SortedProducts],
Rank([Product].[Product].CurrentMember, [SortedProducts])
),
[Measures].[Internet Sales Amount]
),
FORMAT_STRING = "Currency"
-- Накопительный процент
MEMBER [Measures].[Running %] AS
[Measures].[Running Total] / [Measures].[Total Revenue],
FORMAT_STRING = "Percent"
-- Определяем группу Парето
MEMBER [Measures].[Pareto Group] AS
IIF(
[Measures].[Running %] <= 0.8,
"Vital Few (80%)",
"Trivial Many (20%)"
)
-- Количество продуктов для достижения 80% продаж
MEMBER [Measures].[Products for 80%] AS
COUNT(
Filter(
[SortedProducts],
[Measures].[Running %] <= 0.8
)
)
SELECT
{[Measures].[Internet Sales Amount],
[Measures].[Running Total],
[Measures].[Running %],
[Measures].[Pareto Group]} ON COLUMNS,
Head([SortedProducts], 50) ON ROWS -- Показываем первые 50
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]
Пример 3: Динамическая сортировка с переключением
WITH
-- Параметр для выбора критерия (в реальности может быть параметром отчета)
MEMBER [Measures].[Sort Mode] AS 2 -- 1=Sales, 2=Margin, 3=Volume
-- Расчет маржи
MEMBER [Measures].[Margin %] AS
IIF(
[Measures].[Internet Sales Amount] = 0,
0,
([Measures].[Internet Sales Amount] - [Measures].[Internet Total Product Cost]) /
[Measures].[Internet Sales Amount]
),
FORMAT_STRING = "Percent"
-- Универсальный критерий сортировки
MEMBER [Measures].[Dynamic Sort Value] AS
CASE [Measures].[Sort Mode]
WHEN 1 THEN [Measures].[Internet Sales Amount]
WHEN 2 THEN [Measures].[Margin %] * 1000000 -- Умножаем для масштабирования
WHEN 3 THEN [Measures].[Order Quantity]
ELSE [Measures].[Internet Sales Amount]
END
-- Название текущего критерия
MEMBER [Measures].[Sort Criteria Name] AS
CASE [Measures].[Sort Mode]
WHEN 1 THEN "Revenue"
WHEN 2 THEN "Margin %"
WHEN 3 THEN "Volume"
ELSE "Default"
END
SELECT
{[Measures].[Internet Sales Amount],
[Measures].[Margin %],
[Measures].[Order Quantity],
[Measures].[Sort Criteria Name]} ON COLUMNS,
ORDER(
NON EMPTY [Product].[Subcategory].[Subcategory].Members,
[Measures].[Dynamic Sort Value],
DESC
) ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]
Оптимизация производительности
Ключевые принципы эффективной сортировки
1. Минимизация набора перед сортировкой:
-- НЕЭФФЕКТИВНО: сортировка всех элементов
WITH SET [AllSorted] AS
ORDER(
[Customer].[Customer].[Customer].Members, -- 18,484 клиента!
[Measures].[Internet Sales Amount],
DESC
)
-- ЭФФЕКТИВНО: фильтрация перед сортировкой
WITH SET [FilteredSorted] AS
ORDER(
NON EMPTY
FILTER(
[Customer].[Customer].[Customer].Members,
[Measures].[Internet Sales Amount] > 1000
),
[Measures].[Internet Sales Amount],
DESC
)
SELECT
[Measures].[Internet Sales Amount] ON COLUMNS,
Head([FilteredSorted], 100) ON ROWS
FROM [Adventure Works]
2. Кэширование отсортированных наборов:
WITH
-- Создаем и кэшируем отсортированный набор один раз
SET [CachedSorted] AS
ORDER(
NON EMPTY [Product].[Product].[Product].Members,
[Measures].[Internet Sales Amount],
DESC
)
-- Используем кэшированный набор многократно
MEMBER [Measures].[Position] AS
Rank([Product].[Product].CurrentMember, [CachedSorted])
MEMBER [Measures].[In Top 10] AS
IIF([Measures].[Position] <= 10, "Yes", "No")
MEMBER [Measures].[Percentile] AS
([Measures].[Position] - 1) / (COUNT([CachedSorted]) - 1) * 100,
FORMAT_STRING = "#,##0.00"
SELECT
{[Measures].[Internet Sales Amount],
[Measures].[Position],
[Measures].[In Top 10],
[Measures].[Percentile]} ON COLUMNS,
[CachedSorted] ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]
Распространенные ошибки
Ошибка 1: Сортировка с NULL в критерии
-- ПРОБЛЕМА: NULL нарушает порядок
ORDER(
[Product].[Product].Members,
[Measures].[Some Ratio], -- Может содержать NULL
DESC
)
-- РЕШЕНИЕ: Обработка NULL
ORDER(
[Product].[Product].Members,
IIF(
IsEmpty([Measures].[Some Ratio]),
-999999, -- Явное значение для NULL
[Measures].[Some Ratio]
),
DESC
)
Ошибка 2: Избыточная вложенность ORDER
-- ПРОБЛЕМА: Множественные ORDER неэффективны
ORDER(
ORDER(
ORDER(
[Product].[Product].Members,
[Measures].[Sales],
DESC
),
[Measures].[Profit],
DESC
),
[Measures].[Quantity],
DESC
)
-- РЕШЕНИЕ: Комбинированный критерий
WITH MEMBER [Measures].[Combined] AS
[Measures].[Sales] * 1000000 +
[Measures].[Profit] * 1000 +
[Measures].[Quantity]
SELECT * FROM [Adventure Works]
ORDER(
[Product].[Product].Members,
[Measures].[Combined],
DESC
)
Заключение
Сортировка в MDX — это мощный инструмент, который выходит далеко за рамки простого упорядочивания. Мы изучили:
- Различные режимы сортировки и их влияние на иерархии
- Работу с NULL значениями и специальными случаями
- Создание сложных критериев сортировки
- Комбинирование сортировки с фильтрацией
- Техники оптимизации для больших наборов данных
- Реализацию многоуровневой и условной сортировки
Правильное применение сортировки превращает сырые данные в информативные отчеты, где важная информация всегда на виду, тренды очевидны, а аномалии легко обнаружить.
Домашнее задание
Задание 1: Иерархическая сортировка
Создайте отчет, где страны отсортированы по общим продажам, а внутри каждой страны регионы отсортированы по средней сумме заказа.
Задание 2: Адаптивная сортировка
Реализуйте запрос, который автоматически меняет критерий сортировки в зависимости от выбранного измерения (продукты сортируются по марже, клиенты — по частоте покупок).
Задание 3: Комплексный ABC-анализ
Создайте полноценный ABC-анализ с автоматическим определением границ групп и подсчетом статистики по каждой группе.
Контрольные вопросы
- Когда следует использовать BDESC вместо DESC?
- Как эффективно обрабатывать NULL при сортировке?
- Можно ли реализовать сортировку по нескольким критериям одновременно?
- Как оптимизировать сортировку очень больших наборов?
- В чем преимущество кэширования отсортированных наборов?
- Как реализовать условную сортировку с разными направлениями?
- Какие функции часто используются совместно с ORDER?