Урок 4.7: Продвинутые техники фильтрации

Урок 4.7: Продвинутые техники фильтрации

Модуль 4: Фильтрация, сортировка и ранжирование

Введение: Сложная логика фильтрации в MDX

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

В реальных аналитических задачах часто требуется не просто отфильтровать данные по одному условию, а создать сложную логику: исключить определенные элементы, найти пересечение нескольких наборов, объединить результаты разных фильтров или применить фильтрацию после агрегации. Именно эти задачи решают функции, которые мы изучим в этом уроке.

Теоретические основы продвинутой фильтрации

Теория множеств в MDX

MDX работает с наборами (sets), и операции над ними основаны на классической теории множеств. Понимание этих операций критически важно для создания сложных фильтров:

  • Объединение (Union) — создает набор, содержащий все элементы из двух или более наборов. Дубликаты могут быть удалены или сохранены в зависимости от параметров.
  • Пересечение (Intersect) — создает набор, содержащий только те элементы, которые присутствуют во всех исходных наборах одновременно.
  • Разность (Except) — создает набор, содержащий элементы первого набора, которых нет во втором наборе.
  • Фильтрация после агрегации (Having) — позволяет применять условия к агрегированным включая дубликаты

Функция UNION: Объединение наборов

Пример 1: Объединение двух категорий продуктов

-- Пример 1: Объединение двух категорий продуктов
WITH 
-- Набор велосипедов 
SET [Bikes] AS 
    DESCENDANTS(
        [Product].[Product Categories].[Category].[Bikes], 
        [Product].[Product Categories].[Product]
    ) 
-- Набор аксессуаров 
SET [Accessories] AS 
    DESCENDANTS(
        [Product].[Product Categories].[Category].[Accessories], 
        [Product].[Product Categories].[Product]
    ) 
-- Объединение наборов (без дубликатов) 
SET [BikesAndAccessories] AS 
    UNION([Bikes], [Accessories]) 
-- Подсчет элементов 
MEMBER [Measures].[Bikes Count] AS 
    COUNT([Bikes]) 
MEMBER [Measures].[Accessories Count] AS 
    COUNT([Accessories]) 
MEMBER [Measures].[Union Count] AS 
    COUNT([BikesAndAccessories]) 
SELECT 
    {
        [Measures].[Internet Sales Amount], 
        [Measures].[Bikes Count], 
        [Measures].[Accessories Count], 
        [Measures].[Union Count]
    } ON COLUMNS, 
    HEAD([BikesAndAccessories], 20) ON ROWS 
FROM [Adventure Works] 
WHERE [Date].[Calendar Year].&[2013]

Объединение результатов разных фильтров

UNION особенно полезен, когда нужно объединить результаты нескольких разных условий фильтрации:

WITH 
-- Продукты с высокими продажами
SET [HighSalesProducts] AS
    FILTER(
        [Product].[Product].[Product].Members,
        [Measures].[Internet Sales Amount] > 100000
    )

-- Продукты с большим количеством заказов
SET [PopularProducts] AS
    FILTER(
        [Product].[Product].[Product].Members,
        [Measures].[Internet Order Count] > 50
    )

-- Объединение: продукты, удовлетворяющие хотя бы одному условию
SET [HighSalesOrPopular] AS
    UNION([HighSalesProducts], [PopularProducts])

-- Метка для отслеживания критерия
MEMBER [Measures].[Criteria Met] AS
    CASE
        WHEN [Measures].[Internet Sales Amount] > 100000 
             AND [Measures].[Internet Order Count] > 50
        THEN "Both"
        WHEN [Measures].[Internet Sales Amount] > 100000
        THEN "High Sales Only"
        WHEN [Measures].[Internet Order Count] > 50
        THEN "Popular Only"
        ELSE "None"
    END

SELECT 
    {[Measures].[Internet Sales Amount],
     [Measures].[Internet Order Count],
     [Measures].[Criteria Met]} ON COLUMNS,
    NON EMPTY [HighSalesOrPopular] ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]

Функция INTERSECT: Пересечение наборов

Базовое использование INTERSECT

Функция INTERSECT возвращает только те элементы, которые присутствуют в обоих наборах:

WITH 
-- Топ-50 продуктов по продажам
SET [Top50BySales] AS
    TOPCOUNT(
        [Product].[Product].[Product].Members,
        50,
        [Measures].[Internet Sales Amount]
    )

-- Топ-50 продуктов по количеству заказов
SET [Top50ByOrders] AS
    TOPCOUNT(
        [Product].[Product].[Product].Members,
        50,
        [Measures].[Internet Order Count]
    )

-- Пересечение: продукты в обоих топ-50
SET [TopInBoth] AS
    INTERSECT([Top50BySales], [Top50ByOrders])

-- Статистика
MEMBER [Measures].[In Sales Top50] AS
    IIF(COUNT(INTERSECT({[Product].[Product].CurrentMember}, [Top50BySales])) > 0, "Yes", "No")

MEMBER [Measures].[In Orders Top50] AS
    IIF(COUNT(INTERSECT({[Product].[Product].CurrentMember}, [Top50ByOrders])) > 0, "Yes", "No")

MEMBER [Measures].[Intersection Count] AS
    COUNT([TopInBoth])

SELECT 
    {[Measures].[Internet Sales Amount],
     [Measures].[Internet Order Count],
     [Measures].[In Sales Top50],
     [Measures].[In Orders Top50],
     [Measures].[Intersection Count]} ON COLUMNS,
    [TopInBoth] ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]

Множественное пересечение

Для пересечения более двух наборов используем вложенные вызовы INTERSECT:

WITH 
-- Продукты с продажами > 50000
SET [HighSales] AS
    FILTER(
        [Product].[Product].[Product].Members,
        [Measures].[Internet Sales Amount] > 50000
    )

-- Продукты с количеством заказов > 30
SET [ManyOrders] AS
    FILTER(
        [Product].[Product].[Product].Members,
        [Measures].[Internet Order Count] > 30
    )

-- Продукты категории Bikes
SET [BikeProducts] AS
    DESCENDANTS(
        [Product].[Product Categories].[Category].[Bikes],
        [Product].[Product Categories].[Product]
    )

-- Тройное пересечение: велосипеды с высокими продажами и большим количеством заказов
SET [EliteBikes] AS
    INTERSECT(
        INTERSECT([HighSales], [ManyOrders]),
        [BikeProducts]
    )

-- Проверка всех условий
MEMBER [Measures].[All Criteria] AS
    "Sales: " + CStr([Measures].[Internet Sales Amount]) + 
    ", Orders: " + CStr([Measures].[Internet Order Count]) +
    ", Category: " + [Product].[Product Categories].CurrentMember.Parent.Name

SELECT 
    {[Measures].[Internet Sales Amount],
     [Measures].[Internet Order Count],
     [Measures].[All Criteria]} ON COLUMNS,
    [EliteBikes] ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]

Функция EXCEPT: Исключение элементов

Основы работы с EXCEPT

Функция EXCEPT возвращает элементы первого набора, которых нет во втором:

WITH 
-- Все продукты с продажами
SET [AllActiveProducts] AS
    FILTER(
        [Product].[Product].[Product].Members,
        [Measures].[Internet Sales Amount] > 0
    )

-- Продукты с низкими продажами (< 10000)
SET [LowSalesProducts] AS
    FILTER(
        [Product].[Product].[Product].Members,
        [Measures].[Internet Sales Amount] < 10000
    )

-- Исключаем продукты с низкими продажами
SET [NormalAndHighSalesProducts] AS
    EXCEPT([AllActiveProducts], [LowSalesProducts])

-- Статистика
MEMBER [Measures].[Total Active] AS
    COUNT([AllActiveProducts])

MEMBER [Measures].[Low Sales Count] AS
    COUNT([LowSalesProducts])

MEMBER [Measures].[After Exclusion] AS
    COUNT([NormalAndHighSalesProducts])

SELECT 
    {[Measures].[Internet Sales Amount],
     [Measures].[Total Active],
     [Measures].[Low Sales Count],
     [Measures].[After Exclusion]} ON COLUMNS,
    HEAD(
        ORDER(
            [NormalAndHighSalesProducts],
            [Measures].[Internet Sales Amount],
            DESC
        ),
        20
    ) ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]

Использование EXCEPT для очистки данных

EXCEPT часто применяется для исключения нежелательных элементов:

WITH 
-- Все клиенты
SET [AllCustomers] AS
    [Customer].[Customer].[Customer].Members

-- Клиенты без покупок в 2013 году
SET [InactiveCustomers] AS
    FILTER(
        [Customer].[Customer].[Customer].Members,
        ([Measures].[Internet Sales Amount], [Date].[Calendar Year].&[2013]) = 0
        OR 
        ISEMPTY(([Measures].[Internet Sales Amount], [Date].[Calendar Year].&[2013]))
    )

-- Активные клиенты (исключаем неактивных)
SET [ActiveCustomers] AS
    EXCEPT([AllCustomers], [InactiveCustomers])

-- Дополнительная фильтрация активных клиентов
SET [PremiumActiveCustomers] AS
    FILTER(
        [ActiveCustomers],
        [Measures].[Internet Sales Amount] > 2000
    )

SELECT 
    {[Measures].[Internet Sales Amount],
     [Measures].[Internet Order Count]} ON COLUMNS,
    HEAD([PremiumActiveCustomers], 25) ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]

Функция HAVING: Фильтрация после агрегации

Концепция HAVING в MDX

HAVING позволяет фильтровать данные после выполнения агрегации, что критически важно при работе с группированными данными:

WITH 
-- Группировка продуктов по подкатегориям с фильтрацией по агрегированным значениям
SET [SignificantSubcategories] AS
    FILTER(
        [Product].[Product Categories].[Subcategory].Members,
        SUM(
            DESCENDANTS(
                [Product].[Product Categories].CurrentMember,
                [Product].[Product Categories].[Product]
            ),
            [Measures].[Internet Sales Amount]
        ) > 500000
    )

-- Альтернативный подход с использованием HAVING-подобной логики
SET [ProductsInSignificantSubcategories] AS
    GENERATE(
        [SignificantSubcategories],
        DESCENDANTS(
            [Product].[Product Categories].CurrentMember,
            [Product].[Product Categories].[Product]
        )
    )

-- Сумма по подкатегории
MEMBER [Measures].[Subcategory Total] AS
    SUM(
        DESCENDANTS(
            ANCESTOR(
                [Product].[Product Categories].CurrentMember,
                [Product].[Product Categories].[Subcategory]
            ),
            [Product].[Product Categories].[Product]
        ),
        [Measures].[Internet Sales Amount]
    )

SELECT 
    {[Measures].[Internet Sales Amount],
     [Measures].[Subcategory Total]} ON COLUMNS,
    HEAD([ProductsInSignificantSubcategories], 30) ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]

Каскадная фильтрация: Создание зависимых фильтров

Концепция каскадной фильтрации

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

WITH 
-- Уровень 1: Страны с продажами > 1 млн
SET [Level1_Countries] AS
    FILTER(
        [Customer].[Customer Geography].[Country].Members,
        [Measures].[Internet Sales Amount] > 1000000
    )

-- Уровень 2: Штаты в отобранных странах с продажами > 100000
SET [Level2_States] AS
    FILTER(
        GENERATE(
            [Level1_Countries],
            DESCENDANTS(
                [Customer].[Customer Geography].CurrentMember,
                [Customer].[Customer Geography].[State-Province]
            )
        ),
        [Measures].[Internet Sales Amount] > 100000
    )

-- Уровень 3: Города в отобранных штатах с продажами > 50000
SET [Level3_Cities] AS
    FILTER(
        GENERATE(
            [Level2_States],
            DESCENDANTS(
                [Customer].[Customer Geography].CurrentMember,
                [Customer].[Customer Geography].[City]
            )
        ),
        [Measures].[Internet Sales Amount] > 50000
    )

-- Уровень 4: Топ-5 клиентов в каждом отобранном городе
SET [Level4_TopCustomers] AS
    GENERATE(
        [Level3_Cities],
        TOPCOUNT(
            DESCENDANTS(
                [Customer].[Customer Geography].CurrentMember,
                [Customer].[Customer Geography].[Customer]
            ),
            5,
            [Measures].[Internet Sales Amount]
        )
    )

-- Путь в иерархии
MEMBER [Measures].[Location Path] AS
    ANCESTOR(
        [Customer].[Customer Geography].CurrentMember,
        [Customer].[Customer Geography].[Country]
    ).Name + " > " +
    ANCESTOR(
        [Customer].[Customer Geography].CurrentMember,
        [Customer].[Customer Geography].[State-Province]
    ).Name + " > " +
    ANCESTOR(
        [Customer].[Customer Geography].CurrentMember,
        [Customer].[Customer Geography].[City]
    ).Name

SELECT 
    {[Measures].[Internet Sales Amount],
     [Measures].[Location Path]} ON COLUMNS,
    HEAD([Level4_TopCustomers], 30) ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]

Комбинирование продвинутых техник

Сложная логика с множественными операциями

Реальные задачи часто требуют комбинирования всех изученных техник:

WITH 
-- Базовые наборы
SET [HighValueProducts] AS
    FILTER(
        [Product].[Product].[Product].Members,
        [Measures].[Internet Sales Amount] > 75000
    )

SET [FrequentProducts] AS
    FILTER(
        [Product].[Product].[Product].Members,
        [Measures].[Internet Order Count] > 40
    )

SET [BikeProducts] AS
    DESCENDANTS(
        [Product].[Product Categories].[Category].[Bikes],
        [Product].[Product Categories].[Product]
    )

-- Комбинированная логика: 
-- (Высокие продажи И Частые заказы) ИЛИ (Велосипеды с продажами > 50000)
-- НО НЕ продукты с низкой маржой
SET [ComplexFilter] AS
    EXCEPT(
        UNION(
            INTERSECT([HighValueProducts], [FrequentProducts]),
            FILTER(
                [BikeProducts],
                [Measures].[Internet Sales Amount] > 50000
            )
        ),
        FILTER(
            [Product].[Product].[Product].Members,
            ([Measures].[Internet Sales Amount] - [Measures].[Internet Total Product Cost]) / 
            IIF([Measures].[Internet Sales Amount] = 0, NULL, [Measures].[Internet Sales Amount]) < 0.2
        )
    )

-- Анализ результата
MEMBER [Measures].[Profit 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].[Filter Reason] AS
    CASE
        WHEN COUNT(INTERSECT(
                {[Product].[Product].CurrentMember}, 
                INTERSECT([HighValueProducts], [FrequentProducts])
             )) > 0
        THEN "High Value & Frequent"
        WHEN COUNT(INTERSECT(
                {[Product].[Product].CurrentMember}, 
                [BikeProducts]
             )) > 0
             AND [Measures].[Internet Sales Amount] > 50000
        THEN "Bike > 50K"
        ELSE "Other"
    END

SELECT 
    {[Measures].[Internet Sales Amount],
     [Measures].[Internet Order Count],
     [Measures].[Profit Margin],
     [Measures].[Filter Reason]} ON COLUMNS,
    HEAD(
        ORDER(
            [ComplexFilter],
            [Measures].[Internet Sales Amount],
            DESC
        ),
        25
    ) ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]

Практический пример: Комплексный анализ клиентской базы

WITH 
-- Этап 1: Базовая сегментация
SET [VIPCustomers] AS
    TOPCOUNT(
        [Customer].[Customer].[Customer].Members,
        100,
        [Measures].[Internet Sales Amount]
    )

SET [RegularCustomers] AS
    FILTER(
        EXCEPT(
            [Customer].[Customer].[Customer].Members,
            [VIPCustomers]
        ),
        [Measures].[Internet Sales Amount] > 500
    )

-- Этап 2: Анализ активности
SET [ActiveVIP] AS
    FILTER(
        [VIPCustomers],
        [Measures].[Internet Order Count] > 5
    )

SET [InactiveVIP] AS
    EXCEPT([VIPCustomers], [ActiveVIP])

-- Этап 3: Объединение для финального анализа
SET [TargetCustomers] AS
    UNION(
        [ActiveVIP],
        TOPCOUNT([RegularCustomers], 50, [Measures].[Internet Sales Amount])
    )

-- Метрики
MEMBER [Measures].[Customer Type] AS
    CASE
        WHEN COUNT(INTERSECT({[Customer].[Customer].CurrentMember}, [ActiveVIP])) > 0
        THEN "Active VIP"
        WHEN COUNT(INTERSECT({[Customer].[Customer].CurrentMember}, [InactiveVIP])) > 0
        THEN "Inactive VIP"
        WHEN COUNT(INTERSECT({[Customer].[Customer].CurrentMember}, [RegularCustomers])) > 0
        THEN "Regular"
        ELSE "Other"
    END

MEMBER [Measures].[Average Order] AS
    IIF(
        [Measures].[Internet Order Count] = 0,
        0,
        [Measures].[Internet Sales Amount] / [Measures].[Internet Order Count]
    ),
    FORMAT_STRING = "Currency"

SELECT 
    {[Measures].[Internet Sales Amount],
     [Measures].[Internet Order Count],
     [Measures].[Average Order],
     [Measures].[Customer Type]} ON COLUMNS,
    HEAD([TargetCustomers], 30) ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]

Заключение

В этом уроке мы изучили продвинутые техники фильтрации в MDX:

  • UNION — для объединения наборов и создания OR-логики в фильтрах
  • INTERSECT — для нахождения общих элементов и создания AND-логики
  • EXCEPT — для исключения нежелательных элементов
  • HAVING-подобная логика — для фильтрации после агрегации
  • Каскадная фильтрация — для создания многоуровневых зависимых фильтров

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

Домашнее задание

Базовый уровень: Создайте запрос, который находит продукты, входящие в топ-20 по продажам ИЛИ в топ-20 по количеству заказов, но исключает продукты категории "Clothing".

Средний уровень: Реализуйте трехуровневую каскадную фильтрацию для анализа продуктов: категория > подкатегория > продукт, где на каждом уровне применяются разные пороговые значения.

Продвинутый уровень: Создайте систему комплексной фильтрации клиентов, которая использует все изученные функции для выявления целевой аудитории для маркетинговой кампании.

Модуль 4: Фильтрация, сортировка и ранжирование • Урок 4.7

Следующий урок!
Прошлый урок